Java集合框架、Collection体系的单列集合
Java集合框架、Collection
1. 认识Java集合框架及结构
Java集合框架是用于存储和操作对象的容器体系,主要分为Collection和Map两大根接口。其中Collection接口用于存储单个元素的集合,Map接口用于存储键值对映射关系。
1.1 集合框架整体结构
Collection接口是所有单列集合的根接口,它有两个主要子接口:List(有序、可重复)和Set(无序、不可重复)。每个子接口有多个实现类,具体结构如下:
Collection
├─ List(有序、可重复)
│ ├─ ArrayList(动态数组实现,查询快、增删慢)
│ ├─ LinkedList(双向链表实现,增删快、查询慢)
│ └─ Vector(线程安全的动态数组,已被ArrayList替代)
│
└─ Set(无序、不可重复)├─ HashSet(哈希表实现,无序、唯一,查询效率高)├─ LinkedHashSet(哈希表+双向链表实现,**有序(插入顺序)、唯一**,继承自HashSet)└─ TreeSet(红黑树实现,可排序、唯一,基于比较器或自然排序)
关键说明:
- 集合框架中的所有实现类均支持泛型,可指定存储元素的类型,避免类型转换异常。
1.2 集合框架的核心作用
- 统一存储:提供标准化的容器接口,避免重复开发存储逻辑。
- 简化操作:封装了增删改查等常用方法(如
add()
、remove()
、size()
),无需手动实现数据结构细节。 - 灵活扩展:不同实现类适用于不同场景(如ArrayList适合查询,LinkedList适合增删,LinkedHashSet兼顾去重和顺序)。
2. Collection的两大常用集合体系及各个系列集合的特点
Collection接口下的两大核心体系为List系列和Set系列,它们的特点及主要实现类对比如下:
2.1 List系列集合(有序、可重复)
- 核心特点:元素有序(存储顺序=遍历顺序)、可重复(允许包含相同元素)、有索引(可通过下标操作元素)。
- 主要实现类:
- ArrayList:基于动态数组实现,查询效率高(通过索引直接访问),增删效率低(需移动元素)。
- LinkedList:基于双向链表实现,增删效率高(仅需修改指针),查询效率低(需从头/尾遍历)。
2.2 Set系列集合(无序、不可重复)
- 核心特点:元素无序(默认存储顺序与遍历顺序无关)、不可重复(通过特定机制保证元素唯一性)、无索引(不能通过下标操作元素)。
- 主要实现类:
- HashSet:基于哈希表实现,无序、唯一,查询/插入/删除效率高(时间复杂度O(1))。
- LinkedHashSet:继承自HashSet,通过双向链表维护插入顺序,因此遍历顺序与插入顺序一致,同时保留HashSet的去重特性。
- TreeSet:基于红黑树实现,可对元素进行排序(自然排序或定制排序),唯一,查询效率O(log n)。
3. Collection的常用方法
Collection接口定义了所有单列集合的通用方法,以下是最常用的10个方法及简单案例:
方法名 | 作用 | 代码示例(以ArrayList为例) |
---|---|---|
boolean add(E e) | 添加元素到集合末尾 | list.add("苹果"); // 添加"苹果"到集合 |
boolean remove(Object o) | 删除指定元素 | list.remove("苹果"); // 删除"苹果" |
void clear() | 清空集合所有元素 | list.clear(); // 集合变为空 |
boolean contains(Object o) | 判断集合是否包含指定元素 | boolean hasApple = list.contains("苹果"); |
int size() | 返回集合元素个数 | int count = list.size(); // 获取元素数量 |
boolean isEmpty() | 判断集合是否为空 | boolean empty = list.isEmpty(); // 空返回true |
Object[] toArray() | 将集合转换为数组 | Object[] arr = list.toArray(); // 集合转数组 |
boolean addAll(Collection<? extends E> c) | 添加另一个集合的所有元素到当前集合 | list.addAll(anotherList); // 合并两个集合 |
boolean removeAll(Collection<?> c) | 删除当前集合中与另一个集合共有的元素 | list.removeAll(anotherList); // 保留差集 |
boolean retainAll(Collection<?> c) | 保留当前集合中与另一个集合共有的元素 | list.retainAll(anotherList); // 保留交集 |
4. List系列集合的特点和特有方法
List系列集合因有索引,提供了更多基于索引的操作方法,核心特点及特有方法如下:
4.1 List系列核心特点
- 有序:元素存储顺序与遍历顺序一致(如插入顺序为A→B→C,遍历结果也为A→B→C)。
- 可重复:允许添加相同元素(如
list.add("苹果"); list.add("苹果");
会存储两个"苹果")。 - 有索引:可通过
get(int index)
直接访问指定位置元素,类似数组。
4.2 List特有方法(以ArrayList为例)
方法名 | 作用 | 代码示例 |
---|---|---|
void add(int index, E element) | 在指定索引位置插入元素 | list.add(1, "香蕉"); // 在索引1处插入"香蕉" |
E remove(int index) | 删除指定索引位置的元素并返回该元素 | String removed = list.remove(1); // 删除索引1元素 |
E set(int index, E element) | 修改指定索引位置的元素并返回旧元素 | String old = list.set(1, "橙子"); // 替换索引1元素 |
E get(int index) | 返回指定索引位置的元素 | String fruit = list.get(0); // 获取索引0元素 |
5. ArrayList的四种遍历方式及删除元素时的并发修改异常
ArrayList是List接口的最常用实现类,支持多种遍历方式,但在遍历过程中删除元素可能导致并发修改异常(ConcurrentModificationException)。以下详细介绍四种遍历方式及删除元素的注意事项。
5.1 四种遍历方式
以ArrayList<String> list = new ArrayList<>(Arrays.asList("A", "B", "C", "D"))
为例
5.1.1 普通for循环(基于索引)
// 正序遍历
for (int i = 0; i < list.size(); i++) {String element = list.get(i); // 通过索引获取元素System.out.println(element); // 输出:A B C D
}
5.1.2 迭代器(Iterator)
Iterator<String> iterator = list.iterator();
while (iterator.hasNext()) { // 判断是否有下一个元素String element = iterator.next(); // 获取下一个元素System.out.println(element); // 输出:A B C D
}
5.1.3 增强for循环(for-each)
for (String element : list) { // 简化遍历语法,无需索引System.out.println(element); // 输出:A B C D
}
5.1.4 Lambda表达式(Java 8+)
list.forEach(element -> System.out.println(element)); // 输出:A B C D
// 或简化为方法引用
list.forEach(System.out::println);
5.2 删除元素时的并发修改异常及解决方案
并发修改异常原因:集合在遍历过程中,若通过集合自身的remove()
方法修改元素数量(如删除),会导致迭代器的修改次数标记与集合实际修改次数不一致,触发异常。
5.2.1 有索引集合(如ArrayList)的删除解决方案
场景:删除集合中所有" B "元素(原集合:[“A”, “B”, “B”, “C”])。
- 错误方式:正序遍历+集合
remove()
for (int i = 0; i < list.size(); i++) {if ("B".equals(list.get(i))) {list.remove(i); // 删除后集合长度变化,导致后续元素漏判}}// 结果:删除第一个"B"后,索引i=1的元素变为原索引2的"B",但i自增为2,漏删第二个"B"
- 正确方式1:正序遍历+
i--
(删除后回退索引)
for (int i = 0; i < list.size(); i++) {if ("B".equals(list.get(i))) {list.remove(i); i--; // 删除后索引回退,避免漏判}}// 结果:成功删除所有"B",集合变为["A", "C"]
- 正确方式2:倒序遍历(从后往前删,不影响前面元素索引)
for (int i = list.size() - 1; i >= 0; i--) {if ("B".equals(list.get(i))) {list.remove(i); // 后面元素删除不影响前面元素索引}}// 结果:成功删除所有"B",集合变为["A", "C"]
5.2.2 无索引集合(如Set)的删除解决方案
场景:删除HashSet中所有" B "元素(原集合:[“A”, “B”, “C”])。
- 错误方式:增强for循环+集合
remove()
for (String element : set) {if ("B".equals(element)) {set.remove(element); // 触发ConcurrentModificationException}}
- 正确方式:迭代器的
remove()
方法(唯一解决方案)
Iterator<String> iterator = set.iterator();while (iterator.hasNext()) {String element = iterator.next();if ("B".equals(element)) {iterator.remove(); // 使用迭代器自带的删除方法,不会触发异常}}// 结果:成功删除"B",集合变为["A", "C"]
5.2.3 增强for和Lambda遍历的局限性
增强for循环(for-each)和Lambda表达式遍历仅适合读取元素,不适合同时增删元素。因为它们底层依赖迭代器,但不允许通过集合自身方法修改元素数量,否则必然触发并发修改异常。
6. ArrayList和LinkedList的区别
ArrayList和LinkedList是List接口的两个核心实现类,它们的底层结构、特点和应用场景有显著差异。
6.1 核心区别对比
对比维度 | ArrayList | LinkedList |
---|---|---|
底层结构 | 动态数组(可扩容的数组) | 双向链表(每个节点包含前后指针) |
查询效率 | 高(通过索引直接访问,O(1)) | 低(需从头/尾遍历,O(n)) |
增删效率 | 中间增删低(需移动元素,O(n)) | 中间增删高(仅需修改指针,O(1)) |
内存占用 | 低(仅存储元素值) | 高(每个元素需额外存储前后指针) |
特有方法 | 无(依赖List接口方法) | addFirst() /addLast() /removeFirst() /removeLast() 等首尾操作方法 |
6.2 LinkedList的首尾操作特有方法
LinkedList因链表结构,新增了直接操作首尾元素的方法,效率极高(O(1)):
LinkedList<String> linkedList = new LinkedList<>();
linkedList.addFirst("头部元素"); // 添加到链表头部
linkedList.addLast("尾部元素"); // 添加到链表尾部
String first = linkedList.getFirst(); // 获取头部元素
String last = linkedList.getLast(); // 获取尾部元素
String removedFirst = linkedList.removeFirst(); // 删除并返回头部元素
String removedLast = linkedList.removeLast(); // 删除并返回尾部元素
6.3 LinkedList的应用场景
6.3.1 队列(FIFO:先进先出)
队列是一种特殊的线性表,仅允许在队尾添加元素、在队头删除元素。LinkedList的addLast()
(入队)和removeFirst()
(出队)方法完美适配队列操作:
// 模拟队列:添加元素到尾部,删除元素从头部
LinkedList<String> queue = new LinkedList<>();
queue.addLast("任务1"); // 入队
queue.addLast("任务2");
String task = queue.removeFirst(); // 出队,返回"任务1"
6.3.2 栈(LIFO:后进先出)
栈是一种特殊的线性表,仅允许在栈顶添加和删除元素。LinkedList的addLast()
(入栈)和removeLast()
(出栈)方法可模拟栈操作:
// 模拟栈:添加和删除元素都在尾部(栈顶)
LinkedList<String> stack = new LinkedList<>();
stack.addLast("元素1"); // 入栈
stack.addLast("元素2");
String top = stack.removeLast(); // 出栈,返回"元素2"(最后入栈的元素先出)
7. Set系列集合的特点(含LinkedHashSet)
Set系列集合的核心特点是无序、不可重复,但不同实现类的"无序"定义不同(HashSet完全无序,LinkedHashSet保留插入顺序,TreeSet按排序顺序)。以下详细介绍HashSet、LinkedHashSet、TreeSet的特点及实现原理。
7.1 Set系列集合的整体特点
实现类 | 底层结构 | 有序性 | 去重机制 | 线程安全 |
---|---|---|---|---|
HashSet | 哈希表(数组+链表/红黑树) | 完全无序(存储顺序≠遍历顺序) | 基于hashCode()和equals() | 否 |
LinkedHashSet | 哈希表+双向链表 | 有序(插入顺序=遍历顺序) | 同HashSet(继承自HashSet) | 否 |
TreeSet | 红黑树(平衡二叉树) | 有序(自然排序/定制排序) | 基于比较器(Comparable/Comparator) | 否 |
7.2 LinkedHashSet的特点与实现原理
7.2.1 核心特点
- 有序性:通过内部维护的双向链表记录元素的插入顺序,遍历元素时严格按照插入顺序输出。
- 唯一性:继承自HashSet,因此保留HashSet的去重机制(基于hashCode()和equals())。
- 性能:插入/删除效率略低于HashSet(因需额外维护链表),但遍历效率高于HashSet(无需随机访问哈希表)。
7.2.2 实现原理
LinkedHashSet继承自HashSet,其构造方法会调用HashSet的一个特殊构造方法,创建一个LinkedHashMap实例(HashSet底层实际是HashMap,LinkedHashSet底层实际是LinkedHashMap):
// LinkedHashSet的构造方法
public LinkedHashSet() {super(16, 0.75f, true); // 调用HashSet的构造方法,第三个参数为true表示使用LinkedHashMap
}// HashSet中对应的构造方法
HashSet(int initialCapacity, float loadFactor, boolean dummy) {map = new LinkedHashMap<>(initialCapacity, loadFactor); // 底层创建LinkedHashMap
}
LinkedHashMap在HashMap的基础上,为每个元素增加了before
和after
指针,形成双向链表,从而记录插入顺序。
7.2.3 代码案例:LinkedHashSet的有序性和去重性
import java.util.LinkedHashSet;public class LinkedHashSetDemo {public static void main(String[] args) {LinkedHashSet<String> linkedHashSet = new LinkedHashSet<>();linkedHashSet.add("C");linkedHashSet.add("A");linkedHashSet.add("B");linkedHashSet.add("A"); // 重复元素,会被去重// 遍历集合,观察顺序for (String element : linkedHashSet) {System.out.print(element + " "); // 输出:C A B(与插入顺序一致,且"A"只出现一次)}}
}
7.3 HashSet的实现原理和去重机制
7.3.1 JDK8前后的实现原理对比
版本 | 底层结构(数组+链表/红黑树) | 链表转红黑树阈值 |
---|---|---|
JDK8之前 | 数组+单向链表(元素哈希冲突时,在数组位置后追加链表) | 无(始终用链表) |
JDK8及之后 | 数组+链表+红黑树(当链表长度>8且数组容量≥64时,链表转为红黑树) | 链表长度>8 |
7.3.2 去重机制
HashSet通过以下步骤保证元素唯一性:
- 调用元素的
hashCode()
方法计算哈希值,确定在哈希表中的存储位置。 - 若该位置为空,直接存入元素。
- 若该位置不为空,调用
equals()
方法比较元素内容:- 若
equals()
返回true(内容相同),视为重复元素,不存入。 - 若
equals()
返回false(内容不同但哈希冲突),存入链表/红黑树。
- 若
注意:若两个对象内容相同但hashCode()
返回不同值,HashSet会视为不同元素(未去重)。
解决方案:重写hashCode()
和equals()
方法,使内容相同的对象返回相同哈希值且equals()
返回true。
代码案例:重写hashCode()
和equals()
实现对象去重
import java.util.HashSet;class Student {private String name;private int age;public Student(String name, int age) {this.name = name;this.age = age;}// 重写equals():比较对象内容(name和age)@Overridepublic boolean equals(Object o) {if (this == o) return true;if (o == null || getClass() != o.getClass()) return false;Student student = (Student) o;return age == student.age && name.equals(student.name);}// 重写hashCode():基于name和age计算哈希值@Overridepublic int hashCode() {return 31 * name.hashCode() + age; // 31是质数,减少哈希冲突}
}public class HashSetDemo {public static void main(String[] args) {HashSet<Student> students = new HashSet<>();students.add(new Student("张三", 18));students.add(new Student("张三", 18)); // 内容相同,会被去重System.out.println(students.size()); // 输出:1(去重成功)}
}
7.4 TreeSet的排序机制
TreeSet是唯一可排序的Set实现类,排序方式分为自然排序和定制排序,核心依赖比较器接口。
7.4.1 自然排序(实现Comparable接口)
让元素类实现Comparable
接口,重写compareTo()
方法定义排序规则:
import java.util.TreeSet;class Person implements Comparable<Person> {private String name;private int age;public Person(String name, int age) {this.name = name;this.age = age;}// 重写compareTo():按年龄升序排序@Overridepublic int compareTo(Person o) {return this.age - o.age; // 返回负数:当前对象在前;正数:当前对象在后;0:视为重复元素}
}public class TreeSetNaturalSort {public static void main(String[] args) {TreeSet<Person> treeSet = new TreeSet<>();treeSet.add(new Person("张三", 20));treeSet.add(new Person("李四", 18));treeSet.add(new Person("王五", 22));// 遍历集合,按年龄升序输出for (Person p : treeSet) {System.out.println(p.age); // 输出:18 20 22(自然排序结果)}}
}
7.4.2 定制排序(使用Comparator比较器)
创建TreeSet时传入Comparator
接口实现类,自定义排序规则(无需修改元素类):
import java.util.Comparator;
import java.util.TreeSet;public class TreeSetCustomSort {public static void main(String[] args) {// 创建TreeSet时传入Comparator比较器,按年龄降序排序TreeSet<Person> treeSet = new TreeSet<>(new Comparator<Person>() {@Overridepublic int compare(Person o1, Person o2) {return o2.age - o1.age; // 降序:后一个对象年龄 - 前一个对象年龄}});treeSet.add(new Person("张三", 20));treeSet.add(new Person("李四", 18));treeSet.add(new Person("王五", 22));for (Person p : treeSet) {System.out.println(p.age); // 输出:22 20 18(定制排序结果)}}
}