Java集合框架之二:LinkedList源码解析

版权声明:本文为博主原创文章,转载请注明出处,欢迎交流学习!

LinkedList底层是通过双向循环链表来实现的,其结构如下图所示:

链表的组成元素我们称之为节点,节点由三部分组成:前一个节点的引用地址、数据、后一个节点的引用地址。LinkedList的Head节点不包含数据,每一个节点对应一个Entry对象。下面我们通过源码来分析LinkedList的实现原理。

1、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类包含三个属性,其中element存放业务数据;next存放后一个节点的信息,通过next可以找到后一个节点;previous存放前一个节点的信息,通过previous可以找到前一个节点。

2、LinkedList的构造方法:LinkedList提供了两个带不同参数的构造方法。

1) LinkedList(),构造一个空列表。

2) LinkedList(Collection<? extends E> c),构造一个包含指定 collection 中的元素的列表,这些元素按其 collection 的迭代器返回的顺序排列。

 1 private transient Entry<E> header = new Entry<E>(null, null, null); //声明一个空的Entry对象
 2 private transient int size = 0; //集合中节点的个数
 3
 4 public LinkedList() {
 5    header.next = header.previous = header; //构造一个双向循环链表,header的前驱节点和后置节点都指向header自己
 6 }
 7
 8 public LinkedList(Collection<? extends E> c) {
 9    this(); //调用不带参数的构造方法,创建一个空的循环链表
10    addAll(c); //调用addAll方法将Collection的元素添加到LinkedList中
11 }

当调用不带参数的构造方法,header的前驱节点和后置节点都指向header自己,构成了一个双向循环的链表。如下图所示:

当调用带集合参数的构造方法生成LinkedList对象时,会先调用不带参数的构造方法创建一个空的循环链表,然后调用addAll方法将集合元素添加到LinkedList中。

3、向集合中添加元素:LinkedList提供了多种不同的add方法向集合添加元素。

1) add(E e),将指定元素添加到此列表的结尾。

2) add(int index, E element),在此列表中指定的位置插入指定的元素。

3) addAll(Collection<? extends E> c),添加指定 collection 中的所有元素到此列表的结尾,顺序是指定 collection 的迭代器返回这些元素的顺序。

4) addAll(int index, Collection<? extends E> c),将指定 collection 中的所有元素从指定位置开始插入此列表。

5) addFirst(E e),将指定元素插入此列表的开头。

6) addLast(E e),将指定元素添加到此列表的结尾。

通过源码来分析其底层的实现原理:

 1 public boolean add(E o) {
 2     addBefore(o, header);
 3     return true;
 4 }
 5
 6 private Entry<E> addBefore(E o, Entry<E> e) {
 7     Entry<E> newEntry = new Entry<E>(o, e, e.previous); //创建的对象newEntry的数据为o,后置节点为header,前驱节点为header.previous即header本身
 8     newEntry.previous.next = newEntry;  //newEntry.previous即为header,也就是header.next = newEntry,把header的后置节点设置为newEntry
 9     newEntry.next.previous = newEntry;  //newEntry.next即header,也就是header.previous = newEntry,经过以上步骤header节点和newEntry节点形成闭环
10     size++;  //节点数加1
11     modCount++;
12     return newEntry;
13 }

通过以上源码分析可知,add(E elment)方法新添加一个元素element时,在LinkedList底层首先创建一个Entry类型的对象newEntry,并把元素element存放在newEntry中,同时把newEntry的前驱节点设置为header,后置节点也设置为header。接下来,将header节点的后置节点设置为newEntry,将header的前驱节点也设置为newEntry,并且节点数size加1。通过以上操作,节点header和节点newEntry形成闭环(双向循环链表)。其示意图如下:

1 public void add(int index, E element) {
2       addBefore(element, (index==size ? header : entry(index)));//调用addBefore将元素插入指定的位置
3 }

