reified 如何骗过 JVM 类型擦除

上一篇文章中,我们深入探讨了 inline 关键字如何通过代码内联消除函数调用开销,带来性能优化和控制流增强。

趁热打铁,让我们继续探索 Kotlin 泛型世界中另一个强大的特性------reified 关键字。

如果说 inline 是一把打开编译时代码优化大门的钥匙,那么 reified 就是这扇门后最耀眼的宝藏之一。它与 inline 紧密配合,在类型擦除的迷雾中为我们开辟出一条清晰的路径,让泛型类型信息在运行时重获新生。

简单来说,reified 关键字搭配内联函数使用,可以在运行时拿到泛型函数的实际类型参数。

通常 Java 和 Kotlin 因为类型擦除,泛型信息在运行时就没了------但 reified 让你能在 inline 函数里把这个信息留住。

函数标记为 inline 后,Kotlin 编译器会直接把函数代码替换到调用处。如果类型参数声明为 reified,编译器在内联过程中就会把真实的类型信息一并带过去,这样运行时就能访问和操作这个类型了。

kotlin 复制代码
inline fun <reified T> isType(value: Any): Boolean {
    return value is T
}

有了这个,你可以直接用 isType 检查一个对象是否属于某个类型,不需要显式类型转换,也不用手动写类型检查。

聪明的你一定能想到其原理!

主要使用场景

reified 最常见的用途是简化那些在运行时需要类型信息的泛型场景------反射、类型转换、创建类实例都属于这类。

比如取类类型的时候,reified 省掉了显式传 Class 对象的麻烦:

kotlin 复制代码
inline fun <reified T> printClassName() {
    println(T::class.java.name)
}

printClassName<String>()  // 输出: java.lang.String

优势

在 Kotlin 里写泛型代码时,在内联函数里用 reified 能解锁一些因为类型擦除而没法用的能力。核心优势有三点:

  1. 类型安全:减少了未检查的类型转换(unchecked cast),代码更安全。
  2. 语法更干净 :不用再显式传 Class 对象当参数了。
  3. 运行时反射更方便:检查类型、访问类元数据这类操作做起来更轻松。

限制

reified 只能用在 inline 函数上------因为它依赖内联来在运行时保留类型信息,所以在内联以外的通用泛型场景里没法用。

当然,更深层次的原因其实是 JVM 的类型擦除:泛型类型参数在运行时不会被保留。

正常情况下,像 if (value is T) 这样的类型检查,或者 T::class 这样的类元数据访问,都做不了。

此时,只要把函数标记为 inline、把类型参数标记为 reified,Kotlin 编译器就会把函数调用替换成实际代码,同时把真实类型嵌入编译输出------于是类型安全的反射、简化的泛型 API 这些强大功能就都能用了。

虽然很强大,但 reified 要慎用。

它要求函数必须是 inline,对函数设计加了约束;过度使用还会导致代码膨胀。

不过在工具函数或框架代码里------这些地方需要灵活性和类型内省------reified 比起用 Class<T> 的那些冗长又容易出错的写法,确实是个更干净的方案。

举个例子,我们用 reified 可以根据类型过滤列表中的元素:

kotlin 复制代码
inline fun <reified T> List<Any>.filterByType(): List<T> {
    return this.filterIsInstance<T>()
}

val mixedList = listOf(1, "Kotlin", 2.5, "Programming")
val stringList = mixedList.filterByType<String>()

println(stringList)  // 输出: [Kotlin, Programming]

filterByType 通过 reified 保留了类型信息,类型特定的过滤就这样实现了。

总结

reified 关键字让你在内联函数里能在运行时访问泛型类型信息。它简化了基于类型的操作,代码更安全也更干净,特别适合需要运行时反射或类型操作的场景。不过它限于内联函数,没法用在通用泛型编程中。

进阶:字节码

又到了我们最喜欢的字节码环节。

JVM 有一个叫类型擦除的概念------它在运行时把泛型类型信息全部去掉。

也就是说,List<String>List<Int> 编译到字节码里都变成了原始的 List。如果你要写一个在运行时需要知道自己泛型类型的函数,比如检查 value is T,这就成了问题。

注意这里的意思,我说的是在运行的时候知道泛型的类型(即 T),而不是具体实例的类型。

Kotlin 的带 reified 类型参数的内联函数是应对这个 JVM 限制的编译时方案。

分析一下反编译的字节码就能发现:这特性并不是什么新的 JVM 指令,而是 Kotlin 编译器玩的一个巧妙把戏。

先看一个经典例子------检查一个值是否是某个泛型类型 T 的实例:

