[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"$fCqa0GA-HZ2zPaOhtgwGkDdKNpWUauFA_KTXX-D-vss4":3},{"item":4,"related":48},{"id":5,"type":6,"title":7,"slug":8,"summary":9,"body":10,"coverUrl":11,"productScreenshots":12,"productLinks":13,"authorName":14,"authorUrl":15,"authorSubject":16,"category":17,"tags":22,"sourceLabel":39,"sourceName":40,"sourceUrl":41,"status":42,"seoTitle":7,"seoDescription":9,"canonicalUrl":39,"isFeatured":43,"sno":44,"sortOrder":45,"publishedAt":46,"updatedAt":47,"createdAt":47},"cd4e2aae-cbf2-4428-aab0-a6fa7911a424","article","Type Erasure：泛型信息为什么运行时消失了？","type-erasure-java-generics-explained","Java 的类型擦除让泛型主要承担编译期检查，生成字节码时把参数化类型替换为上界，并插入必要的转换。本文解释它带来的兼容性优势、反射限制，以及它和 Rust 单态化路线的区别。","你在 Java 里写了 `List\u003CString>`，运行时却常常只能看到一个普通的 `List`。这不是编译器忘记了类型，而是 Java 采用了 Type Erasure，也就是类型擦除：泛型主要用于编译期检查，生成字节码时，很多参数化类型信息会被移除或替换成它的上界。\n\n## 编译器具体做了什么\n\n对于无界类型参数，编译器通常用 `Object` 替换；如果类型参数有上界，就用上界替换。需要取出具体类型时，编译器会插入必要的类型转换。某些继承场景还会生成 bridge method，以维持多态调用的表现。\n\n结果是，`List\u003CString>` 和 `List\u003CInteger>` 不会在运行时变成两套全新的类。它们共享泛型声明对应的字节码，这降低了运行时开销，也让泛型代码能和早期没有泛型的 Java 代码保持兼容。\n\n## 代价是什么\n\n类型擦除让运行时无法直接知道某些参数化类型。例如，你不能简单地判断一个对象是否是 `List\u003CString>`，因为 `String` 这一层信息已经不在普通运行时类型里。反射、序列化和泛型数组因此需要额外技巧，例如显式传入 `Class\u003CT>`，或保存一个类型令牌。\n\n擦除还解释了为什么某些错误直到取值时才出现：如果代码通过原始类型绕过了编译器检查，编译器可能在读取位置插入转换，真正不匹配时才抛出异常。\n\n## 和 Rust 的路线有什么不同\n\nJava 更重视兼容既有 JVM 和旧代码，于是采用共享字节码与类型擦除；Rust 默认倾向于对每个具体泛型实例做单态化，换来更直接的机器代码和静态分发。两种方案都合理，只是把成本放在不同地方：一个牺牲部分运行时类型信息，一个可能增加编译时间和二进制体积。\n\n## 读者应该记住\n\nType Erasure 的关键词是“编译期知道，运行时不一定保留”。它解释了 Java 泛型的兼容性优势，也解释了反射、序列化和泛型数组中的许多限制。\n\n资料：[Oracle：Type Erasure](https:\u002F\u002Fdocs.oracle.com\u002Fjavase\u002Ftutorial\u002Fjava\u002Fgenerics\u002Ferasure.html)；[Java 语言规范 4.6](https:\u002F\u002Fdocs.oracle.com\u002Fjavase\u002Fspecs\u002Fjls\u002Fse26\u002Fjls26.pdf)","\u002Fuploads\u002F2026-09-20\u002Fad612fe0-a1dd-4c46-a5dc-2ddfedefb22d.jpg",[],[],"Foundit","https:\u002F\u002Ffoundit.cn","foundit-ai-editorial",{"id":18,"name":19,"slug":20,"description":21},"6179d3b6-dc34-4483-9ded-3cd9f1b37a47","科普","abbreviation","介绍各领域新兴概念",[23,27,31,35],{"id":24,"name":25,"slug":26},"144abe77-0dc6-4f66-a176-20bddb1c0bfa","编程","coding",{"id":28,"name":29,"slug":30},"a202d639-99a6-488a-a712-4d4c6ffd7e15","开发","dev",{"id":32,"name":33,"slug":34},"7c76bfc2-f80f-4ee0-a95d-27bd8708b434","技术","slug",{"id":36,"name":37,"slug":38},"4c2bbea6-eab7-40a8-8447-1de478ff7749","分析","analyse",null,"Oracle：Type Erasure","https:\u002F\u002Fdocs.oracle.com\u002Fjavase\u002Ftutorial\u002Fjava\u002Fgenerics\u002Ferasure.html","published",false,46,0,"2026-09-20T00:00:00.000Z","2026-09-20T03:57:57.276Z",[49,57,65],{"id":50,"type":6,"title":51,"slug":52,"summary":53,"coverUrl":54,"authorName":14,"sno":55,"publishedAt":46,"createdAt":56},"d0700249-defa-43a1-a66a-4455c6889072","ABI：为什么源码能编译，二进制却不能互相调用？","application-binary-interface-abi-explained","ABI 是二进制世界的调用合同，规定参数传递、对象布局、符号命名和异常处理。本文区分 ABI 与 API，解释动态库、C++ 兼容性和跨语言绑定为什么不能只看函数签名。","\u002Fuploads\u002F2026-09-20\u002F33666a48-4e8f-42a3-9ffd-f4c490498689.jpg",42,"2026-09-20T03:58:01.058Z",{"id":58,"type":6,"title":59,"slug":60,"summary":61,"coverUrl":62,"authorName":14,"sno":63,"publishedAt":46,"createdAt":64},"c9f4e936-9533-4b9c-a1de-f03ef09fed37","WAL：为什么数据库要先写日志，再写真正数据？","write-ahead-logging-wal-database-explained","WAL 要求描述数据变化的日志先于数据页持久化，让数据库可以延迟刷写并在崩溃后通过重放恢复。本文用账本和收据解释 REDO、检查点、复制与持久性设置的关系。","\u002Fuploads\u002F2026-09-20\u002F6260c953-345b-4c9d-b3d6-6202c7e5539c.jpg",43,"2026-09-20T03:58:15.082Z",{"id":66,"type":6,"title":67,"slug":68,"summary":69,"coverUrl":70,"authorName":14,"sno":71,"publishedAt":46,"createdAt":72},"41d50774-50df-4e2f-b1b3-ab6d9f329726","Backpressure：生产者太快时，系统怎样不被数据淹没？","backpressure-reactive-streams-explained","Backpressure 让下游处理能力反过来影响上游生产速度，避免异步流水线靠无限缓存硬撑。本文用水管和阀门解释响应式流、需求信号、数据丢弃与容量设计，也说明它和普通限流的区别。","\u002Fuploads\u002F2026-09-20\u002F35c47792-3d4b-46ae-9375-c68e4c53330e.jpg",45,"2026-09-20T03:58:08.835Z"]