Java全维度解析(三):‌集合框架之List

在这里插入图片描述

为什么集合框架是Java开发者的必修课?

1、🔥集合框架在Java开发中的使用率高达93%🔥
2、🔥直接影响程序性能的核心要素🔥
3、🔥大厂面试必考知识点TOP3🔥

🌈🌈由于Java集合框架知识点比较庞大,所以在接下来会分成四篇来讲,本篇包括对整个Java集合框架的简单介绍,以及重点梳理List集合。如若文章对大家有帮助,👉欢迎大家订阅专栏👈,本专栏免费订阅,会定期更新,也恳请大家监督,😁😁让大家一起成长🚀🚀🚀,好了,废话不多说,开撸!🎯

一、集合框架介绍


Java 集合框架可以分为两条大的支线:Collection、Map。
我们可能会经常看到或者听到 “List是有序的,Set是无序的” 这样的言论,这种言论是正确的吗?咱们待会一一道来,先画一张集合框架的结构图,让大家对集合框架整体有个认知。
在这里插入图片描述

二、List

List代表有序,可重复的集合,注意,是代表,并不是List自身的特性,好多小伙伴容易混淆概念,在这里提醒,List只是一个接口,并不具备功能特性,而是它的实现类们(如ArrayList、LinkedList等等)共同的特性,List代表了这些特性而已。

List典型的代表就是封装了动态数组的ArrayList和封装了链表的LinkedList,也是我们经常会用到的。项目中需要用到LinkedList的场景几乎都可以使用ArrayList来代替,并且性能通常会更好!就连 LinkedList的作者约书亚 · 布洛克(Josh Bloch)自己都说从来不会使用 LinkedList,所以你们平时喜欢用ArrayList还是LinkedList呢?
在这里插入图片描述

2.1 ArrayList

有的小伙伴不喜欢看源码,觉得密密麻麻的,看得头大,暮辰今天给大家伙配备源码加文字讲解,都是干货,希望各位看官看得开心,收货也满满当当的,简约版源码如下:👇

package java.util;

import java.util.Arrays;

public class ArrayList<E> extends AbstractList<E>
        implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{
    // 默认初始容量
    private static final int DEFAULT_CAPACITY = 10;

    // 空数组实例
    private static final Object[] EMPTY_ELEMENTDATA = {};

    // 默认大小的空数组实例
    private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

    // 存储 ArrayList 元素的数组缓冲区
    transient Object[] elementData; 

    // ArrayList 的大小(包含的元素个数)
    private int size;

    // 构造一个具有指定初始容量的空列表
    public ArrayList(int initialCapacity) {
        if (initialCapacity > 0) {
            this.elementData = new Object[initialCapacity];
        } else if (initialCapacity == 0) {
            this.elementData = EMPTY_ELEMENTDATA;
        } else {
            throw new IllegalArgumentException("Illegal Capacity: "+
                                               initialCapacity);
        }
    }

    // 构造一个初始容量为 10 的空列表
    public ArrayList() {
        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
    }

    // 确保内部容量足够
    private void ensureCapacityInternal(int minCapacity) {
        ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
    }

    // 计算容量
    private static int calculateCapacity(Object[] elementData, int minCapacity) {
        if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
            return Math.max(DEFAULT_CAPACITY, minCapacity);
        }
        return minCapacity;
    }

    // 明确确保容量
    private void ensureExplicitCapacity(int minCapacity) {
        modCount++;

        // 如果最小容量大于当前数组的长度,调用 grow 方法扩容
        if (minCapacity - elementData.length > 0)
            grow(minCapacity);
    }

    // 数组的最大大小
    private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;

    // 扩容的核心方法
    private void grow(int minCapacity) {
        // 获取旧容量
        int oldCapacity = elementData.length;
        // 计算新容量,为旧容量的 1.5 倍
        int newCapacity = oldCapacity + (oldCapacity >> 1);
        // 如果新容量小于所需的最小容量,将新容量设为最小容量
        if (newCapacity - minCapacity < 0)
            newCapacity = minCapacity;
        // 如果新容量超过最大数组大小,调用 hugeCapacity 方法处理
        if (newCapacity - MAX_ARRAY_SIZE > 0)
            newCapacity = hugeCapacity(minCapacity);
        // 调用 Arrays.copyOf 方法创建新数组并复制元素
        elementData = Arrays.copyOf(elementData, newCapacity);
    }

    // 处理大容量需求
    private static int hugeCapacity(int minCapacity) {
        if (minCapacity < 0) // overflow
            throw new OutOfMemoryError();
        return (minCapacity > MAX_ARRAY_SIZE) ?
            Integer.MAX_VALUE :
            MAX_ARRAY_SIZE;
    }

    // 添加元素到列表末尾
    public boolean add(E e) {
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        elementData[size++] = e;
        return true;
    }
}