add(int index, E element)方法的实现原理与add(E elment)方法类似,都是调用addBefore方法实现,其本质还是生成一个新的Entry对象,然后修改指定位置的节点的前驱节点信息以及后置节点信息,这里就不再赘述了。

 1 public boolean addAll(Collection<? extends E> c) {
 2         return addAll(size, c);
 3 }
 4
 5 public boolean addAll(int index, Collection<? extends E> c) {
 6         if (index < 0 || index > size) //检查参数index是否合法
 7             throw new IndexOutOfBoundsException("Index: "+index+
 8                                                 ", Size: "+size);
 9         Object[] a = c.toArray(); //返回一个包含Collection集合所有元素的Object[]
10         int numNew = a.length;
11         if (numNew==0)  //如果需要插入的元素为0,则返回false,表示没有元素需要插入
12             return false;
13     modCount++; //否则插入元素,链表修改次数加1
14
15         Entry<E> successor = (index==size ? header :entry(index)); //获取index位置的节点。插入位置如果等人size,则在头节点header处插入,否则在获取的index处插入
16         Entry<E> predecessor = successor.previous; //获取节点的前驱节点,插入时需要修改此节点的next
17     for (int i=0; i<numNew; i++) { //将数组中第一个元素插入到index处,将之后的元素按顺序插在这个节点后面
18             Entry<E> e = new Entry<E>((E)a[i], successor, predecessor); //创建一个Entry对象
19             predecessor.next = e; //将新插入的节点的前一个节点的next指向当前节点
20             predecessor = e; //将新插入的节点赋值给predecessor,相当于指针向后移,实现循环
21         }
22         successor.previous = predecessor; //将断开的index处的节点的previous指向插入的最后一个节点
23
24         size += numNew; //修改节点个数
25         return true;
26     }
1 public void addFirst(E o) {
2     addBefore(o, header.next);
3 }
4
5 public void addLast(E o) {
6     addBefore(o, header);
7 }

从源码可知,这两个方法都是调用addBefore(E e, Entry entry)方法实现,插入节点的示意图如下:

结合addBefore(E e, Entry entry)方法不难理解addFirst(E e)只需要在header下一个节点之前插入即可;addLast(E e)只需要在header之前插入,因为在循环链表中,header前一个节点也是链表的最后一个节点。

4、获取LinkedList中的元素:

1) get(int index),返回此列表中指定位置处的元素。

2) getFirst(),返回此列表的第一个元素。

3) getLast(),返回此列表的最后一个元素。

 1 public E get(int index) {
 2         return entry(index).element;
 3 }
 4
 5 private Entry<E> entry(int index) {  //获取双向链表指定位置的节点
 6         if (index < 0 || index >= size)  //检查参数index合法性,当指定的位置index小于零或大于集合容量,抛出异常
 7             throw new IndexOutOfBoundsException("Index: "+index+
 8                                                 ", Size: "+size);
 9         Entry<E> e = header;
10         if (index < (size >> 1)) { //获取index处的节点,当index小于链表长度的1/2,则从前向后查找;否则从后向前查找。这样能提供查找效率。
11             for (int i = 0; i <= index; i++)
12                 e = e.next;
13         } else {
14             for (int i = size; i > index; i--)
15                 e = e.previous;
16         }
17         return e;
18 }
19
20  public E getFirst() {
21     if (size==0)
22         throw new NoSuchElementException();
23
24     return header.next.element; //返回header节点的下一个节点。
25  }
26
27 public E getLast()  {
28     if (size==0)
29         throw new NoSuchElementException();
30
31     return header.previous.element; //返回header节点的前一个节点,由于是双向循环链表,header前一个节点也是链表的最后一个节点。
32  }

从LinkedList底层链表的实现原理可知,LinkedList集合的查找操作是从header节点开始的。可以从header节点开始从前向后一个一个节点顺序查找或者从后向前依次顺序查找。而ArrayList的查找操作则是直接获取数组下标处的元素,因此查询效率更高。

5、移除LinkedList中的元素:

1) remove(),获取并移除此列表的头(第一个元素)。

2) remove(int index),移除此列表中指定位置处的元素。

3) remove(Object o),从此列表中移除首次出现的指定元素(如果存在)。

