JAVA类库分析之LinkedList

来源:互联网 发布:布拉热洛纳子爵 知乎 编辑:程序博客网 时间:2024/04/29 02:06

JAVA类库分析之LinkedList

1.概述

        在java源码中对LinkedList有详细的描述:LinkedList实现了List接口和Deque接口,即表示它支持List的一些常规操作如insertgetremove等;同时它还支持FIFO双向队列操作如addpoll操作,以及栈和队列的其他操作等。

        与VectorArrayList不同之处在于它没有实现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;

}

无参构造函数将头节点的nextprevious项都设置为头节点自己本身。

 

 

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()将元素加入到链表中。主要工作就是创建包含新元素值的链表项,然后设置新链表项的下一项为头结点,上一项为头节点的上一项;然后设置新节点的上一项的下一项为新节点,设置新节点的下一项的上一项为新节点。同时将size1以及modCount1。这样解释让人搞不清楚,还是通过图形来说明比较好。

1)没有元素的时候,空链表,只有header节点,其nextprivious都存储自己的引用。如图所示:

2)使用add方法插入元素n1时,此时header.previous==header,所以相当于初始化新元素n1nextprevious都为header,即n1.previous=headern1.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=headern2.previous=header.previous=n1。同时更改n1.next=n2header.previous=n2。则此时链表结构如下:

原链表:

加入n2后的链表:

4)add(E e)方法总结:

        我们可以知道,不管什么时候,header.previous都是尾节点的引用,所以每次在加入新的节点时,此时新节点为尾节点;所以初始化时设置新节点的nextheader,以及设置previousheader.previous即加入新节点前的尾节点。然后需要更改两个地方,一个是header.previous改为新节点,另一个是原来尾节点的next改为新节点。addBefore(E e,  Entry<E> entry)方法的意思就是在entry之前插入元素e,在这里entryheader,则在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=nulle.element=null便于GC回收,size1。注意移除项不能是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()方法。所以有了对这些方法的理解,对其他方法的分析也就不难了。

原创粉丝点击