Java集合源码学习(三)LinkedList分析

2023-05-16

原文地址:https://yq.aliyun.com/articles/38408?spm=5176.8091938.0.0.tjeCwH

前言

前面学习了ArrayList的源码,
数组是顺序存储结构,存储区间是连续的,占用内存严重,故空间复杂的很大。
但数组的二分查找时间复杂度小,为O(1),数组的特点是寻址容易,插入和删除困难。
今天学习另外的一种常用数据结构LinkedList的实现,
LinkedList使用链表作为存储结构,链表是线性存储结构,在内存上不是连续的一段空间,
占用内存比较宽松,故空间复杂度很小,但时间复杂度很大,达O(N),链表的特点是寻址困难,插入和删除容易。
所有的代码都基于JDK 1.6。

关于LinkedList

LinkedList继承了AbstractSequentialList,实现了List,Deque,Cloneable,Serializable 接口,

继承和实现

  • 继承AbstractSequentialList类,提供了相关的添加、删除、修改、遍历等功能。

  • 实现List接口,提供了相关的添加、删除、修改、遍历等功能。

  • 实现 Deque 接口,即能将LinkedList当作双端队列使用,可以用做队列或者栈
  • 实现了Cloneable接口,即覆盖了函数clone(),能被克隆复制,
  • 实现java.io.Serializable接口,LinkedList支持序列化,能通过序列化传输。

线程安全

LinkedList是非同步的,即线程不安全,如果有多个线程同时访问LinkedList,可能会抛出ConcurrentModificationException异常。

final void checkForComodification() {
        if (modCount != expectedModCount)
        throw new ConcurrentModificationException();
    }

代码中,modCount记录了LinkedList结构被修改的次数。Iterator初始化时,expectedModCount=modCount。任何通过Iterator修改LinkedList结构的行为都会同时更新expectedModCount和modCount,使这两个值相等。通过LinkedList对象修改其结构的方法只更新modCount。所以假设有两个线程A和B。A通过Iterator遍历并修改LinkedList,而B,与此同时,通过对象修改其结构,那么Iterator的相关方法就会抛出异常

双向链表结构

和普通的链表不同,双向链表每个节点不止维护指向下一个节点的next指针,还维护着指向上一个节点的previous指针。

内部实现

LinkedList内部使用Entry来封装双向循环链表结点。
LinkedList头结点的定义:

//头结点的定义
    private transient Entry<E> header = new Entry<E>(null, null, null);
    //链表的实际长度
    private transient int size = 0;

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

构造函数

LinkedList内部提供了两个构造函数,

/**
    * 初始化一个空的list,可以理解为一个双向循环链表
    */
   public LinkedList() {
       header.next = header.previous = header;
   }
   /**
    * 使用指定的collection构造一个list
    */
   public LinkedList(Collection<? extends E> c) {
   this();
   addAll(c);
   }

常用方法

遍历方式

LinkedList支持多种遍历方式。建议不要采用随机访问的方式去遍历LinkedList,而采用逐个遍历的方式。
(01) 第一种,通过迭代器遍历。即通过Iterator去遍历。

for(Iterator iter = list.iterator(); iter.hasNext();)
    iter.next();

(02) 通过快速随机访问遍历LinkedList

int size = list.size();
for (int i=0; i<size; i++) {
    list.get(i);        
}

(03) 通过另外一种for循环来遍历LinkedList

for (Integer integ:list) 
    ;

(04) 通过pollFirst()来遍历LinkedList

while(list.pollFirst() != null)
;

(05) 通过pollLast()来遍历LinkedList

while(list.pollLast() != null)
    ;

(06) 通过removeFirst()来遍历LinkedList

try {
    while(list.removeFirst() != null)
        ;
} catch (NoSuchElementException e) {
}

(07) 通过removeLast()来遍历LinkedList

try {
    while(list.removeLast() != null)
        ;
} catch (NoSuchElementException e) {
}

遍历LinkedList时,使用removeFist()或removeLast()效率最高。但用它们遍历时,会删除原始数据;若单纯只读取,而不删除,应该使用第3种遍历方式。

链表的特点是内存空间不连续,插入和删除简单,寻址的时间复杂度较高,随机访问去遍历LinkedList的效率是最低的。

常用方法(从API摘的)

