上一篇我们通过简单的ArrayList的源码分析,分析了ArrayList为什么有序,为什么读操作快?为什么写操作慢?详情请穿越 jdk1.8-Java集合框架之二-ArrayList源码分析
这篇我们讲解的是LinkedList,同样我们是带着问题来的:
可以看出,LinkedList继承了一个父类AbstractSequentialList,另外实现了4个接口,分别是Cloneable、List、Deque、Serializable。
父类AbstractSequentialList是一个抽象类,提供部分已经实现的功能,单与本次讲解的LinkedList内容关系不大,暂且不谈,有兴趣的同学可以自行了解,
回想一下ArrayList的源码,发现ArrayList实现的接口与LinkedList实现的接口差别就在于ArrayList实现了RandomAccess接口而LinkedList没有,LinkedList实现了Deque接口而ArrayList没有。
what?Deque接口又是什么鬼??
public interface Deque<E> extends Queue<E> 复制代码
Deque接口是Queue接口的子接口,且从名字上我们可以猜出,应该是双向队列。
从继承关系图可以得出两个结论,1:LinkedList没有实现RandomAccess接口说明LinkedList不支持快速访问;2.LinkedList实现了Deque接口说明是一个双向队列的数据结构。
//长度 transient int size = 0; //指向第一个节点的指针 transient Node<E> first; //指向最后一个节点的指针 transient Node<E> last; 复制代码
就这样完了??
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; } } 复制代码
其实这个Node类是LinkedList的私有静态内部类。这个类定义了三个属性,一个是与LinkedList范型一致的item,另外两个属性类型都是Node,next指向Node对象的下一个Node对象,prev指向Node对象的前一个Node对象。
很明显,Node类这样的设计,我们可以回答一开始带来的第一个问题,LinkedList就是一个链表的数据结构设计,每一个对象都持有指向前一个对象的引用,也都持有指向后一个对象的引用,这种结构天生就是有序的。
先从构造方法入手,LinkedList提供了两个构造方法,无参数的构造方法没有任何实现,我们直接看有参数的构造方法:
public LinkedList(Collection<? extends E> c) { this(); addAll(c); } 复制代码
该构造方法需要传入一个Collection集合实现类对象,直接将集合对象放进addAll方法里,我们再看allAll方法:
public boolean addAll(Collection<? extends E> c) { return addAll(size, c); } 复制代码
addAll方法又调用了addAll的重载方法,并传入了两个参数,第一个是LinkedList的实例变量size,第二个是构造方法传进来的Collection集合对象。继续看addAll的这个重载方法:
public boolean addAll(int index, Collection<? extends E> c) { //检查index是否合法 checkPositionIndex(index); //将Collection实例转为Object数组 Object[] a = c.toArray(); int numNew = a.length; if (numNew == 0) return false; Node<E> pred, succ; //index等于size时,只有两种情况: //1.LinkedList中没有元素 //2.要添加的元素位置等于LinkedList长度 if (index == size) { succ = null; pred = last; } else { //寻找index位置存在的元素 succ = node(index); //找到index位置存在的元素的前一个元素 pred = succ.prev; } for (Object o : a) { @SuppressWarnings("unchecked") E e = (E) o; //将对象包装成一个Node对象 Node<E> newNode = new Node<>(pred, e, null); if (pred == null) //如果index位置前一个元素为null,表示当前添加的是第一个元素 //将当前元素的包装对象设置为第一个 first = newNode; else //否则设置index位置前一个元素的下一个元素为当前新添加的元素 pred.next = newNode; //添加完成,需要将pred设置为新添加的元素 pred = newNode; } if (succ == null) { //如果添加前长度为0 //或者添加元素的位置在LinkedList长度之外 //那么LinkedList最后元素应该是最后添加的元素 last = pred; } else { //否者表示在已有元素中间添加元素 //那么最后添加的元素的下一个元素应该是添加前index位置对应的元素 pred.next = succ; //添加前index位置的元素,设置前一个元素为最后添加的元素 succ.prev = pred; } //LinkedList长度增加 size += numNew; modCount++; return true; } 复制代码
核心代码的逻辑相对ArrayList复杂了点,但也是比较简单的。其实整个LinkedList的结构就像俄罗斯套娃,每层套娃都用线连着外面和里面的套娃。
那么针对第二个问题,LinkedList为什么是读效率低,写效率高呢,我们接着往下看:
public E get(int index) { checkElementIndex(index); return node(index).item; } Node<E> node(int index) { //因为LinkedList实现了Deque接口,所以LinkedList还是双向队列 //所以此处查找元素,最大的查找范围是最大长度的1/2 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; } } 复制代码
还记得ArrayList的读操作吗,是通过数组下标直接找到对应元素,复杂度是O(1),而LinkedList是链表结构的,并没有下标,所以只能从头到尾遍历,或者从尾到头遍历查找,复杂度是O(n),所以LinkedList相对ArrayList来说,读效率很低。
那么为什么又说写效率高呢??我们接着看:
public void add(int index, E element) { checkPositionIndex(index); if (index == size) linkLast(element); else linkBefore(element, node(index)); } 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++; } 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++; } 复制代码
上面代码不难看出,LinkedList添加元素,其实只需要设置添加的元素的前后元素引用,并将前后元素的相关属性设置为所需要添加的元素的引用,就完成了元素的添加。同理,删除元素,只需要移除元素,并将元素的前后元素相关属性重新设置,则完成删除操作。整个LinkedList的单个元素写操作,最多涉及3个元素的修改。
相对ArrayList添加或者删除元素都需要重新计算容量甚至扩容,再将部分甚至全部元素复制再移位的操作来说,LinkedList的写操作效率实在高太多。
对于最后一个问题,讲到目前为止,相信不用说大家都知道了。LinkedList允许添加重复的值,也允许添加null值,因为LinkedList并不是直接维护添加的元素,而是将添加的元素包装成Node对象来维护,所以添加什么元素都无所谓了。