Java基础集合类(二):LinkedList详解

来源:互联网 发布:nginx 查看模块 编辑:程序博客网 时间:2024/03/29 05:02

LinkedList详解

1、简介

LinkedList是一个双向链表,允许null元素存储,内部是使用Node节点来相互链接的,每个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;        }    }
LinkedList有两个属性,一个是first,指向第一个Node节点,一个是last,指向最后一个节点,因此LinkedList在访问头节点或者尾节点的时候速度很快,还有一个size属性,用于记录当前存储的元素个数。LinkedList继承与AbstractList,因此从父类中继承到了modCount属性,这个可以在迭代时判断链表结构是否被改变,例如add,remove等方法都会改变这个属性,若迭代时链表结构改变,则会抛出ConcurrentModificationException异常。

LinkedList是非线程安全的,因此在多线程环境下使用时,需要注意同步问题。LinkedList不仅实现了List接口,同时也实现了Deque接口,因此LinkedList也可以作为Stack栈以及Queue队列来使用,只不过LinkedList在作为栈以及队列时,效率不如ArrayDeque高,以后我会做一下对比。

2、构造器

LinkedList构造器只有两个,一个是无参构造器,一个是有参构造器LinkedList(Collection<? extends E> c),有参构造器是使用一个集合来构造一个LinkedList,内部使用的是addAll(Collection<? extends E> c)方法来实现,这个在下面讲关键方法时会讲到。

3、关键方法

下面讲一下一些主要的关键方法,好多方法都是重用的,把这些关键方法理解了,其他的方法也就好理解了。链表主要是插入和移除操作,其中插入可以在头节点、尾节点、中间插入,移除可以在头节点、尾节点和中间移除。

3.1、linkLast(E e)

这是一个包访问权限的方法,方法的主要功能是把e元素链接到链表的尾部。大概实现:尾节点l,头节点指针first,尾节点指针last,新节点newNode,newNode.prev指向l,last指向newNode,如果l为空,则表示当前链表是空的,此时还需要把first指针指向newNode,l不为空,则l.next指向newNode。

使用到此方法的关键方法:addLast(E e),add(E e),add(int index, E element),LinkedList的反序列化方法readObject(java.io.ObjectInputStream s)也会用到。其他的比如像offer、offerLast等方法,是直接使用add、addLast方法的,因此也相当于变相使用linkLast方法。