2.1.1 ArrayList的源码浅析

(1)核心成员变量属性
变量名作用
elementData是一个数组,用来存储数据
size元素的个数

咱们重点就记住这两个变量(elementData 、size )就行。
可能有朋友会问,既然elementData是一个数组,可以利用数组的length方法算出长度,为什么需要size呢?,暮辰在这里就多说一嘴,elementData 的数组长度 和 ArrayList的元素个数是不划等号的,比方说数组长度为10,但是里面只存放了8个元素,还有两个位置是空的,elementData 的数组长度为10,但是我们用ArrayList的时候,需要的是元素个数,也就是个数8,因此size是非常必要的一个成员变量。

(2)创建流程

在创建ArrayList对象时,若未指定初始容量,它会使用默认容量。默认情况下,初始容量为 0,当第一次添加元素时,会将容量初始化为 10。当然,你也可以在创建ArrayList对象时指定初始容量,示例代码如下:

import java.util.ArrayList;

public class ArrayListInitialCapacity {
    public static void main(String[] args) {
        // 使用默认初始容量
        ArrayList<String> list1 = new ArrayList<>();
        // 指定初始容量为 20
        ArrayList<String> list2 = new ArrayList<>(20);
    }
}
(3)扩容机制

咱们添加元素的到ArrayList中去的时候,其实是把元素添加到ArrayList的成员变量elementData 数组中去,这里暮辰多一句嘴,既然是数组,容量就是固定的吧,比方说我们初始化创建一个数组对象,容量必须是固定的,那么我们ArrayList理论上可以无限添加元素(实际受到JVM堆内存大小影响),和数组容量是固定的产生了矛盾啊?是的,这时候就出现了一个扩容机制来解决这个矛盾,“用多少扩容多少”这里扩容多少和用多少不是划等号的,扩容机制是在一个合理范围内扩容,不是添加一次元素就扩容一次。当元素超出了数组长度的时候,才会扩容。

扩容核心方法:

  • calculateCapacity 方法:此方法用于计算所需的最小容量。若 ArrayList 是通过无参构造函数创建的,就会返回默认容量(10)和所需最小容量中的较大值。
  • ensureExplicitCapacity 方法:该方法会检查所需的最小容量是否超过当前数组的长度,若超过则调用 grow 方法进行扩容。
  • grow 方法:这是扩容的核心方法,它会先计算新容量(旧容量的 1.5 倍),若新容量小于所需的最小容量,就将新容量设为最小容量。若新容量超过最大数组大小,会调用 hugeCapacity 方法处理。最后,使用 Arrays.copyOf 方法创建新数组并复制元素。
  • hugeCapacity 方法:用于处理大容量需求,若所需容量超过最大数组大小,会返回 Integer.MAX_VALUE 或 MAX_ARRAY_SIZE。

2.1.2 ArrayList的增删改查

先来一段ArrayList的增删改查,有基础的朋友可跳过

		//创建一个集合
        ArrayList<String> list = new ArrayList<>();
        //添加元素
        list.add("暮辰");
        list.add("热爱");
        list.add("JAVA");

        // 遍历集合 for 循环
        for (int i = 0; i < list.size(); i++) {
            String s = list.get(i);
            System.out.println(s);
        }
        // 遍历集合 for each
        for (String s : list) {
            System.out.println(s);
        }

        // 删除元素
        list.remove(1);
        // 遍历集合
        for (String s : list) {
            System.out.println(s);
        }

        // 修改元素
        list.set(1, "暮辰777");
        // 遍历集合
        for (String s : list) {
            System.out.println(s);
        }

2.1.3 ArrayList的优劣势分析

(1)优势:随机访问效率高

