前端进阶之旅前端进阶之旅
基础篇
进阶篇
高频篇
精选篇
手写篇
面经篇
AI 篇
原理篇
每日一题
小程序题库
知识卡片
  • 场景篇按分类整理的大前端场景考点
  • 历年面经按年份追踪真实考点
  • 算法题库NEW在线编码即时判题
  • 专项自测100 题快速查漏
  • 前端基础
    • HTTP从报文一路讲到 HTTPS
    • 浏览器渲染、事件循环、进程
    • 计算机基础Linux、网络、操作系统
  • 进阶专项
    • 设计模式23 种模式怎么用
    • 前端系统进阶学习大型项目工程化
    • 前端综合文章长期沉淀的实践文
  • 工程与工具
    • Node学习指南从环境搭建到服务端
    • NPM工作流script、依赖与发布
    • Docker容器化部署上手
    • Canvas图形与动画实战
  • 路线与导图
    • 思维导图知识点全景图
    • 学习路线按图索骥不跑偏
  • 动态
    • 公众号动态公众号历史文章
    • 博客动态站长的技术博客
    • 开发者导航常用工具与文档站
  • AI 助手随时提问,即时解析
  • AI 模拟面试模拟真实面试 + 报告
  • AI 知识地图串起全站知识点
  • AI 定制路线按你的简历现排
AI 热点
旧版
基础篇
进阶篇
高频篇
精选篇
手写篇
面经篇
AI 篇
原理篇
每日一题
小程序题库
知识卡片
  • 场景篇按分类整理的大前端场景考点
  • 历年面经按年份追踪真实考点
  • 算法题库NEW在线编码即时判题
  • 专项自测100 题快速查漏
  • 前端基础
    • HTTP从报文一路讲到 HTTPS
    • 浏览器渲染、事件循环、进程
    • 计算机基础Linux、网络、操作系统
  • 进阶专项
    • 设计模式23 种模式怎么用
    • 前端系统进阶学习大型项目工程化
    • 前端综合文章长期沉淀的实践文
  • 工程与工具
    • Node学习指南从环境搭建到服务端
    • NPM工作流script、依赖与发布
    • Docker容器化部署上手
    • Canvas图形与动画实战
  • 路线与导图
    • 思维导图知识点全景图
    • 学习路线按图索骥不跑偏
  • 动态
    • 公众号动态公众号历史文章
    • 博客动态站长的技术博客
    • 开发者导航常用工具与文档站
  • AI 助手随时提问,即时解析
  • AI 模拟面试模拟真实面试 + 报告
  • AI 知识地图串起全站知识点
  • AI 定制路线按你的简历现排
AI 热点
旧版
  • 基础进阶

  • 精选模块

    • 精选篇

      • 1 HTML模块
      • 2 CSS模块
      • 3 JS模块
      • 4 ES6模块
      • 5 浏览器模块
      • 6 React模块
      • 7 Vue模块
      • 8 Node模块
      • 9 前端工程相关
      • 10 移动多端开发
      • 11 小程序模块
      • 12 Uniapp模块
      • 13 前端安全模块
      • 14 性能优化相关
      • 15 HTTP模块
      • 16 常用设计模式
      • 17 框架通识
      • 18 排序算法
      • 19 计算机通识
      • 20 React Native模块
      • 21 桌面端模块
      • 22 Flutter模块
      • 23 Android模块
      • 24 iOS模块
    • 进阶篇

# Android 原生面试题精选

覆盖语言基础、系统原理、UI、并发、架构、性能、安全、测试与发布。每题都有 30 秒速记、代码示例、详细解析和面试官追问,流程类题目配了时序图。

# 1 Java 与 JVM 基础

# 1. Java 泛型擦除是什么?Kotlin 里怎么在运行时拿到泛型类型?

