java8 LinkedList源码阅读【2】- 总结

时间:2021-04-06 17:21:42

上一篇文章 java8 LinkedList源码阅读已经分析了LinkedList源码,现对LinkedList做个小结。

  • LinkedList特点
    • 双向链表实现,因此没有固定容量,不需要扩容
    • 元素时有序的,输出顺序与输入顺序一致
    • 允许元素为 null
    • 所有指定位置的操作都是从头开始遍历进行的
    • 和 ArrayList 一样,不是同步容器
    • 需要更多的内存,LinkedList 每个节点中需要多存储前后节点的信息,占用空间更多些。
    • 查找效率低,插入删除效率高。
  • LinkedList结构
    继承AbstractSequentialList并实现了List接口,Deque接口,Cloneable接口,Serializable接口,因此它支持队列操作,可复制,可序列化。
public class LinkedList<E> extends AbstractSequentialList<E> implements List<E>, Deque<E>, Cloneable, java.io.Serializable
  • LinkedList本质
    LinkedList是个双向链表,它有三个成员变量。
//链表节点个数
transient int size = 0;

//头节点指针
transient Node<E> first;

//尾节点指针
transient Node<E> last;

其中节点是一个双向节点

//节点实现
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;
    }
}
  • modCount变量[fail-fast机制]
    类似ArrayList,LinkedList也维护了modCount变量,其记录了数组的修改次数,在LinkedList的所有涉及结构变化的方法中都增加modCount的值。
    该变量迭代器等方面体现。
//检查链表是否修改,根据expectedModCount和modCount判断
final void checkForComodification() {
    if (modCount != expectedModCount)
        throw new ConcurrentModificationException();
}

因此在使用迭代器迭代过程中,不允许对链表结构做修改(如插入新节点),否则会抛出异常 java.util.ConcurrentModificationException。

  • indexOf(Object o)
    该方法会根据是否为null使用不同方式判断。如果是元素为null,则直接比较地址,否则使用equals的方法比较,加快比较效率。lastIndexOf(Object o) 同理。
// 获得指定元素在链表第一次出现的下标,不存在返回-1
public int indexOf(Object o) {
    int index = 0;
    //根据指定元素是否为null采取不同比较方式,加快比较
    if (o == null) {
        for (Node<E> x = first; x != null; x = x.next) {
            if (x.item == null)
                return index;
            index++;
        }
    } else {
        for (Node<E> x = first; x != null; x = x.next) {
            if (o.equals(x.item))
                return index;
            index++;
        }
    }
    return -1;
}
  • clone() 浅拷贝
//返回副本,浅拷贝,与ArrayList.clone()相似
public Object clone() {
    LinkedList<E> clone = superClone(); //将clone构造成一个空的双向循环链表

    // Put clone into "virgin" state
    clone.first = clone.last = null;
    clone.size = 0;
    clone.modCount = 0;

    // Initialize clone with our elements
    for (Node<E> x = first; x != null; x = x.next)
        clone.add(x.item);  //浅拷贝,节点还是同一份引用

    return clone;
}

举个例子如下:

public class Main {
    static class A {
        int a;

        A(int a) {
            this.a = a;
        }

        @Override
        public String toString() {
            return super.toString() + " : " + a;
        }
    }

    public static void main(String[] args) {
        LinkedList<A> a1=new LinkedList<>();
        a1.add(new A(1));
        a1.add(new A(2));
        LinkedList<A> a2= (LinkedList<A>) a1.clone();
        System.out.println(a1);
        System.out.println("----");
        System.out.println(a2);
        System.out.println("----");

        a2.get(1).a=100;
        System.out.println(a1);
        System.out.println("----");
        System.out.println(a2);
        System.out.println("----");
    }
}
/* * 输出结果: [Main$A@1b6d3586 : 1, Main$A@4554617c : 2] ---- [Main$A@1b6d3586 : 1, Main$A@4554617c : 2] ---- [Main$A@1b6d3586 : 1, Main$A@4554617c : 100] ---- [Main$A@1b6d3586 : 1, Main$A@4554617c : 100] ---- */

从上述输出结果可以看出,尽管克隆的链表不是跟原链表同一块内存,但内容引用是同样的,指向同一个地址,可以从输出结果看出,因此改变克隆后链表的元素内容,相应的原链表内容发生相应变化,因此clone()方法是浅拷贝。
(注:对于基本类型,如int,则不会发生这种情况。)

  • toArray() 和 toArray(T[] a)
    跟上述clone()方法类似,同样也是浅拷贝。
//返回一个包含此列表中所有元素的数组
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;
}