boolean add(E e) 
将指定元素添加到此列表的结尾。 
void add(int index, E element) 
在此列表中指定的位置插入指定的元素。 
boolean addAll(Collection<? extends E> c) 
添加指定 collection 中的所有元素到此列表的结尾,顺序是指定 collection 的迭代器返回这些元素的顺序。 
boolean addAll(int index, Collection<? extends E> c) 
将指定 collection 中的所有元素从指定位置开始插入此列表。 
void addFirst(E e) 
将指定元素插入此列表的开头。 
void addLast(E e) 
将指定元素添加到此列表的结尾。 
void clear() 
从此列表中移除所有元素。 
Object clone() 
返回此 LinkedList 的浅表副本。 
boolean contains(Object o) 
如果此列表包含指定元素,则返回 true。 
Iterator<E> descendingIterator() 
返回以逆向顺序在此双端队列的元素上进行迭代的迭代器。 
E element() 
获取但不移除此列表的头(第一个元素)。 
E get(int index) 
返回此列表中指定位置处的元素。 
E getFirst() 
返回此列表的第一个元素。 
E getLast() 
返回此列表的最后一个元素。 
int indexOf(Object o) 
返回此列表中首次出现的指定元素的索引,如果此列表中不包含该元素,则返回 -1。 
int lastIndexOf(Object o) 
返回此列表中最后出现的指定元素的索引,如果此列表中不包含该元素,则返回 -1。 
ListIterator<E> listIterator(int index) 
返回此列表中的元素的列表迭代器(按适当顺序),从列表中指定位置开始。 
boolean offer(E e) 
将指定元素添加到此列表的末尾(最后一个元素)。 
boolean offerFirst(E e) 
在此列表的开头插入指定的元素。 
boolean offerLast(E e) 
在此列表末尾插入指定的元素。 
E peek() 
获取但不移除此列表的头(第一个元素)。 
E peekFirst() 
获取但不移除此列表的第一个元素;如果此列表为空,则返回 null。 
E peekLast() 
获取但不移除此列表的最后一个元素;如果此列表为空,则返回 null。 
E poll() 
获取并移除此列表的头(第一个元素) 
E pollFirst() 
获取并移除此列表的第一个元素;如果此列表为空,则返回 null。 
E pollLast() 
获取并移除此列表的最后一个元素;如果此列表为空,则返回 null。 
E pop() 
从此列表所表示的堆栈处弹出一个元素。 
void push(E e) 
将元素推入此列表所表示的堆栈。 
E remove() 
获取并移除此列表的头(第一个元素)。 
E remove(int index) 
移除此列表中指定位置处的元素。 
boolean remove(Object o) 
从此列表中移除首次出现的指定元素(如果存在)。 
E removeFirst() 
移除并返回此列表的第一个元素。 
boolean removeFirstOccurrence(Object o) 
从此列表中移除第一次出现的指定元素(从头部到尾部遍历列表时)。 
E removeLast() 
移除并返回此列表的最后一个元素。 
boolean removeLastOccurrence(Object o) 
从此列表中移除最后一次出现的指定元素(从头部到尾部遍历列表时)。 
E set(int index, E element) 
将此列表中指定位置的元素替换为指定的元素。 
int size() 
返回此列表的元素数。

双端队列

双端队列是一个限定插入和删除操作的数据结构,具有队列和栈的性质,
双端队列与栈或队列相比,是一种多用途的数据结构,在容器类库中有时会用双端队列来提供栈和队列两种功能。
查看源码的实现,可以发现作为队列和栈时的相关方法。

作为队列使用

add(e) 内部实现是addLast(e)
offer(e) 入队,直接返回add()
remove() 获取并移除列表第一个元素,和poll()相同,内部调用removeFirst()
poll() 出队 获取并移除队头元素 内部实现是调用removeFirst()
element() 返回列表第一个元素但不移除,内部调用getFirst()
peek() 返回列表第一个元素但不移除,和element()相同,内部调用getFirst()

作为栈使用

push(e) 入栈 即addFirst(e)
pop() 出栈 即removeFirst()
peek() 获取栈顶元素但不移除 即peekFirst()

源码分析

主要的节点更新操作

源码中的两个私有方法addBefore和remove是维护了节点更新的主要操作,
这一部分主要是数据结构中对链表的操作,理解起来比较简单,
大部分的add、remode等操作都可以通过这两个方法实现。

/**
    * 在传入节点之前插入新的节点元素
    */
   private Entry<E> addBefore(E e, Entry<E> entry) {
   //构造一个新的节点
   Entry<E> newEntry = new Entry<E>(e, entry, entry.previous);
   //调整新节点前后节点的指向
   newEntry.previous.next = newEntry;
   newEntry.next.previous = newEntry;
   size++;
   modCount++;
   return newEntry;
   }

    /**
    * 在链表中删除这个节点
    */
   private E remove(Entry<E> e) {
       //e等于初始化时的空节点,抛出异常
   if (e == header)
       throw new NoSuchElementException();
       E result = e.element;
       /**
        * 删除操作就是调整前后结点指针的指向,绕过传入节点
        * 然后再把传入节点的前后指针以及value都置为null
        */
   e.previous.next = e.next;
   e.next.previous = e.previous;
       e.next = e.previous = null;
       e.element = null;
   size--;
   modCount++;
       return result;
   }

