您的位置:首页 > 理论基础 > 数据结构算法

2.Java数据结构原理解析-List系列

2017-11-21 22:23 621 查看

一、List家族特点

集合效率线程安全性
ArrayList读取快,插入慢线程不安全
LinkedList插入快,读取慢线程不安全
Vector线程安全
CopyOnWriteArrayList读取快,插入慢线程安全

二、ArrayList

Java中的数组初始化后,长度就是不可变。简单来说,ArrayList就是可变长数组。

由于ArrayList底层是通过数组来实现的,所以必然要解决下面两个问题。

初始容量,最大容量

如何扩容

ArrayList的初始容量不是在初始化时设置的,而是在第一次进行add后者addAll操作时设置的。(这是JDK1.7做的一个优化,老版本中ArrayList在构造完成后容量默认是10)

下面是ArrayList的构造函数,在初始化时,并没有为数组分配空间

private static final int DEFAULT_CAPACITY = 10;

private static final Object[] EMPTY_ELEMENTDATA = {};

//存储数据的数组
private transient Object[] elementData;

public ArrayList(int initialCapacity) {
super();
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal Capacity: "+
initialCapacity);
this.elementData = new Object[initialCapacity];
}

public ArrayList() {
super();
//初始化时,是空数组
this.elementData = EMPTY_ELEMENTDATA;
}

public ArrayList(Collection<? extends E> c) {
elementData = c.toArray();
size = elementData.length;
if (elementData.getClass() != Object[].class)
//将elementData拷贝到一个长度为size的新数组
elementData = Arrays.copyOf(elementData, size, Object[].class);
}


下面是为ArrayList每次进行add或者addAll操作时分配内存的代码。

private void ensureCapacityInternal(int minCapacity) {
//分配的最小容量=max(默认容量10,第一次插入的元素的个数)
if (elementData == EMPTY_ELEMENTDATA) {
minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
}

ensureExplicitCapacity(minCapacity);
}

private void ensureExplicitCapacity(int minCapacity) {
modCount++;
// overflow-conscious code
//当数组的长度不够,调用grow方法对数组进行扩容
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}

/**
* The maximum size of array to allocate.
* Some VMs reserve some header words in an array.
* Attempts to allocate larger arrays may result in
* OutOfMemoryError: Requested array size exceeds VM limit
*/
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;

/**
* Increases the capacity to ensure that it can hold at    least the number of elements specified by the minimum capacity argument.
*
* @param minCapacity the desired minimum capacity
*/
private void grow(int minCapacity) {
// overflow-conscious code
int oldCapacity = elementData.length;
//新的容量=以前的容量+以前容量的1/2
int newCapacity = oldCapacity + (oldCapacity >> 1);

//如果newCapacity 比minCapacity 还要小,就分配minCapacity
//这里存在两种情况,一是newCapacity确实比minCapacity小,二是newCapacity 越界了,超过了int的最大值,导致newCapacity 为负数
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;

if (newCapacity - MAX_ARRAY_SIZE > 0)
//newCapacity超过MAX_ARRAY_SIZE时,根据minCapacity来判断是分配MAX_ARRAY_SIZE还是Integer.MAX_VALUE
newCapacity = hugeCapacity(minCapacity);

elementData = Arrays.copyOf(elementData, newCapacity);
}

private static int hugeCapacity(int minCapacity) {
if (minCapacity < 0) // overflow
throw new OutOfMemoryError();
return (minCapacity > MAX_ARRAY_SIZE) ?
Integer.MAX_VALUE :
MAX_ARRAY_SIZE;
}


int newCapacity = oldCapacity + (oldCapacity >> 1);
这句执行后如果超过int的最大值,那么newCapacity会是一个负数。

总结一下ArrayList的扩容算法:

当前容量:oldCapacity

新的容量:newCapacity

此次分配需要的最小容量:minCapacity

当容量不足时,newCapacity=oldCapacity+oldCapacity*1/2,也就是oldCapacity的3/2倍。如果newCapacity发生越界或者newCapacity< minCapacity,那么newCapacity=minCapacity。如果minCapacity>MAX_ARRAY_SIZE,那么最终分配的容量为Integer.MAX_VALUE 或者MAX_ARRAY_SIZE;

三、LinkedList

LinkedList除了实现List接口外,还实现了Deque接口,也就是说,LinkedList除了作为集合外,还可以用作队列。

由于我们讨论的是List家族,所以,下面主要介绍LinkedList作为List中的成员,是如何实现的。

我们都知道,相对于ArrayList,LinkedList有插入更新快、读取慢的特点,这种特性与它底层的数据结构密切相关。

LinkedList底层的数据结构是双向链表。



注:LinkedList最开始的数据结构是双向循环链表(只有一个header指针),后面改成了双向链表(一个first指针、一个last指针)

public class LinkedList<E>
{
transient int size = 0;
//头节点
transient Node<E> first;
//尾节点
transient Node<E> last;

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插入快,查找慢。

下面是LinkedList插入元素时的代码,可以看出,插入一个元素时newNode时,只需要O(1)的时间复杂度。

public void addLast(E e) {
//插入链表尾部
linkLast(e);
}

void linkLast(E e) {
final Node<E> l = last;
//新增一个节点,前继为尾节点,后继为null
final Node<E> newNode = new Node<>(l, e, null);
last = newNode;
if (l == null)
first = newNode;
else
l.next = newNode;
size++;
modCount++;
}


再看看LinkedList在指定位置插入元素的代码,该操作的复杂度主要在
node(int index)
方法上,最坏的情况下,会循环size*1/2次,但是相对于ArrayList 的整体后移,还是相当快的。

public void add(int index, E element) {
//校验是否越界
checkPositionIndex(index);

if (index == size)
linkLast(element);
else
//将element插入node节点
linkBefore(element, node(index));
}

void linkBefore(E e, Node<E> succ) {
// assert succ != null;
final Node<E> pred = succ.prev;
final Node<E> newNode = new Node<>(pred, e, succ);
succ.prev = newNode;
if (pred == null)
first = newNode;
else
pred.next = newNode;
size++;
modCount++;
}

//找到位置index的node节点,如果index<size*1/2,从前往后找,否则从后往前找
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;
}
}


四、CopyOnWriteArrayList

CopyOnWriteArrayList在JDBC的DriverManager中有使用,用于保存所有注册的驱动。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息