1. 前言
了解过java或者说数据结构的人,应该都知道一个常识,ArrayList是由数组实现的,LinkedList是由双向链表实现的。可他们内部的具体逻辑是什么样子的呢,下面就分别分析一下这两个类的源码。
2.ArrayList
ArrayList是由数组实现的一种集合,那为什么要使用ArrayList而不是直接使用数组呢?因为ArrayList支持泛型,并且它会自动扩容,所以不需要我们去关心初始化时应该定义多大容量的数组,会不会多了或者少了。当然如果非常确定要使用的数组的长度范围,还是要定义数组长度比较好,可以避免扩容时产生的性能消耗。 ArrayList的默认长度为10,如果有需要也可以自己通过构造函数定义
//默认长度
private static final int DEFAULT_CAPACITY = 10;
ArrayList有两个成员变量:数组和size,为什么数组要用transient来标记呢?因为数组默认的序列化方法是会序列化数组当前容量下的所有元素,事实上,我们有用到的有效长度并没有那么多,所以ArrayList自己重写了序列化方法。
transient Object[] elementData;
private int size;
接下来看看ArrayList的构造方法,它提供了三个构造方法给我们,无参、传入长度、传入集合
//可以自定义初始长度的构造方法
public ArrayList(int initialCapacity) {
if (initialCapacity > 0) {
this.elementData = new Object[initialCapacity];
} else if (initialCapacity == 0) {
this.elementData = EMPTY_ELEMENTDATA;
} else {
throw new IllegalArgumentException("Illegal Capacity: "+
initialCapacity);
}
}
//默认初始长度为10的构造方法
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
//将集合转化为ArrayList的构造方法
public ArrayList(Collection<? extends E> c) {
elementData = c.toArray();
if ((size = elementData.length) != 0) {
// c.toArray might (incorrectly) not return Object[] (see 6260652)
if (elementData.getClass() != Object[].class)
elementData = Arrays.copyOf(elementData, size, Object[].class);
} else {
// replace with empty array.
this.elementData = EMPTY_ELEMENTDATA;
}
}
然后看一下最常用的get方法
public E get(int index) {
rangeCheck(index);
return elementData(index);
}
//检查是否超出size的范围,超出的话直接抛异常出去
private void rangeCheck(int index) {
if (index >= size)
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
//返回该索引的元素
E elementData(int index) {
return (E) elementData[index];
}
下面看一下add、addAll方法。
add有两种方式,一种是在数组末尾插入,另一种是指定位置插入。无论是哪种方式,都要在插入之前判断一下当前数组是否已经装满了,如果装满了则执行扩容操作。在制定位置插入的话,需要把指定位置及之后的元素全部向后挪一位,然后再把元素插入指定位置。
&emsp addAll也有两种方式,一种是在数组末尾插入,另一种是指定位置插入,跟单个add的实现方式差不多,判断加上这一批元素后是否超出长度,超出则扩容。然后在末尾或者指定位置加入这一批元素。可以看到这里很容易会发生线程安全的问题,比方说在线程A判断完不需要扩容后,线程B刚好添加了一批元素,则会抛出异常。
public boolean add(E e) {
//判断是否扩容,如果需要则扩容
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}
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++;
}
public boolean addAll(Collection<? extends E> c) {
Object[] a = c.toArray();
int numNew = a.length;
//是否超出长度,超出则扩容
ensureCapacityInternal(size + numNew); // Increments modCount
//将这一批元素放到数组结尾
System.arraycopy(a, 0, elementData, size, numNew);
//更新size
size += numNew;
return numNew != 0;
}
public boolean addAll(int index, Collection<? extends E> c) {
rangeCheckForAdd(index);
Object[] a = c.toArray();
int numNew = a.length;
//判断是否需要扩容
ensureCapacityInternal(size + numNew); // Increments modCount
int numMoved = size - index;
if (numMoved > 0)
//将指定位置的元素都向后挪numNew位
System.arraycopy(elementData, index, elementData, index + numNew,
numMoved);
//在指定位置插入这一批元素
System.arraycopy(a, 0, elementData, index, numNew);
size += numNew;
return numNew != 0;
}
然后看一下remove操作,remove操作也有两种方式,一种是根据索引删除,一种是根据元素删除。根据索引删除就不用说了,根据元素删除的实现逻辑是遍历数组,通过equal来找到元素,然后删除。
public E remove(int index) {
//判断索引是否小于size
rangeCheck(index);
modCount++;
E oldValue = elementData(index);
//判断删除的是否是最后一个元素
int numMoved = size - index - 1;
if (numMoved > 0)
//如果不是,则该元素后的所有元素都要向前挪一位
System.arraycopy(elementData, index+1, elementData, index,
numMoved);
elementData[--size] = null; // clear to let GC do its work
return oldValue;
}
public boolean remove(Object o) {
//判断传入的参数是否为null,因为null调用equals方法会抛异常
if (o == null) {
for (int index = 0; index < size; index++)
if (elementData[index] == null) {
fastRemove(index);
return true;
}
} else {
//根据euqals方法来找到要删除的元素,并且删除
for (int index = 0; index < size; index++)
if (o.equals(elementData[index])) {
fastRemove(index);
return true;
}
}
return false;
}
private void fastRemove(int index) {
modCount++;
int numMoved = size - index - 1;
if (numMoved > 0)
System.arraycopy(elementData, index+1, elementData, index,
numMoved);
elementData[--size] = null; // clear to let GC do its work
}
最后还是看一下扩容操作,当添加元素后,长度比当前容量大时,会定义一个新的数组,长度为oldCapacity + (oldCapacity >> 1),然后将旧数组的元素复制到新数组上,所以说扩容操作会导致不必要的性能损耗。
private void grow(int minCapacity) {
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);
}
3.LinkedList
linkedList是由双向链表构成的,所以它有一个前驱和一个后继,并且定义了头节点和尾节点具体结构如下
private static class Node<E> {
E item;
//后继
Node<E> next;
//前驱
Node<E> prev;
}
//头结点
transient Node<E> first;
//尾结点
transient Node<E> last;
下面看一下get操作,没什么特殊的,就是先判断是否超出了长度,然后遍历链表,找到该索引的结点
public E get(int index) {
checkElementIndex(index);
return node(index).item;
}
add操作就是在结尾插入一个结点,remove和poll都是返回并删除头结点。把它们放到一起的原因是,这三个恰巧是Queue接口的常用方法。remove和poll的区别是,若当前链表为空链表,remove会抛异常出去,而poll会返回null。
public boolean add(E e) {
linkLast(e);
return true;
}
public E poll() {
final Node<E> f = first;
return (f == null) ? null : unlinkFirst(f);
}
public E remove() {
return removeFirst();
}
4.后记
跟HashMap比起来,ArrayList和LinkedList的源码要简单很多,但还是有一些细节的地方是要注意的。
1.ArrayList能定义长度的话尽量定义长度。
2.多线程下要注意线程安全的问题
3.LinkedList是双向链表,而不是单向链表
4.Queue接口是用LinkedList实现的。
本文只作为学习过程中的随笔,如果有不正确的地方欢迎指出。