目录
List接口是Collection接口下的子接口List中的元素是有序的,可以重复的,List接口的主要实现类有三个:ArrayList、LinkedList和Vector。
一、相同之处
- 都是List接口的实现类,因此他们具有List列表的共同特性,他们是有序的,可以容纳重复的元素,允许元素为null。
- 都可以使用Collection下的Iterator接口的iterator()方法和Iterator接口的子接口ListIterator的listIterator()方法,并且Iterator和ListIterator迭代器都具有fail-fast机制。
二、不同之处
1、ArrayList与LinkedList区别
(1)底层数据结构不同,ArrayList和Vector都是基于动态数组实现的,LinkedList是基于双向链表实现的。
ArrayList是由Object类型的数组实现的,如果传入初始容量参数,则创建一个initialCapacity大小的数组,如果没有传入initialCapacity,那么创建一个默认大小(10)的数组。
(2)实现接口不同,除了都有的Serializable、Cloneable、Iterable、Collection和List,ArrayList还实现了RadomAccess接口,LinkedList还实现了Deque接口
(3)不同操作效率不同,对于随机访问get和set效率不同,ArrayList优于LinkedList,因为LinkedList要移动指针遍历得到待查找的node。对于新增和删除操作add和remove效率不同,LinedList比较占优势,因为ArrayList要移动数据。
get和set | add | remove | |
ArrayList | O(I) | O(n) | O(n) |
LinkedList | O(n) | O(I) | O(n) |
这一点要看实际情况的。若只对单条数据插入或删除,ArrayList的速度反而优于LinkedList。但若是批量随机的插入删除数据,LinkedList的速度大大优于ArrayList. 因为ArrayList每插入一条数据,要移动插入点及之后的所有数据。
ArrayList的get
public E get(int index) {
rangeCheck(index);
return elementData(index);
}
E elementData(int index) {
return (E) elementData[index];
//elementData是用于存储元素的动态数组
}
可以看出ArrayList的get就是从数组进行读取,效率为O(1)。
LinkedList的get
public E get(int index) {
checkElementIndex(index);
return node(index).item;
}
Node<E> node(int index) {
// assert isElementIndex(index);
if (index < (size >> 1)) { //判断index是否超过size的一般,如果不是则从head开始遍历
Node<E> x = first;
for (int i = 0; i < index; i++)
x = x.next;
return x;
} else { //否则从tail开始遍历
Node<E> x = last;
for (int i = size - 1; i > index; i--)
x = x.prev;
return x;
}
}
可以看出,虽然LinkedList实现了List接口,因此必须实现get(index)方法,但是get()方法是使用迭代器遍历来实现的,因此效率为O(n)。
ArrayList的add和remove:通过System.arraycopy来实现
add操作:
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++;
}
remove操作:
public E remove(int index) {
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;
}
可以看出ArrayList的add和remove操作都是通过System.arraycopy来实现的,需要移动index后每一个元素,操作效率为O(n)。
LinkedList的add和remove:通过link和unlink来实现
add操作:
public void add(int index, E element) {
checkPositionIndex(index); //查看index是否在链表范围内
if (index == size)
linkLast(element);
else
linkBefore(element, node(index));
}
public void linkLast(E e) {
final Node<E> l = last;
final Node<E> newNode = new Node<>(l, e, null); //Node(NOde <E> pre, E element, Node<E> next)
last = newNode;
if (l == null) //如果链表还是空链表,那么把当前节点设为头节点
first = newNode;
else
l.next = newNode; //否则把当前节点添加到last后面
size++;
modCount++; //用于fail-fast机制
}
public void linkBefore(E e, Node<E> succ) { //把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) //如果succ是头结点,那么把当前节点设置为头结点
first = newNode;
else
pred.next = newNode;
size++;
modCount++;
}
可以看出,由于add操作只需要修改双向链表的next和prev指针的地址,因此操作效率为O(1)。
remove操作:
//删除index下标位置的元素
public E remove(int index) {
checkElementIndex(index); //检查index是否在链表范围内
return unlink(node(index));
}
//删除元素o,因为LinkedList中的元素是可以重复且为null的,所以要遍历整个表,把所有o元素都删掉
public boolean remove(Object o) {
if (o == null) {
for (Node<E> x = first; x != null; x = x.next) {
if (x.item == null) {
unlink(x);
return true;
}
}
} else {
for (Node<E> x = first; x != null; x = x.next) {
if (o.equals(x.item)) {
unlink(x);
return true;
}
}
}
public E unlink(Node<E> x) {
// assert x != null;
final E element = x.item;
final Node<E> next = x.next;
final Node<E> prev = x.prev;
if (prev == null) {
first = next;
} else {
prev.next = next;
x.prev = null;
}
if (next == null) {
last = prev;
} else {
next.prev = prev;
x.next = null;
}
x.item = null;
size--;
modCount++; //用于fail-fast机制
return element;
}
可以看出,虽然unlink操作的效率是O(1),但是不论是索引(index)删除还是元素(object)删除都需要遍历最多半个链表(索引删除调用node方法进行了遍历查找)才能找到要删除的node,因此效率为O(n)
(4)占用内存不同,LinkedList 比 ArrayList 需要更多的内存,因为链表除了数据本身还需要存储next指针指向下一个node的地址。
2、ArrayList与Vector区别
(1)同步性不同,LinkedList和ArrayList是不同步的,Vector是同步的(使用synchronized进行强同步)。
(2)扩容方法不同,LinkedList基于链表实现,容量是无限的;ArrayList和Vector是基于数组实现的,容量是有限的,在需要的时候要扩容,但是ArrayList和Vector的扩容方式稍微不同,ArrayList是newCapacity=oldCapacity*3/2-1,而Vector是与“增长系数有关”,若指定了“增长系数”,且“增长系数有效(即大于0)”;那么,每次容量不足时,“新的容量”=“原始容量+增长系数”。若增长系数无效(即,小于/等于0),则“新的容量”=“原始容量 x 2”。
ArrayList的动态数组的扩容
因为数组的大小是固定的,而ArrayList的大小是动态可调的,那么他是如何实现的呢?首先,由ensureCapacity()方法确认ArrayList容量,然后由grow()方法扩容。
//ensureCapacity()方法用于确认当前的elementData[]数组是否能容纳minCapacity的元素
public void ensureCapacity(int minCapacity) {
int minExpand = (elementData != DEFAULTCAPACITY_EMPTY_ELEMENTDATA)
// any size if not default element table
? 0
// larger than default for default empty table. It's already
// supposed to be at default size.
: DEFAULT_CAPACITY;
if (minCapacity > minExpand) {
ensureExplicitCapacity(minCapacity);
}
}
//ensureExplicitCapacity()方法用于扩容,也就是说调用这个函数说明明确的要扩容,扩容对数组的结构进行了修改,因此modcount加1
private void ensureExplicitCapacity(int minCapacity) {
modCount++;
// overflow-conscious code
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
//数组的扩容:
//首先计算需要的容量,newCapacity = oldCapacity + (oldCapacity >> 1),说明newCapacity是oldCapacity的大概3/2,如果
//确切一点,就是newCapacity = 3/2*oldCapacity-1;
//然后,如果newCapacity<minCapacity,那么newCapacity设为minCapacity
//最后调用Array.copyOf()方法把旧数组复制到新数组中
private void grow(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);
// minCapacity is usually close to size, so this is a win:
elementData = Arrays.copyOf(elementData, newCapacity);
}
这里我们来看看Array.copyOf( )是怎么操作的:
public static <T,U> T[] copyOf(U[] original, int newLength, Class<? extends T[]> newType) {
@SuppressWarnings("unchecked")
T[] copy = ((Object)newType == (Object)Object[].class)
? (T[]) new Object[newLength]
: (T[]) Array.newInstance(newType.getComponentType(), newLength);
System.arraycopy(original, 0, copy, 0,
Math.min(original.length, newLength));
return copy;
}
可以看到Array.copyOf( )方法中,是新建了一个数组,然后把就数组拷贝在新数组中,此外,如果新数组和旧数组的类型不同,还需要把旧类型轻质转换为新类型。那么可以看出,ArrayList的扩容是通过新建一个数组来实现的。
三、使用场景
如果涉及到“队列”、“栈”和“链表”等场景,那么可以使用List下的实现类,具体要根据需求和各个实现类的特点选择。
- 如果需要快速随机访问,那么使用ArrayList
- 如果需要频繁的插入和删除中间节点,那么使用LinkedList
- 如果需要在多线程中同步,那么使用Vector,但是由于历史原因Vector基本已经被弃用,可以用java.util.Concurrent包下的concurrentLinkedDeque或者concurrentQueue来代替Vector