List迭代器 通过ListItr内部类实现

private class ListItr implements ListIterator<E> {
  private Entry<E> lastReturned = header;
  private Entry<E> next;
  private int nextIndex;
  private int expectedModCount = modCount;

  ListItr(int index) {
      if (index < 0 || index > size)
      throw new IndexOutOfBoundsException("Index: "+index+
                          ", Size: "+size);
      if (index < (size >> 1)) {
      next = header.next;
      for (nextIndex=0; nextIndex<index; nextIndex++)
          next = next.next;
      } else {
      next = header;
      for (nextIndex=size; nextIndex>index; nextIndex--)
          next = next.previous;
      }
  }

  public boolean hasNext() {
      return nextIndex != size;
  }

  public E next() {
      checkForComodification();
      if (nextIndex == size)
      throw new NoSuchElementException();

      lastReturned = next;
      next = next.next;
      nextIndex++;
      return lastReturned.element;
  }

  public boolean hasPrevious() {
      return nextIndex != 0;
  }

  public E previous() {
      if (nextIndex == 0)
      throw new NoSuchElementException();

      lastReturned = next = next.previous;
      nextIndex--;
      checkForComodification();
      return lastReturned.element;
  }

  public int nextIndex() {
      return nextIndex;
  }

  public int previousIndex() {
      return nextIndex-1;
  }

  public void remove() {
          checkForComodification();
          Entry<E> lastNext = lastReturned.next;
          try {
              LinkedList.this.remove(lastReturned);
          } catch (NoSuchElementException e) {
              throw new IllegalStateException();
          }
      if (next==lastReturned)
              next = lastNext;
          else
      nextIndex--;
      lastReturned = header;
      expectedModCount++;
  }

  public void set(E e) {
      if (lastReturned == header)
      throw new IllegalStateException();
      checkForComodification();
      lastReturned.element = e;
  }

  public void add(E e) {
      checkForComodification();
      lastReturned = header;
      addBefore(e, next);
      nextIndex++;
      expectedModCount++;
  }

  final void checkForComodification() {
      if (modCount != expectedModCount)
      throw new ConcurrentModificationException();
  }
  }

fail-fast机制

fail-fast,快速失败是Java集合的一种错误检测机制。
当多个线程对集合进行结构上的改变的操作时,有可能会产生fail-fast机制。
记住是有可能,而不是一定。例如:假设存在两个线程(线程1、线程2),线程1通过Iterator在遍历集合A中的元素,在某个时候线程2修改了集合A的结构(是结构上面的修改,而不是简单的修改集合元素的内容),那么这个时候程序就会抛出 ConcurrentModificationException 异常,从而产生fail-fast机制。

ArrayList和LinkedList的区别

ArrayList是一个基于数组的结构,里面的节点相互之间没有特别的联系,默认的大小是10,最大大小是Integer.MAX_VALUE - 8。
当大小不够时会自动增长,它可以通过get,set来直接获取、修改某一个节点数据。

LinkedList是一个基于双向链表的结构,每一个节点都有两个指针来分别指向上一个节点和下一个节点。它是可变长度的。
这两个实现类的区别在于,ArrayList的get()/set()效率比LinkedList高,而LinkedList的add()/remove()效率比ArrayList高。

具体来说:数组申请一块连续的内存空间,是在编译期间就确定大小的,运行时期不可动态改变,但为什么ArrayList可以改变大小呢,
因为在add如果超过了设定的大小就会创建一个新的更大的(增长率好像是0.5)ArrayList,
然后将原来的list复制到新的list中,并将地址指向新的list,旧的list被GC回收,显然这是非常消耗内存而且效率非常低的。
但是由于它申请的内存空间是连续的,可以直接通过下标来获取需要的数据,时间复杂度为O(1),而链表则是O(n),
而链表结构不一样,它可以动态申请内存空间。在需要加入的节点的上一个节点的指针解开并指向新加节点,再将新加节点的指针指向后一个节点即可。速度很快的。

所以说ArrayList适合查询,LinkedList适合增删。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

Java集合源码学习(三)LinkedList分析 的相关文章

  • eclipse mylyn 与 redmine

    是否可以将mylyn连接到redmine而不需要redmine中的rest支持 有一个链接http download eclipse org mylyn incubator 3 8 http download eclipse org myl
  • 为什么在Java中读取易失性和写入字段成员是不可扩展的?

    观察以下用 Java 编写的程序 完整的可运行版本如下 但程序的重要部分在下面的代码片段中 import java util ArrayList A not easy to explain benchmark class MultiVola
  • 扩展 CrudRepository (Spring) 时是否需要 @Repository 注解?

    public interface CarRepository extends CrudRepository
  • RSA Java 加密和 Node.js 解密不起作用

    我有一个系统 需要在 javascript 中生成 RSA 密钥对 然后将公钥存储在服务器端的数据库中 作为字符串 然后 Java 中的服务器端将使用存储的公钥对字符串进行加密密钥并将其发送到客户端 客户端将使用私钥解密该字符串 我在客户端
  • Java(正则表达式)-获取句子中的所有单词

    我需要将 java 字符串拆分为单词数组 假设该字符串是 Hi I need to split this string into a serie s of words 目前我正在尝试使用这个String strs str split w 但
  • Linux 上的 JavaFX

    Linux x86 和 x64 上的 JavaFX 情况如何 JavaFX 应用程序可以在 Linux 操作系统上顺利执行吗 我发现了 2011 年和 2012 年的一些问题 当时应用程序不稳定 目前发布的 JFX 版本是 2 2 4 在
  • JFreeChart 更改现有条形图中的数据

    我想循环更改条形图数据 但我不知道该怎么做 我的代码 DefaultCategoryDataset barChartData new DefaultCategoryDataset barChartData setValue 0 Values
  • Apache HttpClient 4.x 在上传较大文件时表现奇怪?

    我正在使用 java 和 scala 开发和测试一个简单的客户端 服务器应用程序 The server是基于com sun net httpserver HttpServer并允许使用 POST 和 PUT 操作通过基本的 RESTful
  • spring Kafka模型不在可信包中

    我正在研究微服务spring Kafka 2 1 5 and spring boot 2 0 5 第一个服务将向卡夫卡产生一些消息 第二个服务将消耗它们 在消耗时我遇到了问题 Caused by java lang IllegalArgum
  • double 或 BigDecimal 会溢出吗?

    Java 8 给了我们Math addExact https docs oracle com javase 8 docs api java lang Math html addExact int int 适用于整数 但不适用于小数 是否有可
  • Java 多态性中的字段如何工作? [复制]

    这个问题在这里已经有答案了 我正在读书面试问题 http javabypatel blogspot in 2016 04 java interview questions html关于java 发现了很好的例子 但感到困惑 因为没有很好 更
  • 识别包含本机方法实现的库文件/源

    如何识别包含本机方法实现的库文件 Ex public native String intern 我在哪里可以找到实施 source code of String intern 方法 找到了答案String intern 与快速谷歌搜索 ht
  • 菜单项标题未显示

    菜单项的标题未显示在片段内 我在菜单文件中有两个项目 第一个是带有图标和标签的showAsAction always在工具栏中显示图标 第二个只有标题 我不知道这里出了什么问题 菜单项的所有操作均有效 例如下面 菜单 销售 xml menu
  • 将 Tango 3D 点投影到屏幕 Google Project Tango

    Project Tango 提供了点云 如何获取点云中 3D 点的像素位置 以米为单位 我尝试使用投影矩阵 但得到的值非常小 0 5 1 3 等 而不是 1234 324 以像素为单位 我包含我尝试过的代码 Get the current
  • Java - 动态创建子类

    我想以编程方式创建一个子类 我想我的选择很少 Javassist CGLib BCEL 或 ASM 用例是一个应用程序的内部是面向类的 而扩展是基于类的 因此 我不能将单个类作为由外部化脚本驱动的多个扩展的基础 现在 我该怎么做呢 我找到了
  • 在java中访问dll方法

    我正在尝试访问java中用c 编写的dll方法 从下面的代码我试图构建已成功生成的 dll using System using Microsoft Win32 namespace CyberoamWinHelper public clas
  • JPA2+Hibernate 3.6.0 中的 JTA 还是 LOCAL 事务?

    我们正在重新思考我们的技术堆栈 以下是我们的选择 由于应用程序的复杂性等 我们不能没有 Spring 和 Hibernate 我们还从 J2EE 1 4 迁移到 Java EE 5 技术栈 Java EE 5 JPA 2 0 我知道Java
  • Java:当计时器处于活动状态时,JSplitPane 将顶部面板的内容复制到底部面板

    所以我有一个 JSplitPane 和两个 JPanel 一个在顶部 一个在底部 在这两个面板中 我重写了paintComponent方法并添加了我自己的图形 在底部面板中 我想添加动画 当面板不重新绘制时 这很好 但是一旦计时器 java
  • Java中ThreadFactory的使用

    有人可以简要解释一下如何以及何时使用 ThreadFactory 吗 使用和不使用 ThreadFactory 的示例可能确实有助于理解差异 Thanks 这是一种可能的用法 假设您有一个ExecutorService它执行你的Runnab
  • 从 google play 中提取统计信息

    我正在建立一些统计数据 并希望获得来自 google play 应用程序商店 的统计数据 最受欢迎 下载量 价格等信息 有谁知道是否有这个 API 或者我必须自己抓取它 有一个名为 android market api 的项目http co

随机推荐