Kotlin 16:值类和 reified 保留了哪些类型信息
submitValidated(store, "alpha", "task-1") 两个参数都是字符串:如果调用方把任务 ID 和文本调换,编译器无从发现。给 ID 做一层值类后,可以要求调用处明确提供 TaskId。另一个疑问来自 15 的类型擦除:内联函数写了 reified,能否就此识别 List<String> 中的实际元素?本章用同一任务系统的可控调用、预期编译失败及当前构建的 javap 结果回答这两个问题,不根据一次生成物宣称未来所有 Kotlin 编译器都产生同样的 ABI。
实验版本仍为 Kotlin/JVM 2.2.21、JDK 21、Gradle 8.14;编译器上游 tag v2.2.21 对应 SHA 2146684dcba708e5a304758b41a9e4ec9c7eff71,冻结信息见仓库 writing-plans/kotlin/VERSIONS.md。官方 inline 文档与 值类表示文档解释语义边界;本篇只读了生成字节码,尚未跟踪编译器 lowering 内部源码,后续 36 再补符号级追踪。
构造 TaskId 后,错误从哪里转移
任务提交的已有核心函数仍接受 String;在边界上添加一条强类型入口,不一下子改写旧 CLI 的参数解析:
1 | |
测试表明 TaskId(" ") 在构造阶段抛出 IllegalArgumentException;TaskId("task-1") 经 submitTyped 得到 Accepted,内存 Store 查到文本 alpha,相同 ID/文本重试得到 Reused。而隔离源码 submitTyped(store, "raw-id", "alpha") 在冻结编译器下退出 1,给出 Argument type mismatch: actual type is 'String', but 'TaskId' was expected.。强类型接口防止这条调用边界上的原始字符串误传;TaskId.value 仍是字符串、旧入口仍对外可用,也没有新增全局唯一性或持久化保证。值类的校验只拒绝空白,不会自动规范化空格或赋予权限。
值类在这次 JVM 编译里怎样呈现
TaskId 测试还把同一个 ID 赋给 Any,boxedAsAny is TaskId 为 true,assertEquals 得到原值。对当前产物执行 javap -p -c,可在 TaskId 中看到名为 box-impl 与 unbox-impl 的生成方法;TypedTaskIdKt 中本次生成的是 submitTyped-W6ZOYFM(TaskStore, String, String)。这里的第二个参数在 JVM 描述中是底层 String,而非源码接口表面上的 TaskId 类。
这组观察只针对本地编译器与该参数形状。官方文档说明值类在需要泛型、接口或可空包装等情况下可以装箱;测试里的 Any 正是需要通过普通对象引用访问的场景。存在 box-impl 不等于每次调用都产生新对象;看到某个参数的底层类型为 String,也不等于运行中没有分配。本文没有内存分析器、分配计数、JMH 结果或 Java 消费者源码测试,不应从 javap 直接推导性能收益或跨版本二进制兼容性。正式 Java 消费入口在 20、性能测量在 37 单独验收。
reified 能识别哪一层类型
没有内联的泛型函数若写 value is State,其中 State 是类型参数,隔离编译器拒绝:Cannot check for instance of erased type 'State (of fun <State : TaskState> erasedStateCheck)'.。将函数声明为 inline 并给类型参数加 reified,调用处可以利用实际实参做可检查的外层类型判断:
1 | |
用 listOf(Queued, Failed("disk")) 调用 firstStateOfType<TaskState.Failed> 命中失败状态原对象,询问 TaskState.Succeeded 则得到 null。但 isElementType<List<String>>(listOf(123)) 实际为 true:检测到的是外层 List,不是逐个检查里面是否为 String。对应测试类的 javap 调用点出现 instanceof TaskState$Failed 及 instanceof java/util/List,而下一条逐元素 all { it is String } 返回 false。reified 解决调用点外层实参用于部分类型检查的问题,不承诺原先已经擦除的嵌套类型参数能在运行时重新出现。对未知列表仍按 15 的模式逐项验证。
复跑与练习
仓库运行 bash examples/kotlin-language/verification/16.sh 可得到 00–16 十七组 JUnit 回归、CLI 正反例、正常/错误类型的隔离编译记录与 javap 原始输出。旧 CLI 尚未接入 submitTyped,仍只接受文本;Result 包装也不代表任务工作者完成。生成的代码本次有 box-impl 与带后缀的 submitTyped-W6ZOYFM,将来换版本必须重新检查,不应该在 Java 代码里直接假设这个内部符号不变。下载含Wrapper和原始证据的累计工程(SHA-256校验)可解包离线复跑。
练习:若封装一个 inline fun <reified T> matches(value: Any) = value is T,对 listOf(123) 调 matches<List<String>>() 能防止把整数误当字符串吗?本次实验结果不能;若下游要执行字符串操作,必须在边界逐个检查每个元素。若 TaskId("task-1") 被装箱到 Any,函数签名里的 TaskId 静态保护是否就此失效?Any 边界会抹去调用方可用的类型信息;回到 submitTyped 仍需静态 TaskId 值,裸 String 不能通过编译,不能拿 Any 的观察代替源码签名约束。
[PATTERN] 把“源码层是否禁止错传”“调用点能检查哪层泛型类型”“当前 JVM 如何表示这个值”分成三张账:先保住接口语义,再实际编译反例,最后看特定版本生成物。三张账的证据强度不能互相替代。
| 边界 | 本篇已验证 | 本篇未证实 |
|---|---|---|
TaskId / submitTyped |
构造校验、原始 String 调用编译拒绝 | 全系统 ID 唯一、旧 CLI 已替换 |
reified + is List<String> |
外层 List 为 true,内层元素仍可能错误 | 泛型嵌套值被自动检查 |
javap 的 box-impl 与参数类型 |
当前版本生成物实际存在 | 每次调用分配数、未来 ABI 保证 |
下一篇以小型任务构造 DSL 检查多个接收者互相遮蔽的问题,配合显式函数版本作对照。





