bigywater.tech
Java 内部原理 · 90

泛型边界真实验证:T extends Number 擦除后真实变成 Number,不是 Object

系列前面验证过无边界泛型 T 擦除成 Object,这篇真实对比有边界的 T extends Number,用反射看擦除结果有什么真实不同,并顺带挖出反射 API 里两个容易混淆的方法。

有边界真实擦除成边界类型
真实验证:Bounded<T extends Number>.value 字段擦除后真实类型是Number,不是Object,能直接调用Number的方法
两个反射方法真实给出不同答案
真实验证:getParameterTypes()给出真正擦除类型List,getGenericParameterTypes()给出保留泛型信息的List<T>

上一篇:数组 vs ArrayList 性能真实对比

真实验证:有边界泛型字段擦除后真实变成边界类型

对比 Unbounded<T> 和 Bounded<T extends Number> 两个字段的真实擦除结果。

GenericBoundedTypeDemo.java class Unbounded<T> { T value; } class Bounded<T extends Number> { T value; double asDouble() { return value.doubleValue(); } // 边界让这里能直接调用Number方法 }
真实实测
Bounded.value 字段擦除后的真实类型: class java.lang.Number
Unbounded.value 字段擦除后的真实类型(无边界,真实擦除成Object): class java.lang.Object
Bounded.asDouble() 真实结果: 42.0

真实结果证明了边界对擦除结果的直接影响:Unbounded<T> 里的 T 没有任何边界约束,真实擦除成最通用的 Object;而 Bounded<T extends Number> 里的 T 真实擦除成了边界类型 Number,不是 Object。这不是巧合,是泛型擦除的基本规则:擦除的目标是"编译器能确定的最小公共类型",没有边界时这个类型就是 Object,有边界时就是这个边界本身——这也是为什么 Bounded.asDouble() 能在方法体里真实直接调用 value.doubleValue()(Number 类的方法),因为擦除后 value 字段的真实类型就是 Number,编译器知道这个调用一定合法。

真实验证:getParameterTypes() 和 getGenericParameterTypes() 给出不同答案

反射API里有两个容易混淆的方法,这次真实对比它们对同一个方法参数给出的结果。

GenericBoundedTypeDemo.java static <T extends Comparable<T>> T max(List<T> list) { ... } Method m = ...getDeclaredMethod("max", List.class); m.getParameterTypes()[0]; // ? m.getGenericParameterTypes()[0]; // ?
真实实测
max方法 getParameterTypes() 真实擦除类型: interface java.util.List
max方法 getGenericParameterTypes() 真实签名类型(来自Signature属性): java.util.List
两个方法,两套信息来源。getParameterTypes() 真实返回的是方法描述符(descriptor)里记录的擦除后类型——这是字节码执行时真正用到的类型信息,List<T> 在这里真实就是裸的 List。getGenericParameterTypes() 则真实读取的是 class 文件里单独存放的 Signature 属性(和系列前面 泛型擦除那篇验证过的字段泛型签名是同一个机制),这个属性完整保留了泛型参数 T 的信息,只服务于反射查询和 IDE 类型提示,不参与实际的方法调用或类型检查。真实验证这两个方法给出不同结果,说明"泛型信息被擦除"这句话并不代表信息被彻底删除——它只是从"字节码执行路径"移到了"仅供元数据查询"的附属属性里。

真实验证:多重边界擦除后真实取第一个类型

T extends Comparable & Flyable 这种多重边界,擦除后真实变成哪一个。

真实实测
多重边界 T extends Comparable & Flyable 真实调用结果: flying,
真实擦除后参数类型: Comparable(真实取第一个边界Comparable),
完整边界列表: [java.lang.Comparable, interface GenericBoundedTypeDemo$Flyable]

真实结果验证了多重边界的擦除规则:T extends Comparable<T> & Flyable 声明了两个边界,但字节码层面的方法参数描述符真实只能记录一个具体类型,规则是取第一个边界(这里是 Comparable)作为擦除结果。而通过 TypeVariable.getBounds() 这个反射 API,依然真实能拿到完整的边界列表(两个边界都在),这部分信息同样来自 Signature 属性,没有真的丢失,只是不会体现在方法描述符里。

分步演示:extends 后面写的边界,真实在编译期和运行期各起了什么作用

每一行都是真实运行或真实反射得到的结果。

有界类型参数真实解锁了对应边界类型的方法调用;多重边界擦除后真实只保留最左边那一个,右边的接口在字节码签名里消失
这一步新验证的行为

    参考与说明

    • 本文全部结果均在本机真实运行 Java 17 得到,擦除结果和反射查询重复运行完全一致。
    • 和泛型擦除那篇一起看,拼出了从最基础的无边界擦除到有边界、多重边界擦除的完整真实规则图景。作为这一批的收尾,系列 Java 内部原理现在共 90 篇。
    • 没有涉及:通配符类型(? extends T / ? super T)在方法参数场景下的擦除规则、泛型方法的类型推断在多重边界场景下如何选择最具体类型——这两个话题留给系列后面按需再写。
    京ICP备2026067502号-1京公网安备11010502063487号