LinkedList -> AbstractSequentialList -> List
同时实现了接口Deque, Cloneable, Serializable
书同上文,LinkedList就是上学时学的链表,很多公司,比如华为的应届基础面试题很多就是考的这个,比如链表反转,双向链表等。Java openJDK里的LinkedList理念上和这个并没有本质区别,从继承结构可以看出,这个LinkedList实现了List接口,那就是有序的线性结构,同时也实现了Deque(读音,待客)就是双向链表的意思,有head有tail,根据不同场景可以当队列和栈来用。当然它的插入删除都比数组Array的开销要小很多,毕竟就只有一些指针的指向变动,而数组是要真的复制数据,移动数据,有较重的内存拷贝等操作,缺点也很明显,无法RandomAccess,不可能像数组一样做到指哪打哪。这个来看看它是如何实现的,捡重点看。
构造 - Constructor
transient int size = 0;
transient Node<E> first;
transient Node<E> last;
内部有三个变量,大小size,head首元素first和tail元素last,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;
}
}
一个私有静态内部类,方便内部的数组结构组织,其实也很简单,就三个要素,节点的信息item,前一个节点指针prev,后一个节点指针next。
两个构造函数,一个无参构造器和一个集合构造器签名为addAll(Collection<? extends E> c),无参构造器什么也没有不谈了。重点看下这个传入一个Collection的构造器,里面调用的addAll(size,c)。这个函数的意思就是从size所在的index开始把集合c加入到LinkedList中。
要做到这点,首先要定位到这个index所指向的元素,这里源码里用了一个位操作的技巧。
Node<E> node(int index) {
if (index < (size >> 1)) {
Node<E> x = first;
// Iterate & Search
} else {
Node<E> x = last;
// Iterate & Search
}
}
size >> 1
意思就是把size变为2进制然后往右移一位,也就是除以2,这个其实就是为了判断这个index是更靠近head还是tail,如果小于中间的index,那就从head找,否则从tail找。找到这个node后,后面就很简单了,把要插入的node的prev指向这个节点的prev,以此类推可以把整个collection插入进去,其实就是头插法,最后把collection的最后一个元素的next指向之前找到的这个node,然后再把node的prev指向collection里的最后一个元素,完成。整个LinkedList的size和modCount都要变,size变为原大小加collection的大小之和,modCount加一。这里再说下这个modCount,它存在的目的就是为了避免多个修改集合的操作导致脏读,设计原则是fast-fail,如果在遍历期间有其他操作改变了这个集合,那么在下次取操作时立刻抛出异常告诉用户这个集合改变了,无法继续遍历。
增 - Add
一共三个方法
public boolean add(E e)
public void add(int index, E element)
public void addFirst(E e)
public void addLast(E e)
public boolean addAll(Collection<? extends E> c)
这里addAll因为之前提过了就不再进一步讨论了,add(E e)有两个版本一个是尾插法,直接把node插入到尾部,也可以利用addAll,传入尾部的index做插入。add(index,element)如果index不是size的话,就在这个元素之前做插入,如果是size的话就在tail后再插入一个新节点。addFirst就是头插法,在head之前插入一个节点,然后把这个节点变为head,addLast与之相反。
删 - Remove
public E remove()
默认删除head元素,如果有的话把head下一个元素变为head,同时把head的prev和next都指向null,帮助GC回收内存。
E remove(int index)
删除指定index的元素,利用之前提到的方法定位到元素,再用同样的方法把该节点prev和next都置null。
public boolean remove(Object o)
如果object为null,删除第一个item为null的节点,如果不为空,则删除满足equals(object)的节点。
还有removeFirst和removeLast,其实就是判断head和tail,如果不为空则删除。至于像removeFirstOccurrence和removeLastOccurrence,一个从前往后找,一个从后往前找,找打就删掉该节点。
改 - update/replace
LinkedList里没有replace方法,也没有update方法,但有set方法。当然你也可以创建一个新节点,然后把这个节点插入进去并删除原有的节点。或者找到要修改的节点,然后修改节点内部的item。还有个replaceAll,来自List接口的default实现,传入一个UnaryOperator,对集合的所有元素做一个mapping转换操作。set方法原理类似,只不过需要指定下标index,然后用新节点替换这个index所在的节点。
查 - get/read
public E get(int index)
利用前文提到的node方法,找到指定下标的节点并返回。
getFirst和getLast原理很简单,这里不赘述了,就是返回head和tail,如果为null就抛出NoSuchElementException。
offer & poll
生产者消费者模型,相关的方法还有peek,take,remove,add,put等。叫法大同小异。因为LinkedList实现了Deque,所以是有头有尾的双向链表,生产者可以不停的做尾插法往tail加入node,消费者可以不停的从head取走并删除元素。offer就是生产者,提供一个node,poll就是消费者,取走一个node。在LinkedList里的offer和poll是非阻塞的,如果call的时候没有元素可以poll那么就返回null。当然也可以继承这个LinkedList把这个改造成阻塞的版本,加一个timeout即可。后面在讲BlockingQueue的时候会细讲,这里不展开。
push & pop
典型的栈操作,默认实现是从头部插入,从头部删除,这里不赘述了。
以上就是LinkedList里所有的比较重要的方法和用法。