Java源码分析LinkedList源码分析

Posted 流动的城市

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java源码分析LinkedList源码分析相关的知识,希望对你有一定的参考价值。

类的定义如下

public class LinkedList<E>
extends AbstractSequentialList<E>
implements List<E>, Deque<E>, Cloneable, java.io.Serializable
  1. 本质是一个双向链表,实现了List和Deque接口。实现了所有List的操作,可以存储所有类型的对象,包括NULL
  2. 非线程安全多线程调用的时候必须通过外部保证线程安全性,所有的添加或者删除操作也必须保证线程安全性。通常的实现方法是通过对象锁的形式保证线程安全性,或者通过Collections.synchronizedList实现,例如List list = Collections.synchronizedList(new LinkedList(...));
  3. 迭代器进行遍历的时候同样存在fail-fast现象,可以参考ArrayList分析中的描述
  4. 由于是AbstractSequentialList的子类,同时本质是双向链表,所以只能顺序访问集合中的元素
  5. 和ArrayList一样也是支持序列化操作的

成员变量

transient int size = 0; // 集合中对象个数
transient Node<E> first; // 指向第一个对象的指针
transient Node<E> last; // 指向最后一个对象的指针

构造函数有两个,一个是无惨默认构造,一个是根据传递的集合类构造一个包含指定元素的链表。相对于ArrayList而言,LinkedList的构造要简单很多,因为不涉及到容量的变化

public LinkedList() 

public LinkedList(Collection<? extends E> c) 
    this();
    addAll(c);

从头尾添加对象

private void linkFirst(E e) 
    final Node<E> f = first;
    final Node<E> newNode = new Node<>(null, e, f);
    first = newNode;
    if (f == null)
        last = newNode;
    else
        f.prev = newNode;
    size++;
    modCount++;


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

其中所用的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;
    

该类的主要就是一个构造函数,直接指定前驱和后继,new的时候就将指针链接完成。相应的还有unlinkFirst和unlinkLast操作,以及linkBefore和linkAfter操作,以及unlink(Node n)操作,操作都是基本的双链表操作。这些操作的共同点在于操作都是针对集合中的对象而言,所以只需要修改链表就可以了。

删除操作(和之前不同之处在于这里给定的参数并不是集合中的对象而是一个值与集合中对象的节点的item值相同)

public boolean remove(Object o) 
    if (o == null) 
        for (Node<E> x = first; x != null; x = x.next) 
            if (x.item == null) 
                unlink(x);
                return true;
            
        
     else 
        for (Node<E> x = first; x != null; x = x.next) 
            if (o.equals(x.item)) 
                unlink(x);
                return true;
            
        
    
    return false;

和前面的方法相比,这种知道值删除集合对象的操作就是一个遍历查找的过程,由于是链表实现,所以就是一个单链表或者双链表的遍历加上修改指针的操作

清空集合操作

public void clear() 
    // Clearing all of the links between nodes is "unnecessary", but:
    // - helps a generational GC if the discarded nodes inhabit
    //   more than one generation
    // - is sure to free memory even if there is a reachable Iterator
    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++;

这个函数重点部分在注释中已经说明了,对于链表类释放的时候节点间的指针并不是必须要释放的,但是释放了可以帮助GC有效的回收,也可以保证彻底的释放内存,即使是存在迭代器(由于clear()操作更改了LinkedList的结构,所以指向它的迭代器会因为ConcurrentModification而fail-fast)

返回指定下标的节点

Node<E> node(int index) 
    // assert isElementIndex(index);

    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;
    

这里在注释中也提到过,关于如何遍历。是从前向后遍历还是从后向前遍历取决于给定index是靠左半边还是右半边

取首节点的操作

public E peek() 
    final Node<E> f = first;
    return (f == null) ? null : f.item;


public E element() 
    return getFirst();


public E poll() 
    final Node<E> f = first;
    return (f == null) ? null : unlinkFirst(f);

三个方法都可以取首节点的item值,但是前两个只是取出并不删除,但是第三个函数返回首节点item并且删除首节点

添加节点

public boolean offer(E e) 
    return add(e);


public boolean offerFirst(E e) 
    addFirst(e);
    return true;


public boolean offerLast(E e) 
    addLast(e);
    return true;

第一个添加就是在链表的尾部添加,后面两个添加操作一个是添加到链表的首部一个是添加到尾部,这两个方法属于是双端队列Dequeue的操作

两个很类似栈的方法

public void push(E e) 
    addFirst(e);


public E pop() 
    return removeFirst();

Push和Pop都是针对链表的首部元素而言,所以LinkedList不仅可以当做一个双端队列使用,也可以当做一个栈来使用

和ArrayList一样,LinkedList内部类迭代器也同样需要记录modCount,所以也会出现fail-fast

private class ListItr implements ListIterator<E> 
    private Node<E> lastReturned = null;
    private Node<E> next;
    private int nextIndex;
    private int expectedModCount = modCount;
.....

扮演集合Collection与数组Array之间转换桥梁的函数

public Object[] toArray() 
    Object[] result = new Object[size];
    int i = 0;
    for (Node<E> x = first; x != null; x = x.next)
        result[i++] = x.item;
    return result;


@SuppressWarnings("unchecked")
public <T> T[] toArray(T[] a) 
    if (a.length < size)
        a = (T[])java.lang.reflect.Array.newInstance(
                            a.getClass().getComponentType(), size);
    int i = 0;
    Object[] result = a;
    for (Node<E> x = first; x != null; x = x.next)
        result[i++] = x.item;

    if (a.length > size)
        a[size] = null;

    return a;

第二个是泛型版本,将LinkedList内容转换为指定运行时类型的一个数组存储起来。这个方法比第一个方法的优势在于可以动态确定类型,而且可以减少allocation的代价

序列化和反序列化(状态的保存和读取)

private void writeObject(java.io.ObjectOutputStream s)
    throws java.io.IOException 
    // Write out any hidden serialization magic
    s.defaultWriteObject();

    // Write out size
    s.writeInt(size);

    // Write out all elements in the proper order.
    for (Node<E> x = first; x != null; x = x.next)
        s.writeObject(x.item);



@SuppressWarnings("unchecked")
private void readObject(java.io.ObjectInputStream s)
    throws java.io.IOException, ClassNotFoundException 
    // Read in any hidden serialization magic
    s.defaultReadObject();

    // Read in size
    int size = s.readInt();

    // Read in all elements in the proper order.
    for (int i = 0; i < size; i++)
        linkLast((E)s.readObject());

同样需要注意写入顺序必须和读取顺序对应。

以上是关于Java源码分析LinkedList源码分析的主要内容,如果未能解决你的问题,请参考以下文章

LinkedList 源码分析

LinkedList的源码分析

LinkedList插入数据效率不一定比ArrayList高,源码分析+实验对比

Java中arraylist和linkedlist源码分析与性能比较

Java源码分析LinkedList源码分析

Java集合源码分析—LinkedList