注:本文代码基于JDK 11

一、基本概念和原理

“泛型”的字面意思是广泛的类型,类、接口和方法都可以应用于广泛的类型,也就是说,代码与它们能够操作的数据类型不再需要绑定到一起,同一套代码也可以用于多种数据类型,这样不仅可以复用代码,降低耦合,还可以提高代码的可读性和安全性。

1.1、泛型类

举个例子,请看如下代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
public class MyPair<F, S> {

    public final F first;
    public final S second;

    public MyPair(F first, S second) {
        this.first = first;
        this.second = second;
    }
}

解释一下,MyPair是一个泛型类,与普通类的区别是:

1、类名后面多一个<F, S>

2、first的类型是F,second的类型是S

F、S表示类型参数,因此,泛型实际是类型参数化,处理的数据类型不是固定的,而是可以作为参数传入

接下来,我们给MyPair类传递类型参数,请看如下代码:

1
2
3
MyPair<String, Integer> myPair = new MyPair<>("Tony", 26); 
String first = myPair.first;
Integer second = myPair.second;

解释一下,MyPair<String, Integer>中的String、Integer就是传递的实际类型参数,MyPair类的代码和它处理的数据类型不是绑定的,具体类型可以变化。

当然了,有人可能会有疑问:为什么一定要定义类型参数呢? 我们把MyPair定义成普通类,直接使用Object不也可以?请看如下代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
public class MyPair {

    public final Object first;
    public final Object second;

    public MyPair(Object first, Object second) {
        this.first = first;
        this.second = second;
    }
}

使用MyPair,请看如下代码:

1
2
3
MyPair myPair = new MyPair("Tony", 26);
String first = (String) myPair.first;
Integer second = (Integer) myPair.second;

解释一下,这样的写法完全可以,实际上泛型的内部原理就像上面代码描述的那样。为什么这么说呢?我们知道,Java编译器会将Java源代码转换为.class文件,然后由Java虚拟机加载并运行.class文件。对于泛型类,Java编译器会将泛型代码转换为普通的非泛型代码,就像上面的MyPair类代码及其使用代码一样,将类型参数F、S擦除,替换为Object,插入必要的强制类型转换。Java虚拟机实际执行的时候,它是不知道泛型这回事的,只知道普通的类及代码。

泛型是Java 5以后才支持的,这么设计是为了兼容性而不得已的一个选择。

既然只使用普通类和Object就可以,而且泛型最后也转换为了普通类,那泛型的好处是什么?泛型主要的好处是更好的安全性与更好的可读性。

只使用Object,代码写错的时候,编译器发现不了问题,当程序运行时就会抛出类型转换异常ClassCastException,请看如下代码:

1
2
3
MyPair myPair = new MyPair("Tony", 26);
String first = (String) myPair.first;
String second = (String) myPair.second; // 运行错误

但是,如果使用泛型,编译器就会确保不用错类型,为程序多设置一道安全防护网,并且会省去烦琐的强制类型转换,加上明确的类型信息,代码可读性也会更好,请看如下代码:

1
2
3
MyPair myPair = new MyPair("Tony", 26); 
String first = myPair.first;
String second = myPair.second; // 编译错误

1.2、泛型方法

除了泛型类,方法也可以是泛型的,并且一个方法是不是泛型的,与它所在的类是不是泛型没有什么关系,请看如下代码:

1
2
3
4
5
6
7
8
public static <T> int indexOf(T[] arr, T element) {
    for (int i = 0; i < arr.length; i++) {
        if (arr[i].equals(element)) {
            return i;
        }
    }
    return -1;
}

使用indexOf方法,请看如下代码:

1
int index = indexOf(new Integer[]{1, 2, 3}, 2);

indexOf表示一个算法,在给定数组中寻找某个元素,这个算法的基本过程与具体数据类型没有什么关系,通过泛型它可以方便地应用于各种数据类型,且由编译器保证类型安全。

1.3、泛型接口

接口也可以是泛型的,比如JDK中提供的Comparable接口,请看如下代码:

1
2
3
public interface Comparable<T> {
    public int compareTo(T o); 
}

具体的接口实现在JDK中也有提供,比如Integer类,请看如下代码:

