您的位置:首页 > 编程语言 > Java开发

List vs Array:谁适合做java中泛型对象的容器?

2014-06-17 03:34 225 查看
在java中,我们知道有两大类线性的数据结构是数组(Array)和链表(主要是ArrayList和LinkedList)。对于一般的对象来说,我们可以选择这两类数据结构中的任何一种数据结构来存储我们的对象。但是对于泛型对象,我们应该选择哪种数据结构来存储他们呢?答案是:List。那么为什么不选择Array呢?具体的原因看我下面的分析:

首先我需要给出List和Array这两个数据结构在java实现中的两个重要的不同点

类别

不同点1

不同点2

Array

Covariant(协变)

Reified(具体化)

List

Invariant(不变)

Erasure(擦除)

下面我就来解释一下这些术语的含义:

Covariant(协变):协变的本意是指一个变量发生了变化,那么其它的一些变量也会跟着(协同)发生变化,从而保留一些数学特性。在java中也是同样的意思,表示如果一个B类型是A类型的子类,那么B类型声明的数组(B[])也是A类型声明数组(A[])的子类(注意这里是变化了的,千万不要认为是不变的)。

Invariant(不变性):和covariant刚好相反,容器中存储的类型的继承关系不会影响到容器本身。比如:B类型是A类型的子类,那么ArrayList<B>并不是ArrayList<A>的子类,更不是它的父类,总之这两个类什么关系都没有。

Reified(具体化):数组对于其中存储对象的类型的检查是在运行时进行的。

Erasure(擦除):链表对于其中存储对象的类型的检查是在编译时进行的,而在运行时的时候,它会自动“擦除”掉对象的类型。这样做的目的是为了更好的与用低版本写出来的程序兼容,因为毕竟“泛型”是在JDK 1.5才引入的。

正因为这两个不同点,赋予了List在存储泛型对象的时候得天独厚的优势。而且在java中我们不能声明拥有具体类型的泛型数组。

我们假设A这个类是一个泛型类:

A<E>[] a = new A<E>[3]; // 不合法

A<Integer>[] a = new A<Integer>[3]; // 不合法

A[] a = new A[3]; // 合法,但是会有一个rawtypes的警告

如果我们想去除这个警告,有两种解决方法:

1. @SuppressWarnings("rawtypes")

A[] a = new A[3];

2. A<?>[] a = new A<?>[2];

虽然这两种方法都可以消除警告,但是都含有隐藏的风险,都有可能抛出ClassCastException。那么我们还有其他更好的解决方案吗?答案是肯定的,就是用List来存储泛型的对象:

// A是我们自定义的泛型类

ArrayList<A<Integer>> a = new ArrayList<>();

细心的读者可能发现了,上面两个不太好的解决方案有个共同的特这就是:没有为泛型对象指定具体的数据类型,这样就导致我们在写代码的时候要特别的小心数据类型的转换,因为这样的数组可以放入A<Integer>,A<String>,A<Object>...这些不同类型的对象。这样一来我们就失去了使用泛型类的意义了,因为在java中使用泛型类就是为了避免我们做过多的数据类型检查,而这样一来,虽然我们用到了泛型类,但是还是要做数据类型的检查。

那么你们可能会问:如果在java中允许数组中放具体类型的泛型对象,这个问题不就解决了吗?是的,这个问题解决了,但是由于前面我们谈到的数组的两个特性,有一个新的更加严重的问题就产生了:java代码会存在严重的安全隐患(在运行时很容易抛出ClassCastException,这个异常其实在编译时的时候就可以被java虚拟机察觉的,但是一旦我们允许数组中引入具体类型的泛型数组,这个异常就不会被java虚拟机觉察了,当然在运行的时候就容易出错了)。

看下面一段代码,在这个代码中,我们假设java虚拟机允许数组存储具体类型的泛型对象。

ArrayList<Integer>[] a = new ArrayList<Integer>[5]; // 假设这个是合法的,其实在java中不能这么用

Object[] b = a; // 这个 语句肯定也是合法的,由于数组的covariant

ArrayList<String> item1 = new ArrayList<String>();

item1.add("I am dangerous!!!");

b[0] = item1; // 这个语句也是合法的

Integer i = a[0].get(0); // 如果第一条语句合法,那么这条语句肯定也没有问题。但是如果这样写,我就是将一个字符串类型的数据赋给整型变量,肯定会抛出ClassCastException的。

从这个例子中我们可以发现,如果java运行数组存储具体类型的泛型对象,那么最终可能会导致ClassCastException这个异常的抛出。一般来说,ClassCastException这个异常风险java虚拟机是可以发现的,尤其是对于泛型对象而言,但是这里我们虽然用到了泛型对象,但是java虚拟机是不可能发现这个风险的,除非我们执行最后一条语句。这样我们就将这个风险放在了运行时,对于程序员的我们来说这是不能容忍的。我们肯定就会责怪java的设计者,所以java的设计者就一不做二不休,干脆不让数组存储具体类型的泛型对象。如果程序员非要在数组中存储泛型对象,那么就不能指定具体类型,类型检查就留给我们程序员自己去做,或者java虚拟机会给出一个警告,这样做的目的其实就是推卸责任,告诉程序员,后面程序中一旦出错,就不关java的事情了,全是你们程序员自己的责任。

比如我将上面的代码稍微的改一下,变成java中可以运行的代码,但是第一行会有一个警告,整个程序依然会抛出ClassCastException,但是有了上面的警告,java就将自己的“责任”成功的推给了程序员。

@SuppressWarnings("unchecked")

ArrayList<Integer>[] a = new ArrayList[5]; // 合法,有警告

Object[] b = a; // 这个语句肯定也是合法的

ArrayList<String> item1 = new ArrayList<String>();

item1.add("I am dangerous!!!");

b[0] = item1; // 这个语句也是合法的

Integer i = a[0].get(0); // 我将一个字符串类型的数据赋给整型变量,肯定会抛出ClassCastException的。但是与第一个例子不同的是,这个风险java虚拟机在编译的时候已经发现了,所以才会给我们第一行的代码爆出一个警告。这样一来,这个错误的出现就完全是我们程序员的责任了,和java虚拟机没有关系了,因为别个已经告诉了我们会有风险。

综上所述,由于java中数组的特性,我们不应该将泛型的对象放在数组中保存,而是应该放在List中保存。比如上面的例子,如果我们需要存储ArrayList这个泛型的对象,我们应该写成:

ArrayList<ArrayList<Integer>> a = new ArrayList<>();

这样就万无一失了!!!
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: