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

Java集合--深度剖析Vector、Stack、ArrayList、LinkedList(二)

2018-03-02 23:14 591 查看
研究环境:jdk1.8



还是这张图,可以清晰地看到各个类或者接口之间的关系。我们知道集合可以分为两大类:由Collection接口延伸出的集合和由Map接口延伸出的集合。详细地分,集合可以分为4种类型:List类型、Set类型、Queue类型、Map类型,今天我们研究的是List类型的集合,Vector、Stack、ArrayList和LinkedList

从它们最开始的地方说起,也就是Collection接口。这个接口无非定义一些集合通用的方法,比如添加、删除元素啊,size()啊等等。查看Collection源码可以发现,它是继承自Iterable接口的,Iterable接口中有一个方法返回一个迭代器:Iterator< T> iterator(); Collection继承了这个接口就可以使用迭代器了。这里有个疑问,为什么Collection不直接继承Iterator接口,而是要多定义一个Iterable接口,让Collection去继承Iterable,而不是Iterator呢?如果让Collection直接继承Iterator接口,这样Collection本身就变成了一个迭代器了,它有next方法,hasNext方法,也就是说Collection包含着当前迭代的位置(状态),先不说多线程的情况了,就是单线程,如果一个方法遍历Collection,遍历了一半突然终止了,那么Collection中迭代的位置就在中间。而另一个方法接着遍历它,却发现只能遍历后一半的内容,前一半遍历不到,因为Iterator没有提供重置迭代位置的方法。而继承Iterable就不同了,它有一个iterator()方法,它能保证每一次调用Collection,都能返回一个新的迭代器,不同的迭代器之间是互不影响的。随便看一个集合对于iterator方法的实现方式:

ArrayList:

public Iterator<E> iterator() {
return new Itr();
}

private class Itr implements Iterator<E>{
//具体实现不看了
}


可以看到每一次调用iterator方法都会新new一个迭代器

图中从上往下看,说完了Collection,就到了它的子接口List和实现类AbstractCollection,里面的方法都没什么好讲的,再继续往下看就是AbstractList,它继承了AbstractCollection类,并且实现了List接口,它虽然还是一个抽象类,但是它从父类或者父接口中继承来的方法差不多已经有了实现方式,包括iterator方法。从图中的关系可以看出,Vector、Stack、ArrayList、LinkedList全是AbstractList的子孙类,AbstractList派生了Vector、ArrayList、AbstractSequentialList,接着由Vector继续派生出Stack,AbstractSequentialList派生出LinkedList。好了,主角终于登场了,弄清了相应的父子类关系,就可以来说说Vector、Stack、ArrayList、LinkedList这四种集合的内部实现了。

(by the way,看源码,我们会发现Vector、ArrayList、LinkedList都实现了List接口,但是AbstractList已经实现过List接口了啊,Vector、ArrayList、LinkedList作为AbstractList的子孙类,完全没有必要再显示的写implements List啊,即使有不同于AbstractList的实现方式,直接重写就行了啊。难道只是一种规范,增加代码可读性,或者防止以后AbstractList不实现List了,Vector、ArrayList、LinkedList根本不用去修改,降低维护成本)

Vector、ArrayList底层都是由object数组实现,Stack是Vector的子类,底层也是数组;LinkedList由双向链表实现,所以它的核心是组成链表的Node节点。

一、既然都是由数组实
f199
现的,那就先来比较Vector和ArrayList吧:


1. 类声明

public class Vector<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable

public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable


这是这两个类的声明,是一样的,都继承了AbstractList,并实现了List、RandomAccess、 Cloneable、Serializable接口,说明了Vector和ArrayList都是可以快速随机访问、可克隆、可以被序列化和反序列化的,容量大小可变的集合。(网上有人说List是有序可重复的,我觉得这种说法还是有点问题的,你不能说一个接口具有怎么样的特性,如果能通过一些抽象方法就看出接口具有什么特点,也是厉害。那么为什么会说List接口是有序,可重复的呢?因为在Java里,实现了List接口的实现类,比如ArrayList、LinkedList都表现出了这个特点,数据是按录入的顺序存放的,并且可以重复)RandomAccess接口虽然只是一个标记接口,里面什么方法都没有定义,但它就是表明Vector和ArrayList具有随机访问性,毕竟底层是用数组实现的,可以根据数组下标直接找到元素。