1
2
3
4
5
public final class Integer extends Number implements Comparable<Integer>{       
    public int compareTo(Integer anotherInteger) {
        return compare(this.value, anotherInteger.value);
    }
}

上面的Integer类实现了Comparable接口,指定了实际类型参数为Integer,表示Integer只能与Integer对象进行比较。

二、类型参数的限定

无论是泛型类、泛型方法亦或是泛型接口,关于类型参数,我们一般把它当作Object,其实Java支持给类型参数限定一个上界(通过extends关键字来表示),这个上界可以是某个具体的类或者某个具体的接口,也可以是其它的类型参数。

2.1、上界为某个具体类

上面提过的MyPair类,我们可以定义一个子类NumberPair,限定两个类型参数必须为Number,再添加一个求和方法sum,请看如下代码:

1
2
3
4
5
6
7
8
9
public class NumberPair<F extends Number, S extends Number> extends MyPair<F, S> {
    public NumberPair(F first, S second) {
        super(first, second);
    }

    public double sum() {
        return first.doubleValue() + second.doubleValue();
    }
}

使用NumberPair,请看如下代码:

1
2
NumberPair<Integer, Double> numberPair = new NumberPair<>(10, 10.1); 
double sum = numberPair.sum();

限定类型后,如果类型使用错误,编译器会提示,并且指定边界后,类型擦除时就不会转换为Object了,而是会转换为它的边界类型。

2.2、上界为某个接口

在泛型方法中,一种常见的场景是限定类型必须实现某个接口,这里以Comparable接口为例,请看如下代码:

1
2
3
4
5
6
7
8
9
public static <T extends Comparable<T>> T max(T[] arr) {
    T max = arr[0];
    for (int i = 1; i < arr.length; i++) {
        if (arr[i].compareTo(max) > 0) {
            max = arr[i];
        }
    }
    return max;
}

<T extends Comparable>是一种令人费解的语法形式,这种形式称为 递归类型限制,可以这么解读:T表示一种数据类型,必须实现Comparable接口,且必须可以与相同类型的元素进行比较。

2.3、上界为其它类型参数

泛型类最常见的用途是作为容器类,比如我们实现了一个简化版的ArrayList,请看如下代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
public class DynamicArray<E> {
    private static final int DEFAULT_CAPACITY = 10;
    private int size;
    private Object[] elementData;

    public DynamicArray() {
        this.elementData = new Object[DEFAULT_CAPACITY];
    }

    private void ensureCapacity(int minCapacity) {
        int oldCapacity = elementData.length;
        if (oldCapacity >= minCapacity) {
            return;
        }
        int newCapacity = oldCapacity * 2;
        if (newCapacity < minCapacity)
            newCapacity = minCapacity;
        elementData = Arrays.copyOf(elementData, newCapacity);
    }

    public void add(E e) {
        ensureCapacity(size + 1);
        elementData[size++] = e;
    }

    public E get(int index) {
        return (E) elementData[index];
    }

    public int size() {
        return size;
    }

    public E set(int index, E element) {
        E oldValue = get(index);
        elementData[index] = element;
        return oldValue;
    }
}

然后再给DynamicArray类增加一个实例方法addAll,这个方法将参数容器中的所有元素都添加到当前容器里来,直觉上可以这么写,请看如下代码:

1
2
3
4
5
public void addAll(DynamicArray<E> c) { 
    for (int i = 0; i < c.size; i++) {
        add(c.get(i));
    }
}

但是,这么写就会导致一些局限性,出现了编译错误,请看如下代码:

1
2
3
4
5
DynamicArray<Number> numbers = new DynamicArray<>(); 
DynamicArray<Integer> ints = new DynamicArray<>();
ints.add(100);
ints.add(34);
numbers.addAll(ints); // 编译错误

出现编译错误的原因是:我们理所当然地认为numbers是一个Number类型的容器,ints是一个Integer类型的容器,因为IntegerNumber的子类,所以就可以将ints添加到numbers中, 但实际上,编译器认为addAll需要的参数类型为DynamicArray<Number>,而传递过来的参数类型为DynamicArray<Integer>

为什么不行呢?这里我们可以假设编译通过,这时DynamicArray<Integer>中就会出现Double类型的值,而这显然破坏了Java泛型关于类型安全的保证,请看如下代码:

1
2
3
DynamicArray<Integer> ints = new DynamicArray<>(); 
DynamicArray<Number> numbers = ints; // 假设编译通过
numbers.add(new Double(12.34));

Java支持一个类型参数以另一个类型参数作为上界,这个问题可以通过这种方式解决,请看如下代码:

1
2
3
4
5
public <T extends E> void addAll(DynamicArray<T> c) {
    for(int i=0; i<c.size; i++) {
        add(c.get(i));
    }
}

三、解析通配符

3.1、有限定通配符

通配符有着令人费解和混淆的语法,但通配符大量应用于Java容器类中,我们可以优化下DynamicArray中的addAll方法,之前的写法是比较繁琐的,它可以替换为简洁的通配符形式,请看如下代码:

1
2
3
4
5
public void addAll(DynamicArray<? extends E> c) { 
    for(int i=0; i<c.size; i++){
        add(c.get(i));
    }
}

<? extends E> 表示 有限定通配符,匹配E或E的某个子类型,具体什么子类型是未知的。

同样是extends关键字,同样应用于泛型,<T extends E><? extends E> 的区别如下:

1、<T extends E>用于定义类型参数,它声明了一个类型参数T,可放在泛型类定义中类名后面、泛型方法返回值前面

2、<? extends E> 用于实例化类型参数,它用于实例化泛型变量中的类型参数,只是这个具体类型是未知的,只知道它是E或E的某个子类型。

虽然它们不一样,但两种写法经常可以达成相同目标,比如下面这两种写法都可以,请看如下代码:

1
2
public void addAll(DynamicArray<? extends E> c)
public <T extends E> void addAll(DynamicArray<T> c)

3.2、无限定通配符

除了有限定通配符,还有一种通配符,形如DynamicArray<?>,称为 无限定通配符。举个例子,在DynamicArray中查找指定元素,请看如下代码:

1
2
3
4
5
6
7
8
public static int indexOf(DynamicArray<?> arr, Object element) {
    for (int i = 0; i < arr.size(); i++) {
        if (arr.get(i).equals(element)) {
            return i;
        } 
    }
    return -1;
}

虽然通配符形式更为简洁,但上面两种通配符都有一个重要的限制:只能读,不能写,请看如下代码:

1
2
3
4
5
DynamicArray<Integer> ints = new DynamicArray<>(); 
DynamicArray<? extends Number> numbers = ints;
Integer a = 200;
numbers.add(a); // 编译错误
numbers.add((Number)a); // 编译错误

两种add方法都是非法的,无论是Integer还是Number,都会出现编译错误。

出现编译错误的原因是:问号就是表示类型安全无知,? extends Number表示是Number的某个子类型,但不知道具体子类型,如果允许写入,Java就无法确保类型安全性,所以干脆禁止。

为什么不行呢?这里我们可以假设如果允许写入Number及其子类型,则最后两行编译就是正确的,也就是说Java将允许把Double对象放入Integer容器,这显然违背了Java关于类型安全的承诺,请看如下代码:

1
2
3
4
DynamicArray<Integer> ints = new DynamicArray<>(); 
DynamicArray<? extends Number> numbers = ints;
Number n = new Double(23.0);
numbers.add(n); 

大部分情况下,这种限制是好的,但是这使得一些理应正确的基本操作无法完成,比如交换两个元素的位置,请看如下代码:

1
2
3
4
5
public static void swap(DynamicArray<?> arr, int i, int j){ 
    Object tmp = arr.get(i);
    arr.set(i, arr.get(j));
    arr.set(j, tmp);
}

这个代码看上去应该是正确的,但Java会提示编译错误,两行set语句都是非法的,不过借助带类型参数的泛型方法,这个问题得到解决,请看如下代码:

1
2
3
4
5
6
7
8
9
private static <T> void swapInternal(DynamicArray<T> arr, int i, int j){
    T tmp = arr.get(i);
    arr.set(i, arr.get(j));
    arr.set(j, tmp);
}

public static void swap(DynamicArray<?> arr, int i, int j){
    swapInternal(arr, i, j); 
}

Java容器类中就有类似这样的用法,公共的API是通配符形式,形式更简单,但内部调用带类型参数的方法。

