一、标准答案吧
ArrayList 是基于动态数组实现的,可以根据需要调整容量,扩容增加 50%。其内部元素以数组形式顺序存储的,所以非常适合随机访问的场合。除了尾部插入和删除元素,往往性能会相对较差,比如我们在中间位置插入一个元素,需要移动后续所有元素。
LinkedList 是双向链表结构,所以它不需要像上面两种那样调整容量。它进行节点插入、删除却要高效得多,但是随机访问性能则要比动态数组慢。
ArrayList、LinkedList 为非线程安全。
二、集合框架结构
List,它提供了方便的访问、插入、删除等操作。
Set,Set 是不允许重复元素的,这是和 List 最明显的区别,也就是不存在两个对象 equals 返回 true。我们在日常开发中有很多需要保证元素唯一性的场合。
Queue/Deque,则是 Java 提供的标准队列结构的实现,除了集合的基本功能,它还支持类似先入先出(FIFO, First-in-First-Out)或者后入先出(LIFO,Last-In-First-Out)等特定行为。
Collection接口主要方法
//Collection接口中主要方法
int size();
boolean isEmpty();
boolean add(E e);
boolean remove(Object o);
void clear();
//List接口中增加的方法
E get(int index);
E set(int index, E element);
三、ArrayList 源码
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);
}
}
首先看下插入方法,重点要注意执行插入元素是超过当前数组预定义的最大值时,数组需要扩容。可以查看下 add、ensureCapacityInternal、ensureExplicitCapacity、grow 方法
private void grow(int minCapacity) {
// overflow-conscious code
//把数组的长度赋给oldCapacity
int oldCapacity = elementData.length;
//新的数组容量=老的数组长度的1.5倍。oldCapacity >> 1 相当于除以2
int newCapacity = oldCapacity + (oldCapacity >> 1);
//如果新的数组容量newCapacity小于传入的参数要求的最小容量minCapacity,那么新的数组容量以传入的容量参数为准。
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
//如果新的数组容量newCapacity大于数组能容纳的最大元素个数 MAX_ARRAY_SIZE 2^{31}-1-8
//那么再判断传入的参数minCapacity是否大于MAX_ARRAY_SIZE,如果minCapacity大于MAX_ARRAY_SIZE,那么newCapacity等于Integer.MAX_VALUE,否者newCapacity等于MAX_ARRAY_SIZE
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);
}
其中有几个判断不好理解,我已经注释上去。先把 MAX_ARRAY_SIZE 和之后方法中的 Integer.MAX_VALUE 列出来
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
@Native public static final int MAX_VALUE = 0x7fffffff;
再把方法 hugeCapacity 列出来
private static int hugeCapacity(int minCapacity) {
if (minCapacity < 0) // overflow
throw new OutOfMemoryError();
return (minCapacity > MAX_ARRAY_SIZE) ?
Integer.MAX_VALUE :
MAX_ARRAY_SIZE;
}
当重新计算的容量(x1.5那个计算)小于传入要求容量参数,则新容量以传入的比较大的容量参数为准。
当传入容量参数太大,大到超过了数组的容量限定值却又小于整数限定值 -1,那么新的数组容量以整数限定值 -1 为准,但是当传入的容量参数不大于数组的容量限定值时,以容量限定值为准。
接下来看下删除方法
transient Object[] elementData;
public E get(int index) {
if (index >= size)
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
//直接取数组第index位置
return (E) elementData[index];
}
public E remove(int index) {
if (index >= size)
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
//取数组第index位置
modCount++;
E oldValue = (E) 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;
}
删除元素时,如果 numMoved 计算出来大于 0,就需要调用 System.arraycopy 进行拷贝了,但是 remove 并不会减少数组的容量(如果需要缩小数组容量,可以调用 trimToSize() 方法),之后看下。
紧跟着了解下 get 和 set 方法
public E get(int index) {
if (index >= size)
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
return (E) elementData[index];
}
直接取出数组 index 位置下标,我们知道,计算机会给每个内存单元分配一个地址,计算机通过地址来访问内存中的数据。当计算机需要随机访问数组中的某个元素时,它会首先通过下面的寻址公式,计算出该元素存储的内存地址。
//其中 data_type_size 表示数组中每个元素的大小
//base_address 首字节
a[i]_address = base_address + i * data_type_size
比如,我们拿一个长度为 10 的 int 类型的数组 int[] a = new int[10] 来举例。计算机给数组 a[10],分配了一块连续内存空间 1000~1039,其中,内存块的首地址为 base_address = 1000。数组中存储的是 int 类型数据,所以 data_type_size 就为 4 个字节。这个公式非常简单,我就不多做解释了。
下面给出set方法,一目了然了。
public E set(int index, E element) {
if (index >= size)
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
E oldValue = (E) elementData[index];
elementData[index] = element;
return oldValue;
}
trimToSize方法,三目表达式,也是一目了然。
public void trimToSize() {
modCount++;
if (size < elementData.length) {
elementData = (size == 0)
? EMPTY_ELEMENTDATA
: Arrays.copyOf(elementData, size);
}
}
四、LinkedList
我们肯定可以猜到 LinkedList 不会有容量大小的构造函数。既然它是一个双向链表,我们重点看下它的链表结构体。
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;
}
}
代码中注释较多,不多解释了。
添加方法add,注意,LinkedList 更提供了 addFirst和removeFirst、addLast和removeLast、以及getFirst 和 getLast 等有效的添加、删除和访问表两端的项。
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也是尾节点last了
first = newNode;
else
// 不是空链表的情况,将原来的尾部节点的next指向需要插入的节点
l.next = newNode;
// 更新链表大小和修改次数,插入完毕
size++;
modCount++;
}
addFirst。
public void addFirst(E e) {
linkFirst(e);
}
private void linkFirst(E e) {
final Node<E> f = first;
final Node<E> newNode = new Node<>(null, e, f);
first = newNode;
if (f == null)
//即使头又是尾
last = newNode;
else
f.prev = newNode;
size++;
modCount++;
}
remove。
public E remove(int index) {
checkElementIndex(index);
//checkElementIndex与node不必说了,重点看下unlink
return unlink(node(index));
}
E unlink(Node<E> x) {
// 指定节点的值
final E element = x.item;
// 指定节点的后继节点
final Node<E> next = x.next;
// 定节点的前继节点
final Node<E> prev = x.prev;
// 如果prev为null表示删除是头节点,否则就不是头节点
if (prev == null) {
first = next;
} else {
prev.next = next;
// 置空需删除的指定节点的前置节点
x.prev = null;
}
// 如果next为null,则表示删除的是尾部节点,否则就不是尾部节点
if (next == null) {
last = prev;
} else {
next.prev = prev;
x.next = null;
}
// 置空需删除的指定节点的值
x.item = null;
// 数量减1
size--;
modCount++;
return element;
}
而 get 和 set 就需要调用 node 遍历了。
public E get(int index) {
//检查以下,重点是node,它是查询指定位置元素并返回
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;
}
}
//同理set
public E set(int index, E element) {
checkElementIndex(index);
Node<E> x = node(index);
E oldVal = x.item;
x.item = element;
return oldVal;
}
五、《数据结构与算法分析》原文
ArrarList 的优点在于对 get 和 set 的调用花费常数时间。其缺点是新项的插入和现有项的删除代价昂贵,除非变动是在 ArrayList 的末端进行。
LinkedList 的优点在于新项的插入和现有项的删除均开销很小,这里假设变动项的位置是已知的。其缺点是它不容易作索引,因此对 get 的调用是昂贵的,除非调用非常接近表的端点(如果对 get 的调用是对接近表后面的项进行,那么搜索的进行可以从表的后面开始)。
关于ArrayList、LinkedList 中 ListIterator 接口,这里不多赘述了。