给jdk写注释系列之jdk1.6容器(2)-LinkedList源码解析

LinkedList是基于链表结构的一种List,在分析LinkedList源码前有必要对链表结构进行说明。

1.链表的概念

链表是由一系列非连续的节点组成的存储结构,简单分下类的话,链表又分为单向链表和双向链表,而单向/双向链表又可以分为循环链表和非循环链表,下面简单就这四种链表进行图解说明。

    

     1.1.单向链表

单向链表就是通过每个结点的指针指向下一个结点从而链接起来的结构,最后一个节点的next指向null。

     1. 2.单向循环链表

          单向循环链表和单向列表的不同是,最后一个节点的next不是指向null,而是指向head节点,形成一个“环”。

 

          

 

     1. 3.双向链表

          从名字就可以看出,双向链表是包含两个指针的,pre指向前一个节点,next指向后一个节点,但是第一个节点head的pre指向null,最后一个节点的tail指向null。

 

          

 

     1. 4.双向循环链表

          双向循环链表和双向链表的不同在于,第一个节点的pre指向最后一个节点,最后一个节点的next指向第一个节点,也形成一个“环”。而LinkedList就是基于双向循环链表设计的。

 

          

 

更形象的解释下就是:双向循环链表就像一群小孩手牵手围成一个圈,第一个小孩的右手拉着第二个小孩的左手,第二个小孩的左手拉着第一个小孩的右手。。。最后一个小孩的右手拉着第一个小孩的左手。

ok,链表的概念介绍完了,下面进入写注释和源码分析部分,但是在这之前还是要提醒一句,不是啰嗦哦,链表操作理解起来比数组困难了不少,所以务必要理解上面的图解,如果源码解析过程中遇到理解困难,请返回来照图理解。

2.定义   

同样先来看看LinkedList 的定义部分,

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

可以看出LinkedList 继承AbstractSequentialList 抽象类,实现了List,Deque,Cloneable,Serializable 几个接口,AbstractSequentialList 继承 AbstractList,是对其中方法的再抽象,其主要作用是最大限度地减少了实现受“连续访问”数据存储(如链接列表)支持的此接口所需的工作,简单说就是,如果需要快速的添加删除数据等,用AbstractSequentialList  抽象类,若是需要快速随机的访问数据等用AbstractList抽象类(详细说明会在iterator 分析中进行解释)。

Deque 是一个双向队列,也就是既可以先入先出,又可以先入后出,再直白一点就是既可以在头部添加元素又在尾部添加元素,既可以在头部获取元素又可以在尾部获取元素。看下Deque的定义

 1 public interface Deque<E> extends Queue<E> {
 2     void addFirst(E e);
 3     boolean offerFirst(E e);
 4     boolean offerLast(E e);
 5     E removeFirst();
 6     E removeLast();
 7     E pollFirst();
 8     E pollLast();
 9     E getFirst();
10     E getLast();
11     E peekFirst();
12     E peekLast();
13     boolean removeFirstOccurrence(Object o);
14     boolean removeLastOccurrence(Object o);
15     // *** Queue methods ***
16     boolean add(E e);
17     boolean offer(E e);
18     E remove();
19     E poll();
20     E element();
21     E peek();
22     // *** Stack methods ***
23     void push(E e);
24     E pop();
25    // *** Collection methods ***
26     boolean remove(Object o);
27     boolean contains(Object o);
28     public int size();
29     Iterator<E> iterator();
30     Iterator<E> descendingIterator();
31 }

3.底层存储

     明白了上面的链表概念,以及LinkedList是基于双向循环链表设计的,下面在具体来看看LinkedList的底层存储实现方式。

1 private transient Entry<E> header = new Entry<E>(null, null, null);
2 private transient int size = 0;

LinkedList中提供了上面两个属性,其中size和ArrayList中一样用来计数,表示list的元素数量,而header则是链表的头结点,Entry则是链表的节点对象。

 1 private static class Entry<E> {
 2        E element;  // 当前存储元素
 3        Entry<E> next;  // 下一个元素
 4        Entry<E> previous;  // 上一个元素
 5
 6        Entry(E element, Entry<E> next, Entry<E> previous) {
 7            this.element = element;
 8            this.next = next;
 9            this.previous = previous;
10        }
11 }

