泛型
继承关系如上,其中Container为泛型容器
泛型参数命名约定
类型参数一般使用一个大写的字母表示,经常使用的类型参数的名称有
- E: Element(广泛的用于Java Collection中)
- K: Key
- V: Value
- N: Number
- T: Type
- S,U,V: 第2, 3, 4个类型参数
泛型擦除的具体事实
- 虚拟机没有泛型,只有普通的类和方法
- 所有类型参数都用他们的限定类型替换
- 桥方法被合成来保持多态
- 为保证类型安全性,必要时插入强制类型转换
优缺点
优点:
- 由于泛型擦除的原因,相较于真泛型的语言(C#、C++)来说,方法区只需要加载较少字节码,内存负担较少
- 由于java1.5才开始支持泛型,相对于其它语言对旧版本代码兼容性更好
缺点:
- 基本类型无法作为泛型实参,会有装箱拆箱的额外消耗
- 泛型无法当作真实存在的类型进行处理,如数组
new T[]
、新建泛型对象new T()
、方法重载等【注:C#的List和List 是不同的类型】 - 在实际使用中需要类型强转,有运行时的开销,字节码:CHECKCAST ….
- 泛型信息是通过字节码注释实现的,混淆时不做keep的话会被清除掉
字节码是如何记录泛型信息的
在编译字节码时会在字节码上打上signature
注释和declaration
注释
可以通过相关的反射代码进行获取
注意:混淆时会把这些注释给清除,代码上需要用到这些信息时需要在混淆文件中keep住:-keepattributes Signature
在Retrofit这个库中有使用到
骚操作
构建泛型数组
直接通过new数组的方式是不允许的
1 | T[] ts = new T[10]; // Error |
一种简单实现方式
1 | public static <T> T[] minmax(T... t) { |
java8起的一种方式
1 | public static <T> T[] minmax(IntFunction<T[]> constr, T... t) { |
java8之前的方式
1 | public static <T> T[] minmax(T... t) { |
这也是内置方法Arrays.copyOf()
实现的一种方式
消除对受查异常的检查
如果一个方法抛出受查异常,外部调用这个方法需要try或者继续向外出,如下test方法
1 | public static void test() throws FileNotFoundException { |
可通过泛型中间方法, 这样外部调用test方法时不用检查该异常,即不用try
1 | // 中间方法 |
Java和Kotlin中泛型的协变、逆变和不变
前沿
- 什么是泛型的型变(协变、逆变、不型变)
- 为什么需要泛型的型变
- Java和Kotlin分别是如何处理泛型型变的
概念
型变是指我们是否允许对参数类型进行子类型转换
假设Apple类是Fruit类的子类,Container
- invariance(不型变):也就是说,Container
和 Container 之间没有关系 - covariance(协变):也就是说,Container
是 Container 的子类型 - contravariance(逆变):也就是说,Container
是 Container 的子类型
注意:
- 上面在解释协变、逆变概念时的说法只是为了帮助理解,这种说法对于Java而言并不准确。在Java中,Container
和 Container 永远没有关系,对于协变应该这么说, Container 是 Container<? extends Fruit> 的子类型,逆变则是,Container 是 Container<? super Apple> 的子类型。 - 子类(subclass) 和 子类型(subtype)不是一个概念,子类一定是子类型,子类型不一定是子类,例如,Container
是 Container<? extends Fruit> 的子类型,但是Container 并不是 Container<? extends Fruit> 的子类。
Java的默认做法
Java中的泛型类在正常使用时是不型变的,要想型变必须在使用处通过通配符进行(称为使用处型变)。
1 | Container<Apple> apple = new Container<>(); |
禁止这么做主要目的是为了保证运行时的类型安全。
Java的协变做法
采用上界通配符extends,允许Container
1 | Container<Apple> apple = new Container<>(); |
之所以允许调用生产者方法,是因为能够明确知道返回的是Fruit类型,但是消费者方法编译器并不知道它的具体类型是什么,有可能是Apple转型得到的,也有可能是Orange转型得到的,为了保证类型安全,编译器拒绝任何消费者方法。使得像<? extends Fruit>成为单纯的“生产者”
但是这里产生了一个问题,类似于ListArray的contains方法,我们能明确知道内部的逻辑不会修改List中的对象,但是编译器还是拒绝了该方法的正常使用,所以只能写成boolean contains(Object o)
的形式。不过没关系,Kotlin能很好的解决。
Java的逆变做法
采用下界通配符super,允许Container
1 | Container<Object> obj = new Container<>(); |
允许向消费者方法传入Fruit及其子类,是因为编译器知道,Fruit及其子类一定属于Fruit或者Fruit的父类,与协变相反的是,由于编译器不知道是Plant类型转型得到的,还是Food转型得到的,甚至是Object转型得到的,所以调用生产者方法只能返回Object类型。使得像<? super Fruit>成为单纯的“消费者”
Java型变总结
extends限定了通配符类型的上界,所以我们可以安全地从其中读取;而super限定了通配符类型的下界,所以我们可以安全地向其中写入。
1 | // Collections copy方法的泛型使用 |
Kotlin的做法
Kotlin处理型变的做法概括起来是:Kotlin中的泛型类在定义时即可标明型变类型(协变或逆变,当然也可以不标明,那就是不型变的),在使用处可以直接型变(称为声明处型变)。因为Kotlin与Java是100%兼容的,你自己在Kotlin中定义的泛型类当然可以享受声明处型变的方便,但是,如果引入Java库呢?又或者你自己在Kotlin中定义的泛型类恰好是不型变的,然而你又想像Java那样在使用处型变,该这么办呢?Kotlin使用一种称为 类型投影(type projections) 的方式来处理这种型变。这种方式其实跟Java处理型变的方式类似,只是换了一种说法,还是使用处型变。
Kotlin的协变
1 | //kotlin |
使用out修饰符,表明类型参数 T 在泛型类中仅作为方法的返回值,不作为方法的参数,因此,这个泛型类是个协变的。回报是,使用时Source
上面有提到,Java的协变中,向不会修改内容的方法contains声明参数为泛型T会无法使用。在Kotlin中能够通过注解声明的方式解决
1 | //kotlin |
使用注解 @UnsafeVariance
可以让编译器放我们一马,它是在告诉编译器,我保证这个方法不会向泛型类写入数据,你放心。
Kotlin中的逆变
1 | //kotlin |
使用in修饰符,表明类型参数 T 在泛型类中仅作为方法的参数,不作为方法的返回值,因此,这个泛型类是个逆变的。回报是,使用时Comparable
注意:以上所说的in/out修饰符对于类型参数 T 的限制,仅适用于非private(public, protected, internal)函数,对于private函数,类型参数 T 可以存在于任意位置,毕竟private函数仅用于内部调用,不会对泛型类的协变、逆变性产生影响。还有一点例外就是,如果类型参数 T 标记为out,我们仍可以在构造函数的参数中使用它,因为构造函数仅用于实例化,之后不能被调用,所以也不会破坏泛型类的协变性。
扩展-堆污染
Heap pollution(堆污染), 指的是当把一个不带泛型的对象赋值给一个带泛型的变量时, 就有可能发生堆污染。
由于定义带泛型变量时并不强制指定泛型类型, 因此如果借此发生狸猫换太子的操作的话, 那么就会导致堆污染. 堆污染在编译时并不会报错, 只会在编译时提示有可能导致堆污染的警告. 在运行时,如果发生了堆污染, 那么就会抛出类型转换异常。
场景一
1 | // 定义时未指定泛型类型 |
场景二
java语言不允许创建泛型数组,所以当可变参数为带泛型的可变数组时,方法内只能用不带泛型的数组接收
1 | public static void method(List<String>... stringLists) { |
忽略堆污染警告
在方法上进行注解声明,让编译器不提示警告
@SuppressWarnings("unchecked")
:取消所有警告@SafeVarargs
:Java7 专门用来抑制堆污染(Heap pollution)警告提供的注解