上面我们说了Vector和ArrayList都是用数组实现的,看一下这两个类的成员变量

Vector:
protected Object[] elementData;

ArrayList:
transient Object[] elementData;


Vector的elementData是用protected修饰的,而ArrayList的elementData是用transient修饰的,那不就是说ArrayList中的数据元素不会被序列化?对象序列化确切地说有三种方式

只是实现Serializable接口,那么在序列化时,就会自动调用java.io.ObjectOutputStream的defaultWriteObject()方法进行序列化,这时用transient修饰的成员变量是不会被序列化的

实现Serializable接口,重写writeObject()方法,这时用transient修饰的成员变量(除静态变量)能不能被序列化,就取决于重写的writeObject()方法了

实现Externalizable接口,Externalizable继承了Serializable接口,并增加了两个方法:writeExternal(ObjectOutput out) 和readExternal(ObjectInput in) 。在writeExternal方法里定义了哪些属性可以序列化,哪些不可以被序列化

很明显,ArrayList采用的是第二种方式

private void writeObject(java.io.ObjectOutputStream s)
throws java.io.IOException{
// Write out element count, and any hidden stuff
int expectedModCount = modCount;
s.defaultWriteObject();

// Write out size as capacity for behavioural compatibility with clone()
s.writeInt(size);

// Write out all elements in the proper order.
for (int i=0; i<size; i++) {
s.writeObject(elementData[i]);
}

if (modCount != expectedModCount) {
throw new ConcurrentModificationException();
}
}


它先调用defaultWriteObject方法,此时用transient修饰的成员变量还没有被序列化,但是它后来把数组里的元素一个一个遍历并序列化,也就是说ArrayList中用transient修饰的elementData数组是被序列化的。那ArrayList花这么大的功夫来“多此一举”,为什么不直接把elementData的修饰符transient去掉呢?仔细想想还是有点用的,因为elementData数组在初始化或者扩容时,允许有空元素,如果在序列化时不加控制,就会把空元素也序列化,ArrayList这样做,在序列化时,不会把空元素也序列化,它能保证序列化的元素都是有用的,也就是下标从0到size-1的元素。

2. 自动扩容

在构造Vector和ArrayList时,如果不显示给容量赋值,那么它们的默认容量都为10。当容量不够时,它们都会自动扩容,那么扩容的大小是怎么样的呢

ArrayList:
private void grow(int minCapacity) {
// overflow-conscious code
int oldCapacity = elementData.length;
int newCapacity = oldCapacity + (oldCapacity >> 1);
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
// minCapacity is usually close to size, so this is a win:
elementData = Arrays.copyOf(elementData, newCapacity);
}

Vector:
private void grow(int minCapacity) {
// overflow-conscious code
int oldCapacity = elementData.length;
int newCapacity = oldCapacity + ((capacityIncrement > 0) ?
capacityIncrement : oldCapacity);
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
elementData = Arrays.copyOf(elementData, newCapacity);
}


在代码中可以看到,对于ArrayList,int newCapacity = oldCapacity + (oldCapacity >> 1); 也就是说,新容量=旧容量*1.5; 对于Vector, int newCapacity = oldCapacity + ((capacityIncrement > 0) ?capacityIncrement : oldCapacity); 这里涉及到一个成员变量capacityIncrement,它表示Vector每次扩容,增加的数值,在构造Vector时就可以显示给它赋值,如果没有给它赋值,默认为0。所以当capacityIncrement>0时,新容量 = 旧容量+capacityIncrement;当capacityIncrement<=0时,新容量 = 旧容量*2 。