30 秒速记

  • 擦除 = 泛型只在编译期检查,编译完 List<String> 和 List<Int> 都是同一个 List 类
  • 后果:不能 is List<String>、不能 T::class、不能 new T(),as List<String> 只给警告,真错在取元素时才炸
  • 类、字段、方法签名上的泛型会留在 Signature 属性里,Gson 的 TypeToken 就是靠匿名子类读父类签名
  • Kotlin 用 inline + reified 把类型在调用处「抄」进去,本质是编译器替你传了 Class
  • 开 R8 混淆要保留 Signature,不然 TypeToken 拿到的是原始类型,解析出 LinkedTreeMap 然后类型转换崩溃

泛型擦除就是编译器做完类型检查后,把类型参数擦成上界(默认 Object),运行时 List<String> 和 List<User> 是同一个东西。 这么设计是为了兼容 Java 5 以前没有泛型的字节码。日常最常碰到的坑是 Gson 解析列表:传 List::class.java 进去,Gson 不知道元素是什么,只能给你一堆 LinkedTreeMap,到取元素当 User 用的时候才抛 ClassCastException。解决办法要么用 TypeToken 匿名子类把类型留在签名里,要么在 Kotlin 里写 inline fun <reified T>,让编译器在调用处把具体类型填进去。

# 擦掉的是什么,留下的是什么

打个比方,泛型像快递单上的「易碎品」标签,分拣中心(编译器)看一眼就撕了,货车上(运行时)的箱子都长一个样。所以运行时你问一个对象「你是不是 List<String>」,它答不上来,Kotlin 直接不让你这么写:

fun check(x: Any) {
    // if (x is List<String>) {}   // 编译报错:Cannot check for instance of erased type
    if (x is List<*>) println("是个 List,元素类型不知道")
}

fun main() {
    val raw: Any = listOf(1, 2, 3)
    @Suppress("UNCHECKED_CAST")
    val strs = raw as List<String>   // 只有警告,这行不会崩
    println(strs.size)               // 输出 3,也不会崩
    val s: String = strs[0]          // 这里才抛 ClassCastException:Integer cannot be cast to String
}
@前端进阶之旅: 代码已经复制到剪贴板

注意崩溃点不在强转那一行,而在编译器偷偷插入 checkcast 的取值那一行。线上堆栈经常指向一个看起来毫无问题的地方,根因在很远的上游,这就是擦除最烦人的地方。

但擦除不是全擦。声明在类、字段、方法签名上的泛型信息会写进 class 文件的 Signature 属性,反射能读到。Gson 的 TypeToken 就利用了这一点:

data class User(val id: Long, val name: String)

val json = """[{"id":1,"name":"Tom"}]"""

// 错误:只告诉 Gson「这是个 List」
val bad: List<User> = Gson().fromJson(json, List::class.java) as List<User>
// bad[0].name  -> ClassCastException: LinkedTreeMap cannot be cast to User

// 正确:匿名子类 object : TypeToken<List<User>>() {} 的父类签名里保留了 List<User>
val type = object : TypeToken<List<User>>() {}.type
val good: List<User> = Gson().fromJson(json, type)
println(good[0].name) // Tom

// Kotlin 写法:reified 让 T 在调用处变成真实类型
inline fun <reified T> Gson.fromJsonTyped(json: String): T =
    fromJson(json, object : TypeToken<T>() {}.type)

val users: List<User> = Gson().fromJsonTyped(json)
@前端进阶之旅: 代码已经复制到剪贴板

# reified 为什么能做到

reified 只能用在 inline 函数上,因为函数体会被复制到每个调用点,编译器在调用点知道 T 具体是谁,就直接把 User::class.java 写进字节码。代价是函数不能被 Java 调用(Java 那边没有内联),而且函数体大的话会让包体膨胀。startActivity<DetailActivity>()、viewModels<VM>() 这种扩展都是这个套路。

# 混淆后的坑

