在 Java 中,List
是一个非常常用的接口,提供了有序、可重复的元素集合。List
接口有多个实现类,每个实现类都有其特定的特性和适用场景。以下是 Java 中主要实现了 List
接口的类及其详细介绍。
1. 常见的 List
实现类
1.1 ArrayList
-
简介:
- 基于动态数组实现。
- 提供快速的随机访问(
get
和set
操作时间复杂度为 O(1))。 - 适合频繁的读取操作,但在插入和删除时可能较慢,尤其是在列表中间位置。
-
特点:
- 动态扩容:当数组容量不足时,会自动扩展(通常是当前容量的 1.5 倍)。
- 允许
null
元素。 - 非同步:不适合多线程环境下的并发访问,除非外部同步。
-
示例:
javaimport java.util.ArrayList; import java.util.List; public class ArrayListExample { public static void main(String[] args) { List<String> arrayList = new ArrayList<>(); arrayList.add("Apple"); arrayList.add("Banana"); arrayList.add("Cherry"); arrayList.add("Apple"); // 允许重复元素 System.out.println("ArrayList: " + arrayList); } }
输出:
ArrayList: [Apple, Banana, Cherry, Apple]
1.2 LinkedList
-
简介:
- 基于双向链表实现。
- 适合频繁的插入和删除操作,特别是在列表的开头和中间位置。
- 支持作为队列(FIFO)和双端队列(Deque)的实现。
-
特点:
- 插入和删除效率高:在已知位置的插入和删除操作时间复杂度为 O(1)。
- 随机访问较慢 :
get
和set
操作时间复杂度为 O(n),因为需要遍历链表。 - 允许
null
元素。
-
示例:
javaimport java.util.LinkedList; import java.util.List; public class LinkedListExample { public static void main(String[] args) { List<String> linkedList = new LinkedList<>(); linkedList.add("Apple"); linkedList.add("Banana"); linkedList.add("Cherry"); linkedList.add("Apple"); // 允许重复元素 System.out.println("LinkedList: " + linkedList); } }
输出:
LinkedList: [Apple, Banana, Cherry, Apple]
1.3 Vector
-
简介:
- 类似于
ArrayList
,也是基于动态数组实现。 - 是同步的,线程安全的。
- 适合需要线程安全的场景,但由于同步开销,性能略低于
ArrayList
。
- 类似于
-
特点:
- 同步:所有的公开方法都是同步的,适用于多线程环境。
- 动态扩容 :默认扩容策略与
ArrayList
略有不同(通常是当前容量的两倍)。 - 允许
null
元素。
-
示例:
javaimport java.util.List; import java.util.Vector; public class VectorExample { public static void main(String[] args) { List<String> vector = new Vector<>(); vector.add("Apple"); vector.add("Banana"); vector.add("Cherry"); vector.add("Apple"); // 允许重复元素 System.out.println("Vector: " + vector); } }
输出:
Vector: [Apple, Banana, Cherry, Apple]
1.4 Stack
-
简介:
- 继承自
Vector
,表示后进先出(LIFO)的堆栈。 - 提供了额外的方法,如
push
、pop
和peek
。
- 继承自
-
特点:
- 继承自
Vector
:具备线程安全性。 - LIFO 结构:适用于需要堆栈行为的场景。
- 已过时 :由于继承自
Vector
,不推荐使用。Java 推荐使用Deque
接口及其实现类(如ArrayDeque
)来实现堆栈。
- 继承自
-
示例:
javaimport java.util.Stack; public class StackExample { public static void main(String[] args) { Stack<String> stack = new Stack<>(); stack.push("Apple"); stack.push("Banana"); stack.push("Cherry"); System.out.println("Stack: " + stack); String top = stack.pop(); System.out.println("Popped element: " + top); System.out.println("Stack after pop: " + stack); } }
输出:
Stack: [Apple, Banana, Cherry] Popped element: Cherry Stack after pop: [Apple, Banana]
1.5 CopyOnWriteArrayList
-
简介:
- 线程安全的
List
实现,适用于读多写少的并发场景。 - 基于"写时复制"机制,每次修改操作都会创建一个新的数组副本。
- 线程安全的
-
特点:
- 线程安全:无需外部同步。
- 高效的迭代 :迭代时不会抛出
ConcurrentModificationException
,适合并发读取。 - 写操作开销大:每次写操作都会复制整个数组,适合读多写少的场景。
-
示例:
javaimport java.util.List; import java.util.concurrent.CopyOnWriteArrayList; public class CopyOnWriteArrayListExample { public static void main(String[] args) { List<String> cowList = new CopyOnWriteArrayList<>(); cowList.add("Apple"); cowList.add("Banana"); cowList.add("Cherry"); System.out.println("CopyOnWriteArrayList: " + cowList); } }
输出:
CopyOnWriteArrayList: [Apple, Banana, Cherry]
1.6 Stack
和 Deque
的对比
尽管 Stack
是 List
的一个实现类,但 Java 推荐使用 Deque
接口及其实现类(如 ArrayDeque
)来替代 Stack
,因为它们提供了更全面和灵活的堆栈和队列操作。
-
示例(使用
ArrayDeque
作为堆栈):javaimport java.util.ArrayDeque; import java.util.Deque; public class ArrayDequeExample { public static void main(String[] args) { Deque<String> stack = new ArrayDeque<>(); stack.push("Apple"); stack.push("Banana"); stack.push("Cherry"); System.out.println("ArrayDeque as Stack: " + stack); String top = stack.pop(); System.out.println("Popped element: " + top); System.out.println("Stack after pop: " + stack); } }
输出:
ArrayDeque as Stack: [Cherry, Banana, Apple] Popped element: Cherry Stack after pop: [Banana, Apple]
2. List
实现类的选择依据
选择合适的 List
实现类取决于具体的使用场景和性能需求。以下是一些常见的选择依据:
-
需要快速的随机访问:
- 选择 :
ArrayList
- 理由 :
ArrayList
基于动态数组,实现了 O(1) 时间复杂度的随机访问。
- 选择 :
-
需要频繁的插入和删除操作,特别是在列表中间:
- 选择 :
LinkedList
- 理由 :
LinkedList
基于双向链表,在已知位置的插入和删除操作时间复杂度为 O(1)。
- 选择 :
-
需要线程安全的列表:
- 选择 :
Vector
或CopyOnWriteArrayList
- 理由 :
Vector
:适用于需要同步访问且写操作较多的场景。CopyOnWriteArrayList
:适用于读多写少的并发场景。
- 选择 :
-
需要作为堆栈或队列使用:
- 选择 :
Deque
的实现类(如ArrayDeque
) - 理由 :
Deque
提供了比Stack
更灵活和高效的堆栈和队列操作。
- 选择 :
3. List
实现类的性能对比
3.1 ArrayList
与 LinkedList
特性 | ArrayList |
LinkedList |
---|---|---|
随机访问 | 快速(O(1)) | 较慢(O(n)) |
插入和删除 | 末尾插入和删除较快(O(1)),中间位置较慢(O(n)) | 快速(O(1)) |
内存使用 | 较低(仅存储元素) | 较高(存储元素及前后节点的引用) |
迭代性能 | 良好 | 良好 |
适用场景 | 需要频繁随机访问的场景 | 需要频繁插入和删除的场景 |
3.2 Vector
与 CopyOnWriteArrayList
特性 | Vector |
CopyOnWriteArrayList |
---|---|---|
同步性 | 方法级别同步 | 基于写时复制的同步机制 |
性能 | 较低(由于同步开销) | 适用于读多写少的场景 |
迭代安全性 | 在迭代期间修改会抛出 ConcurrentModificationException |
迭代期间修改不会影响当前迭代 |
适用场景 | 需要线程安全且写操作频繁的场景 | 需要线程安全且读操作频繁的场景 |
4. List
接口的常用方法
以下是 List
接口的一些常用方法及其说明:
方法 | 描述 |
---|---|
add(E e) |
在列表末尾添加一个元素。 |
add(int index, E element) |
在指定位置插入一个元素。 |
get(int index) |
返回指定位置的元素。 |
set(int index, E element) |
替换指定位置的元素。 |
remove(int index) |
移除指定位置的元素。 |
remove(Object o) |
移除第一个匹配的元素。 |
size() |
返回列表中的元素个数。 |
isEmpty() |
检查列表是否为空。 |
contains(Object o) |
检查列表是否包含指定元素。 |
clear() |
移除列表中的所有元素。 |
indexOf(Object o) |
返回首次出现指定元素的索引,如果不存在则返回 -1。 |
lastIndexOf(Object o) |
返回最后一次出现指定元素的索引,如果不存在则返回 -1。 |
subList(int fromIndex, int toIndex) |
返回列表的一个子视图,包含从 fromIndex (包含)到 toIndex (不包含)的元素。 |
4.1 示例:List
的常用操作
java
import java.util.List;
import java.util.ArrayList;
public class ListMethodsExample {
public static void main(String[] args) {
List<String> fruits = new ArrayList<>();
// 添加元素
fruits.add("Apple");
fruits.add("Banana");
fruits.add("Cherry");
fruits.add("Date");
fruits.add("Elderberry");
System.out.println("Initial List: " + fruits);
// 插入元素
fruits.add(2, "Coconut");
System.out.println("After inserting Coconut at index 2: " + fruits);
// 获取元素
String fruitAt3 = fruits.get(3);
System.out.println("Element at index 3: " + fruitAt3);
// 修改元素
fruits.set(1, "Blueberry");
System.out.println("After setting index 1 to Blueberry: " + fruits);
// 删除元素(按索引)
fruits.remove(4);
System.out.println("After removing element at index 4: " + fruits);
// 删除元素(按对象)
fruits.remove("Date");
System.out.println("After removing 'Date': " + fruits);
// 查询操作
System.out.println("List size: " + fruits.size());
System.out.println("List contains 'Apple': " + fruits.contains("Apple"));
System.out.println("List contains 'Date': " + fruits.contains("Date"));
// 获取子列表
List<String> subFruits = fruits.subList(1, 3);
System.out.println("Sublist from index 1 to 3: " + subFruits);
// 清空列表
fruits.clear();
System.out.println("After clearing, is list empty? " + fruits.isEmpty());
}
}
输出:
Initial List: [Apple, Banana, Cherry, Date, Elderberry]
After inserting Coconut at index 2: [Apple, Banana, Coconut, Cherry, Date, Elderberry]
Element at index 3: Cherry
After setting index 1 to Blueberry: [Apple, Blueberry, Coconut, Cherry, Date, Elderberry]
After removing element at index 4: [Apple, Blueberry, Coconut, Cherry, Elderberry]
After removing 'Date': [Apple, Blueberry, Coconut, Cherry, Elderberry]
List size: 5
List contains 'Apple': true
List contains 'Date': false
Sublist from index 1 to 3: [Blueberry, Coconut]
After clearing, is list empty? true
5. List
与其他集合的对比
5.1 List
vs Set
特性 | List |
Set |
---|---|---|
元素重复性 | 允许重复元素 | 不允许重复元素 |
元素顺序 | 保持元素的插入顺序,并支持按索引访问 | 一般不保证元素的顺序(某些实现如 LinkedHashSet 保持插入顺序,TreeSet 按自然顺序或自定义顺序排序) |
实现类 | ArrayList 、LinkedList 、Vector 等 |
HashSet 、LinkedHashSet 、TreeSet 等 |
适用场景 | 需要有序且可重复的元素集合 | 需要确保元素唯一性且不关心顺序(或有特定顺序要求) |
5.2 List
vs Queue
特性 | List |
Queue |
---|---|---|
元素访问方式 | 支持按索引访问元素 | 主要支持FIFO(先进先出)或其他特定的访问顺序 |
常用实现类 | ArrayList 、LinkedList |
LinkedList 、PriorityQueue 、ArrayDeque |
适用场景 | 需要按任意顺序访问和操作元素 | 需要按特定顺序处理元素,如任务调度、消息队列等 |
6. 线程安全的 List
实现
在多线程环境下,选择合适的 List
实现类和同步机制非常重要,以确保数据的一致性和避免并发问题。
6.1 使用 Collections.synchronizedList
通过 Collections.synchronizedList
方法可以将任何 List
实现类包装为线程安全的列表。
-
示例:
javaimport java.util.List; import java.util.ArrayList; import java.util.Collections; public class SynchronizedListExample { public static void main(String[] args) { List<String> syncList = Collections.synchronizedList(new ArrayList<>()); syncList.add("Apple"); syncList.add("Banana"); syncList.add("Cherry"); // 迭代时需要手动同步 synchronized (syncList) { for (String fruit : syncList) { System.out.println(fruit); } } } }
6.2 使用 CopyOnWriteArrayList
CopyOnWriteArrayList
是一个线程安全的 List
实现,适用于读多写少的并发场景。
-
示例:
javaimport java.util.List; import java.util.concurrent.CopyOnWriteArrayList; public class CopyOnWriteArrayListThreadSafeExample { public static void main(String[] args) { List<String> cowList = new CopyOnWriteArrayList<>(); cowList.add("Apple"); cowList.add("Banana"); cowList.add("Cherry"); // 迭代时无需额外同步 for (String fruit : cowList) { System.out.println(fruit); } } }
6.3 性能考虑
-
Collections.synchronizedList
:- 优点:适用于需要全面同步的场景。
- 缺点:需要手动同步迭代操作,可能导致性能瓶颈。
-
CopyOnWriteArrayList
:- 优点 :读操作无需同步,迭代时不会抛出
ConcurrentModificationException
。 - 缺点:写操作开销较大,每次写操作都会复制整个数组。
- 优点 :读操作无需同步,迭代时不会抛出
7. List
的泛型与类型安全
List
接口是泛型接口,可以指定存储的元素类型,从而在编译时提供类型安全,避免类型转换错误。
7.1 使用泛型的优势
-
类型安全:在编译时检查元素类型,避免运行时类型错误。
-
无需类型转换:获取元素时无需进行显式的类型转换。
-
示例:
javaimport java.util.List; import java.util.ArrayList; public class GenericListExample { public static void main(String[] args) { List<String> stringList = new ArrayList<>(); stringList.add("Hello"); stringList.add("World"); // stringList.add(123); // 编译错误 for (String str : stringList) { System.out.println(str.toUpperCase()); } } }
7.2 未使用泛型的风险
如果不使用泛型,List
将存储 Object
类型,可能导致运行时类型错误,需要进行类型转换。
-
示例:
javaimport java.util.List; import java.util.ArrayList; public class RawListExample { public static void main(String[] args) { List list = new ArrayList(); list.add("Hello"); list.add(123); // 允许添加不同类型的元素 for (Object obj : list) { String str = (String) obj; // 可能抛出 ClassCastException System.out.println(str.toUpperCase()); } } }
输出:
HELLO Exception in thread "main" java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String at RawListExample.main(RawListExample.java:10)
8. 综合示例:List
的各种操作
以下是一个综合示例,展示了 List
的常用操作,包括添加、插入、删除、修改、查询和遍历等。
java
import java.util.List;
import java.util.ArrayList;
import java.util.Iterator;
public class ComprehensiveListExample {
public static void main(String[] args) {
// 创建一个 ArrayList
List<String> fruits = new ArrayList<>();
// 添加元素
fruits.add("Apple");
fruits.add("Banana");
fruits.add("Cherry");
fruits.add("Date");
fruits.add("Elderberry");
System.out.println("Initial List: " + fruits);
// 插入元素
fruits.add(2, "Coconut");
System.out.println("After inserting Coconut at index 2: " + fruits);
// 获取元素
String fruitAt3 = fruits.get(3);
System.out.println("Element at index 3: " + fruitAt3);
// 修改元素
fruits.set(1, "Blueberry");
System.out.println("After setting index 1 to Blueberry: " + fruits);
// 删除元素(按索引)
fruits.remove(4);
System.out.println("After removing element at index 4: " + fruits);
// 删除元素(按对象)
fruits.remove("Date");
System.out.println("After removing 'Date': " + fruits);
// 查询操作
System.out.println("List size: " + fruits.size());
System.out.println("List contains 'Apple': " + fruits.contains("Apple"));
System.out.println("List contains 'Date': " + fruits.contains("Date"));
// 获取子列表
List<String> subFruits = fruits.subList(1, 3);
System.out.println("Sublist from index 1 to 3: " + subFruits);
// 遍历列表(使用增强的 for 循环)
System.out.println("Iterating using enhanced for loop:");
for (String fruit : fruits) {
System.out.println(fruit);
}
// 遍历列表(使用 Iterator)
System.out.println("Iterating using Iterator:");
Iterator<String> iterator = fruits.iterator();
while (iterator.hasNext()) {
System.out.println(iterator.next());
}
// 使用 Lambda 表达式和 forEach 方法遍历
System.out.println("Iterating using forEach and Lambda:");
fruits.forEach(fruit -> System.out.println(fruit));
// 清空列表
fruits.clear();
System.out.println("After clearing, is list empty? " + fruits.isEmpty());
}
}
输出:
Initial List: [Apple, Banana, Cherry, Date, Elderberry]
After inserting Coconut at index 2: [Apple, Banana, Coconut, Cherry, Date, Elderberry]
Element at index 3: Cherry
After setting index 1 to Blueberry: [Apple, Blueberry, Coconut, Cherry, Date, Elderberry]
After removing element at index 4: [Apple, Blueberry, Coconut, Cherry, Elderberry]
After removing 'Date': [Apple, Blueberry, Coconut, Cherry, Elderberry]
List size: 5
List contains 'Apple': true
List contains 'Date': false
Sublist from index 1 to 3: [Blueberry, Coconut]
Iterating using enhanced for loop:
Apple
Blueberry
Coconut
Cherry
Elderberry
Iterating using Iterator:
Apple
Blueberry
Coconut
Cherry
Elderberry
Iterating using forEach and Lambda:
Apple
Blueberry
Coconut
Cherry
Elderberry
After clearing, is list empty? true
9. 性能优化与最佳实践
9.1 预设容量
在已知大致元素数量时,可以在创建 ArrayList
时预设初始容量,以减少扩容次数,提升性能。
-
示例:
javaList<String> largeList = new ArrayList<>(1000); // 预设初始容量为 1000
9.2 使用接口类型声明
在声明变量时,使用接口类型(如 List
)而不是具体实现类(如 ArrayList
),提高代码的灵活性和可维护性。
-
示例:
javaList<String> list = new ArrayList<>(); // 未来可以轻松切换为其他实现类,如 LinkedList list = new LinkedList<>();
9.3 避免频繁的自动装箱与拆箱
对于存储基本数据类型的 List
,使用其包装类(如 Integer
、Double
)时,应注意避免频繁的自动装箱与拆箱,以提升性能。
-
示例:
javaList<Integer> numbers = new ArrayList<>(); for (int i = 0; i < 1000; i++) { numbers.add(i); // 自动装箱 }
优化 :如果需要大量的数值数据处理,考虑使用
IntStream
或其他专用的数据结构。
10. 总结
List
接口在 Java 集合框架中占据重要地位,提供了有序、可重复的元素集合。通过了解不同 List
实现类的特性、性能和适用场景,可以在项目中选择最合适的实现类,从而优化代码的性能和可维护性。以下是关键要点:
-
选择合适的实现类:
ArrayList
:适用于需要快速随机访问和较少插入删除操作的场景。LinkedList
:适用于需要频繁插入和删除操作,尤其是在列表中间位置的场景。Vector
:适用于需要线程安全的场景,但由于同步开销,性能较低。CopyOnWriteArrayList
:适用于读多写少的并发场景。
-
使用泛型:提高类型安全,避免运行时类型错误。
-
遵循接口编程:使用接口类型声明变量,增强代码的灵活性和可维护性。
-
性能优化:根据具体需求预设容量,避免不必要的装箱与拆箱。