Entry为LinkedList 的内部类,其中定义了当前存储的元素,以及该元素的上一个元素和下一个元素。结合上面双向链表的示意图很容易看懂。

4.构造方法

 1   /**
 2      * 构造一个空的LinkedList .
 3      */
 4     public LinkedList() {
 5         //将header节点的前一节点和后一节点都设置为自身
 6         header.next = header. previous = header ;
 7     }
 8
 9     /**
10      * 构造一个包含指定 collection 中的元素的列表,这些元素按其 collection 的迭代器返回的顺序排列
11      */
12     public LinkedList(Collection<? extends E> c) {
13         this();
14        addAll(c);
15     }

需要注意的是空的LinkedList构造方法,它将header节点的前一节点和后一节点都设置为自身,这里便说明LinkedList 是一个双向循环链表,如果只是单存的双向链表而不是循环链表,他的实现应该是这样的:

1 public LinkedList() {
2        header.next = null;
3        header. previous = null;
4 }

非循环链表的情况应该是header节点的前一节点和后一节点均为null(参见链表图解)。

5.增加

 

     增加方法的代码读起来比较不容易理解,需要的时候请结合链表图解。

 1     /**
 2      * 将一个元素添加至list尾部
 3      */
 4     public boolean add(E e) {
 5        // 在header前添加元素e,header前就是最后一个结点啦,就是在最后一个结点的后面添加元素e
 6        addBefore(e, header);
 7         return true;
 8     }
 9     /**
10      * 在指定位置添加元素
11      */
12     public void add(int index, E element) {
13         // 如果index等于list元素个数,则在队尾添加元素(header之前),否则在index节点前添加元素
14         addBefore(element, (index== size ? header : entry(index)));
15     }
16
17    private Entry<E> addBefore(E e, Entry<E> entry) {
18        // 用entry创建一个要添加的新节点,next为entry,previous为entry.previous,意思就是新节点插入entry前面,确定自身的前后引用,
19        Entry<E> newEntry = new Entry<E>(e, entry, entry.previous);
20         // 下面修改newEntry的前后节点的引用,确保其链表的引用关系是正确的
21        // 将上一个节点的next指向自己
22        newEntry. previous.next = newEntry;
23        // 将下一个节点的previous指向自己
24        newEntry. next.previous = newEntry;
25        // 计数+1
26         size++;
27         modCount++;
28         return newEntry;
29   }

到这里可以发现一点疑虑,header作为双向循环链表的头结点是不保存数据的,也就是说hedaer中的element永远等于null。

 1     /**
 2      * 添加一个集合元素到list中
 3      */
 4     public boolean addAll(Collection<? extends E> c) {
 5             // 将集合元素添加到list最后的尾部
 6         return addAll(size , c);
 7     }
 8
 9     /**
10      * 在指定位置添加一个集合元素到list中
11      */
12     public boolean addAll(int index, Collection<? extends E> c) {
13         // 越界检查
14         if (index < 0 || index > size)
15             throw new IndexOutOfBoundsException( "Index: "+index+
16                                                 ", Size: "+size );
17         Object[] a = c.toArray();
18         // 要插入元素的个数
19         int numNew = a.length ;
20         if (numNew==0)
21             return false;
22         modCount++;
23
24         // 找出要插入元素的前后节点
25         // 获取要插入index位置的下一个节点,如果index正好是lsit尾部的位置那么下一个节点就是header,否则需要查找index位置的节点
26         Entry<E> successor = (index== size ? header : entry(index));
27         // 获取要插入index位置的上一个节点,因为是插入,所以上一个点击就是未插入前下一个节点的上一个
28         Entry<E> predecessor = successor. previous;
29         // 循环插入
30         for (int i=0; i<numNew; i++) {
31             // 构造一个节点,确认自身的前后引用
32             Entry<E> e = new Entry<E>((E)a[i], successor, predecessor);
33             // 将插入位置上一个节点的下一个元素引用指向当前元素(这里不修改下一个节点的上一个元素引用,是因为下一个节点随着循环一直在变)
34             predecessor. next = e;
35             // 最后修改插入位置的上一个节点为自身,这里主要是为了下次遍历后续元素插入在当前节点的后面,确保这些元素本身的顺序
36             predecessor = e;
37         }
38         // 遍历完所有元素,最后修改下一个节点的上一个元素引用为遍历的最后一个元素
39         successor. previous = predecessor;
40
41         // 修改计数器
42         size += numNew;
43         return true;
44     }

