大家好,我是加洛斯。是一名全站工程师👨💻,这里是我的知识笔记与分享,旨在把复杂的东西讲明白。如果发现有误🔍,万分欢迎你帮我指出来!废话不多说,正文开始 👇
本文详细的讲述了List中的特性,接着深入了List的实现类ArrayList的基本知识,涵盖构造方法、常用API、特性以及注意事项,并配有详细的代码讲解。
一、List基础知识
在Java中,List接口是Java集合框架中的一部分,提供了一组有序的 、可重复的元素集合 ,它定义在java.util包下。
1.1 JDK21以前
在JDK21之前List接口继承自Collection接口,具有Collection接口的所有方法,同时还提供了一些额外的方法来处理有序集合中的元素,例如在其基础上添加了允许精确控制元素插入位置的操作。
java.util.Collection 接口是 Java 集合框架的根接口之一,它定义了一组通用的方法,用于操作集合中的元素。Collection 接口是 Java 集合框架中所有集合类的共同父接口。
1.3 JDK21及以后
在JDK21以及之后,List接口继承自SequencedCollection接口(它的父接口是Collection),它为所有定义了明确元素顺序 的集合提供了一个统一、顶层的抽象。核心目的是为了解决 Java 集合框架长期存在的一个问题:对于所有有顺序的集合,缺乏一个统一的、标准的 API 来操作它们的第一个和最后一个元素,以及进行反向遍历。
二、List的特性
2.1 有序性
List会按照元素插入的顺序 来维护它们。当你遍历一个List时,元素的出现顺序与你添加它们的顺序完全一致,这意味着你可以按照特定的顺序获取、插入、删除或替换元素。
java
List<String> list = new ArrayList<>();
list.add("Apple"); // 第1个添加
list.add("Banana"); // 第2个添加
list.add("Cherry"); // 第3个添加
// 遍历结果永远是: Apple, Banana, Cherry
list.forEach(System.out::println);
2.2 可重复性
与
Set不同,List允许存储完全相同的元素。你可以多次向同一个List中添加同一个对象(或equals()方法返回true的不同对象)。
java
List<String> list = new ArrayList<>();
list.add("Apple");
list.add("Apple"); // 完全允许,此时List中有两个"Apple"
System.out.println(list.size()); // 输出: 2
2.3 索引访问
这是
List区别于其他Collection的显著特征。每个元素都有一个精确的索引位置,索引从0开始。你可以通过索引精确地定位、获取、修改或删除元素。
java
List<String> list = new ArrayList<>(Arrays.asList("A", "B", "C"));
String element = list.get(1); // 获取: element = "B"
list.set(1, "X"); // 修改: 列表变为 ["A", "X", "C"]
list.remove(0); // 删除: 列表变为 ["X", "C"]
list.add(1, "Y"); // 插入: 列表变为 ["X", "Y", "C"]
2.4 允许null值
大多数
List的实现(如ArrayList、LinkedList)都允许存储null元素,甚至可以存储多个null。
java
List<String> list = new ArrayList<>();
list.add(null);
list.add("Apple");
list.add(null);
System.out.println(list); // 输出: [null, Apple, null]
- 注意 :虽然允许,但在实际开发中建议谨慎使用
null,因为它可能会导致遍历或处理时的NullPointerException。如果确实需要"空值"的概念,可以考虑使用Java 8的Optional或自定义的空对象。
三、ArrayList
我们在创建一个 List 对象的时候,经常会写以下范式List list = new ArrayList();谁也没见过 List list = new List(); 这种写法吧,这究竟是为什么呢?
在 Java 中,List 是一个接口,而不是一个具体的类。接口本身不能直接实例化为对象,因为它只是一组方法的声明,没有实际的方法实现。所以我们要想实现List,就需要一个实现类。
ArrayList是Java集合框架中的一个常用实现类,它的底层逻辑是通过数组实现的 ,它实现了 List 接口,提供了动态数组的功能。
java
public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable
3.1 三种构造方法
ArrayList的底层是数组,所以它的初始容量并不是无限的,如果不指定的话,其初始容量为10。 
java
public class ArrayListCreation {
public static void main(String[] args) {
// 方式1:默认创建初始容量为10
ArrayList<String> defaultList = new ArrayList<>();
// 方式2:指定容量创建
ArrayList<Integer> sizedList = new ArrayList<>(100);
// 方式3:从数组转换
List<String> fromArray = Arrays.asList("Java", "Python", "C++");
ArrayList<String> arrayList = new ArrayList<>(fromArray);
// Java 9+ 便捷方式
List<String> immutable = List.of("A", "B", "C");
ArrayList<String> list = new ArrayList<>(immutable);
}
}
3.2 核心特性
它除了具有List的四大特性:有序性、可重复性、索引访问、允许null值之外,同时还具有以下特性:
- 动态数组:
ArrayList是基于动态数组实现的,它允许存储和操作一个可变数量的元素。数组的容量会自动增长或减少,以适应元素的添加和删除操作,因此无需手动管理数组的大小。- 随机访问: 由于
ArrayList是基于数组实现,因此它支持随机访问。可以通过索引来直接访问列表中的元素。- 线程不安全:
ArrayList不是线程安全的,这意味着在多线程环境下,如果多个线程同时访问和修改同一个ArrayList实例,可能会导致数据不一致或异常。如果需要在多线程环境中使用,可以考虑使用Collections.synchronizedList()方法来获得一个线程安全的ArrayList。- 性能: 由于
ArrayList支持随机访问,因此对于需要频繁访问元素的场景非常高效。然而,在需要频繁插入或删除元素的情况下,性能可能较差,因为这涉及到数组元素的移动。- 动态扩容: 当 ArrayList 内部数组的容量不足以容纳新元素时,它会自动扩展数组的大小,通常是当前容量的一半。这可以避免频繁的数组重新分配操作,提高了性能。
3.3 动态扩容机制
ArrayList的扩容机制是它内部实现动态数组的关键部分,它允许 ArrayList 在需要时自动增加底层数组的容量,以适应添加元素的需求,从而避免频繁的数组重新分配操作,提高性能。以下是 ArrayList 的扩容机制的详细介绍:
- 初始容量: ArrayList 在创建时,通常会分配一个初始容量10个元素,即使列表为空。这是为了避免在列表中只有一个元素时就立即进行扩容操作。
- 添加元素: 当你向 ArrayList 中添加元素时,它会首先检查当前元素的数量是否达到了底层数组的容量。如果当前元素数量达到了底层数组的容量,就需要扩容。
- 扩容策略: 当需要扩容时,它会创建一个新的更大容量的数组,而新数组的大小通常是当前容量的1.5倍(可以通过增量因子来调整,默认为1.5)。将当前数组中的所有元素复制到新数组中。新数组取代了旧数组成为 ArrayList 的底层数组。
- 性能分析: 扩容操作是 ArrayList 中一个相对昂贵的操作,因为它需要将所有元素从旧数组复制到新数组。通常情况下,扩容的成本是 O(n) 的,其中 n 是当前 ArrayList 的大小。
四、ArrayList的常用API方法
4.1 添加元素
| 方法 | 描述 | 时间复杂度 | 示例 |
|---|---|---|---|
add(E e) |
尾部添加元素 | 均摊O(1) | list.add("Java"); |
add(int index, E element) |
指定位置插入 | O(n) | list.add(1, "Python"); |
addAll(Collection<? extends E> c) |
尾部添加集合 | O(n) | list.addAll(anotherList); |
addAll(int index, Collection<? extends E> c) |
指定位置插入集合 | O(n) | list.addAll(2, anotherList); |
java
// ========== add(E e) 尾部添加元素 ==========
ArrayList<String> list = new ArrayList<>();
list.add("Java");
list.add("Python");
System.out.println("尾部添加: " + list); // [Java, Python]
// ========== add(int index, E element) 指定位置插入 ==========
list = new ArrayList<>(Arrays.asList("Java", "Go"));
System.out.println("原列表: " + list); // [Java, Go]
list.add(1, "Python");
System.out.println("索引1插入Python: " + list); // [Java, Python, Go]
// ========== addAll(Collection<? extends E> c) 尾部添加集合 ==========
list = new ArrayList<>(Arrays.asList("Java", "Python"));
List<String> others = Arrays.asList("Go", "Rust");
list.addAll(others);
System.out.println("尾部添加集合: " + list); // [Java, Python, Go, Rust]
// ========== addAll(int index, Collection<? extends E> c)指定位置插入集合 ==========
list = new ArrayList<>(Arrays.asList("Java", "Python"));
List<String> mid = Arrays.asList("C++", "Go");
list.addAll(1, mid);
System.out.println("索引1插入集合: " + list); // [Java, C++, Go, Python]
4.2 删除元素
| 方法 | 描述 | 时间复杂度 | 示例 |
|---|---|---|---|
remove(int index) |
删除指定位置元素 | O(n) | list.remove(0); |
remove(Object o) |
删除第一个匹配元素 | O(n) | list.remove("Java"); |
removeAll(Collection<?> c) |
删除包含在指定集合中的元素 | O(n²) | list.removeAll(toRemove); |
removeIf(Predicate<? super E> filter) |
按条件删除 | O(n) | list.removeIf(s -> s.length() < 3); |
clear() |
清空所有元素 | O(n) | list.clear(); |
java
// ========== remove(int index) 删除指定位置元素 ==========
ArrayList<String> list = new ArrayList<>(Arrays.asList("A", "B", "C"));
System.out.println("原列表: " + list); // [A, B, C]
String removed = list.remove(1);
System.out.println("删除索引1: " + removed); // B
System.out.println("结果: " + list); // [A, C]
// ========== remove(Object o) 删除第一个匹配元素 ==========
list = new ArrayList<>(Arrays.asList("A", "B", "A", "C"));
System.out.println("原列表: " + list); // [A, B, A, C]
list.remove("A");
System.out.println("删除第一个A: " + list); // [B, A, C]
// ========== removeAll() 删除包含在指定集合中的元素 ==========
list = new ArrayList<>(Arrays.asList("A", "B", "C", "D"));
System.out.println("原列表: " + list); // [A, B, C, D]
list.removeAll(Arrays.asList("B", "D"));
System.out.println("删除B和D: " + list); // [A, C]
// ========== removeIf() 按条件删除 ==========
ArrayList<Integer> nums = new ArrayList<>(Arrays.asList(1, 5, 8, 12, 3));
System.out.println("原列表: " + nums); // [1, 5, 8, 12, 3]
nums.removeIf(n -> n > 5);
System.out.println("删除>5的数: " + nums); // [1, 5, 3]
// ========== clear() 清空所有元素 ==========
list = new ArrayList<>(Arrays.asList("X", "Y", "Z"));
System.out.println("原列表: " + list); // [X, Y, Z]
list.clear();
System.out.println("清空后: " + list); // []
4.3 查询元素
| 方法 | 描述 | 时间复杂度 | 示例 |
|---|---|---|---|
get(int index) |
获取指定位置元素 | O(1) | String s = list.get(0); |
indexOf(Object o) |
查找元素第一次出现位置 | O(n) | int index = list.indexOf("Java"); |
lastIndexOf(Object o) |
查找元素最后一次出现位置 | O(n) | int lastIndex = list.lastIndexOf("Java"); |
contains(Object o) |
是否包含指定元素 | O(n) | boolean has = list.contains("Java"); |
containsAll(Collection<?> c) |
是否包含集合中所有元素 | O(n×m) | list.containsAll(checkList); |
java
ArrayList<String> list = new ArrayList<>(Arrays.asList("Java", "Python", "Java", "Go", "Rust"));
// ========== get(int index) 获取指定位置元素 ==========
String element = list.get(1);
System.out.println("索引1的元素: " + element); // Python
// ========== indexOf(Object o) 查找元素第一次出现位置 ==========
int firstIndex = list.indexOf("Java");
System.out.println("Java第一次出现位置: " + firstIndex); // 0
// ========== lastIndexOf(Object o) 查找元素最后一次出现位置 ==========
int lastIndex = list.lastIndexOf("Java");
System.out.println("Java最后一次出现位置: " + lastIndex); // 2
// ========== contains(Object o) 是否包含指定元素 ==========
boolean hasGo = list.contains("Go");
boolean hasC = list.contains("C++");
System.out.println("是否包含Go: " + hasGo); // true
System.out.println("是否包含C++: " + hasC); // false
// ========== containsAll(Collection<?> c) 是否包含集合中所有元素 ==========
List<String> check1 = Arrays.asList("Java", "Python");
List<String> check2 = Arrays.asList("Java", "C++");
boolean hasAll1 = list.containsAll(check1);
boolean hasAll2 = list.containsAll(check2);
System.out.println("是否包含[Java, Python]: " + hasAll1); // true
System.out.println("是否包含[Java, C++]: " + hasAll2); // false
4.4 修改元素
| 方法 | 描述 | 时间复杂度 | 示例 |
|---|---|---|---|
set(int index, E element) |
替换指定位置元素 | O(1) | list.set(0, "NewValue"); |
retainAll(Collection<?> c) |
保留指定集合中的元素 | O(n²) | list.retainAll(keepList); |
replaceAll(UnaryOperator<E> op) |
对每个元素应用操作 | O(n) | list.replaceAll(String::toUpperCase); |
java
// ========== set(int index, E element) 替换指定位置元素 ==========
ArrayList<String> list = new ArrayList<>(Arrays.asList("Java", "Python", "Go"));
System.out.println("原列表: " + list); // [Java, Python, Go]
String old = list.set(1, "C++");
System.out.println("被替换的值: " + old); // Python
System.out.println("替换后: " + list); // [Java, C++, Go]
// ========== retainAll(Collection<?> c) 保留指定集合中的元素 ==========
list = new ArrayList<>(Arrays.asList("A", "B", "C", "D", "E"));
System.out.println("原列表: " + list); // [A, B, C, D, E]
List<String> keep = Arrays.asList("B", "D", "F");
list.retainAll(keep);
System.out.println("保留[B, D, F]后: " + list); // [B, D]
// ========== replaceAll(UnaryOperator<E> op) 对每个元素应用操作 ==========
ArrayList<Integer> numbers = new ArrayList<>(Arrays.asList(1, 2, 3, 4, 5));
System.out.println("原数字: " + numbers); // [1, 2, 3, 4, 5]
numbers.replaceAll(n -> n * 2);
System.out.println("每个数乘2: " + numbers); // [2, 4, 6, 8, 10]
ArrayList<String> words = new ArrayList<>(Arrays.asList("java", "python", "go"));
System.out.println("原单词: " + words); // [java, python, go]
words.replaceAll(String::toUpperCase);
System.out.println("转大写: " + words); // [JAVA, PYTHON, GO]
4.5 大小与容量
| 方法 | 描述 | 时间复杂度 | 示例 |
|---|---|---|---|
size() |
返回元素个数 | O(1) | int size = list.size(); |
isEmpty() |
判断是否为空 | O(1) | boolean empty = list.isEmpty(); |
trimToSize() |
将容量调整为当前大小 | O(1) | list.trimToSize(); |
ensureCapacity(int minCapacity) |
确保至少能容纳指定数量元素 | O(1) | list.ensureCapacity(100); |
java
// ========== size() 返回元素个数 ==========
ArrayList<String> list = new ArrayList<>(Arrays.asList("A", "B", "C"));
System.out.println("列表: " + list); // [A, B, C]
int size = list.size();
System.out.println("元素个数: " + size); // 3
// ========== isEmpty() 判断是否为空 ==========
System.out.println("是否为空: " + list.isEmpty()); // false
list.clear();
System.out.println("清空后是否为空: " + list.isEmpty()); // true
// ========== ensureCapacity() 将容量调整为当前大小 ==========
ArrayList<Integer> numbers = new ArrayList<>();
System.out.println("初始大小: " + numbers.size()); // 0
// 预分配容量,避免频繁扩容
numbers.ensureCapacity(1000);
for (int i = 1; i <= 100; i++) {
numbers.add(i);
}
System.out.println("添加100个元素后大小: " + numbers.size()); // 100
// ========== trimToSize() 确保至少能容纳指定数量元素 ==========
ArrayList<String> trimList = new ArrayList<>(100); // 初始容量100
trimList.add("X");
trimList.add("Y");
trimList.add("Z");
System.out.println("实际元素: " + trimList); // [X, Y, Z]
System.out.println("实际大小: " + trimList.size()); // 3
// 将容量缩减到与实际大小一致,节省内存
trimList.trimToSize();
System.out.println("容量已调整为3");
4.6 遍历与转换
| 方法 | 描述 | 时间复杂度 | 示例 |
|---|---|---|---|
iterator() |
返回迭代器 | O(1) | Iterator<String> it = list.iterator(); |
listIterator() |
返回列表迭代器 | O(1) | ListIterator<String> lit = list.listIterator(); |
forEach(Consumer<? super E> action) |
遍历每个元素 | O(n) | list.forEach(System.out::println); |
toArray() |
转换为数组 | O(n) | Object[] arr = list.toArray(); |
toArray(T[] a) |
转换为指定类型数组 | O(n) | String[] arr = list.toArray(new String[0]); |
subList(int fromIndex, int toIndex) |
获取子列表 | O(1) | List<String> sub = list.subList(1, 3); |
java
ArrayList<String> list = new ArrayList<>(Arrays.asList("Java", "Python", "Go", "Rust"));
System.out.println("原列表: " + list); // [Java, Python, Go, Rust]
// ========== iterator() 返回迭代器 ==========
Iterator<String> it = list.iterator();
while (it.hasNext()) {
System.out.print(it.next() + " "); // Java Python Go Rust
}
// ========== listIterator() 返回列表迭代器 ==========
ListIterator<String> lit = list.listIterator();
while (lit.hasNext()) { // 正向遍历
System.out.print(lit.next() + " "); // Java Python Go Rust
}
while (lit.hasPrevious()) { // 反向遍历
System.out.print(lit.previous() + " "); // Rust Go Python Java
}
// ========== forEach() 遍历每个元素 ==========
list.forEach(item -> System.out.print(item + " ")); // Java Python Go Rust
// ========== toArray() 转换为数组 ==========
Object[] objArray = list.toArray();
System.out.println("Object数组: " + Arrays.toString(objArray)); // [Java, Python, Go, Rust]
// ========== toArray(T[] a) ==========
String[] strArray = list.toArray(new String[0]);
System.out.println("String数组: " + Arrays.toString(strArray)); // [Java, Python, Go, Rust]
// --------------------------------------
// ========== subList() 获取子列表 ==========
List<String> subList = list.subList(1, 3);
System.out.println("子列表(1-3): " + subList); // [Python, Go]
// 注意:子列表与原列表共享数据
subList.set(0, "C++");
System.out.println("修改子列表后原列表: " + list); // [Java, C++, Go, Rust]
4.7 排序与比较
| 方法 | 描述 | 时间复杂度 | 示例 |
|---|---|---|---|
sort(Comparator<? super E> c) |
排序 | O(n log n) | list.sort(Comparator.naturalOrder()); |
equals(Object o) |
比较是否相等 | O(n) | boolean eq = list.equals(otherList); |
hashCode() |
返回哈希码 | O(n) | int hash = list.hashCode(); |
java
// ========== sort() 排序 ==========
ArrayList<Integer> numbers = new ArrayList<>(Arrays.asList(5, 2, 8, 1, 9));
System.out.println("原数字: " + numbers); // [5, 2, 8, 1, 9]
numbers.sort(Comparator.naturalOrder());
System.out.println("升序排序: " + numbers); // [1, 2, 5, 8, 9]
numbers.sort(Comparator.reverseOrder());
System.out.println("降序排序: " + numbers); // [9, 8, 5, 2, 1]
ArrayList<String> words = new ArrayList<>(Arrays.asList("banana", "apple", "cat"));
words.sort(Comparator.naturalOrder());
System.out.println("字符串排序: " + words); // [apple, banana, cat]
words.sort((a, b) -> a.length() - b.length());
System.out.println("按长度排序: " + words); // [cat, apple, banana]
// ========== equals() 比较是否相等 ==========
ArrayList<String> list1 = new ArrayList<>(Arrays.asList("A", "B", "C"));
ArrayList<String> list2 = new ArrayList<>(Arrays.asList("A", "B", "C"));
ArrayList<String> list3 = new ArrayList<>(Arrays.asList("A", "C", "B"));
System.out.println("list1: " + list1); // [A, B, C]
System.out.println("list2: " + list2); // [A, B, C]
System.out.println("list3: " + list3); // [A, C, B]
boolean eq1 = list1.equals(list2);
boolean eq2 = list1.equals(list3);
System.out.println("list1 等于 list2: " + eq1); // true
System.out.println("list1 等于 list3: " + eq2); // false
// ========== hashCode() 返回哈希码 ==========
int hash1 = list1.hashCode();
int hash2 = list2.hashCode();
int hash3 = list3.hashCode();
System.out.println("list1哈希码: " + hash1);
System.out.println("list2哈希码: " + hash2);
System.out.println("list3哈希码: " + hash3);
System.out.println("list1和list2哈希码相同: " + (hash1 == hash2)); // true
System.out.println("list1和list3哈希码相同: " + (hash1 == hash3)); // false
五、ArrayList的常见注意事项
5.1 Arrays.asList()返回的List不可改变大小
Arrays.asList() 返回的 List 并非完全不可变 ,而是固定大小的 List,其关键特征如下:
- 底层是固定数组 :返回的
List直接使用原始数组存储数据,数组长度固定,无法真正添加或删除元素 - 视图而非副本 :它是数组的一个
List视图,任何修改都会反映到原数组上
不支持的操作(结构性修改)
java
List<String> list = Arrays.asList("A", "B", "C");
// 以下操作会抛出 UnsupportedOperationException
list.add("D"); // 添加元素
list.remove(0); // 删除元素
list.clear(); // 清空列表
支持的操作
java
List<String> list = Arrays.asList("A", "B", "C");
// 以下操作可以正常工作
list.set(1, "X"); // 修改元素 - 成功
System.out.println(list.get(0)); // 访问元素
System.out.println(list.size()); // 获取大小
如果需要可变的List,用ArrayList包装
java
String[] fruits = {"Apple", "Banana", "Cherry"};
List<String> mutableList1 = new ArrayList<>(Arrays.asList(fruits));
mutableList1.add("Date");
mutableList1.remove(0);
System.out.println("可变List: " + mutableList1);
5.2 foreach遍历时不能修改
foreach 循环实际上是使用 Iterator(迭代器)的语法糖:
- 迭代器会维护一个
modCount(修改次数)计数器 - 每次调用
iterator.next()时,会检查集合的修改次数是否与迭代器期望的一致 - 直接调用
list.remove()会修改集合的modCount,但迭代器不知道这个变化 - 下次调用
next()时发现不一致,立即抛出ConcurrentModificationException
java
// 你写的代码
for (String item : list) {
if (item.equals("a")) {
list.remove(item);
}
}
// 编译后等价于
Iterator<String> iterator = list.iterator();
while (iterator.hasNext()) {
String item = iterator.next();
if (item.equals("a")) {
list.remove(item); // 通过集合直接修改
}
}
解决方案
方案1:使用 Iterator 的 remove 方法
java
Iterator<String> iterator = list.iterator();
while (iterator.hasNext()) {
String item = iterator.next();
if (item.equals("a")) {
iterator.remove(); // 通过迭代器删除,不会抛出异常
}
}
方案2:使用 for 循环倒序遍历
java
// 从后往前遍历,避免索引变化问题
for (int i = list.size() - 1; i >= 0; i--) {
if (list.get(i).equals("a")) {
list.remove(i);
}
}
方案3:使用 Java 8+ 的 removeIf
java
list.removeIf(item -> item.equals("a")); // 最简单,一行搞定
5.3 添加对象时候重写equals方法
java
// 创建第一个"Alice"
Person p1 = new Person("Alice");
list.add(p1);
// 创建第二个"Alice"
Person p2 = new Person("Alice");
// 问:list里有"Alice"吗?
list.contains(p2); // false ❌
- p1 和 p2 是两个不同的东西(就像双胞胎,长得一样但不是同一个人)
- 除非你告诉 Java:"只要名字一样,就认为是同一个人"
所以我们需要重写equals方法,当然现在很多开发时候都会使用Lombok注解中的@Data,这个会自动为我们重写equals方法
java
class Person {
String name;
// 告诉 Java:比较两个人时,只比较名字就行
@Override
public boolean equals(Object obj) {
Person p = (Person) obj;
return this.name.equals(p.name); // 名字一样就是同一个人
}
}