6.Java集合-LinkedList实现原理及源码分析

Java中LinkedList的部分源码(本文针对1.7的源码)

LinkedList的基本结构

  jdk1.7之后,node节点取代了 entry ,带来的变化是,将1.6中的环形结构优化为了直线型链表结构,从双向循环链表变成了双向链表

  在LinkedList中,我们把链子的“环”叫做“节点”,每个节点都是同样的结构。节点与节点之间相连,构成了我们LinkedList的基本数据结构,也是LinkedList的核心。

  我们再来看一下LinkedList在jdk1.6和1.7之间结构的区别

LinkedList的构造方法

  LinkedList包含3个全局参数,size存放当前链表有多少个节点。

  first为指向链表的第一个节点的引用

  last为指向链表的最后一个节点的引用

LinkedList的构造方法有两个,一个是无参构造,一个是传入Collection对象的构造

 1 // 什么都没做,是一个空实现
 2 public LinkedList() {
 3 }
 4
 5 public LinkedList(Collection<? extends E> c) {
 6     this();
 7     addAll(c);
 8 }
 9
10 public boolean addAll(Collection<? extends E> c) {
11     return addAll(size, c);
12 }
13
14 public boolean addAll(int index, Collection<? extends E> c) {
15     // 检查传入的索引值是否在合理范围内
16     checkPositionIndex(index);
17     // 将给定的Collection对象转为Object数组
18     Object[] a = c.toArray();
19     int numNew = a.length;
20     // 数组为空的话,直接返回false
21     if (numNew == 0)
22         return false;
23     // 数组不为空
24     Node<E> pred, succ;
25     if (index == size) {
26         // 构造方法调用的时候,index = size = 0,进入这个条件。
27         succ = null;
28         pred = last;
29     } else {
30         // 链表非空时调用,node方法返回给定索引位置的节点对象
31         succ = node(index);
32         pred = succ.prev;
33     }
34     // 遍历数组,将数组的对象插入到节点中
35     for (Object o : a) {
36         @SuppressWarnings("unchecked") E e = (E) o;
37         Node<E> newNode = new Node<>(pred, e, null);
38         if (pred == null)
39             first = newNode;
40         else
41             pred.next = newNode;
42         pred = newNode;
43     }
44
45     if (succ == null) {
46         last = pred; // 将当前链表最后一个节点赋值给last
47     } else {
48         // 链表非空时,将断开的部分连接上
49         pred.next = succ;
50         succ.prev = pred;
51     }
52     // 记录当前节点个数
53     size += numNew;
54     modCount++;
55     return true;
56 }  