增加方法的代码理解起来可能有些困难,但是只要理解了双向链表的存储结构,掌握增加的核心逻辑就可以了,这里总结一下往链表中增加元素的核心逻辑:1.将元素转换为链表节点,2.增加该节点的前后引用(即pre和next分别指向哪一个节点),3.前后节点对该节点的引用(前节点的next指向该节点,后节点的pre指向该节点)。现在再看下就这么简单么,就是改变前后的互相指向关系(看图增加元素前后的变化)。

其实删除也是一样的对不对?下面看看删除方法的实现。

PS:不要问我entry()方法是怎么回事,这里先不讲,打我也不讲。。。

6.删除

 1     /**
 2      * 删除第一个匹配的指定元素
 3      */
 4     public boolean remove(Object o) {
 5          // 遍历链表找到要被删除的节点
 6         if (o==null) {
 7             for (Entry<E> e = header .next; e != header; e = e.next ) {
 8                 if (e.element ==null) {
 9                     remove(e);
10                     return true;
11                 }
12             }
13         } else {
14             for (Entry<E> e = header .next; e != header; e = e.next ) {
15                 if (o.equals(e.element )) {
16                     remove(e);
17                     return true;
18                 }
19             }
20         }
21         return false;
22     }
23
24     private E remove(Entry<E> e) {
25         if (e == header )
26            throw new NoSuchElementException();
27
28        // 被删除的元素,供返回
29         E result = e. element;
30        // 下面修正前后对该节点的引用
31        // 将该节点的上一个节点的next指向该节点的下一个节点
32        e. previous.next = e.next;
33        // 将该节点的下一个节点的previous指向该节点的上一个节点
34        e. next.previous = e.previous;
35        // 修正该节点自身的前后引用
36         e. next = e.previous = null;
37        // 将自身置空,让gc可以尽快回收
38         e. element = null;
39        // 计数器减一
40         size--;
41         modCount++;
42         return result;
43     }

上面对于链表增加元素总结了,一句话就是“改变前后的互相指向关系”,删除也是同样的道理,由于节点被删除,该节点的上一个节点和下一个节点互相拉一下小手就可以了,注意的是“互相”,不能一厢情愿。

     

7.修改

 1     /**
 2      * 修改指定位置索引位置的元素
 3      */
 4     public E set( int index, E element) {
 5         // 查找index位置的节点
 6         Entry<E> e = entry(index);
 7         // 取出该节点的元素,供返回使用
 8         E oldVal = e. element;
 9         // 用新元素替换旧元素
10         e. element = element;
11         // 返回旧元素
12         return oldVal;
13     }    

set方法看起来简单了很多,只要修改该节点上的元素就好了,但是不要忽略了这里的entry()方法,重点就是它。

8.查询

 

     终于到查询了,终于发现了上面经常出现的那个方法entry()根据index查询节点,我们知道数组是有下标的,通过下标操作天然的支持根据index查询元素,而链表中是没有index概念呢,那么怎么样才能通过index查询到对应的元素呢,下面就来看看LinkedList是怎么实现的。

 1     /**
 2      * 查找指定索引位置的元素
 3      */
 4     public E get( int index) {
 5         return entry(index).element ;
 6     }
 7
 8     /**
 9      * 返回指定索引位置的节点
10      */
11     private Entry<E> entry( int index) {
12         // 越界检查
13         if (index < 0 || index >= size)
14             throw new IndexOutOfBoundsException( "Index: "+index+
15                                                 ", Size: "+size );
16         // 取出头结点
17         Entry<E> e = header;
18         // size>>1右移一位代表除以2,这里使用简单的二分方法,判断index与list的中间位置的距离
19         if (index < (size >> 1)) {
20             // 如果index距离list中间位置较近,则从头部向后遍历(next)
21             for (int i = 0; i <= index; i++)
22                 e = e. next;
23         } else {
24             // 如果index距离list中间位置较远,则从头部向前遍历(previous)
25             for (int i = size; i > index; i--)
26                 e = e. previous;
27         }
28         return e;
29     }

     现在知道了,LinkedList是通过从header开始index计为0,然后一直往下遍历(next),直到到底index位置。为了优化查询效率,LinkedList采用了二分查找(这里说的二分只是简单的一次二分),判断index与size中间位置的距离,采取从header向后还是向前查找。

