Java中集合LinkedList的原理与使用方法

前言

LinkedList和ArrayList一样是集合List的实现类,虽然较之ArrayList,其使用场景并不多,但同样有用到的时候,那么接下来,我们来认识一下它。

一. 定义一个LinkedList

public static void main(String[] args) {
 List<String> stringList = new LinkedList<>();
 List<String> tempList = new ArrayList<>();
 tempList.add("牛魔王");
 tempList.add("蛟魔王");
 tempList.add("鹏魔王");
 tempList.add("狮驼王");
 tempList.add("猕猴王");
 tempList.add("禺贼王");
 tempList.add("美猴王");
 List<String> stringList2 = new LinkedList<>(tempList);
}

上面代码中采用了两种方式来定义LinkedList,可以定义一个空集合,也可以传递已有的集合,将其转化为LinkedList。我们看一下源码

public class LinkedList<E> extends AbstractSequentialList<E> implements List<E>, Deque<E>, Cloneable, java.io.Serializable{
 transient int size = 0;

 /**
 * Pointer to first node.
 * Invariant: (first == null && last == null) ||
 * (first.prev == null && first.item != null)
 */
 transient Node<E> first;

 /**
 * Pointer to last node.
 * Invariant: (first == null && last == null) ||
 * (last.next == null && last.item != null)
 */
 transient Node<E> last;

 /**
 * Constructs an empty list.
 */
 public LinkedList() {
 }

 /**
 * Constructs a list containing the elements of the specified
 * collection, in the order they are returned by the collection's
 * iterator.
 *
 * @param c the collection whose elements are to be placed into this list
 * @throws NullPointerException if the specified collection is null
 */
 public LinkedList(Collection<? extends E> c) {
 this();
 addAll(c);
 }
}

LinkedList继承了AbstractSequentialList类,实现了List接口,AbstractSequentialList中已经实现了很多方法,如get(int index)、set(int index, E element)、add(int index, E element) 和 remove(int index),这些方法是我们集合操作时使用最多的,不过这些方法在LinkedList中都已经被重写了,而抽象方法在LinkedList中有了具体实现。因此我们回到LinkedList类

LinkedList类中定义了三个变量

size:集合的长度

first:双向链表头部节点

last:双向链表尾部节点

针对first变量和last变量,我们看到是Node类的实体,这是一个静态内部类,关于静态内部类的讲解,我们在static五大应用场景一章已经有说明

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;
 }
}

我们知道LinkedList是通过双向链表实现的,而双向链表就是通过Node类来体现的,类中通过item变量保存了当前节点的值,通过next变量指向下一个节点,通过prev变量指向上一个节点。

二. LinkedList常用方法

1. get(int index)

我们知道随机读取元素不是LinkedList所擅长的,读取效率比起ArrayList也低得多,那么我来看一下为什么

public E get(int index) {
 checkElementIndex(index);
 return node(index).item;
}

/**
 * 返回一个指定索引的非空节点.
 */
Node<E> node(int index) {
 // assert isElementIndex(index);

 if (index < (size >> 1)) {
 Node<E> x = first;
 for (int i = 0; i < index; i++)
 x = x.next;
 return x;
 } else {
 Node<E> x = last;
 for (int i = size - 1; i > index; i--)
 x = x.prev;
 return x;
 }
}

从上述代码中我们可以看到get(int index)方法是通过node(int index)来实现的,它的实现机制是:

