List为一个接口,直接继承Collection接口,对比他们的接口变化:除了Collection接口中size(),isEmpty()等方法,其增加了基于下标index的一系列方法,摘抄部分接口方法:

get(int)

set(int, E)

add(int, E)

remove(int)

indexOf(Object)

lastIndexOf(Object)

subList(int, int)

我们简单看一下ArrayList,

首先看一下成员变量

private static final int DEFAULT_CAPACITY = 10;
private static final Object[] EMPTY_ELEMENTDATA ={};
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA ={};
transient Object[] elementData;
private int size;

elementData为内部实际存储元素的数组,即ArrayList是基于数组的存储结构,我们get(int),set(int,E)等方法,实质也是操作数组来实现;

看一下加入元素方法:

public void add(intindex, E element) {if (index > size || index < 0)throw newIndexOutOfBoundsException(outOfBoundsMsg(index));
ensureCapacityInternal(size+ 1); //Increments modCount!!
System.arraycopy(elementData, index, elementData, index + 1,
size-index);
elementData[index]=element;
size++;
}

主要做了输入数据合法性检查,容器扩容检查,数组复制,数组插入位置赋值,size尺寸增加;

我们从ensureCapacityInternal()方法开始:

private void ensureCapacityInternal(intminCapacity) {if (elementData ==DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
minCapacity=Math.max(DEFAULT_CAPACITY, minCapacity);
}
ensureExplicitCapacity(minCapacity);
}private void ensureExplicitCapacity(intminCapacity) {
modCount++;if (minCapacity - elementData.length > 0)
grow(minCapacity);
}private void grow(intminCapacity) {//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);
}

关于容器扩容检查的几个方法,我们看一下主要流程,最终参数minCapacity的值为size+1,通过grow()方法,newCapacity其中一种尝试是赋值为elementData数组长度的1.5倍,并通过Arrays.copyOf()方法实现elementData数据最终扩容;

完成扩容检查之后,我们进行数组复制,因为此时elementData数组已经扩容,所以可以从index位置统一移动到index+1位置,预留出index位置;

最后我们把index位置赋值为我们add的元素,整个即完成来ArrayList在特定位置增加元素的过程;

我们在简单看一下LinkedList:

LinkedList除了具备List接口的所有特性,它还实现了Deque接口,可以先看List接口相关的;我们看一下成员变量:

transient int size = 0;
transient Nodefirst;
transient Node last;

成员变量只有三个,主要存储了首尾Node元素,看一下Node结构:

private static class Node{
E item;
Nodenext;
Nodeprev;
Node(Node prev, E element, Nodenext) {this.item =element;this.next =next;this.prev =prev;
}
}

是一个私有数据存储类,包含实际存储的元素item以及指向前后Node的变量;是一种基于链表结构的容器;

我们也看一下加入元素方法:

public void add(intindex, E element) {
checkPositionIndex(index);if (index ==size)
linkLast(element);elselinkBefore(element, node(index));
}

主要包括输入参数检查,如果是末尾添加linkList(),如果不是末尾添加linkBefore();

主要看一下linkBefore()方法,输入参数node(index):

Node node(intindex) {//assert isElementIndex(index);
if (index < (size >> 1)) {
Node x =first;for (int i = 0; i < index; i++)
x=x.next;returnx;
}else{
Node x =last;for (int i = size - 1; i > index; i--)
x=x.prev;returnx;
}
}

从成员变量我们知道,LinkedList不像ArrayList那样维护一个内部数组,而是通过包装类Node持有存入的元素和在它之前和之后的Node引用;所以当我们想要找到某一个元素时,只能够从成员变量的first开始位置的Node或者结束位置last的Node变量逐个逐个查找;通过node()方法可以看到,分析当index小于容器长度一半时,是从first位置开始遍历,由第一个Node的next引用可以找到第二个Node,第二个Node的next引用找下一个,直到小于index时的Node;

回到linkBefore()方法,我们已经找到index位置的Node,在以此为基础add进入一个新的的Node,即add进新的元素;

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

我们简单看一下,主要目的是new一个新的Node对象newNode,newNode需要保存add进入容器的元素element,将prev和next分别赋值上一个Node和下一个Node,上一个Node即从传入的Node参数获取,下一个即传入参数Node本身,同时跟新传入Node的上一个引用指向新插入的newNode,最终完成了LinkedList指定位置add元素的整个流程;

从两种List集合的add方法的分析过程可以看出,删除和插入元素ArrayList涉及到内部数组的赋值移动,而LinkedList又涉及到内部遍历,效率都不是很好;如果我们想拿到具体位置的元素,ArrayList可以通过index索引直接得到,但是LinkedList需要从头部或者尾部逐个遍历才能拿到具体位置的元素;如果操作首尾元素,而linkedList则更具有效率;