List 源码解析
ArrayList
是基于数组实现的。 区别:在ArrayList管理下插入数据时按需动态扩容、数据拷贝等操作。
初始化
- 空构造函数初始化; new ArrayList(); 默认是一个空数组
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
- 指定长度初始化,在已知元素个数的情况下,指定size,可以提供性能,减少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);
}
}
- Collection参构造函数初始化
public ArrayList(Collection<? extends E> c) {
// 1. 先转为数组
Object[] a = c.toArray();
if ((size = a.length) != 0) {
if (c.getClass() == ArrayList.class) {
elementData = a;
} else {
// 2. 在拷贝到新数组
elementData = Arrays.copyOf(a, size, Object[].class);
}
} else {
// replace with empty array.
elementData = EMPTY_ELEMENTDATA;
}
}
- Collections.ncopies Collections.nCopies 是集合方法中用于生成多少份某个指定元素的方法,接下 来就用它来初始化 ArrayList,如下: // 核心使用了这个:Arrays.fill(a, 0, n, element); 创建了数组长度为n,默认元素为element ArrayList list = new ArrayList(Collections.nCopies(10, 0));
public class Collections {
private static class CopiesList<E> extends AbstractList<E> implements RandomAccess, Serializable {
final int n;
final E element;
CopiesList(int n, E e) {
assert n >= 0;
this.n = n;
element = e;
}
public Object[] toArray() {
final Object[] a = new Object[n];
if (element != null)
Arrays.fill(a, 0, n, element);
return a;
}
}
}
炫技:
// 普通方式
ArrayList list = new ArrayList();
list.add("11");
list.add("22");
// 匿名内部类方式
ArrayList list = new ArrayList(){{add("11");add("22");}};
// 用是Arrays.asList
ArrayList<String> list = new ArrayList<String>(Arrays.asList("aaa", "bbb", "ccc"));
/**
* 1.8 中list1.toArray()
* 使用的是:Arrays.copyOf(a, a.length, Class<?extends T[]>);导致类型比较不一致
* 1.9之后修复了这个bug,使用的是:Arrays.copyOf(a, a.length, Object[].class);
* Arrays.asList()得到的ArrayList是Arrays下的内部类,和 java.util下的ArrayList不是* 同一个,Arrays中的ArrayLis不可进行不能赋值给 ArrayList,新增元素,删除元素操作。
*/
List<Integer> list1 = Arrays.asList(1, 2, 3);
System.out.println("通过数组转换: " + (list1.toArray().getClass() == Object[].class));
ArrayList<Integer> list2 = new ArrayList<Integer>(Arrays.asList(1, 2, 3));
System.out.println("通过集合转换: " + (list2.toArray().getClass() == Object[].class));
插入元素
- 判断长度充足;ensureCapacityInternal(size + 1);
- 当判断长度不足时,则通过扩大函数,进行扩容;grow(int minCapacity)
- 扩容的长度计算;int newCapacity = oldCapacity +(oldCapacity >> 1);,旧容量 + 旧容量右移 1 位,这相当于扩容了原来容量的(int)3/2。 4. 10,扩容时:1010 + 1010 >> 1= 1010 + 0101 = 10 + 5 =15 2. 7,扩容时:0111 + 0111 >> 1 = 0111 + 0011 = 7 + 3 = 10
- 当扩容完以后,就需要进行把数组中的数据拷贝到新数组中,这个过程会用到 Arrays.copyOf(elementData, newCapacity);,但他的底层用到的 是;System.arraycopy
>>1 右移一位相当于对原来的数据进行除于2的操作 宽容到了:x + x/2 = 3x/2 所以是扩容到了原来的3/2
return elementData = Arrays.copyOf(elementData,
newCapacity(minCapacity));
指定位置插入
list.add(2, "1");
public void add(int index, E element) {
rangeCheckForAdd(index);
modCount++;
final int s;
Object[] elementData;
if ((s = size) == (elementData = this.elementData).length)
elementData = grow();
// 核心方法
System.arraycopy(elementData, index,
elementData, index + 1,
s - index);
elementData[index] = element;
size = s + 1;
}
private void rangeCheckForAdd(int index) {
// 下标是否大于当前容器size,如果大于抛异常
if (index > size || index < 0)
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
删除
public boolean remove(Object o) {
final Object[] es = elementData;
final int size = this.size;
int i = 0;
found: {
if (o == null) {
for (; i < size; i++)
if (es[i] == null)
break found;
} else {
for (; i < size; i++)
if (o.equals(es[i]))
break found;
}
return false;
}
fastRemove(es, i);
return true;
}
public E remove(int index) {
Objects.checkIndex(index, size);
final Object[] es = elementData;
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;
}
总结
ArrayList是基于数组实现的,在遍历和查询的效率高,但是在插入和删除操作时,涉及到元素的迁移,性能就不是那么高了。
LinkedList
底层是基于链表实现的,由双向链条next、prev,把数据节点穿插起来。
初始化
// 常规操作
LinkedList<Integer> list1 = new LinkedList<>();
list1.add(1);
list1.add(2);
list1.add(3);
System.out.println(list1);
// 使用Arrays.asList
LinkedList<Integer> list2 = new LinkedList<>(Arrays.asList(1,2,3));
System.out.println(list2);
// 使用匿名内部类
LinkedList<Integer> list3 = new LinkedList<>(){{add(1);add(2);add(3);}};
System.out.println(list3);
// 使用Collections.nCopies初始化
LinkedList<Integer> list4 = new LinkedList<>(Collections.nCopies(10,0));
System.out.println(list4);
插入
头插
public void addFirst(E e) {
linkFirst(e);
}
private void linkFirst(E e) {
// first,首节点会一直被记录,这样就非常方便头插。
final Node<E> f = first;
// 插入时候会创建新的节点元素,紧接着把新的头元素赋值给 first
final Node<E> newNode = new Node<>(null, e, f);
first = newNode;
// 之后判断 f 节点是否存在,不存在则把头插节点作为最后一个节点、存在则用 f 节点的上一个链条 prev 链接
if (f == null)
last = newNode;
else
f.prev = newNode;
// 最后记录 size 大小、和元素数量 modCount。modCount 用在遍历时做校验,modCount != expectedModCount
size++;
modCount++;
}
// ArrayList 和 LinkedList 头插性能比较
// ArrayList 需要做大量的位移和复制操作非常耗时,而LinkedList 的优势就体现出来了,耗时只是实例化一个对象。
public static void main(String[] args) {
int total = 10000000;
testLinkedList(total);
testArrayList(total);
}
private static void testArrayList(int total) {
StopWatch stopWatch = new StopWatch();
stopWatch.start("testArrayList");
ArrayList<Integer> list = new ArrayList<>();
for (int i = 0; i < total; i++) {
list.add(0, i);
}
stopWatch.stop();
System.out.println(stopWatch.getLastTaskName() + "耗时:" + stopWatch.getTotalTimeMillis());
}
private static void testLinkedList(int total) {
StopWatch stopWatch = new StopWatch();
stopWatch.start("testLinkedList");
LinkedList<Integer> list = new LinkedList<>();
for (int i = 0; i < total; i++) {
list.addFirst(i);
}
stopWatch.stop();
System.out.println(stopWatch.getLastTaskName() + "耗时:" + stopWatch.getTotalTimeMillis());
}
尾插
public void addLast(E e) {
linkLast(e);
}
void linkLast(E e) {
// 与头插代码相比几乎没有什么区别,只是 first 换成 last
final Node<E> l = last;
// 耗时点只是在创建节点上,Node<E>
final Node<E> newNode = new Node<>(l, e, null);
last = newNode;
if (l == null)
first = newNode;
else
l.next = newNode;
size++;
modCount++;
}
/**
* ArrayList 和 LinkedList 尾插性能比较
* ArrayList 不需要做位移拷贝也就不那么耗时了,而 LinkedList 则需要创建大量的对象。所以这里 ArrayList 尾插的效果更好一些。
*/
public static void main(String[] args) {
int total = 10000000;
testLinkedList(total);
testArrayList(total);
}
private static void testArrayList(int total) {
StopWatch stopWatch = new StopWatch();
stopWatch.start("testArrayList");
List<Integer> list = new ArrayList<>();
for (int i = 0; i < total; i++) {
list.add( i);
}
stopWatch.stop();
System.out.println(stopWatch.getLastTaskName() + "耗时:" + stopWatch.getTotalTimeMillis());
}
private static void testLinkedList(int total) {
StopWatch stopWatch = new StopWatch();
stopWatch.start("testLinkedList");
LinkedList<Integer> list = new LinkedList<>();
for (int i = 0; i < total; i++) {
list.addLast(i);
}
stopWatch.stop();
System.out.println(stopWatch.getLastTaskName() + "耗时:" + stopWatch.getTotalTimeMillis());
}
中间插
public void add(int index, E element) {
checkPositionIndex(index);
if (index == size)
linkLast(element);
else
linkBefore(element, node(index));
}
// 查询node节点的位置,比较耗时
Node<E> node(int index) {
// assert isElementIndex(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;
}
}
// 执行插入,找到指定位置插入的过程就比较简单了,与头插、尾插,相差不大。
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++;
}
总结: ArraysList的耗时操作在扩容和元素拷贝, LinkedList的耗时在遍历寻找元素位置上。
删除
步骤: 1.确定出要删除的元素 x,把前后的链接进行替换。 2. 如果是删除首尾元素,操作起来会更加容易,这也就是为什么说插入和删除快。但中间位置删除,需要遍历找到对应位置。
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;
}
}
}
return false;
}
// 解链
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扣减
size--;
// modCount标识修改的次数,防止并发修改,存在并发问题会抛ConcurrentModificationException
modCount++;
return element;
}
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
遍历
- 按下标遍历,(不推荐,LinkedList遍历操作比较耗时,时间复杂度O(n))
- 普通for循环
- 增强for循环
- Iterator遍历,迭代器遍历
- forEach循环
- stream流
总结
- ArrayList 与 LinkedList 都有自己的使用场景,如果你不能很好的确定,那么就使用ArrayList。但如果你能确定你会在集合的首位有大量的插入、删除以及获取操作,那么可以使用 LinkedList,因为它都有相应的方法;addFirst、addLast、removeFirst、removeLast、getFirst、getLast,这些操作的时间复杂度都是 O(1),非常高效。
- LinkedList 的链表结构不一定会比 ArrayList 节省空间,首先它所占用的内存不是连续的,其次他还需要大量的实例化对象创造节点。虽然不一定节省空间,但链表结构也是非常优秀的数据结构,它能在你的程序设计中起着非常优秀的作用,例如可视化的链路追踪图,就是需要链表结构,并需要每个节点自旋一次,用于串联业务。
- 程序的精髓往往就是数据结构的设计,这能为你的程序开发提供出非常高的效率改变。可能目前你还不能用到,但万一有一天你需要去造🚀火箭了呢?