如果参数类型之间有依赖关系,也只能用类型参数,比如将src容器中的内容复制到dest中,请看如下代码:

1
2
3
4
5
public static <D, S extends D> void copy(DynamicArray<D> dest, DynamicArray<S> src){
    for(int i=0; i<src.size(); i++){
        dest.add(src.get(i));
    }
}

上面的声明可以使用通配符简化,两个参数可以简化为一个,请看如下代码:

1
2
3
4
5
public static <D> void copy(DynamicArray<D> dest, DynamicArray<? extends D> src) {
    for(int i=0; i<src.size(); i++){
        dest.add(src.get(i));
    }
}

如果返回值依赖于类型参数,也不能用通配符,比如计算动态数组中的最大值,请看如下代码:

1
2
3
4
5
6
7
8
9
public static <T extends Comparable<T>> T max(DynamicArray<T> arr){
    T max = arr.get(0);
    for(int i=1; i<arr.size(); i++){
        if(arr.get(i).compareTo(max)>0){
            max = arr.get(i);
        }
    }
    return max;
}

泛型方法到底应该用通配符的形式还是加类型参数,总结一下:

1、通配符形式都可以用类型参数的形式来替代,通配符能做的,用类型参数都能做。

2、通配符形式可以减少类型参数,形式上往往更为简单,可读性也更好,所以能用通配符的就用通配符。

3、如果类型参数之间有依赖关系,或者返回值依赖类型参数,或者需要写操作,则只能用类型参数。

4、通配符形式和类型参数往往配合使用,比如上面的copy方法,定义必要的类型参数,使用通配符表达依赖,并接受更广泛的数据类型。

3.3、超类型通配符

还有一种通配符,它的形式为 <? super E>,称为 超类型通配符,表示E的某个父类型,它是无法用类型参数替代。如果没有这种语法,写入会有一些限制,举个例子,请看如下代码:

1
2
3
4
5
public void copyTo(DynamicArray<E> dest){
    for(int i=0; i<size; i++){
        dest.add(get(i));
    }
}

将当前容器中的元素添加到传入的目标容器中,如果这么用,Java会提示编译错误,理由我们之前也说过了,期望的参数类型是DynamicArray<Integer>DynamicArray<Number>并不适用,请看如下代码:

1
2
3
4
5
DynamicArray<Integer> ints = new DynamicArray<Integer>(); 
ints.add(100);
ints.add(34);
DynamicArray<Number> numbers = new DynamicArray<Number>(); 
ints.copyTo(numbers); // 编译错误

解决这个问题的方法就是超类型通配符,可以将copyTo代码修改,请看如下代码:

1
2
3
4
5
public void copyTo(DynamicArray<? super E> dest){
    for(int i=0; i<size; i++){
        dest.add(get(i));
    }
}

关于通配符的比较,我们来总结一下:

1、<? super E> 用于灵活写入或比较,使得对象可以写入父类型的容器,使得父类型的比较方法可以应用于子类对象,它不能被类型参数形式替代。

2、<?><? extends E> 用于灵活读取,使得方法可以读取E或E的任意子类型的容器对象,它们可以用类型参数的形式替代,但通配符形式更为简洁。

四、细节和局限性

Java泛型是通过类型擦除来实现的,类型参数在编译时会被替换为Object,运行时Java虚拟机不知道泛型这回事,这带来了很多局限性,其中有的部分是比较容易理解的,有的则是非常违反直觉的。

4.1、使用泛型类、方法和接口

在使用泛型类、方法和接口时,有一些值得注意的地方,比如:

1、基本类型不能用于实例化类型参数,类似下面的写法是不合法的,请看如下代码:

1
Pair<int> minmax = new Pair<int>(1,100);

解决方法是使用基本类型对应的包装类。

2、运行时类型信息不适用于泛型。

Java中,这个类型信息也是一个对象,它的类型为ClassClass本身也是一个泛型类,每个类的类型对象可以通过<类名>.class的方式引用,比如String.classInteger.class,这个类型对象也可以通过对象的getClass()方法获得,请看如下代码:

1
Class<? > cls = "hello".getClass();

这个类型对象只有一份,与泛型无关,所以Java不支持类似如下写法,请看如下代码:

1
Pair<Integer>.class