关键代码如下:

    void linkLast(E e) {        final Node<E> l = last;//获取尾节点引用        final Node<E> newNode = new Node<>(l, e, null);//新节点prev指向l        last = newNode;//last指针指向新节点        if (l == null)            first = newNode;//l为空则需要first也指向新节点        else            l.next = newNode;//l的下一个节点指向新节点        size++;        modCount++;    }

3.2、linkFirst(E e)

这是一个私有访问权限的方法,方法的主要功能是把e元素插入到链表的头部。大概实现:头节点f,头节点指针first,尾节点指针last,新节点newNode,newNode.next指向f,first指向newNode,若f为空,则表示链表是空的,last也需要指向newNode,f不为空,则f.prev指向newNode。

使用到此方法的关键方法:addFirst(E e),offerFirst、push有使用到addFirst方法。

关键代码如下:

    private void linkFirst(E e) {        final Node<E> f = first;//获取头节点的引用        final Node<E> newNode = new Node<>(null, e, f);//新节点的next指向f        first = newNode;        if (f == null)            last = newNode;        else            f.prev = newNode;//原头节点的prev指向新节点        size++;        modCount++;    }

3.3、linkBefore(E e, Node<E> succ)

这是一个包访问权限的方法,方法的主要功能是在succ节点前插入e元素。大概实现:succ前节点是prev,头节点指针first,新节点newNode,newNode.prev指向prev节点,newNode.next指向succ,如果prev是null,表示succ是头节点,此时需要first指向newNode,prev不为空,则prev.next指向newNode,succ.prev指向newNode。
使用到此方法的关键方法:add(int index, E element),链表实现的迭代器中的add(E e)也有使用到。

关键代码如下:

    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++;    }

3.4、unlinkLast(Node<E> l)

这是一个私有访问权限的方法,方法的主要功能是移除末尾节点l。大概实现:l的前节点prev,last指针指向prev,如果prev是null,则表示当前链表已经空了,则first=null,如果prev不为空,则prev.next=null。

使用到此方法的关键方法:removeLast(),pollLast()。

关键代码如下:

    private E unlinkLast(Node<E> l) {        // assert l == last && l != null;        final E element = l.item;        final Node<E> prev = l.prev;        l.item = null;        l.prev = null; // help GC        last = prev;        if (prev == null)            first = null;        else            prev.next = null;        size--;        modCount++;        return element;    }

3.5、unlinkFirst(Node<E> f)

这是一个私有访问权限的方法,方法的主要功能是移除头节点f。大概实现:f的后一个节点是next,first指针指向next,如果next为null,则表示当前链表已经空了,last=null,如果next不为空,则next.prev=null。

使用到此方法的关键方法:removeFirst(),poll(),pollFirst(),remove(),pop()等。

关键代码如下:

    private E unlinkFirst(Node<E> f) {        // assert f == first && f != null;        final E element = f.item;        final Node<E> next = f.next;        f.item = null;        f.next = null; // help GC        first = next;        if (next == null)            last = null;        else            next.prev = null;        size--;        modCount++;        return element;    }

3.6、unlink(Node<E> x)

这是一个包访问权限的方法,方法的主要功能是移除节点x。大概实现:x的前节点prev,后节点next,x.preve=null,x.next=null,prev为空,则first=next,prev不为空,则prev.next=next;next为空,则last=prev,next不为空,则next.prev=prev。

使用到此方法的关键方法:remove(Object o),remove(int index),removeLastOccurrence(Object o),LinkedList的列表迭代器中的remove()方法也有使用到。

关键代码如下:

    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--;        modCount++;        return element;    }

3.7、node(int index)

这是一个包访问权限的方法,方法的主要功能是找到index索引位置的元素。这个方法的实现并不是迭代全部的链表元素来查找元素,而是先用index与size的1/2来进行比较,如果小于则从链表的左边来迭代查找,范围是[0,index],如果大于则从链表的右边来迭代查找,范围是[index,size-1]。这个方法比较重要,因为LinkedList是使用双向链表来存储元素的,并不像数组一样可以方便的使用index索引来进行访问,使用此方法能比较方便的查找到index索引位置的元素。例如移除指定索引的元素remove(int index)方法,就需要先使用此方法来查找到index对应的元素,然后再调用unlink(Node<E> x)方法来移除元素。

关键代码如下:

    Node<E> node(int index) {        // assert isElementIndex(index);        if (index < (size >> 1)) {//与size的1/2进行对比            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;        }    }

3.8、addAll(int index, Collection<? extends E> c)

把指定的集合c插入的指定的index索引位置,跟add(int index, E element)方法类似,根据index找到对应的node,然后在node和prev之间插入集合c的所有元素。

3.9、clear()

清空链表,需要迭代链表,把所有Node中的prev、next、item置为null,帮助垃圾回收。关键代码如下:

    public void clear() {        for (Node<E> x = first; x != null; ) {            Node<E> next = x.next;            x.item = null;            x.next = null;            x.prev = null;            x = next;        }        first = last = null;        size = 0;        modCount++;    }

3.10、indexOf(Object o)

获取o元素的索引,不存在则返回-1,内部实现是迭代所有元素,逐一进行equals。

3.11、toArray(T[] a)

把链表所有元素转换成泛型T数组,内部实现有一个创建数组的小技巧,使用Array.newInstance(Class<?> componentType, int length)方法可以创建泛型数组,这个是使用到了反射的技巧。

4、总结

LinkedList是一个双向链表,内部使用Node节点来包装元素,并且指向前节点和后节点,在插入元素时,构建一个新的Node节点并修改前节点和后节点的指针,因此LinkedList的随机插入效率相对会高一些,但是根据索引来随机访问并不像数组那么方便,LinkedList需要进行遍历来寻找索引位置的元素,因此LinkedList的随机访问效率相对会低一些。LinkedList的插入和删除操作大量重用了一些方法,这些方法无非就是头节点插入、尾节点插入、中间节点插入、头节点移除、尾节点移除、中间节点移除这些,在上面的关键方法中已经讲解过。