LinkedList의 (JDK1.8)의 소스 코드를 구문 분석

원본 링크 : http://www.tianxiaobo.com/2018/01/31/LinkedList-%E6%BA%90%E7%A0%81%E5%88%86%E6%9E%90-JDK-1- 8 /

개요

LinkedList의 프레임 자바 구현, 사용되는 이중 연결리스트의 기본 구조의 중요한 세트이다. 그리고, ArrayList를 같이 LinkedList의도 널 (null) 값과 중복 값을 지원합니다. 등 ArrayList를 확장 할 필요없이 LinkedList의 목록 기반 구현 프로세스의 저장 요소 때문에. 그러나 잃게됩니다 이익, LinkedList의 노드 저장 요소는 전구체 및 후속 참조를 저장하기 위해 별도의 공간이 필요합니다. LinkedList의리스트의 다른 한편으로, 머리와 꼬리에보다 효율적으로 삽입하지만, 지정된 삽입 위치, 일반 효율성에서. 그 이유는 노드 위치에 삽입하기 위해 지정된 위치에서,이 위치하도록 동작 시간 복잡도를 필요로한다는 것이다 O(N). 마지막으로, LinkedList의 비 스레드 안전 컬렉션 클래스, 동시 환경, LinkedList의 운영 다중 스레드, 예측할 수없는 오류가 발생할 것입니다.

상기는, 다음, 내가 읽어 계속의 LinkedList의 일반적인 작업의 분석을 확대 할 LinkedList의에 대한 간략한 소개합니다.

상속 시스템

LinkedList의 상속 시스템은 목록 및 Deque와 인터페이스를 달성하면서, 아니고 AbstractSequentialList로부터 상속, 더 복잡합니다. 상속 시스템 (인터페이스의 일부 구현 삭제) 아래에 도시된다 :

여기에 그림 삽입 설명

LinkedList 继承自 AbstractSequentialList,AbstractSequentialList 又是什么呢?从实现上,AbstractSequentialList 提供了一套基于顺序访问的接口。通过继承此类,子类仅需实现部分代码即可拥有完整的一套访问某种序列表(比如链表)的接口。深入源码,AbstractSequentialList 提供的方法基本上都是通过 ListIterator 实现的,比如:

public E get(int index) {
    try {
        return listIterator(index).next();
    } catch (NoSuchElementException exc) {
        throw new IndexOutOfBoundsException("Index: "+index);
    }
}

public void add(int index, E element) {
    try {
        listIterator(index).add(element);
    } catch (NoSuchElementException exc) {
        throw new IndexOutOfBoundsException("Index: "+index);
    }
}

// 留给子类实现
public abstract ListIterator<E> listIterator(int index);

所以只要继承类实现了 listIterator 方法,它不需要再额外实现什么即可使用。对于随机访问集合类一般建议继承 AbstractList 而不是 AbstractSequentialList。LinkedList 和其父类一样,也是基于顺序访问。所以 LinkedList 继承了 AbstractSequentialList,但 LinkedList 并没有直接使用父类的方法,而是重新实现了一套的方法。

另外,LinkedList 还实现了 Deque (double ended queue),Deque 又继承自 Queue 接口。这样 LinkedList 就具备了队列的功能。比如,我们可以这样使用:

Queue<T> queue = new LinkedList<>();

除此之外,我们基于 LinkedList 还可以实现一些其他的数据结构,比如栈,以此来替换 Java 集合框架中的 Stack 类(该类实现的不好,《Java 编程思想》一书的作者也对此类进行了吐槽)。

关于 LinkedList 继承体系先说到这,下面进入源码分析部分。

源码分析

查找

LinkedList 底层基于链表结构,无法向 ArrayList 那样随机访问指定位置的元素。LinkedList 查找过程要稍麻烦一些,需要从链表头结点(或尾节点)向后查找,时间复杂度为 O(N)。相关源码如下:

public E get(int index) {
    checkElementIndex(index);
    return node(index).item;
}