R8 / ProGuard 默认会去掉 Signature 属性。没保留的话,TypeToken 拿到的只是原始的 List,现象和上面「错误写法」一模一样,而且只在正式包出现。规则里要有 -keepattributes Signature,并且保留 TypeToken 的子类。Gson 2.11 起 jar 里自带了这部分规则,老版本要自己加;AGP 8 默认开了 R8 full mode,对没被保留的类会更激进地删签名,升级后正式包突然解析失败,优先查这里。

面试官追问

追问 1List<String> 和 List<Int> 能写成两个重载方法吗?
参考回答

不能,擦除后两个方法签名都是 foo(List),JVM 层面冲突。Kotlin 里可以用 @JvmName("fooInts") 给其中一个改字节码名字绕过去。

追问 2reified 那么好用,为什么 Retrofit 接口不用它?
参考回答

Retrofit 是靠动态代理在运行时读接口方法的返回类型,比如 suspend fun list(): List<User> 的泛型签名本来就保留在方法的 Signature 里,不需要 reified。它反而怕混淆删掉签名,所以官方规则里也保留了 Signature。

追问 3调试包解析正常,正式包列表里每一项都是 LinkedTreeMap,第一步看什么?
参考回答

先看混淆规则有没有 -keepattributes Signature,再看 Gson 版本和数据类有没有被保留。用 jadx 打开正式包看那个 TypeToken 匿名类的父类签名还在不在,一眼就能确认。

追问 4泛型数组为什么不能直接 arrayOf<T>()?
参考回答

数组在运行时是知道自己元素类型的(String[] 和 Object[] 是不同的类),创建时必须给出真实类型,而 T 被擦了。Kotlin 里要写 inline fun <reified T> make() = arrayOf<T>(),或者传一个 Class<T> 进去。

追问 5擦除之后,class Box<T : Number> 里的 T 在字节码里是什么?
参考回答

擦成上界 Number,不是 Object。所以方法里可以直接调 t.toInt(),编译器知道它至少是个 Number。

# 2. Kotlin 的 out / in 和 Java 的 ? extends / ? super 是什么关系?什么时候用哪个?

30 秒速记

  • 默认泛型是不变的:Dog 是 Animal 的子类,但 List<Dog> 不是 List<Animal>(指可变的那种)
  • out T = ? extends T,只往外拿,叫协变;in T = ? super T,只往里放,叫逆变
  • 口诀 PECS:生产者用 extends / out,消费者用 super / in
  • Kotlin 可以在声明处写一次(interface Source<out T>),Java 只能在每个使用处写通配符
  • Kotlin 的 List<out E> 只读所以协变,MutableList<E> 能写所以不变;Java 数组协变是历史设计,会在运行时抛 ArrayStoreException

out 和 in 就是 Kotlin 版的 ? extends 和 ? super,解决的是「装狗的盒子能不能当装动物的盒子用」这个问题。 答案取决于你往盒子里放还是从里面拿:只拿不放,Box<Dog> 当 Box<Animal> 用没问题,这叫协变,写 out;只放不拿,Box<Animal> 当 Box<Dog> 用也安全,这叫逆变,写 in。Kotlin 比 Java 方便的一点是可以在声明处标一次,比如 List<out E>,所以 List<Dog> 能直接传给要 List<Animal> 的参数;Java 得在每个方法参数上写 List<? extends Animal>。

基础篇
进阶篇
高频篇
精选篇
手写篇
面经篇
AI 篇
原理篇
每日一题
小程序题库
知识卡片
  • 场景篇按分类整理的大前端场景考点
  • 历年面经按年份追踪真实考点
  • 算法题库NEW在线编码即时判题
  • 专项自测100 题快速查漏
  • 前端基础
    • HTTP从报文一路讲到 HTTPS
    • 浏览器渲染、事件循环、进程
    • 计算机基础Linux、网络、操作系统
  • 进阶专项
    • 设计模式23 种模式怎么用
    • 前端系统进阶学习大型项目工程化
    • 前端综合文章长期沉淀的实践文
  • 工程与工具
    • Node学习指南从环境搭建到服务端
    • NPM工作流script、依赖与发布
    • Docker容器化部署上手
    • Canvas图形与动画实战
  • 路线与导图
    • 思维导图知识点全景图
    • 学习路线按图索骥不跑偏
  • 动态
    • 公众号动态公众号历史文章
    • 博客动态站长的技术博客
    • 开发者导航常用工具与文档站
  • AI 助手随时提问,即时解析
  • AI 模拟面试模拟真实面试 + 报告
  • AI 知识地图串起全站知识点
  • AI 定制路线按你的简历现排