ArrayList 实现了 RandomAccess 接口,支持快速的随机访问。由于它的底层是数组结构,通过索引可以直接定位到对应的元素,访问时间复杂度为 O(1)。以下是示例代码:

import java.util.ArrayList;
import java.util.List;

public class ArrayListRandomAccess {
    public static void main(String[] args) {
        List<Integer> list = new ArrayList<>();
        for (int i = 0; i < 100; i++) {
            list.add(i);
        }
        // 随机访问第 50 个元素
        int element = list.get(50);
        System.out.println("第 50 个元素是: " + element);
    }
}
(2)优势:实现简单、便于理解和使用

ArrayList 的实现原理基于数组,结构简单,易于开发者理解和使用。它提供了丰富的方法,如 add、remove、get 等,方便对元素进行操作。

(3)优势:可自动扩容

当 ArrayList 中的元素数量超过其当前容量时,它会自动进行扩容操作,开发者无需手动管理数组的大小。扩容机制通常是将原数组容量扩大为原来的 1.5 倍,保证了元素的添加操作可以顺利进行。

(4)劣势:插入和删除操作效率低

在 ArrayList 中进行插入和删除操作时,尤其是在数组中间位置进行操作,需要移动大量的元素,时间复杂度为
O(n)。因为插入或删除一个元素后,后续的元素都需要向前或向后移动。示例代码如下:

import java.util.ArrayList;
import java.util.List;

public class ArrayListInsertDelete {
    public static void main(String[] args) {
        List<Integer> list = new ArrayList<>();
        for (int i = 0; i < 10; i++) {
            list.add(i);
        }
        // 在索引为 2 的位置插入元素
        list.add(2, 100);
        // 删除索引为 5 的元素
        list.remove(5);
    }
}
(5)劣势:占用额外的内存空间

ArrayList 为了支持自动扩容,会预先分配一定的内存空间。当元素数量较少时,会存在一定的内存浪费。而且在扩容过程中,需要创建新的数组并将原数组的元素复制到新数组中,这也会占用额外的内存和时间。

(6)劣势:线程不安全

ArrayList 不是线程安全的类,在多线程环境下,如果多个线程同时对 ArrayList 进行读写操作,可能会导致数据不一致或抛出 ConcurrentModificationException 异常。

2.2 LinkedList

简约版源码如下:

package java.util;

import java.util.AbstractSequentialList;
import java.util.Collection;
import java.util.Deque;
import java.util.Iterator;
import java.util.List;
import java.util.ListIterator;
import java.util.NoSuchElementException;

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

    // 头节点
    transient Node<E> first;

    // 尾节点
    transient Node<E> last;

    // 内部类 Node,代表链表中的节点
    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;
        }
    }
    // ... 其他方法
}

2.2.1 LinkedList的源码浅析

(1)核心成员变量属性
变量名作用
size用于记录链表中元素的数量
first指向链表的头节点
last指向链表的尾节点
Node是一个静态内部类,代表链表中的一个节点,包含三个属性:item 存储节点的数据,next 指向下一个节点,prev 指向前一个节点
(2)创建流程

LinkedList由于是链表结构,因此创建和ArrayList不全相同,代码示例如下:

//使用的是LinkedList的无参构造函数
LinkedList<String> linkedList = new LinkedList<>();

LinkedList不像ArrayList需要扩容,真正意义上的无限添加元素(但还是取决于JVM的堆内存)

2.2.2 LinkedList的增删改查

同时先来一段LinkedList的增删改查,和ArrayList几乎没什么差别,有基础的朋友可跳过

  //创建一个集合
        LinkedList<String> list = new LinkedList<>();
        //添加元素
        list.add("暮辰");
        list.add("热爱");
        list.add("JAVA");

        // 遍历集合 for 循环
        for (int i = 0; i < list.size(); i++) {
            String s = list.get(i);
            System.out.println(s);
        }
        // 遍历集合 for each
        for (String s : list) {
            System.out.println(s);
        }

        // 删除元素
        list.remove(1);
        // 遍历集合
        for (String s : list) {
            System.out.println(s);
        }

        // 修改元素
        list.set(1, "暮辰777");
        // 遍历集合
        for (String s : list) {
            System.out.println(s);
        }

2.2.3 LinkedList的优劣势分析

