热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

源码详解数据结构LinkedList

摘要:java.util.LinkedList是Java集合框架中的成员之一,底层是基于双向链表实现,集合容量可动态变化的。本文分享自华为云社区《LinkedList源码分析》,作
摘要:java.util.LinkedList 是 Java 集合框架中的成员之一,底层是基于双向链表实现,集合容量可动态变化的。

本文分享自华为云社区《LinkedList 源码分析》,作者: 陈皮的JavaLib。

LinkedList 简介

java.util.Linked List 是 Java 集合框架中的成员之一,底层是基于双向链表实现,集合容量可动态变化的。它继承自 Abstract Sequential List 抽象类,实现了 List 接口。同时还实现了 Cloneable 和 Serializable 三个标记接口,说明 Array List 是可克隆复制的,可序列化的。

Array List 数组列表底层是基于动态数组实现的,所以优点是能支持快速随机访问,但是增删操作可能会比较慢(因为可能需要进行数组扩容,数据拷贝)。而且数组需要先申请一定的内存空间,可能会造成浪费。而链表列表 LinkedList 的优点是增删操作速度比较快,而且列表存储多少元素就动态申请多少节点来存储,比较节省内存空间。

为何要使用双向链表呢,主要在于遍历效率比单向链表高。例如当我们需要查找指定下标的节点,在指定下标进行增删改操作时,先判断这个位置是靠近头部还是尾部,从而决定从头部还是从尾部开始查找,提高效率。

public class LinkedList
    extends AbstractSequentialList
    implements List, Deque, Cloneable, java.io.Serializable {

}

2 LinkedList 源码分析

2.1 内部变量

LinkedList 的元素是存储在节点对象中的,节点类是 LinkedList 类的一个内部私有静态类,源码如下所示:

private static class Node {
    E item;
    Node next;
    Node prev;

    Node(Node prev, E element, Node next) {
        this.item = element;
        this.next = next;
        this.prev = prev;
    }
}

LinkedList 中定义了3个变量,一个代表当前列表的元素个数,另外两个变量指向链表的头部和尾部。以及它的父类 AbstractList 中的 modCount 变量,每次对链表的增删改操作都会使它加1。

transient int size = 0;

transient Node first;

transient Node last;

protected transient int modCount = 0;

2.2 构造函数

ArrayList 有2个构造函数,一个无参构造函数,另一个使用指定 Collection 集合来构造集合的构造函数。

无参构造函数,什么都没有操作。

public LinkedList() {}

使用指定 Collection 集合来构造链表,如果 Collection 不能为 null ,否则会抛出 npe 。

public LinkedList(Collection c) {
    this();
    addAll(c);
}

public boolean addAll(Collection c) {
    return addAll(size, c);
}

public boolean addAll(int index, Collection c) {
    checkPositionIndex(index);

    Object[] a = c.toArray();
    int numNew = a.length;
    if (numNew == 0)
        return false;

    Node pred, succ;
    if (index == size) {
        succ = null;
        pred = last;
    } else {
        succ = node(index);
        pred = succ.prev;
    }

    for (Object o : a) {
        @SuppressWarnings("unchecked") E e = (E) o;
        Node newNode = new Node<>(pred, e, null);
        if (pred == null)
            first = newNode;
        else
            pred.next = newNode;
        pred = newNode;
    }

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

    size += numNew;
    modCount++;
    return true;
}

2.3 常用方法

  • public E getFirst()

获取链表的第一个元素,如果不存在第一个节点,抛出异常。

public E getFirst() {
    final Node f = first;
    if (f == null)
        throw new NoSuchElementException();
    return f.item;
}
  • public E getLast()

获取链表的最后一个元素,如果链表为空,则抛出异常。

public E getLast() {
    final Node l = last;
    if (l == null)
        throw new NoSuchElementException();
    return l.item;
}
  • public E removeFirst()

删除第一个元素,如果链表为空,则抛出异常。

public E removeFirst() {
    final Node f = first;
    if (f == null)
        throw new NoSuchElementException();
    return unlinkFirst(f);
}
  • public E removeLast()

删除最后一个元素,如果链表为空,则抛出异常。

public E removeLast() {
    final Node l = last;
    if (l == null)
        throw new NoSuchElementException();
    return unlinkLast(l);
}
  • public void clear()

情况链表,遍历每一个节点,将每一个节点的内部引用都置为 null ,便于进行垃圾回收。

public void clear() {
    for (Node x = first; x != null; ) {
        Node next = x.next;
        x.item = null;
        x.next = null;
        x.prev = null;
        x = next;
    }
    first = last = null;
    size = 0;
    modCount++;
}
  • public boolean add(E e)

在链表尾部添加一个元素。

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

获取 list 的迭代器,用于遍历集合中的元素。

public Iterator iterator() {
    return new Itr();
}
  • public int size():返回集合元素个数。
  • public boolean contains(Object o):是否包含某个元素。
  • public boolean remove(Object o):删除某个元素。
  • public E get(int index):获取指定下标的元素。
  • public E set(int index, E element):在指定下标修改元素值。
  • public void add(int index, E element):在指定下标添加元素。

3 常见面试题分析

3.1 LinkedList 是线程安全的吗?

我们通过分析源码可知,对它的任何操作都是没有加锁的,所以在多线程场景下,它是线程不安全的。它适合在非多线程使用场景下,并且增删操作比较多的情况。

public static void main(String[] args) throws InterruptedException {

    LinkedList list = new LinkedList<>();

    Thread thread1 = new Thread(() -> {
      for (int i = 0; i <1000; i++) {
        list.add(Thread.currentThread().getName() + i);
      }
    }, "Thread01");
    thread1.start();

    Thread thread2 = new Thread(() -> {
      for (int i = 0; i <1000; i++) {
        list.add(Thread.currentThread().getName() + i);
      }
    }, "Thread02");
    thread2.start();

    thread1.join();
    thread2.join();

    System.out.println(list.size()); // 输出不一定是2000,例如1850

}

如果增删操作比较多的话,可以使用 LinkedList ,LinkedList 增删操作速度比较快。

如果需要线程安全的话,可以使用 JDK 集合中的工具类 Collections 提供一个方法 synchronizedList 可以将线程不安全的 List 集合变成线程安全的集合对象,如下所示。

public static void main(String[] args) throws InterruptedException {

    LinkedList list = new LinkedList<>();
    // 封装成线程安全的集合
    List synchrOnizedList= Collections.synchronizedList(list);

    Thread thread1 = new Thread(() -> {
      for (int i = 0; i <1000; i++) {
        synchronizedList.add(Thread.currentThread().getName() + i);
      }
    }, "Thread01");
    thread1.start();

    Thread thread2 = new Thread(() -> {
      for (int i = 0; i <1000; i++) {
        synchronizedList.add(Thread.currentThread().getName() + i);
      }
    }, "Thread02");
    thread2.start();

    thread1.join();
    thread2.join();

    System.out.println(synchronizedList.size());

 }

3.2 LinkedList 优缺点

  • 优点:增删操作速度快,不仅有头部和尾部双指针,可以根据要操作的下标靠近哪边,从而决定从哪一边开始遍历找到指定的下标。找到位置后,删除和插入操作的时间复杂度为 O(1) 。
  • 缺点:不支持快速随机访问,相对 ArrayList 比较慢,但也不是决定的,取决于列表的长度,以及访问的下标位置。

3.3 使用迭代器 Iterator 过程中,可以增删元素吗?

通过源码分析,在获取集合的迭代器方法中,返回的是 AbstractList 抽象类中定义的 ListItr 迭代器对象,在他的父类 Itr 中持有变量 expectedModCount ,在初始化迭代器对象时这个变量的值被赋予此时链表中的操作次数 modCount 。在迭代获取元素时,会检查这两变量是否相等,不相等则抛出并发修改异常。所以不支持在使用迭代器的过程中,对原链表进行增删改操作。但是可以调用迭代器的增删操作。

private class ListItr extends Itr implements ListIterator {
    ListItr(int index) {
        cursor = index;
    }

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

    public E previous() {
        checkForComodification();
        try {
            int i = cursor - 1;
            E previous = get(i);
            lastRet = cursor = i;
            return previous;
        } catch (IndexOutOfBoundsException e) {
            checkForComodification();
            throw new NoSuchElementException();
        }
    }

    public int nextIndex() {
        return cursor;
    }

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

    public void set(E e) {
        if (lastRet <0)
            throw new IllegalStateException();
        checkForComodification();

        try {
            AbstractList.this.set(lastRet, e);
            expectedModCount = modCount;
        } catch (IndexOutOfBoundsException ex) {
            throw new ConcurrentModificationException();
        }
    }

    public void add(E e) {
        checkForComodification();

        try {
            int i = cursor;
            AbstractList.this.add(i, e);
            lastRet = -1;
            cursor = i + 1;
            expectedModCount = modCount;
        } catch (IndexOutOfBoundsException ex) {
            throw new ConcurrentModificationException();
        }
    }
}
private class Itr implements Iterator {
    /**
     * Index of element to be returned by subsequent call to next.
     */
    int cursor = 0;

    /**
     * Index of element returned by most recent call to next or
     * previous.  Reset to -1 if this element is deleted by a call
     * to remove.
     */
    int lastRet = -1;

    /**
     * The modCount value that the iterator believes that the backing
     * List should have.  If this expectation is violated, the iterator
     * has detected concurrent modification.
     */
    int expectedModCount = modCount;

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

    public E next() {
        checkForComodification();
        try {
            int i = cursor;
            E next = get(i);
            lastRet = i;
            cursor = i + 1;
            return next;
        } catch (IndexOutOfBoundsException e) {
            checkForComodification();
            throw new NoSuchElementException();
        }
    }

    public void remove() {
        if (lastRet <0)
            throw new IllegalStateException();
        checkForComodification();

        try {
            AbstractList.this.remove(lastRet);
            if (lastRet < cursor)
                cursor--;
            lastRet = -1;
            expectedModCount = modCount;
        } catch (IndexOutOfBoundsException e) {
            throw new ConcurrentModificationException();
        }
    }

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

3.4 LinkedList 可以存储 null 值吗?元素可以重复吗?

LinkedList 底层是由双向链表实现的,并且在添加元素的时候,没有对元素进行值校验,所以可以存储 null 值,并且存储的元素是可以重复的。

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

void linkLast(E e) {
    final Node l = last;
    final Node newNode = new Node<>(l, e, null);
    last = newNode;
    if (l == null)
        first = newNode;
    else
        l.next = newNode;
    size++;
    modCount++;
}

3.5 如何边遍历 ArrayList 元素,边删除指定元素?

不支持在遍历的同时对原链表进行操作,会抛出 ConcurrentModificationException 并发修改异常,前面我们提到使用迭代器 Iterator 遍历集合时,不能对集合进行增删操作(会导致 modCount 值变化)。应该使用 Iterator 类的 remove 方法。

package com.chenpi;

import java.util.Iterator;
import java.util.LinkedList;

/**
 * @author 陈皮
 * @version 1.0
 * @description
 * @date 2022/3/1
 */
public class ChenPi {