到这里我们明白,基于双向循环链表实现的LinkedList,通过索引Index的操作时低效的,index所对应的元素越靠近中间所费时间越长。而向链表两端插入和删除元素则是非常高效的(如果不是两端的话,都需要对链表进行遍历查找)。

9.是否包含

 1   /**
 2      * Returns <tt>true</tt> if this list contains the specified element.
 3      * More formally, returns <tt>true</tt> if and only if this list contains
 4      * at least one element <tt>e</tt> such that
 5      * <tt>(o==null&nbsp;?&nbsp;e==null&nbsp;:&nbsp;o.equals(e))</tt>.
 6      *
 7      * @param o element whose presence in this list is to be tested
 8      * @return <tt> true</tt> if this list contains the specified element
 9      */
10     public boolean contains(Object o) {
11         return indexOf(o) != -1;
12     }
13
14     /**
15      * Returns the index of the first occurrence of the specified element
16      * in this list, or -1 if this list does not contain the element.
17      * More formally, returns the lowest index <tt>i</tt> such that
18      * <tt>(o==null&nbsp;?&nbsp;get(i)==null&nbsp;:&nbsp;o.equals(get(i)))</tt>,
19      * or -1 if there is no such index.
20      *
21      * @param o element to search for
22      * @return the index of the first occurrence of the specified element in
23      *         this list, or -1 if this list does not contain the element
24      */
25     public int indexOf(Object o) {
26         int index = 0;
27         if (o==null) {
28             for (Entry e = header .next; e != header; e = e.next ) {
29                 if (e.element ==null)
30                     return index;
31                 index++;
32             }
33         } else {
34             for (Entry e = header .next; e != header; e = e.next ) {
35                 if (o.equals(e.element ))
36                     return index;
37                 index++;
38             }
39         }
40         return -1;
41     }
42
43     /**
44      * Returns the index of the last occurrence of the specified element
45      * in this list, or -1 if this list does not contain the element.
46      * More formally, returns the highest index <tt>i</tt> such that
47      * <tt>(o==null&nbsp;?&nbsp;get(i)==null&nbsp;:&nbsp;o.equals(get(i)))</tt>,
48      * or -1 if there is no such index.
49      *
50      * @param o element to search for
51      * @return the index of the last occurrence of the specified element in
52      *         this list, or -1 if this list does not contain the element
53      */
54     public int lastIndexOf(Object o) {
55         int index = size ;
56         if (o==null) {
57             for (Entry e = header .previous; e != header; e = e.previous ) {
58                 index--;
59                 if (e.element ==null)
60                     return index;
61             }
62         } else {
63             for (Entry e = header .previous; e != header; e = e.previous ) {
64                 index--;
65                 if (o.equals(e.element ))
66                     return index;
67             }
68         }
69         return -1;
70     }

public boolean remove(Object o) 一样,indexOf查询元素位于容器的索引位置,都是需要对链表进行遍历操作,当然也就是低效了啦。

10.判断容量

 1   /**
 2      * Returns the number of elements in this list.
 3      *
 4      * @return the number of elements in this list
 5      */
 6     public int size() {
 7         return size ;
 8     }
 9
10     /**
11      * {@inheritDoc}
12      *
13      * <p>This implementation returns <tt>size() == 0 </tt>.
14      */
15     public boolean isEmpty() {
16         return size() == 0;
17     }

和ArrayList一样,基于计数器size操作,容量判断很方便。

到这里LinkedList就分析完了,不对好像还差些什么对不对?是什么呢,就是最开始说的Deque双端队列,明白了链表原理和LinkedList的基本crud操作,Deque的LinkedList实现就已经是so easy了,我们简单看下。

