Java中数组和ArrayList的区别

Java中数组和List泛型的区别:

  1. ArrayList中存放的都是对象,即引用类型,即使我们可以向里面put一个基本数据类型,那么也是基于自动装箱特性,将基本数据类型转换成对象;而数组中可以是任意类型
  2. 从实际工作经历上看,数组中是可以间隔存null的,而ArrayList是做不到这一点的
  3. 对于泛型数组是不能够实例化的,即不能new T[]出来,而new ArrayList()是ok的
  4. 数组的协变的,即如果Sub是Super的一个子类,那么Sub[]是Super[]的一个子类;而List泛型,是不变的,即List既不是List的子类,也不是它的父类。

对于前面三点,大家编写一个简单的示例代码,就能够验证出来。下面重点分析一下第四点:

先看下面的示例代码:

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是支持协变(Covariance)的,与之对应的,List是支持逆变(Contravariance)的。

先看下面的代码:

public static void main(String[] args) {
     List flist = new ArrayList();
     flist.add(new Apple());
     flist.add(new Fruit());
     flist.add(new Object());
}

大家可以猜测一下,上面这三个add方法是否可以编译通过?答案是:不可以的,连Object类型的对象都不可以添加。下面我们来分析原因:

  • List:它表达的语法含义是List中的泛型是?extends Fruit,用它来替换List源码中的T,或者说实际上它会限制什么东西呢(因为我们知道如果放置到类的声明时,它就会限制传入的泛型类型)。我们再看下面四条赋值语句:

    flist = new ArrayList();  //编译通过
    flist = new ArrayList(); // 编译通过
    flist = new ArrayList(); // 编译通过
    flist = new ArrayList();  // 编译不通过  
       
      

    可以看出,实际上限制的就是后面实例化时ArrayList中的类型,它必须是Fruit本身及其子类。搞清楚这个问题之后,我们就可以来解释上面为什么无法add的原因。

    对于List flist中实际上它可以实例化成ArrayList、ArrayList、ArrayList、ArrayList,flist.add(new Fruit());不能满足其他三种情况(虽然从运行期来看,因为泛型都会被擦除,这四种实际上都相同,都是Object类型),即Apple apple =new Fruit()当然是不允许的。其他情况也可以按照这种分析方式类推。所以为了杜绝这些情况,这种协变式是不允许add的。

    • 既然不能add,那么get呢?

      Fruit fruit = flist.get(0);

      这种当然是可以的,而且我们还可以断定get出来的对象肯定是Fruit类型(如果是Fruit子类,那么我们也可以这么说)

    协变聊完了,我们再来看看逆变:

    List fs = new ArrayList<>();
    fs.add(new Fruit());
    fs.add(new Apple());
    • List:说明new ArrayList<>中的类型必须是Fruit或者它的父类,那么我们往fs中添加Fruit对象、添加Apple对象肯定没有问题的。

    而get的时候,我们只能断定它是Object的类型。所以,这种逆变式重点是在add上。所以到了这里,我们可以总结成一句话:将List对象声明为协变,意味着它是只读的;而将List对象声明为逆变的,意味着它是只写的。

    这种协变、逆变以及不变,包括泛型的设计初衷,实际上都是出于一个原则,将这种类型异常扼杀在编译期。

    参考《Thinking in Java》

    ### kotlin协变和逆变可阅读此文

     

    你可能感兴趣的:(Java)