kotlin 复制代码
// 没有 'inline' 和 'reified' 这个函数根本写不出来
inline fun <reified T> isInstanceOf(value: Any): Boolean {
    return value is T
}

fun main() {
    val myString: Any = "Hello, Kotlin!"
    val myInt: Any = 123

    // 调用点 1
    val isString = isInstanceOf<String>(myString)
    println("Is it a String? $isString")

    // 调用点 2
    val isInt = isInstanceOf<Int>(myString)
    println("Is it an Int? $isInt")
}

这段代码里我们居然能用 T 做类型检查(is T),正常情况下因为类型擦除这应该是编译错误。

编译后反编译成 Java 字节码,你会发现:isInstanceOf 的函数调用完全消失了。编译器把函数体直接复制进了 main 函数,同时把 T 替换成了实际类型(StringInteger)。

java 复制代码
import kotlin.jvm.internal.Intrinsics;
import org.jetbrains.annotations.NotNull;

public final class TypeCheckerKt {

    // 'isInstanceOf' 函数本身可能甚至不会出现在反编译代码中
    // 如果只在本文件内部使用的话------因为它完全内联了。
    // 即使是 public 的,函数体会在这里,但调用仍然会被内联。
    
    public static final void main() {
        Object myString = "Hello, Kotlin!";
        Object myInt = 123; // 自动装箱为 Integer

        // --- 调用点 1 (isInstanceOf<String>) ---
        // 'isInstanceOf' 的函数体被复制到这里。
        // 泛型 'T' 被替换成实际类 'String'。
        Object value$iv = myString;
        boolean isString = value$iv instanceof String;
        System.out.println("Is it a String? " + isString);

        // --- 调用点 2 (isInstanceOf<Int>) ---
        // 'isInstanceOf' 的函数体再次被复制到这里。
        // 泛型 'T' 被替换成实际类 'Integer'。
        Object value$iv2 = myString;
        boolean isInt = value$iv2 instanceof Integer;
        System.out.println("Is it an Int? " + isInt);
    }

    // ... main(String[] args) 模板代码 ...
}
  1. inline 机制(代码复制)inline 关键字是魔术的第一步。它告诉编译器别生成标准的方法调用------而是在每个调用 isInstanceOf 的地方,把函数体(return value is T)直接粘贴到调用代码里(这里就是 main 函数)。所以反编译出来的 Java 字节码里根本看不到 isInstanceOf(...) 的调用。

  2. reified 机制(类型替换)reified 关键字才是让运行时类型访问成为可能的核心。它告诉编译器:"内联函数体的时候,必须把泛型类型参数 T 替换成调用处传入的实际类。"

    调用点 1 的调用是 isInstanceOf<String>(...)。编译器看到 TString,内联时就把 is T 替换成 is String(Java 里就是 instanceof String)。

    调用点 2 的调用是 isInstanceOf<Int>(...)。编译器看到 TInt(在 JVM 上对应 Integer),就把 is T 替换成 is Integer(Java 里就是 instanceof Integer)。

类型替换发生在编译时,所以字节码生成的时候泛型 T 已经没了------取而代之的是 JVM 在运行时能检查的具体、真实的类型。

这等于绕过了 JVM 的类型擦除。

这段反编译代码也解释了为什么 reified 类型参数只能用在内联函数上:整个机制依赖于编译器能拿到调用处使用的具体类型(StringInt 等),然后把它粘贴进生成的代码。

如果函数没有被内联,它就得编译成一个单一的泛型字节码版本,可以被任何地方调用------那样 T 就会被擦除,编译器在运行时就根本不知道 T 应该是什么了。

内联,才是 reified 小把戏的根基!

相关推荐
硬件学长森哥1 小时前
成像技术系列-3A算法基础
android·图像处理·计算机视觉
唔661 小时前
Android在局域网中搭建 MQTT服务器 协议V3.1.1
android·运维·服务器
2601_957418802 小时前
Android 手机如何通过 PTP / MTP 连接单反相机?源码级方案分享
android·数码相机·智能手机
阿巴斯甜11 小时前
ARouter
android
Andya_net12 小时前
MySQL | MySQL 8.0 权限管理实践-精确赋予库、表只读等权限
android·数据库·mysql
阿巴斯甜13 小时前
Map
android
巫山老妖13 小时前
鹅厂十年:三段式技术成长复盘
android·人工智能·程序员
阿巴斯甜13 小时前
List集合
android
ooseabiscuit14 小时前
Laravel6.x核心优化与特性全解析
android·开发语言·javascript