4) removeFirst(),移除并返回此列表的第一个元素。此方法等同于remove()。

5) removeLast(),移除并返回此列表的最后一个元素。

 1 public E remove() {
 2         return removeFirst(); //调用removeFirst()方法。
 3  }
 4
 5 public E remove(int index) {
 6         return remove(entry(index));  //调用私有方法remove(Entry<E> e)以及entry(int index)。
 7  }
 8
 9 public E removeFirst() {
10     return remove(header.next);   //调用私有方法remove(Entry<E> e)。
11  }
12
13 public E removeLast() {
14     return remove(header.previous);   //调用私有方法remove(Entry<E> e)。
15  }
16
17 public boolean remove(Object o) {
18         if (o==null) {  如果要删除的元素内容为null
19             for (Entry<E> e = header.next; e != header; e = e.next) {
20                 if (e.element==null) {
21                     remove(e);  //调用私有方法remove(Entry<E> e)。
22                     return true;
23                 }
24             }
25         } else {
26             for (Entry<E> e = header.next; e != header; e = e.next) {
27                 if (o.equals(e.element)) {
28                     remove(e);
29                     return true;
30                 }
31             }
32         }
33         return false;
34  }
35
36 private E remove(Entry<E> e) {
37     if (e == header)
38         throw new NoSuchElementException();
39
40         E result = e.element;  //保存将要被移除的节点的内容。
41     e.previous.next = e.next;  //将节点e的前驱节点的后继设置为e的后继节点。
42     e.next.previous = e.previous; //将e的后继节点的前驱设置为e的前驱节点。
43         e.next = e.previous = null;  //将e节点的后继指向以及前驱指向设置为空。
44         e.element = null;  //将e节点的内容清空。
45     size--; //节点个数减1。
46     modCount++; //链表操作次数加1。
47         return result;  //返回被删除的节点内容。
48   }

从源码可以看出,移除节点操作最终都是调用私有方法remove(Entry<E> e)。删除某一节点的操作本质上是修改其相关节点的指针信息。其原理如下:

e.previous.next = e.next; //预删除节点e的前驱节点的后继指针指向e的后继节点。

e.next.previous = e.previous; //预删除节点e的后继节点的前驱指针指向e的前驱节点。

清空预删除节点的指针信息以及内容:

e.next = e.previous = null; //清空后继指针和前驱指针的信息。

e.element = null; //清空节点的内容。

通过对以上部分关键源码的分析,我们可以知道LinkedList集合的底层是基于双向循环链表来实现的。链表中维护了一个个Entry对象,Entry对象由三部分组成:previous(前驱指针,指向前驱节点)、element(数据)、next(后继指针,指向后继节点)。对LinkedList集合元素的操作本质上是对链表中维护的Entry对象的操作(修改相关对象的指针信息、数据),明白了这一点,就能很容易的理解LinkedList集合的常用方法的底层实现原理。下面对LinkedList源码有几点总结:

1) LinkedList不是线程安全的。在多线程环境下,可以使用Collections.synchronizedList方法声明一个线程安全的ArrayList,例如:

List list = Collections.sychronizedList(new LinkedList());

2) LinkedList是有序集合。LinkedList的查找操作是从header节点开始的,从前往后或者从后往前依次逐个节点查找,因此查询效率低,而ArrayList可以直接通过下标查找,查询效率高;由于LinkedList集合元素的添加、删除操作只需要修改节点的指针信息,因此添加、删除元素的效率高,而ArrayList的添加、删除操作会涉及大量元素位置的迁移,因此添加、删除元素的效率低。

  

时间: 2024-10-05 21:40:15

Java集合框架之二:LinkedList源码解析的相关文章

【Java集合】试读LinkedList源码

LinkedList的本质是双向链表.(01) LinkedList继承于AbstractSequentialList,并且实现了Dequeue接口. (02) LinkedList包含两个重要的成员:header 和 size. header是双向链表的表头,它是双向链表节点所对应的类Entry的实例.Entry中包含成员变量: previous, next, element.其中,previous是该节点的上一个节点,next是该节点的下一个节点,element是该节点所包含的值.  siz

