Java中数组和List泛型的区别:
- ArrayList中存放的都是对象,即引用类型,即使我们可以向里面put一个基本数据类型,那么也是基于自动装箱特性,将基本数据类型转换成对象;而数组中可以是任意类型
- 从实际工作经历上看,数组中是可以间隔存
null值的,而ArrayList是做不到这一点的(###2020.12.24 更新:这块之前的描述有误,已修正,这块举个例子: ArrayList<String> list = new ArrayList<>(5); list.set(3, "3"); // 会抛出异常 感谢评论区的同学的指出) - 对于泛型数组是不能够实例化的,即不能new T[]出来,而new ArrayList()是ok的
- 数组的协变的,即如果Sub是Super的一个子类,那么Sub[]是Super[]的一个子类;而List泛型,是不变的,即List<Sub>既不是List<Super>的子类,也不是它的父类。
对于前面三点,大家编写一个简单的示例代码,就能够验证出来。下面重点分析一下第四点:
先看下面的示例代码:
class Fruit {}
class Apple extends Fruit {}
class Jonathan extends Apple {}
class Orange extends Fruit {}
public class CovariantArrays {
public static void main(String[] args) {
Fruit fruit = new Apple();
Fruit[] fruit = new Apple[10];
fruits[0] = new Fruit();
fruits[1] = new Apple();
fruits[2] = new Jonathan();
fruits[3] = new Orange();
}
}
对于main中的第一行代码,很容易理解,就是多态。第二行代码呢,根据数组是协变的,是可以这么赋值的。紧接着,就是四条对于数组中元素的赋值语句。这四条语句,在编译期间是没有任何问题的,因为是Fruit数组,里面的每个元素都是Fruit类型的引用,而Apple、Jonathan以及Orange都是它的直接或间接子类,自然而然是ok的。但是在运行期间,第一条和第四条就会报错:
Exception in thread "main" java.lang.ArrayStoreException
所以对于第四点,数组的协变从某种程度上来说,倒是它的缺点:它违背了”最好能把所有的异常都在编译期间排除掉“的原则。
虽然List是不变的,但是List<? extends Fruit>是支持协变(Covariance)的,与之对应的,List<? super Fruit>是支持逆变(Contravariance)的。
先看下面的代码:
public static void main(String[] args) {
List<? extends Fruit> flist = new ArrayList<Apple>();
flist.add(new Apple());
flist.add(new Fruit());
flist.add(new Object());
}
大家可以猜测一下,上面这三个add方法是否可以编译通过?答案是:不可以的,连Object类型的对象都不可以添加。下面我们来分析原因:
- List<? extends Fruit>:它表达的语法含义是List中的泛型是?extends Fruit,用它来替换List源码中的T,或者说实际上它会限制什么东西呢(因为我们知道如果放置到类的声明时,它就会限制传入的泛型类型)。我们再看下面四条赋值语句:
flist = new ArrayList<Fruit>(); //编译通过
flist = new ArrayList<Orange>(); // 编译通过
flist = new ArrayList<Jonathan>(); // 编译通过
flist = new ArrayList<Object>(); // 编译不通过
可以看出,实际上限制的就是后面实例化时ArrayList中的类型,它必须是Fruit本身及其子类。搞清楚这个问题之后,我们就可以来解释上面为什么无法add的原因。
对于List<? extends Fruit> flist中实际上它可以实例化成ArrayList<Fruit>、ArrayList<Apple>、ArrayList<Orange>、ArrayList<Jonathan>,flist.add(new Fruit());不能满足其他三种情况(虽然从运行期来看,因为泛型都会被擦除,这四种实际上都相同,都是Object类型),即Apple apple =new Fruit()当然是不允许的。其他情况也可以按照这种分析方式类推。所以为了杜绝这些情况,这种协变式是不允许add的。
- 既然不能add,那么get呢?
Fruit fruit = flist.get(0);
这种当然是可以的,而且我们还可以断定get出来的对象肯定是Fruit类型(如果是Fruit子类,那么我们也可以这么说)
协变聊完了,我们再来看看逆变:
List<? super Fruit> fs = new ArrayList<>();
fs.add(new Fruit());
fs.add(new Apple());
- List<? super Fruit>:说明new ArrayList<>中的类型必须是Fruit或者它的父类,那么我们往fs中添加Fruit对象、添加Apple对象肯定没有问题的。
而get的时候,我们只能断定它是Object的类型。所以,这种逆变式重点是在add上。所以到了这里,我们可以总结成一句话:将List对象声明为协变,意味着它是只读的;而将List对象声明为逆变的,意味着它是只写的。
这种协变、逆变以及不变,包括泛型的设计初衷,实际上都是出于一个原则,将这种类型异常扼杀在编译期。
参考《Thinking in Java》
### kotlin协变和逆变可阅读此文