引言
上一篇文章,我们通过观察反编译后的文件,讲解了java中泛型的实现方式—擦除,并由此引出了擦除带来的问题以及解决方案。这篇文章,我们继续分析java中泛型带来的另一个重要的问题。
协变和逆变
定义
首先说一下Java中协变、逆变和不变的概念:
定义A、B两个类型,A是由B派生出来的子类,f()表示类型转换如new List();
协变:当A<=B时,f(A)<=f(B)成立。
逆变:当A<=B时,f(B)<=f(A)成立。
不变:当A<=B时,协变和逆变都不成立。
数组是协变的
看下面的例子:
public class ArrayTest {
private static void test(Fruit[] fruits){
}
public static void main(String[] args) {
test(new Apple[2]);
}
}
test方法接收Fruit数组作为参数,我传入一个Apple数组也没有问题。
数组的协变使得编译期只要给定了合法的类型就能够通过编译,但是运行时可能出现问题,看下面的例子:
public class Fruit {
}
public class Apple extends Fruit {
}
public class Orange extends Fruit {
}
public class Jonathan extends Apple {
}
public class CovariantArrays {
public static void main(String[] args) {
Fruit[] fruit = new Apple[10];
fruit[0] = new Apple();
fruit[1] = new Jonathan();
fruit[0] = new Fruit();
fruit[0] = new Orange();
}
}
我们将fruit类型的数组变量指向了一个apple类型的数组对象,这样是没问题的。我们也能将数组的元素设置为Apple或者其子类,但是第14行和第15行在运行时会报错:
Exception in thread "main" java.lang.ArrayStoreException: person.andy.concurrency.generic.fruits.Fruit
at person.andy.concurrency.generic.fruits.CovariantArrays.main(CovariantArrays.java:11)
Exception in thread "main" java.lang.ArrayStoreException: person.andy.concurrency.generic.fruits.Orange
at person.andy.concurrency.generic.fruits.CovariantArrays.main(CovariantArrays.java:12)
意思是我们不能向Apple数组中添加不是Apple类型的元素。
这段代码在编译期没有问题,在运行期会报错,编译器认为向Fruit数组中添加任何的Fruit及其子类的对象是没有问题的,但是实际的数组类型是Apple,所以这种问题可能直到运行期才被发现。
泛型是不变的
看下面的例子:
public static void main(String[] args) {
List<Fruit> iHelloList = new ArrayList<Apple>();
}
这样的意思就像是,一个Apple的List不能赋值给一个Fruit的List,在某些情况下,我们是需要这样做的。
通配符
协变
协变泛型的用法就是<? extends Fruit>。我们可以这样来声明:
public static void main(String[] args) {
List<? extends Fruit> fruits = new ArrayList<Apple>();
List<? extends Fruit> fruits1 = new ArrayList<Jonathan>();
List<? extends Fruit> fruits2 = new ArrayList<Orange>();
}
这些都能够编译运行成功。
但是协变有自己的问题:
public static void main(String[] args) {
//这样赋值没有问题
List<? extends Fruit> fruits = new ArrayList<Apple>();
//下面的三行代码不能编译通过
fruits.add(new Fruit());
fruits.add(new Apple());
fruits.add(new Object());
//调用get方法返回的类型是Fruit
Fruit fruit = fruits.get(0);
}
这个示例中的第一行,我们通过通配符达到了协变的目的,告诉编译器我们的泛型可以是Fruit或者任何Fruit的子类。但是后面我们对list调用add方法,无论传入的参数是Fruit或者Apple,甚至是一个Object,都不能编译通过。
为什么编译器要这样做?我们先看一下add方法的声明:
public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}
这个方法参数就是List的泛型参数E,根据之前对擦除的理解,泛型擦除之后,这个方法的参数应该是一个Object,既然是Object,理论上我们就能传递任何引用类型的对象,但是编译器为了泛型的类型安全肯定要做限制,先看下面这个简单的例子:
public static void main(String[] args) {
List<Apple> apples = new ArrayList<>();
apples.add("a String");
}
这段代码肯定不能编译通过。我们不能向Apple的list添加字符串类型的对象,这就是编译器做的限制,它知道我们的泛型类型是Apple,所以能作出这样的限制,保证类型安全 。
回到上面协变的例子,为什么使用了协变之后,add方法什么都不能添加了呢。它可以限制只让我们传入Fruit的,根本问题是
List<? extends Fruit> fruits = new ArrayList<Apple>();
这句代码,使得fruits这个变量指向的是List
所以编译器就直接限制了add方法传入任何类型的参数都不能通过,这样保证了泛型的初衷,在编译期间就保证类型安全。
所以协变虽然达到了将Apple的List赋值给Fruit的List的目的,但是却限制了往list中添加元素的功能。
但是,调用get方法是没有问题的,并且返回类型是Fruit,因为编译器知道容器中的肯定是Fruit或者其子类,所以就默认加上了到Fruit的强制类型转换,这样是没有类型安全问题的。
看下面的示例和反编译后的代码:
public static void main(String[] args) {
List<? extends Fruit> fruits = new ArrayList<Apple>();
Fruit fruit = fruits.get(0);
}
public static void main(String args[])
{
List fruits = new ArrayList();
Fruit fruit = (Fruit)fruits.get(0);
}
可以看到编译器加上的强制类型转换。
但是当我们调用list的contains()或者indexOf()方法的时候,就不会有上面add方法同样的问题:
public static void main(String[] args) {
List<? extends Fruit> apples = new ArrayList<Apple>();
apples.contains(new Apple());
apples.contains(new Object());
apples.indexOf(new Apple());
}
上面的代码完全是可以编译运行的,contains和indexOf方法的参数也是Object,为什么不会出现这个问题?这个得看他们的声明:
public int indexOf(Object o) {}
public boolean contains(Object o) {}
与上面的add方法对比,这两个的参数直接就是Object,而add方法的参数是通配符E,虽然add方法被擦除之后,参数同样会变成Object,但是通配符这样的参数声明会告诉编译器来进行参数类型限制,这也就是为什么上面那个简单的示例中apple的list不能添加String的原因,当使用协变时,编译器做参数类型限制发现变量的准确类型是不确定的,所以就直接都限制了,相反,contains和indexOf方法就是普通方法,编译器不会对参数做限制。
逆变
逆变的声明方式就是<? super Apple>,表明我们的泛型类型需要是Apple的父类。
我们可以这样声明:
public static void main(String[] args) {
List<? super Apple> fruits = new ArrayList<Apple>();
List<? super Apple> fruits1 = new ArrayList<Fruit>();
List<? super Apple> fruits2 = new ArrayList<Object>();
}
这三种声明方式都能够编译运行成功。我们可以让List<? super Apple>这种类型的变量指向List