Java中数组和ArrayList的区别

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

  1. ArrayList中存放的都是对象,即引用类型,即使我们可以向里面put一个基本数据类型,那么也是基于自动装箱特性,将基本数据类型转换成对象;而数组中可以是任意类型
  2. 从实际工作经历上看,数组中是可以间隔存null 值的,而ArrayList是做不到这一点的(###2020.12.24 更新:这块之前的描述有误,已修正,这块举个例子:
    ArrayList<String> list = new ArrayList<>(5);
    list.set(3, "3");  // 会抛出异常     
    
    感谢评论区的同学的指出
  3. 对于泛型数组是不能够实例化的,即不能new T[]出来,而new ArrayList()是ok的
  4. 数组的协变的,即如果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协变和逆变可阅读此文

 

评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值