AI 热点
旧版
  • 基础进阶

  • 精选模块

    • 精选篇

      • 1 HTML模块
      • 2 CSS模块
      • 3 JS模块
      • 4 ES6模块
      • 5 浏览器模块
      • 6 React模块
      • 7 Vue模块
      • 8 Node模块
      • 9 前端工程相关
      • 10 移动多端开发
      • 11 小程序模块
      • 12 Uniapp模块
      • 13 前端安全模块
      • 14 性能优化相关
      • 15 HTTP模块
      • 16 常用设计模式
      • 17 框架通识
      • 18 排序算法
      • 19 计算机通识
      • 20 React Native模块
      • 21 桌面端模块
      • 22 Flutter模块
      • 23 Android模块
        • 1 Java 与 JVM 基础
          • 1. Java 泛型擦除是什么?Kotlin 里怎么在运行时拿到泛型类型?
          • 2. Kotlin 的 out / in 和 Java 的 ? extends / ? super 是什么关系?什么时候用哪个?
          • 3. HashMap 底层是怎么实现的?为什么容量是 2 的幂、链表会转红黑树?
          • 4. ConcurrentHashMap 是怎么保证线程安全的?JDK 7 和 JDK 8 有什么区别?
          • 5. volatile 和 synchronized 有什么区别?双重检查锁单例为什么要加 volatile?
          • 6. 线程池的核心参数有哪些?一个任务提交进去是怎么被执行的?
          • 7. Java 的强、软、弱、虚引用有什么区别?Android 里各自用在哪?
          • 8. LeakCanary 是怎么检测到内存泄漏的?
        • 2 Kotlin 语言基础
        • 3 协程与 Flow
        • 4 四大组件与进程生命周期
        • 5 Intent、任务栈与启动模式
        • 6 View 系统与渲染
        • 7 Jetpack Compose
        • 8 应用架构与数据流
        • 9 Lifecycle、ViewModel 与状态恢复
        • 10 Navigation 与多页面
        • 11 Service、广播与前台任务
        • 12 Binder、AIDL 与 IPC
        • 13 Handler、Looper 与消息队列
        • 14 存储、Room 与文件
        • 15 网络与序列化
        • 16 后台任务与 WorkManager
        • 17 权限、安全与隐私
        • 18 启动与性能优化
        • 19 内存、GC 与泄漏
        • 20 图形、动画与媒体
        • 21 测试与质量
        • 22 Gradle 与构建系统
        • 23 打包、签名与发布
        • 24 兼容性与设备适配
        • 25 系统设计与实战排障
      • 24 iOS模块
    • 进阶篇

完整面试题地址:
作者:程序员poetry
扫码关注作者公众号:「前端进阶之旅」 每天分享技术干货
前端进阶之旅公众号二维码

23-android

# Android 原生面试题精选

覆盖语言基础、系统原理、UI、并发、架构、性能、安全、测试与发布。每题都有 30 秒速记、代码示例、详细解析和面试官追问,流程类题目配了时序图。

# 1 Java 与 JVM 基础

# 1. Java 泛型擦除是什么?Kotlin 里怎么在运行时拿到泛型类型?