  public static void main(String[] args) {

    LinkedList list = new LinkedList<>();
    list.add("Java");
    list.add("C++");
    list.add("Python");
    list.add("Lua");

    Iterator iterator = list.iterator();
    while (iterator.hasNext()) {
      String next = iterator.next();
      if ("C++".equals(next)) {
        iterator.remove();
        continue;
      }
      System.out.println(next);
    }

  }
}

// 输出结果如下
Java
Python
Lua

 

点击关注,第一时间了解华为云新鲜技术~


推荐阅读
  • 本文介绍了Java高并发程序设计中线程安全的概念与synchronized关键字的使用。通过一个计数器的例子,演示了多线程同时对变量进行累加操作时可能出现的问题。最终值会小于预期的原因是因为两个线程同时对变量进行写入时,其中一个线程的结果会覆盖另一个线程的结果。为了解决这个问题,可以使用synchronized关键字来保证线程安全。 ... [详细]
  • Java太阳系小游戏分析和源码详解
    本文介绍了一个基于Java的太阳系小游戏的分析和源码详解。通过对面向对象的知识的学习和实践,作者实现了太阳系各行星绕太阳转的效果。文章详细介绍了游戏的设计思路和源码结构,包括工具类、常量、图片加载、面板等。通过这个小游戏的制作,读者可以巩固和应用所学的知识,如类的继承、方法的重载与重写、多态和封装等。 ... [详细]
  • Iamtryingtomakeaclassthatwillreadatextfileofnamesintoanarray,thenreturnthatarra ... [详细]
  • Java容器中的compareto方法排序原理解析
    本文从源码解析Java容器中的compareto方法的排序原理,讲解了在使用数组存储数据时的限制以及存储效率的问题。同时提到了Redis的五大数据结构和list、set等知识点,回忆了作者大学时代的Java学习经历。文章以作者做的思维导图作为目录,展示了整个讲解过程。 ... [详细]
  • JavaSE笔试题-接口、抽象类、多态等问题解答
    本文解答了JavaSE笔试题中关于接口、抽象类、多态等问题。包括Math类的取整数方法、接口是否可继承、抽象类是否可实现接口、抽象类是否可继承具体类、抽象类中是否可以有静态main方法等问题。同时介绍了面向对象的特征,以及Java中实现多态的机制。 ... [详细]
  • 本文讨论了一个关于cuowu类的问题,作者在使用cuowu类时遇到了错误提示和使用AdjustmentListener的问题。文章提供了16个解决方案,并给出了两个可能导致错误的原因。 ... [详细]
  • 本文介绍了一个Java猜拳小游戏的代码,通过使用Scanner类获取用户输入的拳的数字,并随机生成计算机的拳,然后判断胜负。该游戏可以选择剪刀、石头、布三种拳,通过比较两者的拳来决定胜负。 ... [详细]
  • 阿,里,云,物,联网,net,core,客户端,czgl,aliiotclient, ... [详细]
  • 本文主要解析了Open judge C16H问题中涉及到的Magical Balls的快速幂和逆元算法,并给出了问题的解析和解决方法。详细介绍了问题的背景和规则,并给出了相应的算法解析和实现步骤。通过本文的解析,读者可以更好地理解和解决Open judge C16H问题中的Magical Balls部分。 ... [详细]
  • Spring特性实现接口多类的动态调用详解
    本文详细介绍了如何使用Spring特性实现接口多类的动态调用。通过对Spring IoC容器的基础类BeanFactory和ApplicationContext的介绍,以及getBeansOfType方法的应用,解决了在实际工作中遇到的接口及多个实现类的问题。同时,文章还提到了SPI使用的不便之处,并介绍了借助ApplicationContext实现需求的方法。阅读本文,你将了解到Spring特性的实现原理和实际应用方式。 ... [详细]
  • 个人学习使用:谨慎参考1Client类importcom.thoughtworks.gauge.Step;importcom.thoughtworks.gauge.T ... [详细]
  • 本文详细介绍了Java中vector的使用方法和相关知识,包括vector类的功能、构造方法和使用注意事项。通过使用vector类,可以方便地实现动态数组的功能,并且可以随意插入不同类型的对象,进行查找、插入和删除操作。这篇文章对于需要频繁进行查找、插入和删除操作的情况下,使用vector类是一个很好的选择。 ... [详细]
  • 如何自行分析定位SAP BSP错误
    The“BSPtag”Imentionedintheblogtitlemeansforexamplethetagchtmlb:configCelleratorbelowwhichi ... [详细]
  • 本文介绍了在Java中gt、gtgt、gtgtgt和lt之间的区别。通过解释符号的含义和使用例子,帮助读者理解这些符号在二进制表示和移位操作中的作用。同时,文章还提到了负数的补码表示和移位操作的限制。 ... [详细]
  • 本文介绍了如何在给定的有序字符序列中插入新字符,并保持序列的有序性。通过示例代码演示了插入过程,以及插入后的字符序列。 ... [详细]
author-avatar
000冷000
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有