List 接口继承自Collection接口,并在Collection接口的基础上添加了大量的方法,使得可以在List 的中间插入和删除元素。
List 按照插入的顺序来保存元素,它常用的实现类主要有两种:ArrayList和LinkedList,其中ArrayList 擅长随机访问元素操作,但是在中间插入和存取元素时比较慢;而LinkedList在中间进行插入和删除的操作比较快,随机访问方面相对较慢。只要阅读以下这两个类的底层实现方式,就很容易理解它们各自的优势。
ArrayList:
ArrayList 的列表对象实质是存放在一个引用型的数组内,并通过不断的改变该引用型数组的指向来改变数组的容量。
ArrayList 类内部:
private transient Object[] element;//存放列表对象的数组的引用,
private int size; //用来记录列表中实际对象的数量;
同时还引入Capacity,用来不断调整存放对象的数组的大小。
ArrayList 随机访问元素:
因为底层是通过Object类型的数组实现,所以可以直接通过数组下标来访问任何元素。
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++;
}
可以看出,当向ArrayList的index位置中插入元素时,包括index以后的所有元素都要向后移动一位,需要进行很多的赋值操作,所以ArrayList在中间插入和删除代价较大。
当向ArrayList增加元素时,必须保证数组的容量能够确保这个元素还能再加进去。这时候如果数组的size与capacity的值一样,就必须增大capacity的值,使add操作能顺利进行。ArrayList 通过下面方法实现自动改变数组大小机制:
private void ensureCapacityInternal(intminCapacity) {
modCount++;
// add 方法前面通常会有ensureCapacityInternal(size + 1),当size + 1的值比数组容量的值小时,则不做任何操作,当size + 1的值比数组容量的值大时,则要扩大数组的容量。
if (minCapacity - elementData.length> 0)
grow(minCapacity);
}
private staticfinal int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
private voidgrow(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);
//这步操作是关键,通过改变elementData的引用来实现数组容量的自动改变机制。
element = Arrays.copyOf(elementData, newCapacity);
}
private static int hugeCapacity(intminCapacity) {
if (minCapacity < 0) //overflow
throw newOutOfMemoryError();
return (minCapacity > MAX_ARRAY_SIZE)?
Integer.MAX_VALUE :
MAX_ARRAY_SIZE;
}
通过以上代码,我们可知java自动增加ArrayList大小的思路是:向ArrayList添加对象时,原对象数目加1后如果大于数组长度,则建一个适当长度的新数组,并将原数组的内容进行拷贝,然后让element对这个新数组进行引用。原数组会自动抛弃(java垃圾回收机制会自动回收)。
LinkedList:
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 通过Node<E> first,Node<E> last两个属性用来标记该链表的首结点和尾结点。
LinkedList 在中间增加和删除元素:
LinkedList 找到index所在位置的结点,然后通过改变该位置结点的prev和next 的引用来进行插入和删除操作。
public void add(intindex, E element) {
checkPositionIndex(index);
if (index == size)
linkLast(element);
else
linkBefore(element, node(index));
}
void linkBefore(E e, Node<E>succ) {
// assert succ != null;
final Node<E> pred = succ.prev;
final Node<E> newNode = newNode<>(pred, e, succ);
succ.prev = newNode;
if (pred == null)
first = newNode;
else
pred.next = newNode;
size++;
modCount++;
}
LinkedList随机查找的操作:
public E get(intindex) {
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;
}
}
如果index比size/2小,就从前往后找,如果index比size/2大,就从后往前找。虽然对查找进行了一定的优化,但是查找时间级别仍是O(n),而ArrayList随机访问的时间是O(1)。