Java集合框架之一:ArrayList源码分析

版权声明:本文为博主原创文章,转载请注明出处,欢迎交流学习! ArrayList底层维护的是一个动态数组,每个ArrayList实例都有一个容量.该容量是指用来存储列表元素的数组的大小.它总是至少等于列表的大小.随着向 ArrayList 中不断添加元素,其容量也自动增长. ArrayList不是同步的(也就是说不是线程安全的),如果多个线程同时访问一个ArrayList实例,而其中至少一个线程从结构上修改了列表,那么它必须保持外部同步,在多线程环境下,可以使用Collections.synch

二.jQuery源码解析之构建jQuery之构建函数jQuery的7种用法

一:$(selectorStr[,限制范围]),接受一个选择器(符合jQuery规范的字符串),返回一个jQuery对象;二:$(htmlStr[,文档对象]),$(html[,json对象])传入html字符串,创建一个新的dom元素 三:$(dom元素),$(dom元素集合)将dom元素转换成jQuery对象.四:$(自定义对象)封装普通对象为jQuery对象.五:$(回调函数)绑定ready事件监听函数,当Dom加载完成时执行.六:$(jQuery对象)接受一个jQuery对象,返回一个j

十二.jQuery源码解析之.eq().first().last().slice()

eq(index):将集合中的索引为index的元素提取出来. first():返回集合中的第一个元素. .last():防护集合中的最后一个元素. .slice(start[,end]):返回集合中的给定区间段的元素. first()和last()调用eq(),eq()通过slice()实现,slice()通过 .pushStack()实现. 相关源码 285行:用法很奇特,通过一个"+"把可能为字符串的i转换成一个数值. 300~301:先借用数组方法slice()从当前jQuer

jdk源码阅读笔记之java集合框架(二)(ArrayList)

关于ArrayList的分析,会从且仅从其添加(add)与删除(remove)方法入手. ArrayList类定义: p.p1 { margin: 0.0px 0.0px 0.0px 0.0px; font: 18.0px Monaco } span.s1 { color: #931a68 } public class ArrayList<E> extends AbstractList<E> implements List<E> ArrayList基本属性: /** *

Java集合---LinkedList源码解析

一.源码解析1. LinkedList类定义2.LinkedList数据结构原理3.私有属性4.构造方法5.元素添加add()及原理6.删除数据remove()7.数据获取get()8.数据复制clone()与toArray()9.遍历数据:Iterator()二.ListItr 一.源码解析 1. LinkedList类定义. public class LinkedList<E> extends AbstractSequentialList<E> implements List&

【Java集合】-- LinkedList源码解析

目录 继承体系 数据结构 源码解析 1.属性 2.构造方法 LinkedList() LinkedList(Collection<? extends E> c) 3.添加元素 add(E e) addFirst(E e) addLast(E e) add(int index, E element) offer(E e) offerFirst(E e) offerLast(E e) 总结 4.获取元素 get(int index) getFirst() getLast() peek() 5.删除

1.Java集合-HashMap实现原理及源码分析

哈希表(Hash  Table)也叫散列表,是一种非常重要的数据结构,应用场景及其丰富,许多缓存技术(比如memcached)的核心其实就是在内存中维护一张大的哈希表,而HashMap的实现原理也常常出现在各类的面试题中,这里对java集合框架中的对应实现HashMap的实现原理进行讲解,然后对JDK7的HashMap的源码进行分析 哈希算法,是一类算法: 哈希表(Hash  Table)是一种数据结构: 哈希函数:是支撑哈希表的一类函数: HashMap 是 Java中用哈希数据结构实现的Ma

【Java深入研究】2、LinkedList源码解析

一.源码解析 1. LinkedList类定义. public class LinkedList<E> extends AbstractSequentialList<E> implements List<E>, Deque<E>, Cloneable, java.io.Serializable LinkedList 是一个继承于AbstractSequentialList的双向链表.它也可以被当作堆栈.队列或双端队列进行操作.LinkedList 实现 Li