(1)优势:插入和删除效率高

在链表的头部、尾部或已知节点附近进行插入和删除操作时,LinkedList 的时间复杂度为 O(1)。因为链表的插入和删除操作只需修改相邻节点的指针,无需像数组那样移动大量元素。例如,在链表尾部添加元素:

import java.util.LinkedList;

public class LinkedListInsertExample {
    public static void main(String[] args) {
        LinkedList<Integer> list = new LinkedList<>();
        // 在尾部添加元素,时间复杂度 O(1)
        list.add(1); 
    }
}
(2)优势:内存灵活使用

LinkedList 不需要像数组那样预先分配连续的内存空间,它的节点可以分散存储在内存中。这使得它在处理大量元素或者元素数量动态变化的场景下,内存使用更加灵活。

(3)优势:实现了 Deque 接口

LinkedList 实现了 Deque 接口,这意味着它可以作为双端队列使用,支持在队列的两端进行高效的插入和删除操作,可用于实现栈、队列等数据结构。例如,将 LinkedList 作为栈使用:

import java.util.LinkedList;

public class LinkedListAsStack {
    public static void main(String[] args) {
        LinkedList<Integer> stack = new LinkedList<>();
        // 入栈操作
        stack.push(1); 
        // 出栈操作
        int element = stack.pop(); 
    }
}
(4)劣势:随机访问效率低

LinkedList 不支持高效的随机访问,若要访问链表中指定位置的元素,需要从头节点或尾节点开始遍历链表,平均时间复杂度为
O(n)。例如,访问链表中第 n 个元素:

import java.util.LinkedList;

public class LinkedListRandomAccess {
    public static void main(String[] args) {
        LinkedList<Integer> list = new LinkedList<>();
        for (int i = 0; i < 100; i++) {
            list.add(i);
        }
        // 访问第 50 个元素,需要遍历链表,时间复杂度 O(n)
        int element = list.get(50); 
    }
}
(5)占用更多内存

由于 LinkedList 的每个节点都需要额外的指针来指向前一个节点和后一个节点,因此相比于数组,它会占用更多的内存空间。特别是在存储大量元素时,这种额外的内存开销会更加明显。

(6)遍历效率相对较低

虽然 LinkedList 可以使用迭代器进行遍历,但由于节点在内存中不是连续存储的,会导致缓存命中率较低,从而影响遍历效率。而数组在内存中是连续存储的,缓存命中率较高,遍历效率相对较高。

2.3 List遍历删除的几种方式

在 Java 里,对List进行遍历并删除元素时,若操作不当,容易引发ConcurrentModificationException异常。

下面介绍四种方式优雅的遍历删除方式

(1)使用Java8的removeIf方法 (推荐)

Java 8 为Collection接口引入了removeIf方法,它接收一个Predicate参数,能根据条件删除元素。这种方式代码简洁,且内部处理了并发修改的问题。

import java.util.ArrayList;
import java.util.List;

public class RemoveIfExample {
    public static void main(String[] args) {
        List<String> list = new ArrayList<>();
        list.add("apple");
        list.add("banana");
        list.add("cherry");

        list.removeIf(element -> element.equals("banana"));
        System.out.println(list);
    }
}

(2)使用Iterator迭代器

Iterator提供了安全的遍历和删除元素的方法。在遍历List时,借助Iterator的remove()方法可以安全地删除当前元素,不会引发ConcurrentModificationException异常。

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

public class IteratorRemoveExample {
    public static void main(String[] args) {
        List<String> list = new ArrayList<>();
        list.add("apple");
        list.add("banana");
        list.add("cherry");

        Iterator<String> iterator = list.iterator();
        while (iterator.hasNext()) {
            String element = iterator.next();
            if (element.equals("banana")) {
                iterator.remove();
            }
        }
        System.out.println(list);
    }
}

(3) 使用ListIterator迭代器

ListIterator是Iterator的子接口,它在Iterator的基础上增加了向前遍历、添加元素等功能。同样,使用ListIterator的remove()方法也能安全地删除元素。

import java.util.ArrayList;
import java.util.List;
import java.util.ListIterator;