30 秒速记

  • 擦除 = 泛型只在编译期检查,编译完 List<String> 和 List<Int> 都是同一个 List 类
  • 后果:不能 is List<String>、不能 T::class、不能 new T(),as List<String> 只给警告,真错在取元素时才炸
  • 类、字段、方法签名上的泛型会留在 Signature 属性里,Gson 的 TypeToken 就是靠匿名子类读父类签名
  • Kotlin 用 inline + reified 把类型在调用处「抄」进去,本质是编译器替你传了 Class
  • 开 R8 混淆要保留 Signature,不然 TypeToken 拿到的是原始类型,解析出 LinkedTreeMap 然后类型转换崩溃

泛型擦除就是编译器做完类型检查后,把类型参数擦成上界(默认 Object),运行时 List<String> 和 List<User> 是同一个东西。 这么设计是为了兼容 Java 5 以前没有泛型的字节码。日常最常碰到的坑是 Gson 解析列表:传 List::class.java 进去,Gson 不知道元素是什么,只能给你一堆 LinkedTreeMap,到取元素当 User 用的时候才抛 ClassCastException。解决办法要么用 TypeToken 匿名子类把类型留在签名里,要么在 Kotlin 里写 inline fun <reified T>,让编译器在调用处把具体类型填进去。

# 擦掉的是什么,留下的是什么

打个比方,泛型像快递单上的「易碎品」标签,分拣中心(编译器)看一眼就撕了,货车上(运行时)的箱子都长一个样。所以运行时你问一个对象「你是不是 List<String>」,它答不上来,Kotlin 直接不让你这么写:

fun check(x: Any) {
    // if (x is List<String>) {}   // 编译报错:Cannot check for instance of erased type
    if (x is List<*>) println("是个 List,元素类型不知道")
}

fun main() {
    val raw: Any = listOf(1, 2, 3)
    @Suppress("UNCHECKED_CAST")
    val strs = raw as List<String>   // 只有警告,这行不会崩
    println(strs.size)               // 输出 3,也不会崩
    val s: String = strs[0]          // 这里才抛 ClassCastException:Integer cannot be cast to String
}
@前端进阶之旅: 代码已经复制到剪贴板

注意崩溃点不在强转那一行,而在编译器偷偷插入 checkcast 的取值那一行。线上堆栈经常指向一个看起来毫无问题的地方,根因在很远的上游,这就是擦除最烦人的地方。

但擦除不是全擦。声明在类、字段、方法签名上的泛型信息会写进 class 文件的 Signature 属性,反射能读到。Gson 的 TypeToken 就利用了这一点:

data class User(val id: Long, val name: String)

val json = """[{"id":1,"name":"Tom"}]"""

// 错误:只告诉 Gson「这是个 List」
val bad: List<User> = Gson().fromJson(json, List::class.java) as List<User>
// bad[0].name  -> ClassCastException: LinkedTreeMap cannot be cast to User

// 正确:匿名子类 object : TypeToken<List<User>>() {} 的父类签名里保留了 List<User>
val type = object : TypeToken<List<User>>() {}.type
val good: List<User> = Gson().fromJson(json, type)
println(good[0].name) // Tom

// Kotlin 写法:reified 让 T 在调用处变成真实类型
inline fun <reified T> Gson.fromJsonTyped(json: String): T =
    fromJson(json, object : TypeToken<T>() {}.type)

val users: List<User> = Gson().fromJsonTyped(json)
@前端进阶之旅: 代码已经复制到剪贴板

# reified 为什么能做到

reified 只能用在 inline 函数上,因为函数体会被复制到每个调用点,编译器在调用点知道 T 具体是谁,就直接把 User::class.java 写进字节码。代价是函数不能被 Java 调用(Java 那边没有内联),而且函数体大的话会让包体膨胀。startActivity<DetailActivity>()、viewModels<VM>() 这种扩展都是这个套路。

# 混淆后的坑

