百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 热门文章 > 正文

精解四大集合框架:List核心知识总结

bigegpt 2024-10-12 06:54 6 浏览

Java集合框架早也是个老话题了,今天主要是总结一下关于Java中的集合框架List的核心知识点。肯定有人会问,这篇写的是List那接下来就还有三篇?是的,java集合框架一共会有四篇。希望通过这个系列能让你全面的get到Java集合框架的核心知识点。

目的

更希望通过这个系列的文章有所收获,不仅可以用于工作中,也可以用于面试中。

Java 集合是一个存储相同类型数据的容器,类似数组,集合可以不指定长度,但是数组必须指定长度。集合类主要从 Collection 和 Map 两个根接口派生出来,比如常用的 ArrayList、LinkedList、HashMap、HashSet、ConcurrentHashMap 等等。包目录:java.util

学过Java的都知道Java有四大集合框架,JDK版本1.8

List

Set

Queue

Map

常用集合UML类图

下面展示常用的集合框架(下面图中的两种线:虚线为实现,实线为继承)

ArrayList

ArrayList 是基于动态数组实现,容量能自动增长的集合。随机访问效率高,随机插入、随机删除效率低。线程不安全,多线程环境下可以使用 Collections.synchronizedList(list) 函数返回一个线程安全的 ArrayList 类,也可以使用 concurrent 并发包下的 CopyOnWriteArrayList 类。

动态数组,是指当数组容量不足以存放新的元素时,会创建新的数组,然后把原数组中的内容复制到新数组。

主要属性:

1//存储实际数据,使用transient修饰,序列化的时不会被保存
2transient Object[] elementData;
3//元素的数量,即容量。
4private int size;

数据结构:动态数组

特征:

  1. 允许元素为 null;
  2. 查询效率高、插入、删除效率低,因为大量 copy 原来元素;
  3. 线程不安全。

使用场景:

  1. 需要快速随机访问元素
  2. 单线程环境

常用方法介绍

add(element) 流程:添加元素

 1    private static final int DEFAULT_CAPACITY = 10;    
 2    //添加的数据e放在list的后面
 3    public boolean add(E e) {
 4        ensureCapacityInternal(size + 1); 
 5        elementData[size++] = e;
 6        return true;
 7    }
 8    private void ensureCapacityInternal(int minCapacity) {
 9        ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
10    }
11    private static int calculateCapacity(Object[] elementData, int minCapacity) {
12        if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
13            return Math.max(DEFAULT_CAPACITY, minCapacity);
14        }
15        return minCapacity;
16    }
  1. 判断当前数组是否为空,如果是则创建长度为 10(默认)的数组,因为 new ArrayList 的时没有初始化;
  2. 判断是否需要扩容,如果当前数组的长度加 1(即 size+1)后是否大于当前数组长度,则进行扩容 grow();
  3. 最后在数组末尾添加元素,并 size+1。

grow() 流程:扩容

 1    private void grow(int minCapacity) {
 2        // overflow-conscious code
 3        int oldCapacity = elementData.length;
 4        int newCapacity = oldCapacity + (oldCapacity >> 1);
 5        if (newCapacity - minCapacity < 0)
 6            newCapacity = minCapacity;
 7        if (newCapacity - MAX_ARRAY_SIZE > 0)
 8            newCapacity = hugeCapacity(minCapacity);
 9        // minCapacity is usually close to size, so this is a win:
10        elementData = Arrays.copyOf(elementData, newCapacity);
11    }
  1. 创建新数组,长度扩大为原数组的 1.5 倍(oldCapacity >> 1就是相当于10>>1==10/2=5,二进制位运算);
  2. 如果扩大 1.5 倍还是不够,则根据实际长度来扩容,比如 addAll() 场景;
  3. 将原数组的数据使用 System.arraycopy(native 方法)复制到新数组中。

add(index,element) 流程:向指定下标添加元素

 1    public void add(int index, E element) {
 2        rangeCheckForAdd(index);
 3
 4        ensureCapacityInternal(size + 1);  // Increments modCount!!
 5        System.arraycopy(elementData, index, elementData, index + 1,
 6                         size - index);
 7        elementData[index] = element;
 8        size++;
 9    }
