从面向对象说起
Java作为一门面相对象的语言,当然是支持面相对象的三大基本特性的,反手就蹦出三个词:封装、继承、多态。
我们假设有三个类,动物、猫、狗。父类是动物Animal,有两个子类猫Cat和狗Dog。
那在Java中或其它任何支持面相对象的语言中,子类可以把引用赋值给父类。下面这段代码没有任何问题:
Animal animalOne = new Cat();
Animal animalTwo = new Dog();
1
理论上来说,一只猫是一只动物,一只狗也是一只动物,所以这完全是可以理解的。其实,这也是SOLID原则中的“里氏替换原则”的一种体现。
数组的协变
如果一只猫是一只动物,那一群猫是一群动物吗?一群狗是一群动物吗?Java数组认为是的。于是你可以这样写:
Animal[] animals = new Cat[2];
1
这看起来也没有什么问题。但既然都是一群动物了,我往这一群动物中添加一只猫、一只狗,它还是一群动物,这应该是合理的对吧?来看看这段代码:
Animal[] animals = new Cat[2];
animals[0] = new Cat();
// 下面这行代码会抛运行时异常 animals[1] = new Dog();
Animal animal = animal[0];
1
很好,编译没有任何问题。但是一运行,会抛出一个运行时异常:ArrayStoreException。这个异常头顶的注释已经写得很明显了,如果你往数组中添加一个类型不对的对象,就会抛这个异常。它是从JDK 1.0就存在的一个异常。
这么一想,对啊,animals虽然门面上是一个Animal数组,但是它运行时的本质还是一个Cat数组啊,一个Cat数组怎么能添加一个Dog呢?但Java编译器并没有这么智能,而且上述代码在编
译器看来也是合理合法的,所以也就让它编译过了。
所以这种情况,编译器100%过,而运行时100%抛异常,这不是大写的BUG是啥?
如果Cat是Animal的子类型,那么Cat[]也是Animal[]的子类型,我们称这种性质为协变(covariance)。Java中,数组是协变的。
泛型的不变性
在Java 1.5之前,是没有泛型的。那个时候从集合中存取对象都是Object类型,所以每次取出对象后必须进行强转:
List list = new LinkedList(); list.add(123); list.add(“123”); int a = (int)list.get(0); // 下面这段代码会在运行时抛异常 int b = (int)list.get(1); 复制代码
1
如果不小心存入集合中对象类型是错的,会在运行时报强转异常。而1.5提供泛型以后,可以让编译器自动帮助转换,并对代码进行检查,使程序更加安全。
在Java8又加入了泛型的类型推导功能,使用泛型以后,我们的代码看起来变得简洁又安全了:
List list = new LinkedList<>();
list.add(123);