reified 这个词很多人写得出,但被问"它为什么依赖 inline"就答不上。这题真正的考点是泛型擦除这个 JVM 机制:为什么 x is T 在普通函数里编译不过、reified 到底做了什么、以及它有哪些不能做的事。把这三层讲清,后面关于泛型、序列化、依赖注入的追问都能接上。
先把结论放在前面:JVM 的泛型是擦除的——List<String> 与 List<Int> 在字节码里都是 List,类型参数在编译后被替换成上界(通常是 Object),所以运行期拿不到 T 的 Class 对象。reified 只能用在 inline 函数上,编译器会把调用点的实际类型写进函数体,等于把泛型"具体化"了。于是 x is T、T::class、T::class.java、typeOf<T>()、泛型数组创建这些操作就都能编译通过。
机制拆解
用一个具体例子讲清。fun <T> isString(x: Any) = x is T —— 擦除后 T 变成 Object,x is Object 恒为真,逻辑完全失效,编译器直接报错 "Cannot check for instance of erased type"。加上 inline 与 reified 后:inline fun <reified T> isString(x: Any) = x is T —— 编译时把调用点的 T 替换成实际类型,比如 isString<String>(x) 时函数体被展开成 x is String,运行时就能做真正的类型判断。
T::class.java 是另一个高频用途。普通泛型里 T::class 不存在(KClass<T> 需要运行期类型),但 reified 后可以拿来做反射构造:inline fun <reified T> createByClass(json: String): T = GSON.fromJson(json, T::class.java)。这是 Android 上最常见的 reified 用法——把 JSON 解析结果转成调用方期望的类型。
需要区分 T::class 与 T::class.java:T::class 得到的是 KClass<T>(Kotlin 反射类型),T::class.java 得到的是 JVM 的 Class<T>(Java 反射用)。做 Gson/Moshi 反射时必须用 .java。Kotlin 反射(kotlin-reflect)能拿到 T::class 但拿不到 KType,所以想取泛型类型参数(比如 List<User> 里的 User)需要 typeOf<T>() 配合 KType.jvmErasure——而 typeOf 同样要求 reified。
这些坑的正确绕法
最常见的坑是把 reified 用在普通函数上,编译失败,忘记它依赖 inline 前提。 报错信息通常是 "'reified' type parameter is not allowed in a regular function" 或者 "Cannot use 'T' as reified type parameter"。根因是编译器需要把类型参数展开到函数体里,而只有 inline 函数才做这件事。修法是给函数加 inline;如果函数体很大(不适合 inline),另一个思路是用 class 参数替代 reified——inline fun <T> parse(json: String, clazz: Class<T>): T = GSON.fromJson(json, clazz),把类型判断的责任交给调用方显式传 Class。
其次是 reified 用在了会"泄漏类型参数"的地方,泛型信息被固化成具体类型。 表现是一个通用的工具类里 inline fun <reified T> fromJson(json: String): T,被十几处调用,编译产物里每个调用点都生成了一份带具体类型判断的代码;更隐蔽的是这个工具类无法接受运行时才确定的类型(比如从数据库读出一个类名字符串再解析),因为编译期不知道 T 是什么。修法是提供两个版本:编译期已知类型用 reified 版本,运行期动态类型用 Class<T> 版本。Android 上 JSON 解析、Intent 参数取回、Fragment 创建这三类场景都需要这个双版本设计。
还有一个更隐蔽的坑:reified 泛型 + 内联让泛型信息"固化",破坏了一次写多次用的工具设计。 具体表现是 inline fun <reified T> firstOf(list: List<Any>): T? 被复用于多种类型,看似通用,实际每个调用点都展开一份,代码体积随类型数增长。Android 上的 dex 方法数限制会让这类"看似通用"的 reified 工具成为隐患。修法是:把 reified 限制在最外层的一层(入口处做一次类型判断),内部用普通泛型传递,避免 reified 扩散。
代码里见真章
看一段能直接跑的代码,把上面的机制落到具体写法上:
// 1) 擦除导致编译失败
// fun <T> isString(x: Any) = x is T // 错误:Cannot check for instance of erased type
// fun <T> newList() = Array<T>() // 错误:Cannot create array of erased type
// 2) inline + reified 恢复运行期类型
inline fun <reified T> isType(x: Any): Boolean = x is T
inline fun <reified T> emptyListOf(): List<T> = emptyList() // 类型从调用点推断
inline fun <reified T> arrayOfDefault(size: Int, init: (Int) -> T) = Array(size) {
init(it) }
val r1 = isType<String>("a") // 展开成 "a" is String
val r2: List<User> = emptyListOf() // 展开成 List<User>
// 3) 最常见用法:JSON 反序列化
inline fun <reified T> fromJson(json: String): T? =
Gson().fromJson(json, T::class.java) // 反射需要 Class<T>
data class User(val id: Long, val name: String)
val u: User? = fromJson(jsonStr) // 类型由左侧期望推断
// 4) 运行期动态类型:用 Class 参数版本
fun <T> fromJsonWith(json: String, clazz: Class<T>): T? = Gson().fromJson(json, clazz)
val klass = Class.forName(className) // 名字来自配置/数据库
val obj: Any? = fromJsonWith(jsonStr, klass)
// 5) 双版本共存:这是工程上最稳的做法
inline fun <reified T> parse(json: String): T? = fromJsonWith(json, T::class.java)
fun <T> parseAs(json: String, clazz: Class<T>): T? = Gson().fromJson(json, clazz)
// 6) Intent / Fragment 取回
inline fun <reified T> Bundle.get(key: String): T? = getParcelable(key, T::class.java)
inline fun <reified T : Any> Intent.getExtra(key: String): T? = getParcelableExtra(key, T::class.java)
// 7) KClass vs java Class,别搞混
inline fun <reified T> kotlinClassName(): String = T::class.simpleName ?: "?"
inline fun <reified T> javaClassObj(): Class<T> = T::class.java // 反射/JSON 用这个
// 8) reified 也要节制:每个调用点一份展开
// 泛型扩散:外层 reified + 内层也 reified → 类型数 × 调用点
inline fun <reified T> deepFilter(list: List<Any?>): List<T> =
list.filterIsInstance<T>() // 一次展开即可,别再嵌 reified
这段代码值得盯四处:第一处,第 1 段用注释列出两个"擦除导致编译失败"的写法,让机制可见;第二处,第 3 段与第 4 段并列,展示了 reified 版本与 Class<T> 版本并存的双版本设计——这是本篇最该记住的工程结论;第三处,第 7 段把 T::class 与 T::class.java 的区别单列,因为这是实际写代码时最常搞混的一对;第四处,第 6 段那两个扩展是反例,照抄会踩坑——Bundle 已经有成员函数 get(String),而 Kotlin 的规则是成员函数永远胜过同名扩展函数,所以 bundle.get(key) 命中的始终是 Bundle 自己的方法,扩展函数体一次都不会执行(编译器还会给 shadowed 警告),Intent.getExtra 是同一个毛病。这段想表达的类型化取回必须写成 Bundle.parcelable(key) / Intent.parcelableExtra(key) 这类不撞名的名字才真正可用。
这题在面试里怎么问、怎么答
"为什么泛型要擦除?"答:JVM 的字节码层面没有泛型信息(Java 5 引入泛型时为了向后兼容已有字节码与库),擦除带来的好处是不需要为每个泛型实例生成独立的类(否则 List<String> 与 List<Integer> 各一份字节码),代价就是运行期拿不到类型。Kotlin 沿用了 JVM 的这套机制。
"reified 是怎么实现的?"答:不是真的"恢复"了泛型,而是在编译期把类型参数替换成调用点的实际类型。因为 inline 函数会被展开到每个调用点,编译器在展开时已经知道 T 是什么,于是直接替换。于是"reified"更准确的含义是"在调用点被具体化的类型参数"。
"T::class.java 和 T::class 有什么区别?"答:前者是 java.lang.Class<T>,Java 反射与 Gson/Jackson 需要它;后者是 kotlin.reflect.KClass<T>,Kotlin 反射 API 用它,且它还能访问属性名等信息。做 JSON 序列化必须用 .java,用 KClass 会类型不匹配。
"使用中遇到过什么问题?"案例一:一个 JSON 工具类用 reified 写死了 T::class.java,某次需要解析一个由配置下发类名的模块,编译期拿不到类型;修复为补一个接收 Class<T> 的重载。案例二:把 reified 泛型扩散到嵌套的内层泛型函数里,Release 后 dex 增长明显;修复为只在外层做一次类型判断,内层改用普通泛型。
再补一个工程上值得讲清的一点:Android 上 reified 最实用的两个落点,是 Intent/Bundle 的类型化取回与 JSON 反序列化。 前者解决"取回时类型被抹平成 LinkedHashMap"的老问题(API 33 之后的 getParcelableExtra(key, Class) 重载本质就是把 Class 显式传出来,与 reified 等价);后者解决"解析结果不知道该转成什么类型"。写这类扩展有个容易踩的规则:扩展函数名不能和宿主类的成员函数同名(Kotlin 里成员函数永远胜过同名扩展函数),Bundle.get 已有 get(String) 这个成员,把扩展命名成 get 就永远不会被调用——这条也是面试里"你踩过哪些 Kotlin 编译期坑"的高频素材。写这类扩展有个容易踩的规则:扩展函数名不能和宿主类的成员函数同名(Kotlin 里成员函数永远胜过同名扩展函数),Bundle.get 已有 get(String) 这个成员,把扩展命名成 get 就永远不会被调用——这条也是面试里"你踩过哪些 Kotlin 编译期坑"的高频素材。但要注意 AndroidX 已经提供了显式类型 API(getParcelableExtra(String, Class<T>)、SavedStateHandle.get<T>(key)),能用官方 API 就不必自己写 reified 扩展——官方 API 有明确的类型检查与文档,比扩展函数更可靠。这条"先查官方库再造轮子"的判断,面试里能体现工程成熟度。
给正在准备面试的你
把这题画成一张"擦除 vs reified"的对照图:上半部分画普通泛型的执行路径——调用方 parse<User>(json) → 编译器把 User 擦成 Object → 函数体里的 T::class 无处可寻 → 报错;下半部分画 reified 的执行路径——inline 展开到调用点 → 编译器把 T 替换成 User → User::class.java 成功生成 → 反射正常。图右侧加一个竖排的三条限制清单:"只能用在 inline 函数"、"每个调用点一份展开(dex 成本)"、"不能用于运行期才知道的类型"。图下方写工程结论:"能显式传 Class 就用 Class;AndroidX 有官方 API 就用官方 API。"这张图覆盖了本篇的考点。
再补工程案例与踩坑——应用落点是清点项目里的 reified 扩展,把"需要运行期类型"的调用点改用 Class<T> 重载;给混用 reified 与反射的 JSON 工具补两套 API;把过度扩散的 reified 收敛到最外层;确认 Intent/Bundle 取回优先用 AndroidX 的显式类型重载而非自定义扩展。
复习时别孤立刷题:inline 内联函数——reified 是 inline 的三大能力之一(另两个是非局部 return 与消除分配),前提都是"内联展开"。
划两句重点:泛型擦除导致 x is T / T::class / 泛型数组创建在普通函数里不可用;reified 只能配 inline,靠"在调用点把 T 替换成实际类型"生效;T::class.java 给 Java 反射/Gson,T::class 给 Kotlin 反射;运行期才知道类型就用 Class<T> 重载;reified 别扩散,避免 dex 膨胀。
下一篇聊 作用域函数五兄弟:let、run、with、apply、also——沿着今天这条主线继续往前走。
如果这篇文章对你有帮助,欢迎点赞、在看、转发三连。你的支持就是这个系列持续更新的动力。
「Android软件开发面试·从入门到精通」连载系列
上一篇:inline-内联函数:高阶函数零开销的秘密
下一篇预告:作用域函数五兄弟:let、run、with、apply、also
有任何问题欢迎在评论区留言交流。