
为什么集合框架是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
🙋♂️🙋♂️🙋♂️ 好啦,这就是今天要分享给大家的全部内容了,我们下期再见!🙋♂️🙋♂️🙋♂️
😘😘😘 如果你喜欢的话,就不要吝惜你的一键三连了~😘😘😘
:集合框架之List&spm=1001.2101.3001.5002&articleId=147149784&d=1&t=3&u=b584526dc91a4a61b88d68345b19592e)
4158

被折叠的 条评论
为什么被折叠?



