Java泛型

泛型

继承关系如上,其中Container为泛型容器

泛型参数命名约定

类型参数一般使用一个大写的字母表示,经常使用的类型参数的名称有

  • E: Element(广泛的用于Java Collection中)
  • K: Key
  • V: Value
  • N: Number
  • T: Type
  • S,U,V: 第2, 3, 4个类型参数

泛型擦除的具体事实

  • 虚拟机没有泛型,只有普通的类和方法
  • 所有类型参数都用他们的限定类型替换
  • 桥方法被合成来保持多态
  • 为保证类型安全性,必要时插入强制类型转换

优缺点

优点:

  1. 由于泛型擦除的原因,相较于真泛型的语言(C#、C++)来说,方法区只需要加载较少字节码,内存负担较少
  2. 由于java1.5才开始支持泛型,相对于其它语言对旧版本代码兼容性更好

缺点:

  1. 基本类型无法作为泛型实参,会有装箱拆箱的额外消耗
  2. 泛型无法当作真实存在的类型进行处理,如数组new T[]、新建泛型对象new T()、方法重载等【注:C#的List和List是不同的类型】
  3. 在实际使用中需要类型强转,有运行时的开销,字节码:CHECKCAST ….
  4. 泛型信息是通过字节码注释实现的,混淆时不做keep的话会被清除掉

字节码是如何记录泛型信息的

在编译字节码时会在字节码上打上signature注释和declaration注释

可以通过相关的反射代码进行获取

注意:混淆时会把这些注释给清除,代码上需要用到这些信息时需要在混淆文件中keep住:-keepattributes Signature

在Retrofit这个库中有使用到

骚操作

构建泛型数组

直接通过new数组的方式是不允许的

1
T[] ts = new T[10]; // Error

一种简单实现方式

1
2
3
4
public static <T> T[] minmax(T... t) {
return t;
}
String[] minmax = minmax("a", "b", "c"); // 这是允许的

java8起的一种方式

1
2
3
4
5
6
public static <T> T[] minmax(IntFunction<T[]> constr, T... t) {
T[] ts = constr.apply(t.length);
System.arraycopy(t, 0, ts, 0, t.length);
return ts;
}
String[] minmax = minmax(String[]::new, "a", "b", "c"); // 这是允许的

java8之前的方式

1
2
3
4
5
6
public static <T> T[] minmax(T... t) {
T[] ts = (T[]) Array.newInstance(t.getClass().getComponentType(), t.length);
System.arraycopy(t, 0, ts, 0, t.length);
return ts;
}
String[] minmax = minmax("a", "b", "c"); // 这是允许的

这也是内置方法Arrays.copyOf()实现的一种方式

消除对受查异常的检查

如果一个方法抛出受查异常,外部调用这个方法需要try或者继续向外出,如下test方法

1
2
3
4
public static void test() throws FileNotFoundException {
//...
throw new FileNotFoundException();
}

可通过泛型中间方法, 这样外部调用test方法时不用检查该异常,即不用try

1
2
3
4
5
6
7
8
// 中间方法
public static <T extends Throwable> void throwAs(Throwable e) throws T {
throw(T) e;
}
public static void test() {
//...
throwAs(new FileNotFoundException());
}

Java和Kotlin中泛型的协变、逆变和不变

前沿
  • 什么是泛型的型变(协变、逆变、不型变)
  • 为什么需要泛型的型变
  • Java和Kotlin分别是如何处理泛型型变的
概念

型变是指我们是否允许对参数类型进行子类型转换

假设Apple类是Fruit类的子类,Container 是一个泛型类,那么,Container 是 Container 的子类型吗?答案是No。对于Java而言,两者没有关系。对于Kotlin而言,Container可能是Container的子类型,或者其超类型,或者两者没有关系,这取决于Container中的 T 在类Container中是如何使用的。简单来说,型变就是指 Container 和 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
2
Container<Apple> apple = new Container<>();
Container<Fruit> fruit = apple; // java默认禁止此类操作

禁止这么做主要目的是为了保证运行时的类型安全。

Java的协变做法

采用上界通配符extends,允许Container向上转型成为Container的子类型,允许正常使用生产者方法如getT,但消费者方法是不允许的如setT

1
2
3
4
Container<Apple> apple = new Container<>();
Container<? extends Fruit> fruit = apple; // 这是允许的
Fruit t = fruit.getT(); // 取也是允许的
fruit.setT(new Apple());// 存是不允许的

之所以允许调用生产者方法,是因为能够明确知道返回的是Fruit类型,但是消费者方法编译器并不知道它的具体类型是什么,有可能是Apple转型得到的,也有可能是Orange转型得到的,为了保证类型安全,编译器拒绝任何消费者方法。使得像<? extends Fruit>成为单纯的“生产者”

但是这里产生了一个问题,类似于ListArray的contains方法,我们能明确知道内部的逻辑不会修改List中的对象,但是编译器还是拒绝了该方法的正常使用,所以只能写成boolean contains(Object o)的形式。不过没关系,Kotlin能很好的解决。

Java的逆变做法

采用下界通配符super,允许Container向下转型成为Container的子类型,允许正常使用消费者方法,但是生产者方法只能返回Object类型

1
2
3
4
5
6
7
8
9
10
Container<Object> obj = new Container<>();
Container<? super Fruit> fruit = obj; // 这是允许的
Container<Plant> plant = new Container<>();
fruit = plant; // 这是允许的
Container<Food> food = new Container<>();
fruit = food; // 这也是允许的
fruit.setT(new Apple());
fruit.setT(new Orange());
fruit.setT(new Fruit());
Object t = fruit.getT();

允许向消费者方法传入Fruit及其子类,是因为编译器知道,Fruit及其子类一定属于Fruit或者Fruit的父类,与协变相反的是,由于编译器不知道是Plant类型转型得到的,还是Food转型得到的,甚至是Object转型得到的,所以调用生产者方法只能返回Object类型。使得像<? super Fruit>成为单纯的“消费者”

Java型变总结

extends限定了通配符类型的上界,所以我们可以安全地从其中读取;而super限定了通配符类型的下界,所以我们可以安全地向其中写入。

1
2
3
4
// Collections copy方法的泛型使用
public static <T> void copy(List<? super T> dest, List<? extends T> src) {
...
}
Kotlin的做法

Kotlin处理型变的做法概括起来是:Kotlin中的泛型类在定义时即可标明型变类型(协变或逆变,当然也可以不标明,那就是不型变的),在使用处可以直接型变(称为声明处型变)。因为Kotlin与Java是100%兼容的,你自己在Kotlin中定义的泛型类当然可以享受声明处型变的方便,但是,如果引入Java库呢?又或者你自己在Kotlin中定义的泛型类恰好是不型变的,然而你又想像Java那样在使用处型变,该这么办呢?Kotlin使用一种称为 类型投影(type projections) 的方式来处理这种型变。这种方式其实跟Java处理型变的方式类似,只是换了一种说法,还是使用处型变。

Kotlin的协变
1
2
3
4
5
6
7
8
9
//kotlin
abstract class Source<out T> {
abstract fun nextT(): T
}

fun demo(oranges: Source<Orange>) {
val fruits: Source<Fruit> = oranges // 没问题,因为 T 是一个 out-参数,Source<T>是协变的
val oneFruit: Fruit = fruits.nextT() //可以安全读取
}

使用out修饰符,表明类型参数 T 在泛型类中仅作为方法的返回值,不作为方法的参数,因此,这个泛型类是个协变的。回报是,使用时Source可以作为Source的子类型。

上面有提到,Java的协变中,向不会修改内容的方法contains声明参数为泛型T会无法使用。在Kotlin中能够通过注解声明的方式解决

1
2
3
4
5
6
//kotlin
public interface Collection<out E> : Iterable<E> {
...
public operator fun contains(element: @UnsafeVariance E): Boolean
...
}

使用注解 @UnsafeVariance 可以让编译器放我们一马,它是在告诉编译器,我保证这个方法不会向泛型类写入数据,你放心。

Kotlin中的逆变
1
2
3
4
5
6
7
8
9
//kotlin
abstract class Comparable<in T> {
abstract fun compareTo(other: T): Int
}

fun demo(x: Comparable<Number>) {
val y: Comparable<Double> = x // OK!逆变,Comparable<Number>可以作为Comparable<Double>的子类型
y.compareTo(1.0) //1.0 拥有类型 Double
}

使用in修饰符,表明类型参数 T 在泛型类中仅作为方法的参数,不作为方法的返回值,因此,这个泛型类是个逆变的。回报是,使用时Comparable可以作为Comparable的子类型。

注意:以上所说的in/out修饰符对于类型参数 T 的限制,仅适用于非private(public, protected, internal)函数,对于private函数,类型参数 T 可以存在于任意位置,毕竟private函数仅用于内部调用,不会对泛型类的协变、逆变性产生影响。还有一点例外就是,如果类型参数 T 标记为out,我们仍可以在构造函数的参数中使用它,因为构造函数仅用于实例化,之后不能被调用,所以也不会破坏泛型类的协变性。

扩展-堆污染

Heap pollution(堆污染), 指的是当把一个不带泛型的对象赋值给一个带泛型的变量时, 就有可能发生堆污染。

由于定义带泛型变量时并不强制指定泛型类型, 因此如果借此发生狸猫换太子的操作的话, 那么就会导致堆污染. 堆污染在编译时并不会报错, 只会在编译时提示有可能导致堆污染的警告. 在运行时,如果发生了堆污染, 那么就会抛出类型转换异常。

场景一
1
2
3
4
5
6
// 定义时未指定泛型类型
List list = new ArrayList<>();
list.add(1);
// 将无泛型对象复制给指定类型变量,此处已经发生堆污染
List<String> strList = list;
String s = strList.get(0);
场景二

java语言不允许创建泛型数组,所以当可变参数为带泛型的可变数组时,方法内只能用不带泛型的数组接收

1
2
3
4
5
6
7
public static void method(List<String>... stringLists) {
Object[] array = stringLists;
List<Integer> tmpList = Arrays.asList(42);
// 此处发生堆污染
array[0] = tmpList;
String s = stringLists[0].get(0);
}
忽略堆污染警告

在方法上进行注解声明,让编译器不提示警告

  • @SuppressWarnings("unchecked"):取消所有警告
  • @SafeVarargs:Java7 专门用来抑制堆污染(Heap pollution)警告提供的注解