JDK核心JAVA源码解析(7)- 集合相关(1) - LinkedList

想写这个系列很久了,对自己也是个总结与提高。原来在学JAVA时,那些JAVA入门书籍会告诉你一些规律还有法则,但是用的时候我们一般很难想起来,因为我们用的少并且不知道为什么。知其所以然方能印象深刻并学以致用。

本篇文章针对JAVA中集合类LinkedList进行分析,通过代码解释Java中的Fail-fast设计思想,以及LinkedList底层实现和与ArrayList对比下的就业场景。

本文会通过QA的方式行文

本文基于JDK 11

1. LinkedList实际结构是什么样子的?是单向链表还是双向?

是双向链表。LinkedList不光实现了List接口,还实现了Deque接口。

public class LinkedList<E>
    extends AbstractSequentialList<E>
    implements List<E>, Deque<E>, Cloneable, java.io.Serializable

2. 为何LinkedList更加耗费空间?

因为要实现双向链表,双向队列的机制,同时可以存储null值(也就是有额外的引用变量指向实际的节点数据),每个节点都是由三个引用组成:

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

这样,存储占用了更多空间。

3. LinkedList有哪些基本操作,复杂度如何?

  • 除了基本的add(E element)还有addAll(Collection<E> elements)以外,还有addFisrt(E element)addLast(E element)可以使用。由于LinkedList而外维护了指向头结点还有尾节点的指针,所以复杂度都是O(1)
/**
* Pointer to first node.
*/
//由于这些没必要序列化,所以加上transient
transient Node<E> first;

/**
* Pointer to last node.
*/
transient Node<E> last;
  • get(int index)等所有涉及到下标操作的方法,由于需要遍历,复杂度都是O(N/2).实现遍历的核心方法是:
Node<E> node(int index) {
   //看index是否大于size一半或者小于,决定从链表头遍历还是末尾遍历
   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;
   }
}
  • add(E element)类似,remove(E element)也有对应的removeFirst()removeLast()存在(对于空的链表会抛出NoSuchElementException),并且由于LinkedList而外维护了指向头结点还有尾节点的指针,所以复杂度都是O(1)。但是remove(Object o)的复杂度是O(N),因为是从开头遍历寻找第一个equals返回true的。同时还有removeFirstOccurrence(Object o)(和remove等价)和removeLastOccurrence(Object o)方法。
public boolean removeLastOccurrence(Object o) {
   //对于null的值,直接寻找为null的,否则通过调用equals判断是否相等
   if (o == null) {
       for (Node<E> x = last; x != null; x = x.prev) {
           if (x.item == null) {
               unlink(x);
               return true;
           }
       }
   } else {
       for (Node<E> x = last; x != null; x = x.prev) {
           if (o.equals(x.item)) {
               unlink(x);
               return true;
           }
       }
   }
   return false;
}
  • 基本队列操作也有。poll()pop()效果相同,都是将队列第一个元素剔除并返回。不同的是,针对空链表,poll()返回null,pop()抛出NoSuchElementException异常(因为底层实现就是removeFirst()

4. 什么是fail-fast,集合类中是如何实现的?LinkedList如何实现的?

fail-fast指的是在可能的损害发生前,直接失败。在JAVA中的一种体现就是当某个集合的Iterator已经创建后,如果修改了集合,再访问Iterator进行遍历就会抛出ConcurrentModificationException

LinkedList和其它集合类似,通过modCount这个field实现。

所有修改都会让modCount++。生成的Iterator会记录这个modCount,如果modCount发生改变,抛出ConcurrentModificationException

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

    ListItr(int index) {
        // assert isPositionIndex(index);
        next = (index == size) ? null : node(index);
        nextIndex = index;
    }
    //举一个方法作为例子,其他省略...

    public void forEachRemaining(Consumer<? super E> action) {
        Objects.requireNonNull(action);
        while (modCount == expectedModCount && nextIndex < size) {
            action.accept(next.item);
            lastReturned = next;
            next = next.next;
            nextIndex++;
        }
        checkForComodification();
    }

    final void checkForComodification() {
        if (modCount != expectedModCount)
            throw new ConcurrentModificationException();
    }
}

还有一些其他的扩展迭代器,例如Spliterator,也是fail-fast的

5. 如果需要对List排序,用ArrayList比较好还是LinkedList?

集合排序基本上都是基于这个方法:Collections.swap()

public static void swap(List<?> list, int i, int j) {
    final List l = list;
    l.set(i, l.set(j, l.get(i)));
}

基本都是下标操作,所以,ArrayList更好

6. 为何LinkedList非线程安全?

我们来看在末尾加元素的方法:

void linkLast(E e) {
    //假设原始last为1,新加入两个节点2,3
    final Node<E> l = last;
    final Node<E> newNode = new Node<>(l, e, null);
    //假设这里发生了线程切换,将会发生,1->2->null之后变成1->3->null的情况
    last = newNode;
    if (l == null)
        first = newNode;
    else
        l.next = newNode;
    //++都不是线程安全的,导致size误判
    size++;
    modCount++;
}

如果想实现线程安全的list:

  • 使用Vector(适用于更新比较多)或者CopyOnWriteArrayList(适用于读取比较多)
  • 通过:
List list = Collections.synchronizedList(new LinkedList(...));

获取并发安全LinkedList

7. 为什么JDK默认的List实现是ArrayList而不是LinkedList?

在大部分场景(遍历,下标查找,排序等)下,ArrayList性能更佳并且占用存储空间小。只有下标插入删除这种场景,LinkedList更有优势。

发布了194 篇原创文章 · 获赞 266 · 访问量 145万+

猜你喜欢

转载自blog.csdn.net/zhxdick/article/details/100544843