11.LinkedList实现的Deque双端队列    

 1     /**
 2      * Adds the specified element as the tail (last element) of this list.
 3      *
 4      * @param e the element to add
 5      * @return <tt> true</tt> (as specified by {@link Queue#offer})
 6      * @since 1.5
 7      */
 8     public boolean offer(E e) {
 9         return add(e);
10     }
11
12     /**
13      * Retrieves and removes the head (first element) of this list
14      * @return the head of this list, or <tt>null </tt> if this list is empty
15      * @since 1.5
16      */
17     public E poll() {
18         if (size ==0)
19             return null;
20         return removeFirst();
21     }
22
23     /**
24      * Removes and returns the first element from this list.
25      *
26      * @return the first element from this list
27      * @throws NoSuchElementException if this list is empty
28      */
29     public E removeFirst() {
30         return remove(header .next);
31     }
32
33     /**
34      * Retrieves, but does not remove, the head (first element) of this list.
35      * @return the head of this list, or <tt>null </tt> if this list is empty
36      * @since 1.5
37      */
38     public E peek() {
39         if (size ==0)
40             return null;
41         return getFirst();
42     }
43
44     /**
45      * Returns the first element in this list.
46      *
47      * @return the first element in this list
48      * @throws NoSuchElementException if this list is empty
49      */
50     public E getFirst() {
51         if (size ==0)
52            throw new NoSuchElementException();
53
54         return header .next. element;
55     }
56
57     /**
58      * Pushes an element onto the stack represented by this list.  In other
59      * words, inserts the element at the front of this list.
60      *
61      * <p>This method is equivalent to {@link #addFirst}.
62      *
63      * @param e the element to push
64      * @since 1.6
65      */
66     public void push(E e) {
67         addFirst(e);
68     }
69
70     /**
71      * Inserts the specified element at the beginning of this list.
72      *
73      * @param e the element to add
74      */
75     public void addFirst(E e) {
76        addBefore(e, header.next );
77     }

看看Deque 的实现是不是很简单,逻辑都是基于上面讲的链表操作的,对于队列的一些概念我不打算在这里讲,是因为后面队列会单独拿出来分析啦,这里只要理解基于链表实现的list内部是怎么操作的就可以啦。。。

LinkedList 完!

时间: 2024-10-13 12:27:46

给jdk写注释系列之jdk1.6容器(2)-LinkedList源码解析的相关文章

给jdk写注释系列之jdk1.6容器(6)-HashSet源码解析&amp;Map迭代器

今天的主角是HashSet,Set是什么东东,当然也是一种java容器了. 现在再看到Hash心底里有没有会心一笑呢,这里不再赘述hash的概念原理等一大堆东西了(不懂得需要先回去看下HashMap了),需要在啰嗦一句的是hash表是基于快速存取的角度设计的,也是一种典型的空间换时间的做法(这个在分析HashMap中都有讲过).那么今天的HashSet它又是怎么一回事的,他的存在又是为了解决什么问题呢? 先来看下Set的特点:Set元素无顺序,且元素不可以重复. .想到了什么?无顺序,由于散列的

给jdk写注释系列之jdk1.6容器(12)-PriorityQueue源码解析

PriorityQueue是一种什么样的容器呢?看过前面的几个jdk容器分析的话,看到Queue这个单词你一定会,哦~这是一种队列.是的,PriorityQueue是一种队列,但是它又是一种什么样的队列呢?它具有着什么样的特点呢?它的底层实现方式又是怎么样的呢?我们一起来看一下. PriorityQueue其实是一个优先队列,什么是优先队列呢?这和我们前面讲的先进先出(First In First Out )的队列的区别在于,优先队列每次出队的元素都是优先级最高的元素.那么怎么确定哪一个元素的优

给jdk写注释系列之jdk1.6容器(5)-LinkedHashMap源码解析

前面分析了HashMap的实现,我们知道其底层数据存储是一个hash表(数组+单向链表).接下来我们看一下另一个LinkedHashMap,它是HashMap的一个子类,他在HashMap的基础上维持了一个双向链表(hash表+双向链表),在遍历的时候可以使用插入顺序(先进先出,类似于FIFO),或者是最近最少使用(LRU)的顺序. 来具体看下LinkedHashMap的实现. 1.定义  1 public class LinkedHashMap<K,V> 2 extends HashMap&