注:Node是LinkedList的内部私有类,也是我们的核心节点类

 1 private static class Node<E> {
 2     E item;
 3     Node<E> next;
 4     Node<E> prev;
 5
 6     Node(Node<E> prev, E element, Node<E> next) {
 7         this.item = element;
 8         this.next = next;
 9         this.prev = prev;
10     }
11 }

  

  对与两种构造方法,总结起来,可以概括为:无参构造为空实现。有参构造传入Collection对象,将对象转为数组,并按遍历顺序将数组首尾相连,全局变量first和last分别指向这个链表的第一个和最后一个。

 LinkList部分方法分析

  addFirst/addLast分析

 1 public void addFirst(E e) {
 2     linkFirst(e);
 3 }
 4
 5 private void linkFirst(E e) {
 6     final Node<E> f = first;
 7     final Node<E> newNode = new Node<>(null, e, f); // 创建新的节点,新节点的后继指向原来的头节点,即将原头节点向后移一位,新节点代替头结点的位置。
 8     first = newNode;
 9     if (f == null)
10         last = newNode;
11     else
12         f.prev = newNode;
13     size++;
14     modCount++;
15 }  

  加入一个新的节点,看方法名就能知道,是在现在的链表的头部加一个节点,既然是头结点,那么头结点的前继必然为null,所以这也是Node<E> newNode = new Node<>(null, e, f);这样写的原因。

  之后将first指向了newNode ,指定这个节点以后就就是我们的头结点

  之后对原来头节点进行了判断,若在插入元素之前头结点为null,则当前加入的元素就是第一个几点,也就是头结点,所以当前的状况就是:头结点=刚刚加入的节点=尾节点。

  若在插入元素之前头结点不为null,则证明之前的链表是有值的,那么我们只需要把新加入的节点的后继指向原来的头结点,而尾节点则没有发生变化。这样一来,原来的头结点就变成了第二个节点了。达到了我们的目的。

  addLast方法在实现上是个addFirst是一致的,这里就不在赘述了。有兴趣的朋友可以看看源代码。

  其实,LinkedList中add系列的方法都是大同小异的,都是创建新的节点,改变之前的节点的指向关系。仅此而已。

  getFirst/getLast方法分析

 1 public E getFirst() {
 2     final Node<E> f = first;
 3     if (f == null)
 4         throw new NoSuchElementException();
 5     return f.item;
 6 }
 7
 8 public E getLast() {
 9     final Node<E> l = last;
10     if (l == null)
11         throw new NoSuchElementException();
12     return l.item;
13 }

  get方法分析(node方法的调用)

 1 public E get(int index) {
 2     // 校验给定的索引值是否在合理范围内
 3     checkElementIndex(index);
 4     return node(index).item;
 5 }
 6
 7 Node<E> node(int index) {
 8     if (index < (size >> 1)) {
 9         Node<E> x = first;
10         for (int i = 0; i < index; i++)
11             x = x.next;
12         return x;
13     } else {
14         Node<E> x = last;
15         for (int i = size - 1; i > index; i--)
16             x = x.prev;
17         return x;
18     }
19 }  

  注:关键在于,判断给定的索引值,若索引值大于整个链表长度的一半,则从后往前找,若索引引用值小于整个链表长度的一半,则从前往后找。这样就可以保证,不管链表的长度有多大,搜索的时候最多只搜索链表长度的一半就可以找打,大大提升了效率

  removeFirst/removeLast方法分析

 1 public E get(int index) {
 2     // 校验给定的索引值是否在合理范围内
 3     checkElementIndex(index);
 4     return node(index).item;
 5 }
 6
 7 Node<E> node(int index) {
 8     if (index < (size >> 1)) {
 9         Node<E> x = first;
10         for (int i = 0; i < index; i++)
11             x = x.next;
12         return x;
13     } else {
14         Node<E> x = last;
15         for (int i = size - 1; i > index; i--)
16             x = x.prev;
17         return x;
18     }
19 }  

  摘掉头结点,将原来的第二个节点变为头结点,改变first的指向,若之前仅剩一个节点,移除之后全部置为null

  对于LinkList的其他方法,大致上都是包装了以上这几个方法

关于集合的一个小补充:

  在ArrayList,LinkedList,HashMap等等的增、删、改方法中,我们总能看到modCount的身影,modCount字面意思就是修改次数,但为什么要记录modCount的修改次数呢?

  大家发现一个公共特点没有,所有使用modCount属性的集合全是线程不安全的,这是为什么呢?说明modCount 可能和线程安全有关

  

  阅读源码,发现这玩意只有在本数据结构对应的迭代器中才使用,以HashMap为例:

  

 1 private abstract class HashIterator<E> implements Iterator<E> {
 2         Entry<K,V> next;        // next entry to return
 3         int expectedModCount;   // For fast-fail
 4         int index;              // current slot
 5         Entry<K,V> current;     // current entry
 6
 7         HashIterator() {
 8             expectedModCount = modCount;
 9             if (size > 0) { // advance to first entry
10                 Entry[] t = table;
11                 while (index < t.length && (next = t[index++]) == null)
12                     ;
13             }
14         }
15
16         public final boolean hasNext() {
17             return next != null;
18         }
19
20         final Entry<K,V> nextEntry() {
21             if (modCount != expectedModCount)
22                 throw new ConcurrentModificationException();
23             Entry<K,V> e = next;
24             if (e == null)
25                 throw new NoSuchElementException();
26
27             if ((next = e.next) == null) {
28                 Entry[] t = table;
29                 while (index < t.length && (next = t[index++]) == null)
30                     ;
31             }
32             current = e;
33             return e;
34         }
35
36         public void remove() {
37             if (current == null)
38                 throw new IllegalStateException();
39             if (modCount != expectedModCount)
40                 throw new ConcurrentModificationException();
41             Object k = current.key;
42             current = null;
43             HashMap.this.removeEntryForKey(k);
44             expectedModCount = modCount;
45         }
46     }

  由以上代码可以看出,在一个迭代器初始的时候会赋予它调用这个迭代器的对象的mCount,如果在迭代器遍历的过程中,一旦发现这个对象的mcount和迭代器存储的mcount 不一样,那就抛出异常

  下面详细解释:

  Fail-Fast机制

  我们知道java.util.HashMap不是线程安全的,因此如果在使用迭代器的过程中有其他线程修改了map,那么将抛出ConcurrentModificationException,这就是所谓的fail-fast策略。这一策略在源码中的实现是通过 modCount 域,modCount顾名思义就是修改次数,对HashMap内容的修改都将增加这个值,那么在迭代器初始化过程中会将这个值赋给迭代器的expectedModCount。在迭代过程中,判断 modCount跟expectedModCount是否相等,如果不相等就表示,我还在迭代呢,就有其他线程对Map进行了修改,注意到 modCount 声明为 volatile,保证线程之间修改的可见性。

  所以在这里和大家建议,当大家遍历那些非线程安全的数据结构时,尽量使用迭代器