R8 / ProGuard 默认会去掉 Signature 属性。没保留的话,TypeToken 拿到的只是原始的 List,现象和上面「错误写法」一模一样,而且只在正式包出现。规则里要有 -keepattributes Signature,并且保留 TypeToken 的子类。Gson 2.11 起 jar 里自带了这部分规则,老版本要自己加;AGP 8 默认开了 R8 full mode,对没被保留的类会更激进地删签名,升级后正式包突然解析失败,优先查这里。

面试官追问

追问 1List<String> 和 List<Int> 能写成两个重载方法吗?
参考回答

不能,擦除后两个方法签名都是 foo(List),JVM 层面冲突。Kotlin 里可以用 @JvmName("fooInts") 给其中一个改字节码名字绕过去。

追问 2reified 那么好用,为什么 Retrofit 接口不用它?
参考回答

Retrofit 是靠动态代理在运行时读接口方法的返回类型,比如 suspend fun list(): List<User> 的泛型签名本来就保留在方法的 Signature 里,不需要 reified。它反而怕混淆删掉签名,所以官方规则里也保留了 Signature。

追问 3调试包解析正常,正式包列表里每一项都是 LinkedTreeMap,第一步看什么?
参考回答

先看混淆规则有没有 -keepattributes Signature,再看 Gson 版本和数据类有没有被保留。用 jadx 打开正式包看那个 TypeToken 匿名类的父类签名还在不在,一眼就能确认。

追问 4泛型数组为什么不能直接 arrayOf<T>()?
参考回答

数组在运行时是知道自己元素类型的(String[] 和 Object[] 是不同的类),创建时必须给出真实类型,而 T 被擦了。Kotlin 里要写 inline fun <reified T> make() = arrayOf<T>(),或者传一个 Class<T> 进去。

追问 5擦除之后,class Box<T : Number> 里的 T 在字节码里是什么?
参考回答

擦成上界 Number,不是 Object。所以方法里可以直接调 t.toInt(),编译器知道它至少是个 Number。

# 2. Kotlin 的 out / in 和 Java 的 ? extends / ? super 是什么关系?什么时候用哪个?

30 秒速记

  • 默认泛型是不变的:Dog 是 Animal 的子类,但 List<Dog> 不是 List<Animal>(指可变的那种)
  • out T = ? extends T,只往外拿,叫协变;in T = ? super T,只往里放,叫逆变
  • 口诀 PECS:生产者用 extends / out,消费者用 super / in
  • Kotlin 可以在声明处写一次(interface Source<out T>),Java 只能在每个使用处写通配符
  • Kotlin 的 List<out E> 只读所以协变,MutableList<E> 能写所以不变;Java 数组协变是历史设计,会在运行时抛 ArrayStoreException

out 和 in 就是 Kotlin 版的 ? extends 和 ? super,解决的是「装狗的盒子能不能当装动物的盒子用」这个问题。 答案取决于你往盒子里放还是从里面拿:只拿不放,Box<Dog> 当 Box<Animal> 用没问题,这叫协变,写 out;只放不拿,Box<Animal> 当 Box<Dog> 用也安全,这叫逆变,写 in。Kotlin 比 Java 方便的一点是可以在声明处标一次,比如 List<out E>,所以 List<Dog> 能直接传给要 List<Animal> 的参数;Java 得在每个方法参数上写 List<? extends Animal>。

# 为什么默认不能互相赋值

假设 MutableList<Dog> 能赋给 MutableList<Animal>,那就能往里塞一只 Cat,原来拿着 MutableList<Dog> 的人再取出来当狗用,直接崩。编译器为了堵这个洞,默认让泛型「不变」。Java 的数组当年没堵,所以有这段经典反例:

Object[] arr = new String[1]; // 编译通过,数组是协变的
arr[0] = 1;                   // 运行时抛 ArrayStoreException
@前端进阶之旅: 代码已经复制到剪贴板

Kotlin 里 Array<T> 是不变的,这段代码根本写不出来。

# 声明处型变 vs 使用处型变

← 22 Flutter模块24 iOS模块 →

fe