一个泛型对象的getClass方法的返回值与原始类型对象也是相同的,比如下面代码的输出都是true,请看如下代码:

1
2
3
4
Pair<Integer> p1 = new Pair<Integer>(1,100);
Pair<String> p2 = new Pair<String>("hello","world");
System.out.println(Pair.class==p1.getClass()); //true
System.out.println(Pair.class==p2.getClass()); //true

instanceof后面是接口或类名,instanceof是运行时判断也与泛型无关,所以Java也不支持类似如下写法,请看如下代码:

1
if(p1 instanceof Pair<Integer>)

不过,Java支持如下写法,请看如下代码:

1
if(p1 instanceof Pair<?>)

3、类型擦除可能会引发一些冲突。

有两个类BaseChild,它们是这样声明的,请看如下代码:

1
2
3
class Base implements Comparable<Base>

class Child extends Base

如果这样修改Child类,会发生什么?请看如下代码:

1
class Child extends Base implements Comparable<Child>

Java编译器会提示错误,Comparable接口不能被实现两次,且两次实现的类型参数还不同,一次是Comparable<Base>,一次是Comparable<Child>。因为类型擦除后,实际上只能有一个。解决办法是重写Base类的实现,请看如下代码:

1
2
3
4
5
6
class Child extends Base {
    @Override
    public int compareTo(Base o) {
        ...
    }
}

另外,你可能认为可以如下定义重载方法,请看如下代码:

1
2
public static void test(DynamicArray<Integer> intArr) 
public static void test(DynamicArray<String> strArr)

虽然参数都是DynamicArray,实例化类型也不同,一个是DynamicArray<Integer>,另一个是DynamicArray<String>,同样遗憾的是,Java不允许这种写法,理由同样是类型擦除后它们的声明是一样的。

4.2、定义泛型类、方法和接口

在定义泛型类、方法和接口时,也有一些需要注意的地方,比如:

1、不能通过类型参数创建对象

比如,T是类型参数,下面的写法都是非法的,请看如下代码:

1
2
T t = new T();
T[] arr = new T[10];

如果允许,那么用户会以为创建的就是对应类型的对象,但由于类型擦除,Java只能创建Object类型的对象,而无法创建T类型的对象,容易引起误解,所以Java干脆禁止这么做。

解决办法是如果类型有默认构造方法,可以调用ClassnewInstance方法构建对象,请看如下代码:

1
2
3
4
5
6
7
public static <T> T create(Class<T> type){
    try {
        return type.newInstance();
    } catch (Exception e) {
        return null;
    }
}

2、泛型类的类型参数不能用于静态变量和方法

下面这种写法是非法的,请看如下代码:

1
2
3
4
5
6
7
8
9
public class Singleton<T> {
    private static T instance;
    public synchronized static T getInstance(){
        if(instance==null){
             // 创建实例
        }
        return instance;
    }
}

如果合法,那么对于每种实例化类型,都需要有一个对应的静态变量和方法。但由于类型擦除,Singleton类型只有一份,静态变量和方法都是类型的属性,且与类型参数无关,所以不能使用泛型类类型参数。

3、了解多个类型限定的语法

Java中还支持多个上界,多个上界之间以&分隔,请看如下代码:

1
T extends Base & Comparable & Serializable

Base为上界类,ComparableSerializable为上界接口。如果有上界类,类应该放在第一个,类型擦除时,会用第一个上界替换。

4.3、泛型和数组

不能创建泛型数组,比如我们可能想这样创建一个Pair的泛型数组,请看如下代码:

1
2
3
Pair<Object,Integer>[] options = new Pair<Object,Integer>[]{
        new Pair("1元",7), new Pair("2元", 2), new Pair("10元", 1)
};

如果Java允许创建泛型数组,则会发生非常严重的问题,请看如下代码:

1
2
3
Pair<Object,Integer>[] options = new Pair<Object,Integer>[3];
Object[] objs = options;
objs[0] = new Pair<Double,String>(12.34, "hello");

如果可以创建泛型数组options,那它就可以赋值给其他类型的数组objs,而最后一行明显错误的赋值操作,则既不会引起编译错误,也不会触发运行时异常,因为Pair<Double, String>的运行时类型是Pair,和objs的运行时类型 Pair[] 是匹配的。但我们知道,它的实际类型是不匹配的,在程序的其它地方,当把 objs[0] 作为 Pair<Object,Integer> 进行处理的时候,一定会触发异常。

但现实需要能够存放泛型对象的容器,怎么办呢?可以使用原始类型的数组,请看如下代码:

1
2
3
4
Pair[] options = new Pair[]{
      new Pair<String,Integer>("1元",7),
      new Pair<String,Integer>("2元", 2),
      new Pair<String,Integer>("10元", 1)};

有时,我们希望转换泛型容器为一个数组,比如对于DynamicArray,我们可能希望它有这么一个方法,请看如下代码:

1
public E[] toArray()

而希望可以这么用,请看如下代码:

1
2
3
4
DynamicArray<Integer> ints = new DynamicArray<Integer>();
ints.add(100);
ints.add(34);
Integer[] arr = ints.toArray();

那如何实现这个方法呢?Java必须在运行时知道要转换成的数组类型,类型可以作为参数传递给toArray方法,请看如下代码:

1
2
3
4
5
public E[] toArray(Class<E> type){
    Object copy = Array.newInstance(type, size);
    System.arraycopy(elementData, 0, copy, 0, size);
    return (E[])copy;
}

Class表示要转换成的数组类型信息,有了这个类型信息,Array类的newInstance方法就可以创建出真正类型的数组对象。调用toArray方法时,需要传递需要的类型,请看如下代码:

1
Integer[] arr = ints.toArray(Integer.class);

总结下泛型与数组的关系:

1、Java不支持创建泛型数组

2、如果要存放泛型对象,可以使用原始类型的数组,或者使用泛型容器。

3、泛型容器内部使用Object数组,如果要转换泛型容器为对应类型的数组,需要使用反射。

五、概念扩展

5.1、协变与逆变

1、协变

前面讲有限定通配符时,涉及这么一段代码:

1
2
3
4
5
public void addAll(DynamicArray<? extends E> c) { 
    for(int i=0; i<c.size; i++){
        add(c.get(i));
    }
}

我们也已经知道,下面的代码addAll方法执行是正确的。

1
2
3
4
5
DynamicArray<Number> numbers = new DynamicArray<>(); 
DynamicArray<Integer> ints = new DynamicArray<>();
ints.add(100);
ints.add(34);
numbers.addAll(ints); 

这其实涉及一个概念,也就是泛型型变,对于上面例子来说,只要DynamicArray泛型参数是Number的子类即可。这种型变被称为covariant(中文翻译为协变)一一Java将这种语法称为 通配符上限。对于“通配符上限”语法而言,从该集合对象中“拿出”对象总是安全的。

2、逆变

前面讲超类型通配符时,涉及这么一段代码:

1
2
3
4
5
public void copyTo(DynamicArray<? super E> dest){
    for(int i=0; i<size; i++){
        dest.add(get(i));
    }
}

我们也已经知道,下面的代码copyTo方法执行是正确的。

1
2
3
4
5
DynamicArray<Integer> ints = new DynamicArray<Integer>(); 
ints.add(100);
ints.add(34);
DynamicArray<Number> numbers = new DynamicArray<Number>(); 
ints.copyTo(numbers); 

对于上面例子来说,只要DynamicArray泛型参数是Integer的父类即可。这种型变被称为contravariance(中文翻译为逆变)一一Java将这种语法称为 通配符下限。对于“通配符下限”语法而言,将对象传给泛型对象是安全的。

5.2、PECS原则

PECS原则的全称是"Producer Extends Consumer Super"。

1、PE原则

PE原则说明:使用extends确定上界的只能是生产者,只能往外生产东西,取出的就是上界类型

分析:对于使用了<? extends T>的泛型结构,我们知道它存储的值是T的子类,T是可以有多个不同表现的子类的,因此当进行写值时,编译器并不知道其中存储的到底是哪个子类,因此写值操作必然会出现问题,所以编译器直接禁止在使用<? extends T>泛型结构时进行写操作,只能进行取值,这就是所说的PE原则。

2、CS原则

CS原则说明:使用super确定下界的只能做消费者,只能往里塞东西

