一.ArrayList
1.1核心成员变量
ArrayList的底层是一个动态数组,默认大小为10。
transient Object[] elementData;//动态数组
private static final int DEFAULT_CAPACITY = 10;//默认大小
private int size;//已经存在的元素个数
protected transient int modCount = 0;//modCount 用来记录 ArrayList 结构发生变化的次数。
需要注意的是modCount用来记录ArrayList结构变化的次数,结构发生变化是指添加或者删除至少一个元素的所有操作,或者是调整内部数组的大小,仅仅只是设置元素的值不算结构发生变化。主要应用是迭代器的fail-fast中运用。modCount如果出现了预料之外的改变,会抛出ConcurrentModificationException异常。后面会讲到。
1.2核心方法
1.2.1 add()
//add第一个方法 private类型
private void add(E e, Object[] elementData, int s) {
if (s == elementData.length)//如果超过了数组大小,需要扩容
elementData = grow();//扩容
elementData[s] = e;
size = s + 1;
}
//add第二方法 public类型
public boolean add(E e) {
modCount++;//修改次数+1
add(e, elementData, size);//调用add的另一个方法
return true;
}
//add第三个方法,用于特定位置插入
public void add(int index, E element) {
rangeCheckForAdd(index);//判断index是否在数组大小范围内
modCount++;
final int s;
Object[] elementData;
if ((s = size) == (elementData = this.elementData).length)
elementData = grow();
/*
arraycopy的参数为
@param src the source array. 原数组
@param srcPos starting position in the source array. 原数组下标
@param dest the destination array. 目标数组
@param destPos starting position in the destination data. 目标数组下标
@param length the number of array elements to be copied.
移动的元素个数
*/
System.arraycopy(elementData, index,
elementData, index + 1,
s - index);//把elementData中index开始到结尾的元素全部后移一位
elementData[index] = element;
size = s + 1;
}
1.2.2 grow()扩容(了解一下,扩容1.5倍)
注意扩容的时候会重新建一个数组,然后调用copyof(),时间复杂度为O(n),所以初始化的时候,尽量估计数组大小,减少扩容的次数
private Object[] grow(int minCapacity) {
int oldCapacity = elementData.length;
if (oldCapacity > 0 || elementData != DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
int newCapacity = ArraysSupport.newLength(oldCapacity,
minCapacity - oldCapacity, /* minimum growth,正常情况也等于旧容量的一半。 */
oldCapacity >> 1 /*旧容量的一半*/);
return elementData = Arrays.copyOf(elementData, newCapacity);
} else {
return elementData = new Object[Math.max(DEFAULT_CAPACITY, minCapacity)];
}
}
public static int newLength(int oldLength, int minGrowth, int prefGrowth) {
// assert oldLength >= 0
// assert minGrowth > 0
//这里其实就是扩容1.5倍
int newLength = Math.max(minGrowth, prefGrowth) + oldLength;
if (newLength - MAX_ARRAY_LENGTH <= 0) {
return newLength;
}
return hugeLength(oldLength, minGrowth);
}
1.2.3 remove()
同样要把后面的元素全部前移,复杂度比较高。
public E remove(int index) {
Objects.checkIndex(index, size);
final Object[] es = elementData;
@SuppressWarnings("unchecked") E oldValue = (E) es[index];
fastRemove(es, index);
return oldValue;
}
private void fastRemove(Object[] es, int i) {
modCount++;
final int newSize;
if ((newSize = size - 1) > i)
System.arraycopy(es, i + 1, es, i, newSize - i);
es[size = newSize] = null;
}
1.2.4 get()
支持下标查找,时间复杂度为O(1)
public E get(int index) {
Objects.checkIndex(index, size);
return elementData(index);
}
1.3ArrayList和Vector的区别
- Vector使用Synchronize同步,是线程安全的,ArrayList线程不安全。
- Vector扩容时扩容两倍,ArrayList扩容1.5倍
1.4如何实现安全的ArrayList
- 使用 Collections.synchronizedList();
List<String> list = new ArrayList<>();
List<String> synList = Collections.synchronizedList(list);
- 使用concurrent 并发包下的 CopyOnWriteArrayList 类
List<String> list = new CopyOnWriteArrayList<>();
读写分离
写操作在一个复制的数组上进行,读操作还是在原始数组中进行,读写分离,互不影响。
写操作需要加锁,防止并发写入时导致写入数据丢失。
写操作结束之后需要把原始数组指向新的复制数组。
public boolean add(E e) {
synchronized (lock) {
Object[] es = getArray();
int len = es.length;
es = Arrays.copyOf(es, len + 1);
es[len] = e;
setArray(es);
return true;
}
}
public E get(int index) {
return elementAt(getArray(), index);
}
适用场景
CopyOnWriteArrayList 在写操作的同时允许读操作,大大提高了读操作的性能,因此很适合读多写少的应用场景。但是 CopyOnWriteArrayList 有其缺陷:
- 内存占用:在写操作时需要复制一个新的数组,使得内存占用为原来的两倍左右;
- 数据不一致:读操作不能读取实时性的数据,因为部分写操作的数据还未同步到读数组中。
所以 CopyOnWriteArrayList 不适合内存敏感以及对实时性要求很高的场景。
二.LinkedList
2.1核心成员变量
LinkList的底层是一个双向链表,记录了链表的头节点,尾节点和元素个数
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;
}
}
transient int size = 0;
/**
* Pointer to first node.
*/
transient Node<E> first;
/**
* Pointer to last node.
*/
transient Node<E> last;
2.2 核心方法
2.2.1 add()
比较简单,无非就是链到尾端或者前面,增加modCount,增加size
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 = newNode;
else
l.next = newNode;
size++;
modCount++;
}
public void add(int index, 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 = new Node<>(pred, e, succ);
succ.prev = newNode;
if (pred == null)
first = newNode;
else
pred.next = newNode;
size++;
modCount++;
}
2.2.2 get()
需要去遍历链表,时间复杂度为O(n)
public E get(int index) {
checkElementIndex(index);
return node(index).item;
}
Node<E> node(int index) {
// assert isElementIndex(index);
if (index < (size >> 1)) {//小于size的一半,从前往后找
Node<E> x = first;
for (int i = 0; i < index; i++)
x = x.next;
return x;
} else {//大于size的一半,从后往前找
Node<E> x = last;
for (int i = size - 1; i > index; i--)
x = x.prev;
return x;
}
}
三.ArrayList和LinkList的区别
1.ArrayList底层是动态数组,支持下标访问,但是删除要把index后面的元素往前移,增加可能需要扩容,扩容大小为1.5倍,而且要把旧数组复制到新的数组,时间上不友好。LinkedList的底层是一个双向链表,不支持下标访问,查找不够友好,但是删除和增加只需要改变一下链接节点即可,速度很快。
2.ArrayList和LinkList遍历的选择有所差异。ArrayList支持下标访问,所以用for循环通过下标遍历速度最快,LinkedList底层双向链表,通过迭代器iterator速度更快。