Java 泛型
泛型 Genrics
Java 泛型是在jdk5引入的新特性。它指定了接收参数的类型,避免了调用者乱传参,保留了代码的通用性和独特性。
泛型类和泛型方法
一般使用大写字母声明泛型,例如<T>
类型擦除 Type erasure
思考:能否插入一个字符串元素到一个整型数组中?
答案:通过反射是可以的,原理就是类型擦除。
add(E e)
方法在字节码文件中显示的是add(Object o)
什么是类型擦除?
Java泛型机制是在编译级别实现的,通过编译后,编译器就会将泛型给擦除掉。到了运行阶段时,对于JVM来说便没有了泛型类型的对象,其中的泛型参数都会被替换成它的第一个上界或Object,这个过程称为泛型擦除。
为什么要有类型擦除?
这是为了兼容jdk5以前的代码。
Java 泛型弊端
较之其他语言,Java的泛型存在的弊端有:
- 不支持基本类型:泛型最终会被擦除成具体类型,而Object不能存储基本数据类型
- 只有原始类型的class:只能对原始类型进行检测,无法判断带泛型的类型(jdk17中可以判断)
- 不能实例化泛型参数:因为运行时无法确定具体类型
- 不能实例化泛型数组:类型擦除会让实例化的数组成为Object[]
变型 Variant
协变 Covariant
如果B是A的子类,F(B)也是F(A)的子类。
逆变 Contravariant
如果B是A的子类,F(B)是F(A)的父类。
不变 Invariant
如果B是A的子类,F(B)和F(A)无关。
在Java中,泛型默认是不变的;而数组支持协变。那么是否可以让泛型支持逆变和协变呢?答案是可以的。
泛型的协变和逆变
协变和上界
使用通配符?
和关键字extends
,定义泛型的上界,来实现协变。
可以想象,协变存在类型安全隐患,泛型为了杜绝这种隐患采取一刀切的方式:只要声明了上界,就不允许插入null以外的元素。
逆变和下界
使用通配符?
和关键字super
,定义泛型的下界,来实现逆变。
PECS原则
作为元素的生产者Producer
,要用extends
声明上界支持协变,以便获取元素;
作为元素的消费者Consumer
,要用super
来声明下界支持逆变,以便新增元素。
应用
一个典型的应用是Collections
类的copy
方法:
- 协变适用于只读不写的场景。
- 逆变适用于只写不读的场景。
- 既要读有要写时,使用不变。
桥接方法
泛型方法重写
重写父类泛型方法时,在子类方法的入参类型和父类不一致,似乎违背了重写的原则(返回类型,方法名,参数列表都相同),但依然重写成功。
这是因为编译时生成了桥接方法setValue(Object)
去调用子类方法setValue(String)
。
子类方法重写
顺便一提,当子类重写方法的返回类型是父类方法返回类型时的子类时,编译器也会自动生成桥接方法。