public class ListIteratorRemoveExample {
    public static void main(String[] args) {
        List<String> list = new ArrayList<>();
        list.add("apple");
        list.add("banana");
        list.add("cherry");

        ListIterator<String> listIterator = list.listIterator();
        while (listIterator.hasNext()) {
            String element = listIterator.next();
            if (element.equals("banana")) {
                listIterator.remove();
            }
        }
        System.out.println(list);
    }
}

(4)反向遍历for循环

通过反向遍历for循环,从列表的最后一个元素开始往前遍历,这样在删除元素时不会影响后续元素的索引,从而避免ConcurrentModificationException异常。

import java.util.ArrayList;
import java.util.List;

public class ReverseForLoopRemoveExample {
    public static void main(String[] args) {
        List<String> list = new ArrayList<>();
        list.add("apple");
        list.add("banana");
        list.add("cherry");

        for (int i = list.size() - 1; i >= 0; i--) {
            String element = list.get(i);
            if (element.equals("banana")) {
                list.remove(i);
            }
        }
        System.out.println(list);
    }
}

2.4 List如何保证线程安全?

在 Java 中,许多常用的 List 实现类(如 ArrayList、LinkedList)并非线程安全的,在多线程环境下对它们进行并发操作可能会引发数据不一致、抛出 ConcurrentModificationException 异常等问题。以下是几种保证 List 线程安全的方式:

(1)使用 CopyOnWriteArrayList(推荐)

CopyOnWriteArrayList 是 Java 并发包(java.util.concurrent)中的一个类,它采用写时复制(Copy-On-Write)的策略。当进行写操作(如 add、remove 等)时,会先复制一份原数组,在新数组上完成操作,最后将新数组替换原数组;而读操作则不需要加锁。因此,在读写分离、读多写少的场景下,CopyOnWriteArrayList 具有较好的性能表现。

import java.util.Iterator;
import java.util.concurrent.CopyOnWriteArrayList;

public class CopyOnWriteArrayListExample {
    public static void main(String[] args) {
        CopyOnWriteArrayList<String> cowList = new CopyOnWriteArrayList<>();
        cowList.add("fig");
        cowList.add("grape");

        Iterator<String> iterator = cowList.iterator();
        while (iterator.hasNext()) {
            System.out.println(iterator.next());
        }
    }
}

(2)使用 Collections.synchronizedList(推荐)

Collections.synchronizedList 是一个工具方法,它可以将一个非线程安全的 List 转换为线程安全的 List。它通过在原 List 的基础上添加同步包装器,对所有操作进行同步处理。

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

public class SynchronizedListExample {
    public static void main(String[] args) {
        List<String> arrayList = new ArrayList<>();
        List<String> synchronizedList = Collections.synchronizedList(arrayList);
        synchronizedList.add("cherry");
        System.out.println(synchronizedList.get(0));
    }
}

(3)使用 Vector

Vector 是 Java 早期提供的线程安全的 List 实现类,它的大部分方法都使用 synchronized 关键字进行同步,从而保证在同一时刻只有一个线程可以访问这些方法,以此确保线程安全。不过,这种同步机制会带来一定的性能开销。

import java.util.Vector;

public class VectorExample {
    public static void main(String[] args) {
        Vector<String> vector = new Vector<>();
        vector.add("apple");
        vector.add("banana");
        System.out.println(vector.get(0));
    }
}

(4)手动加锁

通过手动使用 synchronized 关键字或者 ReentrantLock 来对 List 的操作进行同步,从而保证线程安全。以下是使用 synchronized 关键字的示例:

import java.util.ArrayList;
import java.util.List;

public class ManualLockListExample {
    private final List<String> list = new ArrayList<>();
    private final Object lock = new Object();

    public void addElement(String element) {
        synchronized (lock) {
            list.add(element);
        }
    }

    public String getElement(int index) {
        synchronized (lock) {
            return list.get(index);
        }
    }

    public static void main(String[] args) {
        ManualLockListExample example = new ManualLockListExample();
        example.addElement("honeydew");
        System.out.println(example.getElement(0));
    }
}

END

🙋‍♂️🙋‍♂️🙋‍♂️ 好啦,这就是今天要分享给大家的全部内容了,我们下期再见!🙋‍♂️🙋‍♂️🙋‍♂️
😘😘😘 如果你喜欢的话,就不要吝惜你的一键三连了~😘😘😘

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值