Java集合干货——LinkedList源码分析

简介: 前言在上篇文章中我们对ArrayList对了详细的分析,今天我们来说一说LinkedList。他们之间有什么区别呢?最大的区别就是底层数据结构的实现不一样,ArrayList是数组实现的(具体看上一篇文章),LinedList是链表实现的。

前言

在上篇文章中我们对ArrayList对了详细的分析,今天我们来说一说LinkedList。他们之间有什么区别呢?最大的区别就是底层数据结构的实现不一样,ArrayList是数组实现的(具体看上一篇文章),LinedList是链表实现的。至于其他的一些区别,可以说大部分都是由于本质不同衍生出来的不同应用。

LinkedList

链表

在分析LinedList之前先对链表做一个简单的介绍,毕竟链表不像数组一样使用的多,所以很多人不熟悉也在所难免。

链表是一种基本的线性数据结构,其和数组同为线性,但是数组是内存的物理存储上呈线性,逻辑上也是线性;而链表只是在逻辑上呈线性。在链表的每一个存储单元中不仅存储有当前的元素,还有下一个存储单元的地址,这样的可以通过地址将所有的存储单元连接在一起。

每次查找的时候,通过第一个存储单元就可以顺藤摸瓜的找到需要的元素。执行删除操作只需要断开相关元素的指向就可以了。示意图如下:

 
2018-01-10_114030
 
2018-01-10_114053
 
2018-01-10_114109

当然了在?LinkedList中使用的并不是最基本的单向链表,而是双向链表。

在LinedList中存在一个基本存储单元,是LinkedList的一个内部类,节点元素存在两个属性,分别保存前一个节点和后一个节点的引用。

//静态内部类
private static class Node<E> {
  //存储元素的属性
  E item;
  //前后节点引用
  Node<E> next;
  Node<E> prev;
  Node(Node<E> prev, E element, Node<E> next) {
    this.item = element;
    this.next = next;
    this.prev = prev;
  }
}

 

定义

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

 

在定义上和ArrayList大差不差,但是需要注意的是,LinkedList实现了Deque(间接实现了Qeque接口),Deque是一个双向对列,为LinedList提供了从对列两端访问元素的方法。

初始化

在分析ArrayList的时候我们知道ArrayList使用无参构造方法时的初始化长度是10,并且所有无参构造出来的集合都会指向同一个对象数组(静态常量,位于方法区),那么LinkedList的初始化是怎样的呢?

打开无参构造方法

public LinkedList() {
}

 

什么都没有,那么只能够去看属性了。

//初始化长度为0
transient int size = 0;
//有前后节点
transient Node<E> first;
transient Node<E> last;

 

图示初始化

LinkedList<String> list = new LinkedList<String>();
        String s = "sss";
        list.add(s);

 

 
 

方法

add(E e)
public boolean add(E e) {
  linkLast(e);
  return true;
}

 

从方法中我们知道在调用添加方法之后,并不是立马添加的,而是调用了linkLast方法,见名知意,新元素的添加位置是集合最后。

void linkLast(E e) {
 // 将最后一个元素赋值(引用传递)给节点l final修饰符  修饰的属性赋值之后不能被改变
  final Node<E> l = last;
 // 调用节点的有参构造方法创建新节点 保存添加的元素 
  final Node<E> newNode = new Node<>(l, e, null);
  //此时新节点是最后一位元素 将新节点赋值给last
  last = newNode;
  //如果l是null 意味着这是第一次添加元素 那么将first赋值为新节点  这个list只有一个元素 存储元素 开始元素和最后元素均是同一个元素
  if (l == null)
    first = newNode;
  else
    //如果不是第一次添加,将新节点赋值给l(添加前的最后一个元素)的next
    l.next = newNode;
  //长度+1
  size++;
  //修改次数+1
  modCount++;
}

 

从以上代码中我们可以看到其在添加元素的时候并不依赖下标。

而其中的处理是,通过一个last(Node对象)保存最后一个节点的信息(实际上就是最后一个节点),每次通过不断的变化最后一个元素实现元素的添加。(想要充分理解此处,需要理解java值传递和引用传递的区别和本质)。

add(int index, E element)
添加到指定的位置

public void add(int index, E element) {
  //下标越界检查
  checkPositionIndex(index);
//如果是向最后添加 直接调用linkLast
  if (index == size)
    linkLast(element);
  //反之 调用linkBefore
  else
    linkBefore(element, node(index));
}
//在指定元素之前插入元素
void linkBefore(E e, Node<E> succ) {
  // assert succ != null; 假设断言 succ不为null
  //定义一个节点元素保存succ的prev引用 也就是它的前一节点信息
  final Node<E> pred = succ.prev;
  //创建新节点 节点元素为要插入的元素e prev引用就是pred 也就是插入之前succ的前一个元素 next是succ
  final Node<E> newNode = new Node<>(pred, e, succ);
  //此时succ的上一个节点是插入的新节点 因此修改节点指向
  succ.prev = newNode;
 // 如果pred是null 表明这是第一个元素
  if (pred == null)
    //成员属性first指向新节点
    first = newNode;
  //反之
  else
    //节点前元素的next属性指向新节点
    pred.next = newNode;
  //长度+1
  size++;
  modCount++;
}

 