时间: 2024-08-06 11:47:41

6.Java集合-LinkedList实现原理及源码分析的相关文章

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

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

2.Java集合-ConcurrentHashMap实现原理及源码分析

一.为何用ConcurrentHashMap 在并发编程中使用HashMap可能会导致死循环,而使用线程安全的HashTable效率又低下. 线程不安全的HashMap 在多线程环境下,使用HashMap进行put操作会引起死循环,导致CPU利用率接近100%,所以在并发情况下不能使用HashMap 效率低下的HashTable Hashtable使用synchronized来保证线程的安全,但是在线程竞争激烈的情况下Hashtable的效率非常低下.当一个线程访问Hashtable的同步方法,

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

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

Java集合(12)--TreeSet源码分析

TreeSet 底层实际使用的存储容器就是 TreeMap,他们的关系就像HashMap和HashSet的关系. TreeSet采用了TreeMap作为其Map保存“键-值”对,所以TreeSet判断元素重复是依靠Comparable接口或Comparator接口实现的.

【转】HashMap实现原理及源码分析

哈希表(hash table)也叫散列表,是一种非常重要的数据结构,应用场景极其丰富,许多缓存技术(比如memcached)的核心其实就是在内存中维护一张大的哈希表,而HashMap的实现原理也常常出现在各类的面试题中,重要性可见一斑.本文会对java集合框架中的对应实现HashMap的实现原理进行讲解,然后会对JDK7中的HashMap源码进行分析. 一.什么是哈希表 在讨论哈希表之前,我们先大概了解下其它数据结构在新增.查找等基础操作上的执行性能. 数组:采用一段连续的存储单元来存储数据.对

【Spring】Spring&amp;WEB整合原理及源码分析

表现层和业务层整合: 1. Jsp/Servlet整合Spring: 2. Spring MVC整合SPring: 3. Struts2整合Spring: 本文主要介绍Jsp/Servlet整合Spring原理及源码分析. 一.整合过程 Spring&WEB整合,主要介绍的是Jsp/Servlet容器和Spring整合的过程,当然,这个过程是Spring MVC或Strugs2整合Spring的基础. Spring和Jsp/Servlet整合操作很简单,使用也很简单,按部就班花不到2分钟就搞定了

ConcurrentHashMap实现原理及源码分析

ConcurrentHashMap实现原理 ConcurrentHashMap源码分析 总结 ConcurrentHashMap是Java并发包中提供的一个线程安全且高效的HashMap实现(若对HashMap的实现原理还不甚了解,可参考我的另一篇文章HashMap实现原理及源码分析),ConcurrentHashMap在并发编程的场景中使用频率非常之高,本文就来分析下ConcurrentHashMap的实现原理,并对其实现原理进行分析(JDK1.7). ConcurrentHashMap实现原

【Spring】Spring&amp;WEB整合原理及源码分析(二)

一.整合过程 Spring&WEB整合,主要介绍的是Jsp/Servlet容器和Spring整合的过程,当然,这个过程是Spring MVC或Strugs2整合Spring的基础. Spring和Jsp/Servlet整合操作很简单,使用也很简单,按部就班花不到2分钟就搞定了,本节只讲操作不讲原理,更多细节.原理及源码分析后续过程陆续涉及. 1. 导入必须的jar包,本例spring-web-x.x.x.RELEASE.jar: 2. 配置web.xml,本例示例如下: <?xml vers

深度理解Android InstantRun原理以及源码分析

深度理解Android InstantRun原理以及源码分析 @Author 莫川 Instant Run官方介绍 简单介绍一下Instant Run,它是Android Studio2.0以后新增的一个运行机制,能够显著减少你第二次及以后的构建和部署时间.简单通俗的解释就是,当你在Android Studio中改了你的代码,Instant Run可以很快的让你看到你修改的效果.而在没有Instant Run之前,你的一个小小的修改,都肯能需要几十秒甚至更长的等待才能看到修改后的效果. 传统的代