Node<E> node(int index) {
    /*
     * 则从头节点开始查找,否则从尾节点查找
     * 查找位置 index 如果小于节点数量的一半,
     */    
    if (index < (size >> 1)) {
        Node<E> x = first;
        // 循环向后查找,直至 i == index
        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 与节点数量 size/2 的大小,决定从头结点还是尾节点进行查找。查找操作的代码没什么复杂的地方,这里先讲到这里。

遍历

链表的遍历过程也很简单,和上面查找过程类似,我们从头节点往后遍历就行了。但对于 LinkedList 的遍历还是需要注意一些,不然可能会导致代码效率低下。通常情况下,我们会使用 foreach 遍历 LinkedList,而 foreach 最终转换成迭代器形式。所以分析 LinkedList 的遍历的核心就是它的迭代器实现,相关代码如下:

public ListIterator<E> listIterator(int index) {
    checkPositionIndex(index);
    return new ListItr(index);
}

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

    /** 构造方法将 next 引用指向指定位置的节点 */
    ListItr(int index) {
        // assert isPositionIndex(index);
        next = (index == size) ? null : node(index);
        nextIndex = index;
    }

    public boolean hasNext() {
        return nextIndex < size;
    }

    public E next() {
        checkForComodification();
        if (!hasNext())
            throw new NoSuchElementException();

        lastReturned = next;
        next = next.next;    // 调用 next 方法后,next 引用都会指向他的后继节点
        nextIndex++;
        return lastReturned.item;
    }
    
    // 省略部分方法
}

上面的方法很简单,大家应该都能很快看懂,这里就不多说了。下面来说说遍历 LinkedList 需要注意的一个点。

我们都知道 LinkedList 不擅长随机位置访问,如果大家用随机访问的方式遍历 LinkedList,效率会很差。比如下面的代码:

List<Integet> list = new LinkedList<>();
list.add(1)
list.add(2)
......
for (int i = 0; i < list.size(); i++) {
    Integet item = list.get(i);
    // do something
}

当链表中存储的元素很多时,上面的遍历方式对于效率来说就是灾难。原因在于,通过上面的方式每获取一个元素,LinkedList 都需要从头节点(或尾节点)进行遍历,效率不可谓不低。在电脑配置如下(MacBook Pro Early 2015, 2.7 GHz Intel Core i5)实测10万级的数据量,耗时约7秒钟。20万级的数据量耗时达到了约34秒的时间。50万级的数据量耗时约250秒。从测试结果上来看,上面的遍历方式在大数据量情况下,效率很差。大家在日常开发中应该尽量避免这种用法。

插入

LinkedList 除了实现了 List 接口相关方法,还实现了 Deque 接口的很多方法,所以我们有很多种方式插入元素。但这里,我只打算分析 List 接口中相关的插入方法,其他的方法大家自己看吧。LinkedList 插入元素的过程实际上就是链表链入节点的过程,学过数据结构的同学对此应该都很熟悉了。这里简单分析一下,先看源码吧:

/** 在链表尾部插入元素 */
public boolean add(E e) {
    linkLast(e);
    return true;
}

/** 在链表指定位置插入元素 */
public void add(int index, E element) {
    checkPositionIndex(index);

    // 判断 index 是不是链表尾部位置,如果是,直接将元素节点插入链表尾部即可
    if (index == size)
        linkLast(element);
    else
        linkBefore(element, node(index));
}

/** 将元素节点插入到链表尾部 */
void linkLast(E e) {
    final Node<E> l = last;
    // 创建节点,并指定节点前驱为链表尾节点 last,后继引用为空
    final Node<E> newNode = new Node<>(l, e, null);
    // 将 last 引用指向新节点
    last = newNode;
    // 判断尾节点是否为空,为空表示当前链表还没有节点
    if (l == null)
        first = newNode;
    else
        l.next = newNode;    // 让原尾节点后继引用 next 指向新的尾节点
    size++;
    modCount++;
}

/** 将元素节点插入到 succ 之前的位置 */
void linkBefore(E e, Node<E> succ) {
    // assert succ != null;
    final Node<E> pred = succ.prev;
    // 1. 初始化节点,并指明前驱和后继节点
    final Node<E> newNode = new Node<>(pred, e, succ);
    // 2. 将 succ 节点前驱引用 prev 指向新节点
    succ.prev = newNode;
    // 判断尾节点是否为空,为空表示当前链表还没有节点    
    if (pred == null)
        first = newNode;
    else
        pred.next = newNode;   // 3. succ 节点前驱的后继引用指向新节点
    size++;
    modCount++;
}

위가 삽입 과정의 소스, 내가 더 상세한 소스 메모를 수행, 이해하기 어렵지 않을 것이다. 위의 두 가지 방법만을 목록으로 만들었다 linkLast 래퍼를 동작시키는 방법에있어서, 코어 로직 linkBefore 추가. linkBefore이 예에서, 논리 흐름은 다음과 같다 :

  1. 새 노드를 만들고, 새로운 노드 전임자와 후임자를 나타냅니다
  2. SUCC 전구체 참조는 새로운 노드를 가리
  3. SUCC 전구체가 하늘이 아닌 경우, SUCC 전임자의 후속 참조는 새로운 노드를 가리

다음 그림에 해당 :

여기에 그림 삽입 설명

다음은, 관련 소스 코드 분석에 삽입이 대답, 복잡하지 않습니다. 아래 분석하기 위해 계속합니다.

삭제

당신이 분석 위에 삽입 소스 코드를 읽을 경우, 실제로는 매우 간단합니다 삭제를 찾습니다. 삭제 작업은 작업을 완료하기 전에 연결 한 후 노드와 노드를 삭제하여 발표한다. 공정이 비교적 간단 소스 코드 그것을 보면 :

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 E remove(int index) {
    checkElementIndex(index);
    // 通过 node 方法定位节点,并调用 unlink 将节点从链表中移除
    return unlink(node(index));
}

/** 将某个节点从链表中移除 */
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;
    
    // prev 为空,表明删除的是头节点
    if (prev == null) {
        first = next;
    } else {
        // 将 x 的前驱的后继指向 x 的后继
        prev.next = next;
        // 将 x 的前驱引用置空,断开与前驱的链接
        x.prev = null;
    }

    // next 为空,表明删除的是尾节点
    if (next == null) {
        last = prev;
    } else {
        // 将 x 的后继的前驱指向 x 的前驱
        next.prev = prev;
        // 将 x 的后继引用置空,断开与后继的链接
        x.next = null;
    }

    // 将 item 置空,方便 GC 回收
    x.item = null;
    size--;
    modCount++;
    return element;
}