Vector和ArrayList都有一个ensureCapacity(int minCapacity)方法,它最后都会调用grow(int minCapacity)方法,如果不看API或者源码,很可能会被它误导,以为它是按照minCapacity参数扩容。比如ArrayList,默认容量为10,当调用ensureCapacity(11)后,实际容量并不是11,而是15。在API中,这个方法表示:如果需要,增加此向量的容量,以确保它可以至少保存最小容量参数指定的组件数。至于调用后实际容量的值就根据源码计算吧

3. 同步

Vector使用synchronized来保证线程安全(同步),并且是读写都加了synchronized。ArrayList则是非线程安全的(不同步),所以在效率上ArrayList要优于Vector。

4. 遍历

ArrayList支持用迭代器Iterator和foreach/for去遍历,而Vector除了这两种方式,还支持Enumeration去遍历

Vector:
public Enumeration<E> elements() {
return new Enumeration<E>() {
int count = 0;

public boolean hasMoreElements() {
return count < elementCount;
}

public E nextElement() {
synchronized (Vector.this) {
if (count < elementCount) {
return elementData(count++);
}
}
throw new NoSuchElementException("Vector Enumeration");
}
};
}


5. 其他方面

Vector和ArrayList在其他方面都差不多,添加、删除元素等方法都很类似。但是,对于删除元素,虽然Vector的坑比ArrayList要少,但还是会一不小心就出错,具体可以看我的另一篇博客那些年,我们在Java ArrayList Remove方法遇到的坑

二、Stack与Vector

Stack和Vector是父子关系,Vector是父,Stack是子,所以Stack也是用数组实现的,确切地说,是一个用数组实现的栈,满足先进后出原则(FILO),入栈和出栈都是在数组下标为size-1的地方。

三、链表与数组

上面说了用数组实现的集合,现在可以来说说用链表实现的集合了,代表就是LinkedList了。

1. 底层数据结构

LinkedList的底层数据结构是节点Node

private static class Node<E> {
E item;
Node<E> next;
Node<E> prev;

Node(Node<E> prev, E element, Node<E> next) {
this.item = element;
this.next = next;
this.prev = prev;
}
}


每个节点由三部分所组成,分别是前节点(引用地址)、后节点(引用地址)以及本节点所包含的实体数据。

我们先来看一下LinkedList到底是一个怎么样的链表,它的成员变量由三个:

transient int size = 0;      //链表实际存有数据的节点个数(允许数据为空)

transient Node<E> first;     //指向链表第一个节点

transient Node<E> last;      //指向链表最后一个节点


回想一下,为LinkedList添加元素的语句:

List< String> list = new LinkedList<>();

list.add(“abc”);

好,我们就从add方法开始

public boolean add(E e) {
linkLast(e);
return true;
}

void linkLast(E e) {
final Node<E> l = last;
final Node<E> newNode = new Node<>(l, e, null);
last = newNode;
if (l == null)
first = newNode;
else
l.next = newNode;
size++;
modCount++;
}


代码很简单,就不详细说明了,到这里我们可以知道LinkedList是一个双向链表,大致如下图(原谅我又盗图了,其实在看linkLast代码的时候,让我想起了以前我学数据结构的时候,那时候还是用C++写的。时间过的很快,转眼大学四年就要过去了,马上也要实习了,还是比较伤感的)



2. 类声明

public class LinkedList<E> extends AbstractSequentialList<E>
implements List<E>, Deque<E>, Cloneable, java.io.Serializable


AbstractSequentialList是一个抽象类,它继承于AbstrctList。实现了链表中,根据index索引值操作链表的全部方法。

它没有实现RandomAccess接口,说明不具备快速随机访问

实现了Deque接口(通常可以被用来当作栈、队列使用)

3. LinkedList与ArrayList的区别

底层实现:ArrayList由数组实现;LinkedList由链表实现

随机访问:ArrayList随机访问速度快,LinkedList访问速度慢

