playground

泛型

协变

Java的数组是协变的,比如下面的代码。

SuperClass[] array = new SubClass[10];

其中SubClassSuperClass的子类。

泛型是不支持协变的,比如以下代码无法通过编译。

ArrayList<SuperClass> list = new ArrayList<SubClass>();

这时候就需要用到上边界限定通配符。

ArrayList<? extends SuperClass> list = new ArrayList<SubClass>();

上边界限定通配符

我们把<? extends T>的形式称为上边界限定通配符,它指的是有一个类是T或其子类,我们可以记为SST或者T的子类,但是我们不知道它具体是哪一个类。

class SuperClass {
}

class SubClassA extends SuperClass {
}

class SubClassB extends SuperClass {
}

public static void main(String[] args) {
    List<? extends SuperClass> list = new ArrayList<>();
    list.add(new SubClassA()); //此句编译不通过。
}

上面的代码尝试把SuperClass的子类SubClassA的一个对象放入List<? extends SuperClass>类型的集合中,结果却编译不通过,这是为什么呢?

原因是上边界限定通配符是用来描述尖括号里的泛型的,比如上面的List<? extends SuperClass>,我们可以把它看作List<S>,而SSuperClass的一个子类,至于是哪个子类我们不得而知,因此List<? extends SuperClass>可能是List<SubClassA>,也可能是List<SubClassB>,所以我们无法把任何对象放入集合中(只能放入null)。

虽然我们不能往集合中增加任何null以外的元素,但是读取是没有问题的。

SuperClass superClass = list.get(0);

可以看到读取出来的类型就是SuperClass,虽然我们不知道泛型具体是SuperClass的哪一个子类,但是它们对应的元素都一定能转成SuperClass

既然这样的写法会导致我们无法插入元素,那么有什么用呢?上面的例子比较奇怪,通常我们是用下面这种方式使用上边界限定通配符的。

void f(ArrayList<SuperClass> list) {
}

f(new ArrayList<SubClass>());

上面代码中当我们尝试调用f()方法时提示编译不通过,因为ArrayList<SubClass>是无法转成ArrayList<SuperClass>,解决方法就是使用上边界限定通配符。

void f(ArrayList<? extends SuperClass> list) {
}

下边界限定通配符

和上边界限定通配符类似,我们把<? super T>的形式称为下边界限定通配符,它指的是有一个类是T或其基类,我们可以记为SST或者T的基类,但是我们不知道它具体是哪一个类。

下面的代码试图把SuperClass子类的实例放入集合中,和上边界限定通配符中的例子相反,这种做法在下边界限定通配符中是允许的。因为虽然我们不知道泛型中的类具体是SuperClass的哪一个基类(从继承层次上看),但是由于SuperClass的子类一定是SuperClass基类的子类,所以可以插入到集合中。但是SuperClass的基类是不能插入到集合中的,理由同上边界限定通配符类似。

public static void main(String[] args) {
    List<? super SuperClass> list = new ArrayList<>();
    list.add(new SubClassA());
    list.add(new SubClassB());

    list.add(new SuperSuperClass()); //编译不通过,这里的SuperSuperClass是SuperClass的基类。
}

当我们尝试去读取时,由于编译器不知道是基类具体是什么类型,所以只能返回Object类型,因为它是所有类型的基类。

Object object = list.get(0);

通常我们使用如下的方式应用下边界限定通配符。

void f(ArrayList<SubClass> list) {
}

f(new ArrayList<SuperClass>());

上面代码中的ArrayList<SuperClass>无法转成ArrayList<SubClass>,因此我们需要用到下边界限定通配符从而通过编译。

void f(ArrayList<? super SubClass> list) {
}

无边界通配符

无边界通配符的形式是<?>,等价于<? extends Object>,它表示没有任何限制,可以是任何的类型。由于是任何类型,所以编译器不知道它是什么类型,因此以下代码是不能通过编译的。

List<?> list = new ArrayList<>();
list.add(1);

既然无边界通配符是任意类型,那么为什么当我们试图往集合中插入整数1时却无法通过编译呢?其实原理和上边界限定通配符是类似的。 我们把List<?>看作List<S>,这里的S可以是任何类型,可能是List<Boolean>,也可能是List<String>,编译器是不知道的。当你往集合中插入元素1时不代表编译器就认为它是一个List<Integer>类型的集合。

以下两行代码是有区别的。

List<?> listA = new ArrayList<>();
List listB = new ArrayList<>();

第二行等价于new ArrayList<Object>(),因此我们可以插入任何类型的对象。

自限定

形如以下代码的泛型称为自限定泛型。

// java.lang.Enum
Enum<E extends Enum<E>>

自限定的目的是强制要求将正在定义的类当做参数传递给基类。下面我们看一个例子。

首先我们定义一个抽象基类。

abstract class Generics<T> implements Comparable<T>{
}

然后定义一个子类。

class A extends Generics<A>{

    @Override
    public int compareTo(A o) {
        return 0;
    }
}

main()方法中对两个A类型的对象进行比较。

public static void main(String[] args) {
    new A().compareTo(new A());
}

上面的代码可以正常运行,我们对A进行一些修改,泛型参数从A修改为Integer

class A extends Generics<Integer>{

    @Override
    public int compareTo(Integer o) {
        return 0;
    }
}

这时原本应该比较两个A类型对象的compareTo()方法只能把A对象和一个整数对象进行比较,这是不合理的,通常我们只应该对两个同类型的对象进行比较,而在这里我们却可以随意修改泛型参数,那么我们能不能增加一些限制呢?

答案是可以的,这里我们就用到了自限定泛型。这时如果A里的泛型参数是Integer那么是无法通过编译的。通过这种方法我们对子类的泛型参数进行了限制。

在这个例子里,上面提到的“将正在定义的类当做参数传递给基类”一句中,“正在定义的类”指class A,“传给基类”指把基类中compareTo方法里的参数替换为正在定义的类A

abstract class Generics<T extends Generics<T>> implements Comparable<T>{
}

泛型中的异常

泛型也可用在异常中。

interface Generics<E extends Exception> {
    void f() throws E;
}

但是我们不能在catch中使用泛型,以下代码不能编译。

void f(){
    try {}
    catch (E e){}
}

类型擦除

泛型信息只存在代码编译阶段,编译完成后相关的泛型信息会被擦除并被替换为它们的非泛型上界,例如:List<T>被擦除为List,普通类型擦除为Object。比如下面的代码中,Integer的信息被擦除了,导致程序打印结果是“true”。

public static void main(String[] args) {
    List<Integer> list = new ArrayList<>();
    System.out.println(list.getClass() == ArrayList.class); //此处打印true。
}