//返回一个数组,使用运行时确定类型,该数组包含在这个列表中的所有元素(从第一到最后一个元素)
//如果参数数组容量比链表节点数少,则返回链表数组;否则覆盖参数数组前size位,且第size位赋null,剩余不变。
@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;
}
  • clear()
    因为底层实现不是数组,LinkedList中的 clear方法稍微复杂一些,需要对每个节点的所有属性置null。
//清空链表
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++;
}
  • node(int index)
    该方法获得指定位置index的节点,其实现虽然也是遍历链表,但由于该链表是双向链表,因此支持双向查找。查找前会根据指定位置index判断是在链表的前半段还是后半段,从而决定是从前往后找或是从后往前找,提升查找效率。
// 获得指定位置的节点
Node<E> node(int 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;
    }
}
  • Queue 队列操作
    由于LinkedList实现了Deque接口,而Deque继承了Queue,因此LinkedList也可以进行队列操作,包括:
// 队列操作,获取表头节点的值,表头为空返回null
public E peek() {
    final Node<E> f = first;
    return (f == null) ? null : f.item;
}

// 队列操作,获取表头节点的值,表头为空抛出异常
public E element() {
    return getFirst();
}

// 队列操作,获取表头节点的值,并删除表头节点,表头为空返回null
public E poll() {
    final Node<E> f = first;
    return (f == null) ? null : unlinkFirst(f);
}

// 队列操作,获取表头节点的值,并删除表头节点,表头为空抛出异常
public E remove() {
    return removeFirst();
}

// 队列操作,将指定的元素添加为此列表的尾部(最后一个元素)。
public boolean offer(E e) {
    return add(e);
}
  • Deque 双向队列操作
    由于LinkedList实现了Deque接口,因此可用于双向队列。
// 双向队列操作,链表首部插入新节点
public boolean offerFirst(E e) {
    addFirst(e);
    return true;
}

// 双向队列操作,链表尾部插入新节点
public boolean offerLast(E e) {
    addLast(e);
    return true;
}

// 双向队列操作,获取链表头节点值
public E peekFirst() {
    final Node<E> f = first;
    return (f == null) ? null : f.item;
}

// 双向队列操作,获取尾节点值
public E peekLast() {
    final Node<E> l = last;
    return (l == null) ? null : l.item;
}

// 双向队列操作,获取表头节点的值,并删除表头节点,表头为空返回null
public E pollFirst() {
    final Node<E> f = first;
    return (f == null) ? null : unlinkFirst(f);
}

// 双向队列操作,获取表尾节点的值,并删除表尾节点,表尾为空返回null
public E pollLast() {
    final Node<E> l = last;
    return (l == null) ? null : unlinkLast(l);
}
  • listIterator(int index)
// 返回从指定位置开始的ListIterator迭代器
public ListIterator<E> listIterator(int index) {
    checkPositionIndex(index);  //检查位置合法性
    return new ListItr(index);
}

其中ListItr是LinkedList的一个内部类,实现了ListIterator接口,是个支持双向的迭代器。其实现细节较长,就不贴了,见上一节源码解析即可。

  • descendingIterator()
// 返回一个迭代器在此双端队列以逆向顺序的元素
public Iterator<E> descendingIterator() {
    return new DescendingIterator();
}

// DescendingIterator的实现,从后往前的迭代
private class DescendingIterator implements Iterator<E> {
    private final ListItr itr = new ListItr(size());    //获得链表尾部的ListItr
    public boolean hasNext() {
        return itr.hasPrevious();
    }
    public E next() {
        return itr.previous();
    }
    public void remove() {
        itr.remove();
    }
}

利用上面实现的双向迭代器类ListItr,可轻易得实现了逆向的迭代器。

  • spliterator()
    java8新增方法,类似Iiterator, 可以理解为 Iterator 的 Split 版本,可用于多线程,仍需学习。

【引用网上的说明】
使用 Iterator 的时候,我们可以顺序地遍历容器中的元素,使用 Spliterator 的时候,我们可以将元素分割成多份,分别交于不于的线程去遍历,以提高效率。

使用 Spliterator 每次可以处理某个元素集合中的一个元素 — 不是从 Spliterator 中获取元素,而是使用 tryAdvance() 或 forEachRemaining() 方法对元素应用操作。

但 Spliterator 还可以用于估计其中保存的元素数量,而且还可以像细胞分裂一样变为一分为二。这些新增加的能力让流并行处理代码可以很方便地将工作分布到多个可用线程上完成。

  • 同步问题
    LinkedList 和 ArrayList 一样,不是同步容器。所以需要外部做同步操作,或者直接用 Collections.synchronizedList 方法包一下:
List list = Collections.synchronizedList(new LinkedList(...));

此时list是线程安全类,自身提供的方法也是线程安全的。当然list进行其他非原子操作仍需自己同步。