Java集合框架之二:LinkedList源码解析
版权声明:本文为博主原创文章,转载请注明出处,欢迎交流学习!
LinkedList底层是通过双向循环链表来实现的,其结构如下图所示:
链表的组成元素我们称之为节点,节点由三部分组成:前一个节点的引用地址、数据、后一个节点的引用地址。LinkedList的Head节点不包含数据,每一个节点对应一个Entry对象。下面我们通过源码来分析LinkedList的实现原理。
1、Entry类源码:
private static class Entry<E> {
E element;
Entry<E> next;
Entry<E> previous; Entry(E element, Entry<E> next, Entry<E> previous) {
this.element = element;
this.next = next;
this.previous = previous;
}
}
Entry类包含三个属性,其中element存放业务数据;next存放后一个节点的信息,通过next可以找到后一个节点;previous存放前一个节点的信息,通过previous可以找到前一个节点。
2、LinkedList的构造方法:LinkedList提供了两个带不同参数的构造方法。
1) LinkedList(),构造一个空列表。
2) LinkedList(Collection<? extends E> c),构造一个包含指定 collection 中的元素的列表,这些元素按其 collection 的迭代器返回的顺序排列。
private transient Entry<E> header = new Entry<E>(null, null, null); //声明一个空的Entry对象
private transient int size = 0; //集合中节点的个数 public LinkedList() {
header.next = header.previous = header; //构造一个双向循环链表,header的前驱节点和后置节点都指向header自己
} public LinkedList(Collection<? extends E> c) {
this(); //调用不带参数的构造方法,创建一个空的循环链表
addAll(c); //调用addAll方法将Collection的元素添加到LinkedList中
}
当调用不带参数的构造方法,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),将指定元素添加到此列表的结尾。
通过源码来分析其底层的实现原理:
public boolean add(E o) {
addBefore(o, header);
return true;
} private Entry<E> addBefore(E o, Entry<E> e) {
Entry<E> newEntry = new Entry<E>(o, e, e.previous); //创建的对象newEntry的数据为o,后置节点为header,前驱节点为header.previous即header本身
newEntry.previous.next = newEntry; //newEntry.previous即为header,也就是header.next = newEntry,把header的后置节点设置为newEntry
newEntry.next.previous = newEntry; //newEntry.next即header,也就是header.previous = newEntry,经过以上步骤header节点和newEntry节点形成闭环
size++; //节点数加1
modCount++;
return newEntry;
}
通过以上源码分析可知,add(E elment)方法新添加一个元素element时,在LinkedList底层首先创建一个Entry类型的对象newEntry,并把元素element存放在newEntry中,同时把newEntry的前驱节点设置为header,后置节点也设置为header。接下来,将header节点的后置节点设置为newEntry,将header的前驱节点也设置为newEntry,并且节点数size加1。通过以上操作,节点header和节点newEntry形成闭环(双向循环链表)。其示意图如下:
public void add(int index, E element) {
addBefore(element, (index==size ? header : entry(index)));//调用addBefore将元素插入指定的位置
}
add(int index, E element)方法的实现原理与add(E elment)方法类似,都是调用addBefore方法实现,其本质还是生成一个新的Entry对象,然后修改指定位置的节点的前驱节点信息以及后置节点信息,这里就不再赘述了。
public boolean addAll(Collection<? extends E> c) {
return addAll(size, c);
} public boolean addAll(int index, Collection<? extends E> c) {
if (index < 0 || index > size) //检查参数index是否合法
throw new IndexOutOfBoundsException("Index: "+index+
", Size: "+size);
Object[] a = c.toArray(); //返回一个包含Collection集合所有元素的Object[]
int numNew = a.length;
if (numNew==0) //如果需要插入的元素为0,则返回false,表示没有元素需要插入
return false;
modCount++; //否则插入元素,链表修改次数加1 Entry<E> successor = (index==size ? header :entry(index)); //获取index位置的节点。插入位置如果等人size,则在头节点header处插入,否则在获取的index处插入
Entry<E> predecessor = successor.previous; //获取节点的前驱节点,插入时需要修改此节点的next
for (int i=0; i<numNew; i++) { //将数组中第一个元素插入到index处,将之后的元素按顺序插在这个节点后面
Entry<E> e = new Entry<E>((E)a[i], successor, predecessor); //创建一个Entry对象
predecessor.next = e; //将新插入的节点的前一个节点的next指向当前节点
predecessor = e; //将新插入的节点赋值给predecessor,相当于指针向后移,实现循环
}
successor.previous = predecessor; //将断开的index处的节点的previous指向插入的最后一个节点 size += numNew; //修改节点个数
return true;
}
public void addFirst(E o) {
addBefore(o, header.next);
} public void addLast(E o) {
addBefore(o, header);
}
从源码可知,这两个方法都是调用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(),返回此列表的最后一个元素。
public E get(int index) {
return entry(index).element;
} private Entry<E> entry(int index) { //获取双向链表指定位置的节点
if (index < 0 || index >= size) //检查参数index合法性,当指定的位置index小于零或大于集合容量,抛出异常
throw new IndexOutOfBoundsException("Index: "+index+
", Size: "+size);
Entry<E> e = header;
if (index < (size >> 1)) { //获取index处的节点,当index小于链表长度的1/2,则从前向后查找;否则从后向前查找。这样能提供查找效率。
for (int i = 0; i <= index; i++)
e = e.next;
} else {
for (int i = size; i > index; i--)
e = e.previous;
}
return e;
} public E getFirst() {
if (size==0)
throw new NoSuchElementException(); return header.next.element; //返回header节点的下一个节点。
} public E getLast() {
if (size==0)
throw new NoSuchElementException(); return header.previous.element; //返回header节点的前一个节点,由于是双向循环链表,header前一个节点也是链表的最后一个节点。
}
从LinkedList底层链表的实现原理可知,LinkedList集合的查找操作是从header节点开始的。可以从header节点开始从前向后一个一个节点顺序查找或者从后向前依次顺序查找。而ArrayList的查找操作则是直接获取数组下标处的元素,因此查询效率更高。
5、移除LinkedList中的元素:
1) remove(),获取并移除此列表的头(第一个元素)。
2) remove(int index),移除此列表中指定位置处的元素。
3) remove(Object o),从此列表中移除首次出现的指定元素(如果存在)。
4) removeFirst(),移除并返回此列表的第一个元素。此方法等同于remove()。
5) removeLast(),移除并返回此列表的最后一个元素。
public E remove() {
return removeFirst(); //调用removeFirst()方法。
} public E remove(int index) {
return remove(entry(index)); //调用私有方法remove(Entry<E> e)以及entry(int index)。
} public E removeFirst() {
return remove(header.next); //调用私有方法remove(Entry<E> e)。
} public E removeLast() {
return remove(header.previous); //调用私有方法remove(Entry<E> e)。
} public boolean remove(Object o) {
if (o==null) { 如果要删除的元素内容为null
for (Entry<E> e = header.next; e != header; e = e.next) {
if (e.element==null) {
remove(e); //调用私有方法remove(Entry<E> e)。
return true;
}
}
} else {
for (Entry<E> e = header.next; e != header; e = e.next) {
if (o.equals(e.element)) {
remove(e);
return true;
}
}
}
return false;
} private E remove(Entry<E> e) {
if (e == header)
throw new NoSuchElementException(); E result = e.element; //保存将要被移除的节点的内容。
e.previous.next = e.next; //将节点e的前驱节点的后继设置为e的后继节点。
e.next.previous = e.previous; //将e的后继节点的前驱设置为e的前驱节点。
e.next = e.previous = null; //将e节点的后继指向以及前驱指向设置为空。
e.element = null; //将e节点的内容清空。
size--; //节点个数减1。
modCount++; //链表操作次数加1。
return result; //返回被删除的节点内容。
}
从源码可以看出,移除节点操作最终都是调用私有方法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的添加、删除操作会涉及大量元素位置的迁移,因此添加、删除元素的效率低。
Java集合框架之二:LinkedList源码解析的更多相关文章
- Java集合框架之三:HashMap源码解析
版权声明:本文为博主原创文章,转载请注明出处,欢迎交流学习! HashMap在我们的工作中应用的非常广泛,在工作面试中也经常会被问到,对于这样一个重要的集合模型我们有必要弄清楚它的使用方法和它底层的实 ...
- Java集合框架之接口Collection源码分析
本文我们主要学习Java集合框架的根接口Collection,通过本文我们可以进一步了解Collection的属性及提供的方法.在介绍Collection接口之前我们不得不先学习一下Iterable, ...
- Java 集合系列Stack详细介绍(源码解析)和使用示例
Stack简介 Stack是栈.它的特性是:先进后出(FILO, First In Last Out). java工具包中的Stack是继承于Vector(矢量队列)的,由于Vector是通过数组实现 ...
- 【Java集合】试读LinkedList源码
LinkedList的本质是双向链表.(01) LinkedList继承于AbstractSequentialList,并且实现了Dequeue接口. (02) LinkedList包含两个重要的成员 ...
- Java集合框架之一:ArrayList源码分析
版权声明:本文为博主原创文章,转载请注明出处,欢迎交流学习! ArrayList底层维护的是一个动态数组,每个ArrayList实例都有一个容量.该容量是指用来存储列表元素的数组的大小.它总是至少等于 ...
- Java集合【9】-- Vector源码解析
目录 1.Vector介绍 2. 成员变量 3. 构造函数 4. 常用方法 4.1 增加 4.2 删除 4.3 修改 4.4 查询 4.5 其他常用函数 4.6 Lambda表达式相关的方法 4.7 ...
- Java 集合框架 LinkedHashSet 和 LinkedHashMap 源码剖析
总体介绍 如果你已看过前面关于HashSet和HashMap,以及TreeSet和TreeMap的讲解,一定能够想到本文将要讲解的LinkedHashSet和LinkedHashMap其实也是一回事. ...
- Java集合:LinkedList源码解析
Java集合---LinkedList源码解析 一.源码解析1. LinkedList类定义2.LinkedList数据结构原理3.私有属性4.构造方法5.元素添加add()及原理6.删除数据re ...
- 【集合框架】JDK1.8源码分析之HashMap(一) 转载
[集合框架]JDK1.8源码分析之HashMap(一) 一.前言 在分析jdk1.8后的HashMap源码时,发现网上好多分析都是基于之前的jdk,而Java8的HashMap对之前做了较大的优化 ...
- 【集合框架】JDK1.8源码分析之ArrayList详解(一)
[集合框架]JDK1.8源码分析之ArrayList详解(一) 一. 从ArrayList字表面推测 ArrayList类的命名是由Array和List单词组合而成,Array的中文意思是数组,Lis ...
随机推荐
- This iPhone 6s is running iOS 11.3.1 (15E302), which may not be supported by this version of Xcode.
This iPhone 6s is running iOS 11.3.1 (15E302), which may not be supported by this version of Xcode.
- maven pom.xml 项目报错
Failed to read artifact descriptor for org.springframework.boot:spring-boot-starter-web:jar:2.1.0.RE ...
- Cuda9.2发布
经过一次跳票,没有很多人注意(才怪)的cuda9.2终于发布了,官方介绍如下: CUDA 9.2 includes updates to libraries, a new library for ac ...
- dagScheduler
由一个action动作触发sparkcontext的runjob,再由此触发dagScheduler.runJob,然后触发submitJob,封装一个JobSubmitted放入一个队列.然后再通过 ...
- Springboot学习07-数据源Druid
Springboot学习07-数据源Druid 关键字 Druid 前言 学习笔记 正文 1-Druid是什么 Druid是阿里巴巴开源平台上的一个项目,整个项目由数据库连接池.插件框架和SQL解析器 ...
- 飞鱼星、H3C企业路由器配置
飞鱼星企业路由器配置外网访问IIS 只配置端口映射就行,配置好了,如果不立即重启,需要等几分钟才能生效 H3C路由器配置虚拟服务器即可
- thinkphp 查表返回的数组,js解析有UNICode编码,解决办法
public function getDeviceMsg(){ $allDevicesMsg = M("newdevicesstatus")->getField(" ...
- Django之Form、ModelForm 组件
Django之Form.ModelForm 组件 一.Form组件: django框架提供了一个form类,来处理web开发中的表单相关事项.众所周知,form最常做的是对用户输入的内容进行验证,为此 ...
- Jmeter获取redis数据
背景:jmeter写注册登录接口时,需要获取验短信验证码,一般都是存在数据库,但我们的开发把验证码存到redis里面了 步骤:1.写个redis工具类 2.打成jar包,导入jmeter lib\ex ...
- mybatis进阶--一对多查询
首先,我们还是先给出一个需求:根据订单id查询订单明细——我们知道,一个订单里面可以有多个订单的明细(需求不明确的同学,请留言或者去淘宝网上的订单处点一下就知道了).这个时候,一个订单,对应多个订单的 ...