比较传入的索引参数index与集合长度size/2,如果是index小,那么从第一个顺序循环,直到找到为止;如果index大,那么从最后一个倒序循环,直到找到为止。也就是说越靠近中间的元素,调用get(int index方法遍历的次数越多,效率也就越低,而且随着集合的越来越大,get(int index)执行性能也会指数级降低。因此在使用LinkedList的时候,我们不建议使用这种方式读取数据,可以使用getFirst(),getLast()方法,将直接用到类中的first和last变量。

2. add(E e) 和 add(int index, E element)

大家都在说LinkedList插入、删除操作效率比较高,以stringList.add(“猪八戒”)为例来看到底发生了什么?

在LinkedList中我们找到add(E e)方法的源码

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

/**
 * 设置元素e为最后一个元素
*/
void linkLast(E e) {
 final Node<E> l = last;
 final Node<E> newNode = new Node<>(l, e, null);
 last = newNode;
 if (l == null)
 first = newNode;
 else
 l.next = newNode;
 size++;
 modCount++;
}

很好理解:

情况1:假如stringList为空,那么添加进来的node就是first,也是last,这个node的prev和next都为null;

情况2:假如stringList不为空,那么添加进来的node就是last,node的prev指向以前的最后一个元素,node的next为null;同时以前的最后一个元素的next.

而如果通过stringList.add(1, “猪八戒”)这种方式将元素添加到集合中呢?

//在指定位置添加一个元素
public void add(int index, E element) {
 checkPositionIndex(index);
 if (index == size)
 linkLast(element);
 else
 linkBefore(element, node(index));
}

/**
 * 在一个非空节点前插入一个元素
 */
void linkBefore(E e, Node<E> succ) {
 // assert succ != null;
 final Node<E> pred = succ.prev;
 final Node<E> newNode = new Node<>(pred, e, succ);
 succ.prev = newNode;
 if (pred == null)
 first = newNode;
 else
 pred.next = newNode;
 size++;
 modCount++;
}

其实从代码中看到和add(E e)的代码实现没有本质区别,都是通过新建一个Node实体,同时指定其prev和next来实现,不同点在于需要调用node(int index)通过传入的index来定位到要插入的位置,这个也是比较耗时的,参考上面的get(int index)方法。

其实看到这里,大家也都明白了。

LinkedList插入效率高是相对的,因为它省去了ArrayList插入数据可能的数组扩容和数据元素移动时所造成的开销,但数据扩容和数据元素移动却并不是时时刻刻都在发生的。

3. remove(Object o) 和 remove(int index)

这里removeFirst()和removeLast()就不多说了,会用到类中定义的first和last变量,非常简单,我们看一下remove(Object o) 和 remove(int index)源码

//删除某个对象
public boolean remove(Object o) {
 if (o == null) {
 for (Node<E> x = first; x != null; x = x.next) {
 if (x.item == null) {
 unlink(x);
 return true;
 }
 }
 } else {
 for (Node<E> x = first; x != null; x = x.next) {
 if (o.equals(x.item)) {
 unlink(x);
 return true;
 }
 }
 }
 return false;
}
//删除某个位置的元素
public E remove(int index) {
 checkElementIndex(index);
 return unlink(node(index));
}
//删除某节点,并将该节点的上一个节点(如果有)和下一个节点(如果有)关联起来
E unlink(Node<E> x) {
 final E element = x.item;
 final Node<E> next = x.next;
 final Node<E> prev = x.prev;

 if (prev == null) {
 first = next;
 } else {
 prev.next = next;
 x.prev = null;
 }

 if (next == null) {
 last = prev;
 } else {
 next.prev = prev;
 x.next = null;
 }

 x.item = null;
 size--;
 modCount++;
 return element;
}

其实实现都非常简单,先找到要删除的节点,remove(Object o)方法遍历整个集合,通过 == 或 equals方法进行判断;remove(int index)通过node(index)方法。

4. LinkedList遍历

我们主要列举一下三种常用的遍历方式,

普通for循环,增强for循环,Iterator迭代器

public static void main(String[] args) {
 LinkedList<Integer> list = getLinkedList();
 //通过快速随机访问遍历LinkedList
 listByNormalFor(list);
 //通过增强for循环遍历LinkedList
 listByStrengThenFor(list);
 //通过快迭代器遍历LinkedList
 listByIterator(list);
}

/**
 * 构建一个LinkedList集合,包含元素50000个
 * @return
 */
private static LinkedList<Integer> getLinkedList() {
 LinkedList list = new LinkedList();
 for (int i = 0; i < 50000; i++){
 list.add(i);
 }
 return list;
}

/**
 * 通过快速随机访问遍历LinkedList
 */
private static void listByNormalFor(LinkedList<Integer> list) {
 // 记录开始时间
 long start = System.currentTimeMillis();
 int size = list.size();
 for (int i = 0; i < size; i++) {
 list.get(i);
 }
 // 记录用时
 long interval = System.currentTimeMillis() - start;
 System.out.println("listByNormalFor:" + interval + " ms");
}

/**
 * 通过增强for循环遍历LinkedList
 * @param list
 */
public static void listByStrengThenFor(LinkedList<Integer> list){
 // 记录开始时间
 long start = System.currentTimeMillis();
 for (Integer i : list) { }
 // 记录用时
 long interval = System.currentTimeMillis() - start;
 System.out.println("listByStrengThenFor:" + interval + " ms");
}

/**
 * 通过快迭代器遍历LinkedList
 */
private static void listByIterator(LinkedList<Integer> list) {
 // 记录开始时间
 long start = System.currentTimeMillis();
 for(Iterator iter = list.iterator(); iter.hasNext();) {
 iter.next();
 }
 // 记录用时
 long interval = System.currentTimeMillis() - start;
 System.out.println("listByIterator:" + interval + " ms");
}

执行结果如下:

listByNormalFor:1067 ms
listByStrengThenFor:3 ms
listByIterator:2 ms

通过普通for循环随机访问的方式执行时间远远大于迭代器访问方式,这个我们可以理解,在前面的get(int index)方法中已经有过说明,那么为什么增强for循环能做到迭代器遍历差不多的效率?

通过反编译工具后得到如下代码

public static void listByStrengThenFor(LinkedList<Integer> list)
 {
 long start = System.currentTimeMillis();
 Integer localInteger;
 for (Iterator localIterator = list.iterator(); localIterator.hasNext();
 localInteger = (Integer)localIterator.next()) {}
 long interval = System.currentTimeMillis() - start;
 System.out.println("listByStrengThenFor:" + interval + " ms");
}

很明显了,增强for循环遍历时也调用了迭代器Iterator,不过多了一个赋值的过程。

还有类似于pollFirst(),pollLast()取值后删除的方法也能达到部分的遍历效果。

三. 总结

本文基于java8从定义一个LinkList入手,逐步展开,从源码角度分析LinkedList双向链表的结构是如何构建的,同时针对其常用方法进行分析,包括get,add,remove以及常用的遍历方法,并简单的说明了它的插入、删除操作为何相对高效,而取值操作性能相对较低,若有不对之处,请批评指正,望共同进步,谢谢!

好了,以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,谢谢大家对我们的支持。

(0)

相关推荐

  • Java集合系列之LinkedList源码分析

    上篇我们分析了ArrayList的底层实现,知道了ArrayList底层是基于数组实现的,因此具有查找修改快而插入删除慢的特点.本篇介绍的LinkedList是List接口的另一种实现,它的底层是基于双向链表实现的,因此它具有插入删除快而查找修改慢的特点,此外,通过对双向链表的操作还可以实现队列和栈的功能.LinkedList的底层结构如下图所示. F表示头结点引用,L表示尾结点引用,链表的每个结点都有三个元素,分别是前继结点引用(P),结点元素的值(E),后继结点的引用(N).结点由内部类No

  • 详解Java 集合系列(三)—— LinkedList

    LinkedList LinkedList是一种可以在任何位置进行高效地插入和删除操作的有序序列. 它的最基本存储结构是一个节点:每个节点将存储对象,以及前后节点的引用. 结构图 从上面的结构图中,我们可以了解到 ListedList 底层是基于双向链表实现的. 围起来的可以看成 LinkedList 类,它定义了三个 transient 成员变量:first.last.size.这三个变量是整个 LinkedList 类的关键点. 由于是双向链表(每个node都有保存前后节点的引用),因此我们

  • 解析Java中的队列和用LinkedList集合模拟队列的方法

    API中对队列的说明: public interface Queue<E> extends Collection<E> 在处理元素前用于保存元素的 collection.除了基本的 Collection 操作外,队列还提供其他的插入.提取和检查操作.每个方法都存在两种形式:一种抛出异常(操作失败时),另一种返回一个特殊值(null 或 false,具体取决于操作).插入操作的后一种形式是用于专门为有容量限制的 Queue 实现设计的:在大多数实现中,插入操作不会失败. 队列通常(但

  • java 集合之实现类ArrayList和LinkedList的方法

    List 的方法列表 方法名 功能说明 ArrayList() 构造方法,用于创建一个空的数组列表 add(E e) 将指定的元素添加到此列表的尾部 get(int index) 返回此列表中指定位置上的元素 size() 返回此列表中的元素数 clear() 移除此列表中的所有元素 isEmpty() 如果此列表中没有元素,则返回true remove(int index) 移除此列表中指定位置上的元素 indextof(Object o) 返回此列表中首次出现的指定元素的索引,或如果此列表不

  • Java集合框架LinkedList详解及实例

    Java集合框架LinkedList详解 LinkedList定义 package java.util; public class LinkedList<E> extends AbstractSequentialList<E> implements List<E>, Deque<E>, Cloneable, java.io.Serializable{ transient int size = 0; transient Node<E> first;

  • java集合 collection-list-LinkedList详解

    如下所示: import java.util.*; /* LinkedList:特有方法: addFirst(); addLast(); getFirst(); getLast(); 获取元素,但不删除元素.如果集合中没有元素,会出现NoSuchElementException removeFirst(); removeLast(); 获取元素,但是元素被删除.如果集合中没有元素,会出现NoSuchElementException 在JDK1.6出现了替代方法. offerFirst(); of

  • Java中集合LinkedList的原理与使用方法

    前言 LinkedList和ArrayList一样是集合List的实现类,虽然较之ArrayList,其使用场景并不多,但同样有用到的时候,那么接下来,我们来认识一下它. 一. 定义一个LinkedList public static void main(String[] args) { List<String> stringList = new LinkedList<>(); List<String> tempList = new ArrayList<>(

  • Java集合 LinkedList的原理及使用详解

    LinkedList和ArrayList一样是集合List的实现类,虽然较之ArrayList,其使用场景并不多,但同样有用到的时候,那么接下来,我们来认识一下它. 一. 定义一个LinkedList public static void main(String[] args) { List<String> stringList = new LinkedList<>(); List<String> tempList = new ArrayList<>();

  • Java中集合List、Set和Map的入门详细介绍

    目录 一.Collection接口 二.List集合 2.1介绍 2.1.1 ArrayList(数组) 2.1.2 Vector(数组实现.线程同步) 2.1.3 LinkList(链表) 2.2 List特性 2.3 List常用方法 2.4 List总结 三.Set集合 3.1介绍 3.2 分类 3.2.1 HashSet(Hash表) 3.2.2 TreeSet(二叉树) 3.2.3 LinkHashSet(HashSet+LinkedHashMap) 四.Map集合 4.1 HashM

  • Java中GC的工作原理详细介绍

    Java中GC的工作原理 引子:面试时被问到垃圾回收机制,只是粗略的讲'程序员不能直接对内存操作,jvm负责对已经超过作用域的对象回收处理',面官表情呆滞,也就没再继续深入. 转文: 一个优秀的Java程序员必须了解GC的工作原理.如何优化GC的性能.如何与GC进行有限的交互,有一些应用程序对性能要求较高,例如嵌入式系统.实时系统等,只有全面提升内存的管理效率,才能提高整个应用程序的性能.本文将从GC的工作原理.GC的几个关键问题进行探讨,最后提出一些Java程序设计建议,如何从GC角度提高Ja

  • 基于java中集合的概念(详解)

    1.集合是储存对象的,长度可变,可以封装不同的对象 2.迭代器: 其实就是取出元素的方式(只能判断,取出,移除,无法增加) 就是把取出方式定义在集合内部,这样取出方式就可以直接访问集合内部的元素,那么取出方式就被定义成了内部类. 二每一个容器的数据结构不同,所以取出的动作细节也不一样.但是都有共性内容判断和取出,那么可以将共性提取,这些内部类都符合一个规则Iterator Iterator it = list.iterator(); while(it.hasNext()){ System.out

  • Java Set集合去重的原理及实现

    在开发中经常使用到Set集合去重,那么去重的原理是怎样实现的呢?在此文章记录一下去重原理!!! 下面是set集合类图 下面我们来跟踪一下执行过程: 首先我们实例化一个Set对象; Set<8大基本类型> set = new HashSet<8大基本类型>(); set.add(8大基本类型); add操作会调用HashMap中的add方法; public boolean add(E e) { return map.put(e, PRESENT)==null; } HashMap中的

  • Java中枚举的实现原理介绍

    目录 基本概述 使用方式 条件选择 循环遍历 集合映射 常用方法 总结 基本概述 在 JDK1.5 之前,通过定义常量使用的都是:public static fianl.而枚举的设计,就是把相关的常量分组到一个枚举类型里,方便创建和管理. 比如我们要定义一个颜色常量: public enum Colour { RED, YELLOW, BLUE, GREEN } 这段代码通过底层编译后,实际创建了4个枚举对象: new Enum<EnumTest>("RED", 0); n

  • 详解Java中AC自动机的原理与实现

    目录 简介 工作过程 数据结构 初始化 构建字典树 构建失败指针 匹配 执行结果 简介 AC自动机是一个多模式匹配算法,在模式匹配领域被广泛应用,举一个经典的例子,违禁词查找并替换为***.AC自动机其实是Trie树和KMP 算法的结合,首先将多模式串建立一个Tire树,然后结合KMP算法前缀与后缀匹配可以减少不必要比较的思想达到高效找到字符串中出现的匹配串. 如果不知道什么是Tire树,可以先查看:详解Java中字典树(Trie树)的图解与实现 如果不知道KMP算法,可以先查看:详解Java中

  • Java中Prime算法的原理与实现详解

    目录 Prim算法介绍 1.点睛 2.算法介绍 3. 算法步骤 4.图解 Prime 算法实现 1.构建后的图 2.代码 3.测试 Prim算法介绍 1.点睛 在生成树的过程中,把已经在生成树中的节点看作一个集合,把剩下的节点看作另外一个集合,从连接两个集合的边中选择一条权值最小的边即可. 2.算法介绍 首先任选一个节点,例如节点1,把它放在集合 U 中,U={1},那么剩下的节点为 V-U={2,3,4,5,6,7},集合 V 是图的所有节点集合. 现在只需要看看连接两个集合(U 和 V-U)

  • 详解Java中跳跃表的原理和实现

    目录 一.跳跃表的引入 二.算法分析 1.时间复杂度 2.空间复杂度 三.跳跃表介绍 四.跳跃表的实现 1.数据结构定义 2.查找 3.插入 4.删除 五.实战 1.代码 2.测试结果 一.跳跃表的引入 对有序顺序表可以采用二分查找,查找的时间复杂度为O (logn),插入.删除的时间复杂度为 O(n ).但是对有序链表不可以采用二分查找,查找.插入和删除的时间复杂度均为O (n). 有序链表如下图所示,若查找 8,则必须从第 1 个节点开始,依次比较 8 次才能查找成功. 如何利用链表的有序性

随机推荐