节点元素插入图示

 
 
 
 

在上面的代码中我们应该注意到了,LinkedList在插入元素的时候也要进行一定的验证,也就是下标越界验证,下面我们看一下具体的实现。

private void checkPositionIndex(int index) {
  if (!isPositionIndex(index))
    throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
//如果输入的index在范围之内返回ture
private boolean isPositionIndex(int index) {
  return index >= 0 && index <= size;
}

 

通过对两个添加方法的分析,我们可以很明显的感受到LinkedList添加元素的效率,不需要扩容,不需要复制数组。

get
public E get(int index) {
  //检查下标元素是否存在 实际上就是检查下标是否越界
  checkElementIndex(index);
  //如果没有越界就返回对应下标节点的item 也就是对应的元素
  return node(index).item;
}

//下标越界检查 如果越界就抛异常
private void checkElementIndex(int index) {
  if (!isElementIndex(index))
    throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
private boolean isElementIndex(int index) {
  return index >= 0 && index < size;
}
//该方法是用来返回指定下标的非空节点
Node<E> node(int index) {
  //假设下标未越界  实际上也没有越界 毕竟在此之前执行了下标越界检查 
  // assert isElementIndex(index);

  //如果index小于size的二分之一  从前开始查找(向后查找)  反之向前查找  
  if (index < (size >> 1)) {//左移 效率高  值得学习
    Node<E> x = first;
    //遍历
    for (int i = 0; i < index; i++)
      //每一个节点的next都是他的后一个节点引用 遍历的同时x会不断的被赋值为节点的下一个元素  遍历到index是拿到的就是index对应节点的元素
      x = x.next;
    return x;
  } else {
    Node<E> x = last;
    for (int i = size - 1; i > index; i--)
      x = x.prev;
    return x;
  }
}

 

在这段代码中充分体现了双向链表的优越性,可以从前也可以从后开始遍历,通过对index范围的判断能够显著的提高效率。但是在遍历的时候也可以很明显的看到LinkedList get方法获取元素的低效率,时间复杂度O(n)。

remove(int index)

所谓删除节点 就是把节点的前后引用置为null,并且保证没有任何其他节点指向被删除节点。

public E remove(int index) {
  //下标越界检查
  checkElementIndex(index);
  //此处的返回值实际上是执行了两个方法
  //node获取制定下标非空节点
  //unlink 断开指定节点的联系
  return unlink(node(index));
}
E unlink(Node<E> x) {
  //假设x不是null
  // assert x != null;
  //定义一个变量element接受x节点中的元素 最后会最后返回值返回
  final E element = x.item;
  //定义连个节点分别获得x节点的前后节点引用
  final Node<E> next = x.next;
  final Node<E> prev = x.prev;
  //如果节点前引用为null 说明这是第一个节点
  if (prev == null) {
    //x是第一个节点 即将被删除  那么first需要被重新赋值
    first = next;
  } else {
    //如果不是x不是第一个节点  将prev(x的前一个节点)的next指向x的后一个节点(绕过x)
    prev.next = next;
    //x的前引用赋值null
    x.prev = null;
  }
//如果节点后引用为null 说明这是最后一个节点  一系列类似前引用的处理方式 不再赘述
  if (next == null) {
    last = prev;
  } else {
    next.prev = prev;
    x.next = null;
  }
//将x节点中的元素赋值null
  x.item = null;
  size--;
  modCount++;
  return element;
}

 

说明

  1. prev,item,next均置为null 是为了让虚拟机回收
  2. 我们可以看到LinkedList删除元素的效率也不错
LinkedList总结
  1. 查询速度不行,每次查找都需要遍历,这就是在ArrayList分析时提到过的顺序下标遍历
  2. 添加元素,删除都很有速度优势
  3. 实现对列接口

ArrayList和LinkedList的区别

  1. 顺序插入,两者速度都很快,但是ArrayList稍快于LinkedList,数组实现,数组是提前创建好的;LinkedList每次都需要重新new新节点
  2. LinedList需要维护前后节点,会更耗费内存
  3. 遍历,LinedList适合用迭代遍历;ArrayList适合用循环遍历
    1. 不要使用普通for循环遍历LinedList
    2. 也不要使用迭代遍历遍历ArrayList(具体看上篇文章《ArrayList分析》)
  4. 删除和插入就不说了,毕竟ArrayList需要复制数组和扩容。

我不能保证每一个地方都是对的,但是可以保证每一句话,每一行代码都是经过推敲和斟酌的。希望每一篇文章背后都是自己追求纯粹技术人生的态度。

永远相信美好的事情即将发生。

目录
相关文章
|
10天前
|
算法 Java 数据处理
从HashSet到TreeSet,Java集合框架中的Set接口及其实现类以其“不重复性”要求,彻底改变了处理唯一性数据的方式。
从HashSet到TreeSet,Java集合框架中的Set接口及其实现类以其“不重复性”要求,彻底改变了处理唯一性数据的方式。HashSet基于哈希表实现,提供高效的元素操作;TreeSet则通过红黑树实现元素的自然排序,适合需要有序访问的场景。本文通过示例代码详细介绍了两者的特性和应用场景。
30 6
|
10天前
|
存储 Java
深入探讨了Java集合框架中的HashSet和TreeSet,解析了两者在元素存储上的无序与有序特性。
【10月更文挑战第16天】本文深入探讨了Java集合框架中的HashSet和TreeSet,解析了两者在元素存储上的无序与有序特性。HashSet基于哈希表实现,添加元素时根据哈希值分布,遍历时顺序不可预测;而TreeSet利用红黑树结构,按自然顺序或自定义顺序存储元素,确保遍历时有序输出。文章还提供了示例代码,帮助读者更好地理解这两种集合类型的使用场景和内部机制。
27 3
|
10天前
|
存储 Java 数据处理
Java Set接口凭借其独特的“不重复”特性,在集合框架中占据重要地位
【10月更文挑战第16天】Java Set接口凭借其独特的“不重复”特性,在集合框架中占据重要地位。本文通过快速去重和高效查找两个案例,展示了Set如何简化数据处理流程,提升代码效率。使用HashSet可轻松实现数据去重,而contains方法则提供了快速查找的功能,彰显了Set在处理大量数据时的优势。
20 2
|
6天前
|
安全 Java 程序员
深入Java集合框架:解密List的Fail-Fast与Fail-Safe机制
本文介绍了 Java 中 List 的遍历和删除操作,重点讨论了快速失败(fail-fast)和安全失败(fail-safe)机制。通过普通 for 循环、迭代器和 foreach 循环的对比,详细解释了各种方法的优缺点及适用场景,特别是在多线程环境下的表现。最后推荐了适合高并发场景的 fail-safe 容器,如 CopyOnWriteArrayList 和 ConcurrentHashMap。
34 5
|
8天前
|
安全 Java 程序员
Java集合之战:ArrayList vs LinkedList,谁才是你的最佳选择?
本文介绍了 Java 中常用的两个集合类 ArrayList 和 LinkedList,分析了它们的底层实现、特点及适用场景。ArrayList 基于数组,适合频繁查询;LinkedList 基于链表,适合频繁增删。文章还讨论了如何实现线程安全,推荐使用 CopyOnWriteArrayList 来提升性能。希望帮助读者选择合适的数据结构,写出更高效的代码。
27 3
|
10天前
|
存储 Java 数据处理
Set 是 Java 集合框架中的一个接口,不包含重复元素且不保证元素顺序。
【10月更文挑战第16天】Java Set:无序之美,不重复之魅!Set 是 Java 集合框架中的一个接口,不包含重复元素且不保证元素顺序。通过 hashCode() 和 equals() 方法实现唯一性,适用于需要唯一性约束的数据处理。示例代码展示了如何使用 HashSet 添加和遍历元素,体现了 Set 的高效性和简洁性。
18 4
|
12天前
|
存储 Java 数据处理
Set 是 Java 集合框架中的一个接口,不包含重复元素且不保证元素顺序。
Java Set:无序之美,不重复之魅!Set 是 Java 集合框架中的一个接口,不包含重复元素且不保证元素顺序。它通过 hashCode() 和 equals() 方法确保元素唯一性,适用于需要唯一性约束的数据处理。示例代码展示了如何使用 HashSet 实现这一特性。
16 5
|
10天前
|
Java 开发者
在Java集合世界中,Set以其独特的特性脱颖而出,专门应对重复元素
在Java集合世界中,Set以其独特的特性脱颖而出,专门应对重复元素。通过哈希表和红黑树两种模式,Set能够高效地识别并拒绝重复元素的入侵,确保集合的纯净。无论是HashSet还是TreeSet,都能在不同的场景下发挥出色的表现,成为开发者手中的利器。
22 2
|
12天前
|
存储 算法 Java
Java的Set集合以其严格的“不重复性”著称,使开发者既好奇又困惑
Java的Set集合以其严格的“不重复性”著称,使开发者既好奇又困惑。本文将探讨Set为何如此“挑剔”。Set接口不包含重复元素,适用于需要唯一性约束的场景。其内部通过哈希表或红黑树等数据结构和哈希算法、equals()方法来确保元素的唯一性。示例代码展示了Set如何自动过滤重复元素,体现了其高效性和便利性。
27 2
|
12天前
|
算法 Java 数据处理
从HashSet到TreeSet,Java集合框架中的Set接口及其实现类以其独特的“不重复性”要求,彻底改变了处理唯一性约束数据的方式。
【10月更文挑战第14天】从HashSet到TreeSet,Java集合框架中的Set接口及其实现类以其独特的“不重复性”要求,彻底改变了处理唯一性约束数据的方式。本文深入探讨Set的核心理念,并通过示例代码展示了HashSet和TreeSet的特点和应用场景。
14 2