ArrayList:
public E get(int index) {
rangeCheck(index);
return elementData(index);
}
E elementData(int index) {
return (E) elementData[index];
}

LinkedList
public E get(int index) {
checkElementIndex(index);
return node(index).item;
}
Node<E> node(int index) {

if (index < (size >> 1)) {
Node<E> x = first;
for (int i = 0; i < index; i++)
x = x.next;
return x;
} else {
Node<E> x = last;
for (int i = size - 1; i > index; i--)
x = x.prev;
return x;
}
}


可以看到,ArrayList可以直接根据下标从数组中返回相应的元素;而LinkedList就不同了,它要在链表中循环,一个节点一个节点地往下找,虽然已经做了优化,根据参数来判断是在左半边链表还是右半边链表,然而还是没有ArrayList速度快。

删除、插入元素:这个还真不好说

//只说明插入元素,删除元素原理相同
ArrayList:
public void add(int index, E element) {
rangeCheckForAdd(index);

ensureCapacityInternal(size + 1);  // Increments modCount!!
System.arraycopy(elementData, index, elementData, index + 1,
size - index);
elementData[index] = element;
size++;
}

LinkedList:
public void add(int index, E element) {
checkPositionIndex(index);

if (index == size)
linkLast(element);
else
linkBefore(element, node(index));
}

Node<E> node(int index) {
if (index < (size >> 1)) {
Node<E> x = first;
for (int i = 0; i < index; i++)
x = x.next;
return x;
} else {
Node<E> x = last;
for (int i = size - 1; i > index; i--)
x = x.prev;
return x;
}
}


没看源码之前,我是非常相信网上所说的LinkedList在插入、删除元素方面,要优于ArrayList的,因为ArrayList要移动数据。但是看了源码之后,我就不这么认为了。每次插入(删除)元素,ArrayList的部分元素都要向前或向后移动,而LinkedList只要直接插入(删除)即可,但是在通过下标找节点时要花费一定时间,那就要看是遍历链表的代价大,还是移动数据的代价大了。所以这应该要看你集合中原本的数据量,以及插入(删除)元素的位置了,至于到底是怎么样的结果,因为懒就不做实验了,哈哈。网上有一个实验的结果是这样的:如果待插入、删除的元素是在数据结构的前半段尤其是非常靠前的位置的时候,LinkedList的效率将大大快过ArrayList,因为ArrayList将批量copy大量的元素;越往后,对于LinkedList来说,优势就不是很明显了,需要寻址的时间越来越长,ArrayList要批量copy的元素越来越少,操作速度必然追上乃至超过LinkedList

还是看情况吧,比如都是remove(Object o)方法,就不止是LinkedList要遍历整个容器,找到相应的元素了,ArrayList也需要在整个容器中遍历,找到需要删除的元素,这样,就明显要选择LinkedList,而不是ArrayList了,因为LinkedList在删除后不需要移动数据,而且顺序遍历,链表是要快于数组的;还有就是,如果是顺序插入,并且数据量也知道,那肯定是选择ArrayList,因为ArrayList不存在移动数据和自动扩容造成的时间、空间浪费问题,反而是LinkedList每次插入数据,在定义节点时会浪费时间和内存。

有一种情况特别要注意,在插入元素的时候,ArrayList可能会扩容,这是一个耗费时间和空间的事情,因为数组在堆上需要一块很大的连续的空间,如果当时连续的空间不能满足扩容要求,就会发生gc,那代价就更高了,所以一般来说,在数据量不知道,需要经常插入操作的,还是选择LinkedList吧。

同步:都是不同步的(非线程安全)

顺序迭代遍历:LinkedList比ArrayList要快,毕竟是链表

空间浪费:ArrayList的空间浪费主要体现在自动扩容后,有一部分多余空间没有被利用(可以在ArrayList分配完空间后使用trimToSize来去掉浪费的空间),而LinkedList的空间花费则体现在它的每一个元素都需要消耗相当的空间
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: 
相关文章推荐