给jdk写注释系列之jdk1.6容器(13)-总结篇之Java集合与数据结构

是的,这篇blogs是一个总结篇,最开始的时候我提到过,对于java容器或集合的学习也可以看做是对数据结构的学习与应用.在前面我们分析了很多的java容器,也接触了好多种常用的数据结构,今天我们就来总结下这些内容. 下面我们以数据结构的维度来总结下,在Java集合的实现过程中,底层到底使用了哪些常用的数据结构中,他们分别又有什么特点.      1. 数组(Array) 结构说明:在程序设计中,为了处理方便, 把具有相同类型的若干变量按有序的形式组织起来.这些按序排列的同类数据元素的集合称为数组

给jdk写注释系列之jdk1.6容器(10)-Stack&amp;Vector源码解析

前面我们已经接触过几种数据结构了,有数组.链表.Hash表.红黑树(二叉查询树),今天再来看另外一种数据结构:栈. 什么是栈呢,我就不找它具体的定义了,直接举个例子,栈就相当于一个很窄的木桶,我们往木桶里放东西,往外拿东西时会发现,我们最开始放的东西在最底部,最先拿出来的是刚刚放进去的.所以,栈就是这么一种先进后出( First In Last Out,或者叫后进先出) 的容器,它只有一个口,在这个口放入元素,也在这个口取出元素. 栈最主要了两个动作就是入栈和出栈操作,其实还是很容易的明白的对不

给jdk写注释系列之jdk1.6容器(1):ArrayList源码解析

原文出自吞噬天地,链接整理自ImportNew 给jdk写注释系列之jdk1.6容器(2):LinkedList源码解析 给jdk写注释系列之jdk1.6容器(3):Iterator设计模式 给jdk写注释系列之jdk1.6容器(4)-HashMap源码解析 给jdk写注释系列之jdk1.6容器(5)-LinkedHashMap源码解析 给jdk写注释系列之jdk1.6容器(6)-HashSet源码解析&Map迭代器 给jdk写注释系列之jdk1.6容器(1):ArrayList源码解析 工作中

给jdk写注释系列之jdk1.6容器(1)-ArrayList

工作中经常听到别人讲“容器”,各种各样的容器,话说到底什么是容器,通俗的讲“容器就是用来装东西的器皿,比如:水桶就是用来盛水的,水桶就是一个容器.” ok,在我们写程序的时候常常要对大量的对象进行管理,比如查询,遍历,修改等.jdk为我们提供的容器位于java.util包,也是我们平时用的最多的包之一. 但是为什么不用数组(其实也不是不用,只是不直接用)呢,因为数组的长度需要提前确定,而且不能改变大小,用起来手脚受限嘛. 下面步入正题,首先我们想,一个对象管理容器需要哪些功能?增加,删除,修改,

Feign 系列(05)Spring Cloud OpenFeign 源码解析

Feign 系列(05)Spring Cloud OpenFeign 源码解析 [TOC] Spring Cloud 系列目录(https://www.cnblogs.com/binarylei/p/11563952.html#feign) 在 上一篇 文章中我们分析 Feign 参数解析的整个流程,Feign 原生已经支持 Feign.JAX-RS 1/2 声明式规范,本文着重关注 Spring Cloud 是如果整合 OpenFeign 的,使之支持 Spring MVC? 1. Sprin

vue系列---响应式原理实现及Observer源码解析(一)

_ 阅读目录 一. 什么是响应式? 二:如何侦测数据的变化? 2.1 Object.defineProperty() 侦测对象属性值变化 2.2 如何侦测数组的索引值的变化 2.3 如何监听数组内容的增加或减少? 2.4 使用Proxy来实现数据监听 三. Observer源码解析 回到顶部 一. 什么是响应式? 我们可以这样理解,当一个数据状态发生改变的时候,那么与这个数据状态相关的事务也会发生改变.用我们的前端专业术语来讲,当我们JS中的对象数据发生改变的时候,与JS中对象数据相关联的DOM