10    private void rangeCheckForAdd(int index) {
11        if (index > size || index < 0)
12            throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
13    }
  1. 检查 index 是否在数组范围内,假如数组长度是 2,则 index 必须 >=0 并且 <=2,否则报 IndexOutOfBoundsException 异常;
  2. 扩容检查;
  3. 通过拷贝方式,把数组位置为 index 至 size-1 的元素都往后移动一位,腾出位置之后放入元素,并 size+1。

set(index,element) 流程:设置新值,返回旧值

 1   public E set(int index, E element) {
 2        rangeCheck(index);
 3
 4        E oldValue = elementData(index);
 5        elementData[index] = element;
 6        return oldValue;
 7    }
 8    private void rangeCheck(int index) {
 9        if (index >= size)
10            throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
11    }
  1. 检查 index 是否在数组范围内,假如数组长度是 2,则 index 必须 小于<2,下标是从0开始的,size=2说明下标只有0和1;
  2. 保留被覆盖的值,因为最后需要返回旧的值;
  3. 新元素覆盖位置为 index 的旧元素,返回旧值。

get(index) 流程:通过下标获取list中的数据

1    public E get(int index) {
2        rangeCheck(index);
3        return elementData(index);
4    }
  1. 判断下标有没有越界;
  2. 直接通过数组下标来获取数组中对应的元素,get 的时间复杂度是 O(1)。

remove(index) 流程:按照下标移除lsit中的数据

 1    public E remove(int index) {
 2        rangeCheck(index);
 3        modCount++;
 4        E oldValue = elementData(index);
 5        int numMoved = size - index - 1;
 6        if (numMoved > 0)
 7            System.arraycopy(elementData, index+1, elementData, index,
 8                             numMoved);
 9        elementData[--size] = null; // clear to let GC do its work
10        return oldValue;
11    }
  1. 检查指定位置是否在数组范围内,假如数组长度是 2,则 index 必须 >=0 并且 < 2;
  2. 保留要删除的值,因为最后需要返回旧的值;
  3. 计算出需要移动元素个数,再通过拷贝使数组内位置为 index+1 到 size-1 的元素往前移动一位,把数组最后一个元素设置为 null(精辟小技巧),返回旧值。

remove(object) 流程:

 1    public boolean remove(Object o) {
 2        if (o == null) {
 3            for (int index = 0; index < size; index++)
 4                if (elementData[index] == null) {
 5                    fastRemove(index);
 6                    return true;
 7                }
 8        } else {
 9            for (int index = 0; index < size; index++)
10                if (o.equals(elementData[index])) {
11                    fastRemove(index);
12                    return true;
13                }
14        }
15        return false;
16    } 
17    private void fastRemove(int index) {
18        modCount++;
19        int numMoved = size - index - 1;
20        if (numMoved > 0){
21        //数组拷贝    
22        System.arraycopy(elementData, index+1, elementData, index,
23                             numMoved);
24        }    
25        //方便GC
26        elementData[--size] = null;
27    }
  1. 遍历数组,比较obejct是否存在于数组中;
  2. 计算出需要移动元素个数,再通过拷贝使数组内位置为 index+1 到 size-1 的元素往前移动一位,把数组最后一个元素设置为 null(精辟小技巧)。

总结:

  1. new ArrayList 创建对象时,如果没有指定集合容量则初始化为 0;如果有指定,则按照指定的大小初始化;
  2. 扩容时,先将集合扩大 1.5 倍,如果还是不够,则根据实际长度来扩容,保证都能存储所有数据,比如 addAll() 场景。
  3. 如果新扩容后数组长度大于(Integer.MAX_VALUE-8),则抛出 OutOfMemoryError。
  4. 建议在使用的时候,先评估一下要存多少数据,然后就可以大致或者准确地给ArrayList指定大小,这样就会避免不断多次扩容对系统带来的开销。

LinkedList

LinkedList 是可以在任何位置进行插入移除操作的有序集合,它是基于双向链表实现的,线程不安全。LinkedList 功能比较强大,可以实现队列双向队列

主要属性:

 1//链表长度
 2transient int size = 0;
 3//头部节点
 4transient Node<E> first;
 5//尾部节点
 6transient Node<E> last;
 7
 8/**
 9* 静态内部类,存储数据的节点
10* 前驱结点、后继结点,那证明至少是双向链表
11*/
12private static class Node<E> {
13    //自身结点
14    E item;
15    //下一个节点
16    Node<E> next;
17    //上一个节点
18    Node<E> prev;
19}

数据结构:双向链表

特征:

  1. 允许元素为 null;
  2. 插入和删除效率高,查询效率低;
  3. 顺序访问会非常高效,而随机访问效率(比如 get 方法)比较低;
  4. 既能实现栈 Stack(后进先出),也能实现队列(先进先出), 也能实现双向队列,因为提供了 xxxFirst()、xxxLast() 等方法;
  5. 线程不安全。

使用场景:

  1. 需要快速插入,删除元素
  2. 按照顺序访问其中的元素
  3. 单线程环境

add() 流程:

 1  public boolean add(E e) {
 2        linkLast(e);
 3        return true;
 4    }
 5    void linkLast(E e) {
 6        final Node<E> l = last;
 7        final Node<E> newNode = new Node<>(l, e, null);
 8        last = newNode;
 9        if (l == null)
10            first = newNode;
11        else
12            l.next = newNode;
13        size++;
14        modCount++;
15    }
  1. 创建一个新结点,结点元素 e为传入参数,前继节点 prev 为“当前链表 last 结点”,后继节点 next 为 null;
  2. 判断当前链表 last 结点是否为空,如果是则把新建结点作为头结点,如果不是则把新结点作为 last 结点。
  3. 最后返回 true。

get(index) 流程:

 1    public E get(int index) {
 2        checkElementIndex(index);
 3        return node(index).item;
 4    }
 5    /**
 6     * Returns the (non-null) Node at the specified element index.
 7     */
 8    Node<E> node(int index) {
 9        if (index < (size >> 1)) {
10            Node<E> x = first;
11            for (int i = 0; i < index; i++)
12                x = x.next;
13            return x;
14        } else {
15            Node<E> x = last;
16            for (int i = size - 1; i > index; i--)
17                x = x.prev;
18            return x;
19        }
20    }
21    private void checkElementIndex(int index) {
22        if (!isElementIndex(index))
23            throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
24    }
25    private boolean isElementIndex(int index) {
26        return index >= 0 && index < size;
27    }
  1. 检查 index 是否在数组范围内,假如数组长度是 2,则 index 必须 >=0 并且 < 2;
  2. index 小于“双向链表长度的 1/2”则从头开始往后遍历查找,否则从链表末尾开始向前遍历查找。

remove() 流程:

1    public E remove(int index) {
2        checkElementIndex(index);
3        return unlink(node(index));
4    }
5    private void checkElementIndex(int index) {
6        if (!isElementIndex(index))
7            throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
8    }
  1. 判断 first 结点是否为空,如果是则报 NoSuchElementException 异常;
  2. 如果不为空,则把待删除结点的 next 结点的 prev 属性赋值为 null,达到删除头结点的效果。
  3. 返回删除值。

Vector

Vector 是矢量队列,也是基于动态数组实现,容量可以自动扩容。跟 ArrayList 实现原理一样,但是 Vector 是线程安全,使用 Synchronized 实现线程安全,性能非常差,已被淘汰,使用 CopyOnWriteArrayList 替代 Vector

主要属性:

1//存储实际数据
2protected Object[] elementData;
3//动态数组的实际大小
4protected int elementCount;
5//动态数组的扩容系数
6protected int capacityIncrement;

数据结构:动态数组

特征:

  1. 允许元素为 null;
  2. 查询效率高、插入、删除效率低,因为需要移动元素;
  3. 默认的初始化大小为 10,没有指定增长系数则每次都是扩容一倍,如果扩容后还不够,则直接根据参数长度来扩容;
  4. 线程安全,性能差(Synchronized),使用 CopyOnWriteArrayList 替代 Vector。

使用场景:多线程环境

为什么是线程安全的,看看下面的几个常用方法就知道了。

 1    public synchronized void addElement(E obj) {
 2        modCount++;
 3        ensureCapacityHelper(elementCount + 1);
 4        elementData[elementCount++] = obj;
 5    }
 6    public boolean remove(Object o) {
 7        return removeElement(o);
 8    }
 9    public synchronized boolean removeElement(Object obj) {
10        modCount++;
11        int i = indexOf(obj);
12        if (i >= 0) {
13            removeElementAt(i);
14            return true;
15        }
16        return false;
17    }
18    public synchronized E get(int index) {
19        if (index >= elementCount)
20            throw new ArrayIndexOutOfBoundsException(index);
21
22        return elementData(index);
23    }
24    public synchronized boolean add(E e) {
25        modCount++;
26        ensureCapacityHelper(elementCount + 1);
27        elementData[elementCount++] = e;
28        return true;
29    }

这几个常用方法中,方法都是使用同步锁synchronized修饰,所以它是线程安全的。

Stack

Stack 是栈,先进后出原则,Stack 继承 Vector,也是通过数组实现,线程安全。因为效率比较低,不推荐使用,可以使用 LinkedList(线程不安全)或者 ConcurrentLinkedDeque(线程安全)来实现先进先出的效果。

数据结构:动态数组

构造函数:只有一个默认 Stack()

特征:先进后出

实现原理:

  1. Stack 执行 push() 时,将数据推进栈,即把数据追加到数组的末尾。
  2. Stack 执行 peek 时,取出栈顶数据,不删除此数据,即获取数组首个元素。
  3. Stack 执行 pop 时,取出栈顶数据,在栈顶删除数据,即删除数组首个元素。
  4. Stack 继承于 Vector,所以 Vector 拥有的属性和功能,Stack 都拥有,比如 add()、set() 等等。
1public class Stack<E> extends Vector<E> {
2//....
3}

CopyOnWriteArrayList

CopyOnWriteArrayList 是线程安全的 ArrayList,写操作(add、set、remove 等等)时,把原数组拷贝一份出来,然后在新数组进行写操作,操作完后,再将原数组引用指向到新数组。CopyOnWriteArrayList 可以替代 Collections.synchronizedList(List list)。这个是在JUC包目录下的。内部使用了AQS实现的锁。

java.util.concurrent

数据结构:动态数组

特征:

  1. 线程安全;
  2. 读多写少,比如缓存;
  3. 不能保证实时一致性,只能保证最终一致性。

缺点:

  1. 写操作,需要拷贝数组,比较消耗内存,如果原数组容量大的情况下,可能触发频繁的 Young GC 或者 Full GC;
  2. 不能用于实时读的场景,因为读取到数据可能是旧的,可以保证最终一致性。

实现原理:

CopyOnWriteArrayList 写操作加了锁,不然多线程进行写操作时会复制多个副本;读操作没有加锁,所以可以实现并发读,但是可能读到旧的数据,比如正在执行读操作时,同时有多个写操作在进行,遇到这种场景时,就会都到旧数据。

 1public class CopyOnWriteArrayList<E> implements List<E>, RandomAccess, Cloneable, java.io.Serializable {
 2    private static final long serialVersionUID = 8673264195747942595L;
 3
 4    /** The lock protecting all mutators */
 5    final transient ReentrantLock lock = new ReentrantLock();
 6    //添加数据
 7    public boolean add(E e) {
 8        //使用到了锁机制
 9        final ReentrantLock lock = this.lock;
10        lock.lock();
11        try {
12            Object[] elements = getArray();
13            int len = elements.length;
14            Object[] newElements = Arrays.copyOf(elements, len + 1);
15            newElements[len] = e;
16            setArray(newElements);
17            return true;
18        } finally {
19            //释放锁
20            lock.unlock();
21        }
22    }
23    //移除数据
24    public E remove(int index) {
25        //锁机制
26        final ReentrantLock lock = this.lock;
27        lock.lock();
28        try {
29            Object[] elements = getArray();
30            int len = elements.length;
31            E oldValue = get(elements, index);
32            int numMoved = len - index - 1;
33            if (numMoved == 0)
34                setArray(Arrays.copyOf(elements, len - 1));
35            else {
36                Object[] newElements = new Object[len - 1];
37                System.arraycopy(elements, 0, newElements, 0, index);
38                System.arraycopy(elements, index + 1, newElements, index,
39                                 numMoved);
40                setArray(newElements);
41            }
42            return oldValue;
43        } finally {
44            //释放锁
45            lock.unlock();
46        }
47    }

好的,以上便是今天分享的内容。希望你有所收获。

对老铁最大鼓励就是点个在看&&转发 再看||转发。可二选一。哈哈哈!!!

相关推荐

恢复软件6款汇总推荐,帮你减轻数据恢复压力!

在当今数字化生活中,数据丢失的风险如影随形。无论是误删文件、硬盘故障,还是遭遇病毒攻击,丢失的数据都可能给我们带来不小的麻烦。此时,一款优秀的数据恢复软件就成为了挽救数据的关键。今天,为大家汇总推荐...

中兴星星一号刷回官方原版recovery的教程

【搞科技教程】中兴星星一号的官方recovery也来说一下了,因为之前给大家分享过了第三方的recovery了,之前给大家分享的第三方recovery也是采用一键刷入的方式,如果细心的朋友会发现,之前...

新玩机工具箱,Uotan柚坛工具箱软件体验

以前的手机系统功能比较单调,各厂商的重视程度不一样,所以喜欢玩机的朋友会解锁手机系统的读写权限,来进行刷机或者ROOT之类的操作,让使用体验更好。随着现在的手机系统越来越保守,以及自身功能的增强,...

三星g906k刷recovery教程_三星g906k中文recovery下载

【搞科技教程】看到有一些机友在找三星g906k的第三方recovery,下面就来说一下详细的recovery的刷入方法了,因为手机只有有了第三方的recovery之后才可以刷第三方的root包和系统包...

中兴星星2号刷recovery教程_星星二号中文recovery下载

【搞科技教程】咱们的中兴星星2手机也就是中兴星星二号手机的第三方recovery已经出来了,并且是中文版的,有了这个recovery之后,咱们的手机就可以轻松的刷第三方的系统包了,如果没有第三方的re...

数据恢复软件有哪些值得推荐?这 6 款亲测好用的工具汇总请收好!

在数字生活中,数据丢失的阴霾常常突如其来。无论是误删工作文档、格式化重要磁盘,还是遭遇系统崩溃,都可能让我们陷入焦虑。关键时刻,一款得力的数据恢复软件便是那根“救命稻草”。今天,为大家精心汇总6...

中兴u956刷入recovery的教程(中兴e5900刷机)

【搞科技教程】这次主要来给大家说说中兴u956手机如何刷入第三方的recovery,因为第三方的recovery工具是咱们刷第三方rom包的基础,可是很我欠却不会刷,所以太这里来给大家整理了一下详细的...

联想A850+刷recovery教程 联想A850+第三方recovery下载

【搞科技教程】联想A850+的第三方recovery出来了,这个第三方的recovery是非常的重要的,比如咱们的手机要刷第三方的系统包的时候,都是需要用到这个第三方的recovery的,在网上也是有...

工具侠重大更新 智能机上刷机一条龙完成

工具侠是针对玩机的机油开发的一款工具,不管是发烧级别的粉丝,还是普通小白用户,都可以在工具侠上找到你喜欢的工具应用。这不,最新的工具侠2.0.16版本,更新了专门为小白准备的刷机助手工具,以及MTK超...

shift+delete删除的文件找回6种硬盘数据恢复工具

硬盘作为电脑的重要存储设备,如同一个巨大的数字仓库,承载着我们日常工作、学习和生活中的各种文件,从珍贵的照片、重要的工作文档到喜爱的视频、音乐等,都依赖硬盘来安全存放。但有时,我们可能会不小心用sh...

使用vscode+Deepseek 实现AI编程 基于Cline和continue

尊敬的诸位!我是一名专注于嵌入式开发的物联网工程师。关注我,持续分享最新物联网与AI资讯和开发实战。期望与您携手探寻物联网与AI的无尽可能。这两天deepseek3.0上线,据说编程能力比肩Cl...

详解如何使用VSCode搭建TypeScript环境(适合小白)

搭建Javascript环境因为TypeScript不能直接在浏览器上运行。它需要编译器来编译并生成JavaScript文件。所以需要首先安装好javascript环境,可以参考文章:https://...

使用VSCode来书写你的Jupyter Notebooks

现在你可以在VScode里面来书写你的notebook了,使用起来十分的方便。下面来给大家演示一下环境的搭建。首先需要安装一个jupyter的包,使用下面的命令安装:pip3install-ih...

使用VSCode模板提高Vue开发效率(vscode开发vue插件)

安装VSCode安装Vetur和VueHelper插件,安装完成后需要重启VScode。在扩展插件搜索框中找到如下Vetur和VueHelper两个插件,注意看图标。添加Vue模板打...

干货!VsCode接入DeepSeek实现AI编程的5种主流插件详解

AI大模型对编程的影响非常之大,可以说首当其冲,Cursor等对话式编程工具渐渐渗透到开发者的工作中,作为AI编程的明星产品,Cursor虽然好用,但是贵啊,所以咱们得找平替,最好免费那种。俗话说,不...