JAVA类库分析之LinkedList
1.概述
在java源码中对LinkedList有详细的描述:LinkedList实现了List接口和Deque接口,即表示它支持List的一些常规操作如insert,get,remove等;同时它还支持FIFO双向队列操作如add,poll操作,以及栈和队列的其他操作等。
与Vector和ArrayList不同之处在于它没有实现RandomAccess接口,即表示它不能随机存取,若要访问其中索引为i的元素,则需要从链表头部开始一个个遍历。
此外,LinkedList不是线程安全的,其返回的迭代器也是采用fail-fast的方式,fail-fast方式只是便于用来调试且有一定的性能提升,但并不能保证线程安全。如果多个线程同时访问一个List,则必须自己实现访问同步。一种解决方法是在创建List时构造一个同步的List,如List list = Collections.synchronizedList(new LinkedList(...))。
2.代码分析
LinkedList变量主要有两个,一是代表元素数目的变量size,初始化为0;另一个则是链表的头结点,这里有个重要的类Entry,在很多集合类中都用到了类似的结构,与C语言中的实现原理差不多,初始化为Entry<E> header = new Entry<E>(null, null, null);即存储的元素为null,且前一项和后一项都初始化为null。
privatestaticclass Entry<E> { E element;//存储元素值 Entry<E> next; //后一项 Entry<E> previous; //前一项
Entry(E element, Entry<E> next, Entry<E> previous) { this.element = element; this.next = next; this.previous = previous; } } |
LinkedList():初始化链表。
public LinkedList() { header.next =header.previous =header; } |
无参构造函数将头节点的next和previous项都设置为头节点自己本身。
add(E e):加入元素到链表末尾。
publicboolean add(E e) { addBefore(e, header); returntrue; } private Entry<E> addBefore(E e, Entry<E> entry) { //根据元素值构造链表项,初始化next与previous项都为头节点。 Entry<E> newEntry = new Entry<E>(e, entry, entry.previous); newEntry.previous.next = newEntry; newEntry.next.previous = newEntry; size++; modCount++; return newEntry; } |
add(E e)调用方法addBefore()将元素加入到链表中。主要工作就是创建包含新元素值的链表项,然后设置新链表项的下一项为头结点,上一项为头节点的上一项;然后设置新节点的上一项的下一项为新节点,设置新节点的下一项的上一项为新节点。同时将size加1以及modCount加1。这样解释让人搞不清楚,还是通过图形来说明比较好。
1)没有元素的时候,空链表,只有header节点,其next和privious都存储自己的引用。如图所示:
2)使用add方法插入元素n1时,此时header.previous==header,所以相当于初始化新元素n1的next和previous都为header,即n1.previous=header,n1.next=header;而由于n1.previous=header以及n1.next=header,所以n1.previous.next=header.next=n1, n1.next.previous=header.previous=n1。而故而节点n1加入后,链表结构如下所示:
初始链表:
加入n1后的链表:
3)再次使用add方法加入元素n2,此时header.previous为节点n1。即初始化新节点n2.next=header,n2.previous=header.previous=n1。同时更改n1.next=n2,header.previous=n2。则此时链表结构如下:
原链表:
加入n2后的链表:
4)add(E e)方法总结:
我们可以知道,不管什么时候,header.previous都是尾节点的引用,所以每次在加入新的节点时,此时新节点为尾节点;所以初始化时设置新节点的next为header,以及设置previous为header.previous即加入新节点前的尾节点。然后需要更改两个地方,一个是header.previous改为新节点,另一个是原来尾节点的next改为新节点。addBefore(E e, Entry<E> entry)方法的意思就是在entry之前插入元素e,在这里entry为header,则在header之前插入e,也就相当于说将e插入到尾部。
add(int index, E e):在指定位置处插入元素。
publicvoid add(int index, E element) { addBefore(element, (index==size ?header : entry(index))); } private Entry<E> entry(int index) { //判断index是否有效。 if (index < 0 || index >=size) thrownew IndexOutOfBoundsException("Index: "+index+ ", Size: "+size); Entry<E> e = header; if (index < (size >> 1)) { //若index<size/2,则从前往后找。 for (int i = 0; i <= index; i++) e = e.next; } else { //否则,index>=size/2,从后往前找。 for (int i =size; i > index; i--) e = e.previous; } return e; } |
需要注意的一点是这个索引也是从0开始算起的,所以如果原来链表中链表项的元素为”zero”, “one”, “two”,则调用add(1, “newone”)后,会变成”zero”, “newone”, “one”, “two”。
该函数也通过addBefore()方法来实现,第二个参数为该索引对应的链表项。如果index==size,则相当于在链表尾部插入该元素,故参数为header。否则,调用entry(index)方法,该方法从头节点header开始,遍历链表找到相应的节点并返回。该方法用到了点小技巧,即index>size/2时,从头往后遍历;否则从后往前遍历。
remove(Entry<E> e):移除链表中指定的项并返回该项的元素值。
private E remove(Entry<E> e) { if (e ==header) thrownew NoSuchElementException();
E result = e.element; e.previous.next = e.next; e.next.previous = e.previous; e.next = e.previous =null; e.element =null; size--; modCount++; return result; } |
移除指定项e的操作主要就是更改e的前一项的next引用和e的后一项的previous引用的值。此外设置e.next=e.previous=null,e.element=null便于GC回收,size减1。注意移除项不能是header。
remove(Object o):移除元素值与指定对象相等的项。
publicboolean remove(Object o) { if (o==null) { for (Entry<E> e =header.next; e !=header; e = e.next) { if (e.element==null) { remove(e); returntrue; } } } else { for (Entry<E> e =header.next; e !=header; e = e.next) { if (o.equals(e.element)) { remove(e); returntrue; } } } returnfalse; } |
移除链表中元素值等于指定对象值的项,如果找了相应项则移除并返回true,否则返回false。注意需要判断元素值是否为null,如果为null需要单独考虑,因为不能对null调用equals方法。
还有个方法removeFirstOccurrence(Object o)用来移除链表中第一次出现指定元素的项,其实最终就是调用remove(Object o)方法实现的。而另一个方法removeLastOccurrence(Object o)移除指定元素在链表中最后出现的项与该方法类似,只是它从后往前扫描链表而已。
addFirst(E e):在链表开始处插入元素e
publicvoid addFirst(E e) { addBefore(e, header.next); } |
这个方法很简单,就是调用addBefore方法,传递的参数与add不同,第二个参数传递的是header.next,即该链表的第一个节点的引用。即将新节点插入到头节点header的后面,原来第一个节点的前面。
addLast(E e):在链表末尾插入元素e。
跟add(E e)方法完全一样。
getFirst():返回链表第一个元素,O(1)时间。
public EgetFirst() { if (size==0) thrownew NoSuchElementException(); returnheader.next.element; } |
该方法返回链表第一个元素(注意,不算header),即header.next.element。注意该方法会先判断size是否为0,如果size==0,则表示链表中除了头节点没有其他任何节点,则调用本方法或者getLast()方法时都会抛出NoSuchElementException异常。
getLast():返回链表中最后一个元素,O(1)时间。
public E getLast() { if (size==0) thrownew NoSuchElementException(); returnheader.previous.element; } |
3.总结
本文主要分析了LinkedList类的一些常用方法的源码,其他的一些操作大都最终调用了这些方法。如pop()调用了removeFirst(),最终是调用了remove(header.next);push()调用了addFirst()方法。所以有了对这些方法的理解,对其他方法的分析也就不难了。