삽입 동작, 삭제하는 방법을 확보하는 기초 처리 층이, 기본 연결 해제 방법에있어서, 코어 로직. 그래서 운전 똑바로 직접 분석 방법은 연결을 해제. 다음 논리 연결 해제 방법은 (제거 된 노드를 가정하는 헤드 노드도 아니고 테일 노드이다)

  1. 전구체는 X의 노드 X 포인팅 후속 후계자를 삭제할
  2. 전구체는 전구체 분리 링크, 노드 X의 빈에 대한 참조를 삭제
  3. 이후 전구체는 x의 이전에 노드 X 점을 삭제할
  4. 후속 노드에 대한 참조를 삭제 X 빈, 분리 및 후속 링크

아래 도면 :

여기에 그림 삽입 설명

도면과 함께, LinkedList의 삭제를 이해하는 것은 어렵지 않을 것이다. 음, 소스 코드 분석 LinkedList의 삭제는 이것에 대해 이야기했다.

개요

위의 분석을 통해, 기본 LinkedList의를 구현하는 것이 매우 명확해야한다. 전체 LinkedList의 소스 코드를 우리는 이해할 수 일반적으로 환자를 보면, 복잡하지 않습니다. 동시에,이 논문을 통해, 우리는 LinkedList의 구덩이의 사용을 보여, 난 당신이 개발 피하기 위해 노력하겠습니다. 음,이 문서는 여기에서 끝으로, 읽어 주셔서 감사합니다!

추천

출처blog.csdn.net/ThinkWon/article/details/102573923