分析:对于使用了<? super T>的泛型结构,我们知道它存储的值是T的父类,若将T的父类进行取值,取出时会无法确定父类类型只能转成Object类型,并且父类缺少子类中的一些信息,这显然是不合理的,因此编译器直接禁止在使用<? super T>泛型结构时进行取值操作,只能进行写值,这就是所说的CS原则。

总结:如果要从集合中读取类型T的数据,并且不能写入,可以使用通配符上限<?extends T>,也就是PE原则。如果要从集合中写入类型T的数据,并且不需要读取,可以使用通配符下限<? super T>,也就是CS原则。如果既要存又要取,那么就不要使用通配符,使用类型参数。

5.3、泛型桥方法

假设有一个Parent类与Child类,Child继承自Parent,并且重写了showData方法,如下代码所示:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
public class Child extends Parent<String> {

    @Override
    public void showData(String s) {
        System.out.println("Child->showData: " + s);
    }
}

class Parent<T> {

    public void showData(T t) {
        System.out.println("Parent->showData: " + t);
    }
}

依据之前学的泛型擦除原理,我们知道在Parent类编译之后,showData方法上的泛型没有了,参数类型变成了Object类型。而Child中的showData方法是重写了Parent的方法,但是根据Java重写的要求,方法的参数类型必须是一致的,那这样不就出错了吗?子类showData方法参数类型是String但是父类是Object。其实这样并不会出错,原因是编译器为了维护这种重写的原则,在Child类中自动生成了一个桥方法。

1
2
3
public void showData(Object value){
    showData((String)value);
}

可以看出,这个桥方法实际上就是对超类中 showData(Obejct) 的重写。桥方法并不需要自己手动生成,一切都是编译器自动完成的。

通过反编译Child类的字节码,可以发现多了一个showData方法,并且前面是synthetic bridge修饰,这个方法就是桥方法了。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
// class version 65.0 (65)
// access flags 0x21
// signature LParent<Ljava/lang/String;>;
// declaration: Child extends Parent<java.lang.String>
public class Child extends Parent {

  // compiled from: Child.java
  // access flags 0x19
  public final static INNERCLASS java/lang/invoke/MethodHandles$Lookup java/lang/invoke/MethodHandles Lookup

  // access flags 0x1
  public <init>()V
   L0
    LINENUMBER 1 L0
    ALOAD 0
    INVOKESPECIAL Parent.<init> ()V
    RETURN
   L1
    LOCALVARIABLE this LChild; L0 L1 0
    MAXSTACK = 1
    MAXLOCALS = 1

  // access flags 0x1
  public showData(Ljava/lang/String;)V
   L0
    LINENUMBER 5 L0
    GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
    ALOAD 1
    INVOKEDYNAMIC makeConcatWithConstants(Ljava/lang/String;)Ljava/lang/String; [
      // handle kind 0x6 : INVOKESTATIC
      java/lang/invoke/StringConcatFactory.makeConcatWithConstants(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/String;[Ljava/lang/Object;)Ljava/lang/invoke/CallSite;
      // arguments:
      "Child->showData: \u0001"
    ]
    INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/String;)V
   L1
    LINENUMBER 6 L1
    RETURN
   L2
    LOCALVARIABLE this LChild; L0 L2 0
    LOCALVARIABLE s Ljava/lang/String; L0 L2 1
    MAXSTACK = 2
    MAXLOCALS = 2

  // access flags 0x1041
  public synthetic bridge showData(Ljava/lang/Object;)V
    // parameter synthetic  <no name>
   L0
    LINENUMBER 1 L0
    ALOAD 0
    ALOAD 1
    CHECKCAST java/lang/String
    INVOKEVIRTUAL Child.showData (Ljava/lang/String;)V
    RETURN
   L1
    LOCALVARIABLE this LChild; L0 L1 0
    MAXSTACK = 2
    MAXLOCALS = 2
}

除此之外,也可以使用使用Java反射判断showData方法是否桥方法,这个判断定义在Method类中。

1
2
3
public boolean isBridge() {
    return (getModifiers() & Modifier.BRIDGE) != 0;
}

演示下isBridge方法的使用。

1
2
3
4
5
6
try {
    Method method = Child.class.getMethod("showData", Object.class);
    System.out.println(method.isBridge());      // 输出true
} catch (NoSuchMethodException e) {
    e.printStackTrace();
}