目录
8.4 ConcurrentHashMap的实现原理及与HashMap的区别?
8.6 Java集合框架中的fail-fast和fail-safe是什么?
1. 引言:为什么需要集合框架
在日常编程中,我们经常需要存储和操作一组相关的数据对象。例如,一个电商应用需要管理商品列表、用户购物车、订单记录等;一个社交应用需要处理好友列表、消息队列、兴趣分类等。虽然数组可以存储多个对象,但它存在明显的局限性:
- 固定长度:创建数组时必须指定大小,无法动态调整
- 类型单一:只能存储同一类型(Java 5 以后数组也支持泛型对象数组,但集合更灵活)
- 功能有限:没有提供内置的添加、删除、查找等操作方法
Java集合框架(Java Collections Framework)应运而生,它提供了一套设计良好的接口和类,用于存储、管理和操作对象组。它的核心价值包括:
- 灵活性:集合可以动态增长或缩小,适应不同数据量
- 类型安全:通过泛型机制确保类型安全
- 功能丰富:提供多种操作方法(添加、删除、查找、排序等)
- 性能优化:不同实现针对不同场景进行了性能优化
- 代码复用:统一的API减少重复代码
无论是企业级应用开发、数据处理、算法实现,还是日常编程,集合框架都是Java开发者必须熟练掌握的核心工具。
2. 基础概念:集合框架概述
2.1 集合框架的结构
Java集合框架主要分为两大类:单列集合(Collection)和双列集合(Map)。
-
单列集合:每个位置存储单个元素,由Collection接口定义
- List:有序、可重复的元素序列
- Set:不允许重复元素的集合
- Queue/Deque:队列,通常用于按特定顺序处理元素
-
双列集合:每个位置存储键值对,由Map接口定义
- 每个元素包含键和值,键不能重复,值可以重复
Java集合框架
├── Collection(单列集合)
│ ├── List(有序、可重复)
│ │ ├── ArrayList(数组实现)
│ │ ├── LinkedList(链表实现)
│ │ └── Vector(线程安全)
│ │ └── Stack(栈)
│ ├── Set(无序、不可重复)
│ │ ├── HashSet(哈希表实现)
│ │ │ └── LinkedHashSet(维护插入顺序)
│ │ └── TreeSet(有序、树实现)
│ └── Queue(队列)
│ └── Deque(双端队列)
│ ├── ArrayDeque
│ └── LinkedList
│
└── Map(双列集合)
├── HashMap(哈希表实现)
│ └── LinkedHashMap(维护插入顺序)
├── Hashtable(线程安全)
│ └── Properties(属性文件)
└── TreeMap(有序、树实现)
虚线代表接口,实线代表类
2.2 集合与数组的比较
特性 | 数组 | 集合 |
---|---|---|
长度 | 固定长度 | 动态可变 |
内容类型 | 只能存储同一类型 | 可以存储不同类型(通过泛型限制) |
存储方式 | 可存储基本类型和引用类型 | 只能存储引用类型(对象) |
API丰富性 | 有限,主要通过下标操作 | 丰富,提供多种操作方法 |
类型安全 | 编译时检查 | 通过泛型提供类型安全 |
3. 前置知识:理解集合框架背后的基础数据结构
在深入学习集合框架之前,了解几种核心数据结构的基本原理是非常重要的。这些数据结构是各种集合实现的基础。
3.1 数组
数组是最基本的数据结构,由连续的内存空间组成,用于存储固定数量的相同类型元素。
特点:
- 随机访问:通过索引直接访问元素,时间复杂度为O(1)
- 固定大小:创建后大小不可变
- 内存连续:物理存储连续,访问效率高
- 数据类型固定:同一数组只能存储相同类型的数据
Java集合中的应用:ArrayList、Vector的底层就是使用数组实现的。当元素数量超出当前数组容量时,会创建一个更大的新数组,并将元素复制过去。
数组的逻辑表示:
┌───┬───┬───┬───┬───┬───┐
│ 0 │ 1 │ 2 │ 3 │ 4 │ 5 │ ← 索引
├───┼───┼───┼───┼───┼───┤
│ A │ B │ C │ D │ E │ F │ ← 元素
└───┴───┴───┴───┴───┴───┘
代码示例:
// 创建并初始化数组
int[] numbers = new int[5]; // 创建长度为5的整数数组
numbers[0] = 10; // 赋值
numbers[1] = 20;
// 使用数组字面量创建并初始化
String[] fruits = {"Apple", "Banana", "Orange"};
// 访问元素
System.out.println(fruits[1]); // 输出:Banana
// 遍历数组
for (int i = 0; i < fruits.length; i++) {
System.out.println(fruits[i]);
}
// 使用增强for循环遍历
for (String fruit : fruits) {
System.out.println(fruit);
}
3.2 链表
链表是由节点组成的线性数据结构,每个节点包含数据和指向下一个节点的引用。
类型:
- 单向链表:每个节点只有指向下一个节点的引用
- 双向链表:每个节点有指向前后节点的引用
- 循环链表:最后一个节点指向第一个节点
特点:
- 动态大小:可以灵活增长或缩小
- 插入和删除高效:不需要移动其他元素,时间复杂度为O(1)(已知位置)
- 随机访问低效:必须从头开始遍历,时间复杂度为O(n)
- 额外内存开销:每个节点需要额外空间存储引用
Java集合中的应用:LinkedList使用双向链表实现,允许从两端高效地添加和删除元素。
双向链表的逻辑表示:
┌───────┐ ┌───────┐ ┌───────┐
│ Node1 │ │ Node2 │ │ Node3 │
├───────┤ ├───────┤ ├───────┤
│ Data:A│ │ Data:B│ │ Data:C│
┌───┤ Prev │◄───┤ Prev │◄───┤ Prev │
│ │ Next ├───►│ Next ├───►│ Next │───┐
└───► │ │ │ │ │◄──┘
└───────┘ └───────┘ └───────┘
代码示例:
// 自定义简单的单向链表实现
class Node {
int data;
Node next;
public Node(int data) {
this.data = data;
this.next = null;
}
}
class SinglyLinkedList {
Node head;
// 添加节点到末尾
public void append(int data) {
Node newNode = new Node(data);
// 如果链表为空,新节点成为头节点
if (head == null) {
head = newNode;
return;
}
// 否则,遍历至末尾并添加新节点
Node last = head;
while (last.next != null) {
last = last.next;
}
last.next = newNode;
}
// 打印链表
public void printList() {
Node current = head;
while (current != null) {
System.out.print(current.data + " → ");
current = current.next;
}
System.out.println("null");
}
}
// 使用示例
SinglyLinkedList list = new SinglyLinkedList();
list.append(1);
list.append(2);
list.append(3);
list.printList(); // 输出:1 → 2 → 3 → null
3.3 哈希表
哈希表(Hash Table)是一种基于键的哈希值进行数据访问的数据结构,提供了快速的插入、删除和查找操作。
工作原理:
- 计算键的哈希值
- 将哈希值映射到数组索引(通常使用取模运算)
- 在该索引位置存储或查找数据
- 处理哈希冲突(当不同的键映射到相同的索引时)
哈希冲突解决方法:
- 链式法:在同一索引位置创建链表存储冲突元素
- 开放寻址法:寻找下一个空闲位置存储冲突元素
- 再哈希法:使用另一个哈希函数重新计算
特点:
- 快速访问:平均O(1)时间复杂度的插入、删除、查找
- 无序存储:元素的存储位置由哈希函数决定,不保留插入顺序
- 键的唯一性:每个键只能出现一次
- 空间换时间:通过额外的空间提高时间效率
Java集合中的应用:HashMap、HashSet、Hashtable的底层实现都基于哈希表,其中HashMap是最常用的实现。
哈希表的逻辑表示(链式法解决冲突):
索引 链表(处理哈希冲突)
──────┼──────────────────────
0 │ null
──────┼──────────────────────
1 │ ┌───┐ ┌───┐
│ │K:A├────►│K:D│
│ │V:1│ │V:4│
│ └───┘ └───┘
──────┼──────────────────────
2 │ null
──────┼──────────────────────
3 │ ┌───┐
│ │K:B│
│ │V:2│
│ └───┘
──────┼──────────────────────
4 │ ┌───┐
│ │K:C│
│ │V:3│
│ └───┘
──────┼──────────────────────
代码示例:
// 简单的哈希表实现
class SimpleHashMap<K, V> {
private static final int DEFAULT_CAPACITY = 16;
@SuppressWarnings("unchecked")
private Entry<K, V>[] buckets = new Entry[DEFAULT_CAPACITY];
// 内部节点类
private static class Entry<K, V> {
final K key;
V value;
Entry<K, V> next;
Entry(K key, V value) {
this.key = key;
this.value = value;
}
}
// 计算哈希索引
private int getIndex(K key) {
return key == null ? 0 : Math.abs(key.hashCode()) % buckets.length;
}
// 添加键值对
public void put(K key, V value) {
int index = getIndex(key);
// 如果bucket为空,创建新节点
if (buckets[index] == null) {
buckets[index] = new Entry<>(key, value);
return;
}
// 检查是否键已经存在,存在则更新,否则添加到链表头
Entry<K, V> entry = buckets[index];
if (key == null) {
if (entry.key == null) {
entry.value = value;
return;
}
} else if (key.equals(entry.key)) {
entry.value = value;
return;
}
// 添加到链表头
Entry<K, V> newEntry = new Entry<>(key, value);
newEntry.next = buckets[index];
buckets[index] = newEntry;
}
// 获取值
public V get(K key) {
int index = getIndex(key);
Entry<K, V> entry = buckets[index];
while (entry != null) {
if (key == null) {
if (entry.key == null) {
return entry.value;
}
} else if (key.equals(entry.key)) {
return entry.value;
}
entry = entry.next;
}
return null; // 键不存在
}
}
// 使用示例
SimpleHashMap<String, Integer> map = new SimpleHashMap<>();
map.put("Apple", 10);
map.put("Banana", 20);
System.out.println(map.get("Apple")); // 输出:10
3.4 二叉树与二叉查找树
二叉树是一种树形数据结构,每个节点最多有两个子节点,通常称为左子节点和右子节点。
二叉查找树(Binary Search Tree,BST)是一种特殊的二叉树,具有以下特点:
- 左子树上所有节点的值都小于根节点的值
- 右子树上所有节点的值都大于根节点的值
- 左右子树都是二叉查找树
特点:
- 有序性:中序遍历得到有序序列
- 查找效率:平均情况下O(log n)的查找、插入、删除操作
- 最坏情况:当树退化为链表时(如顺序插入元素),性能下降至O(n)
Java集合中的应用:TreeMap和TreeSet内部使用红黑树(一种自平衡的二叉查找树)实现。
二叉查找树的逻辑表示:
1
/ \
2 3
/ \ / \
4 5 6 7
-
前序遍历:先访问根节点,再前序遍历左子树,最后前序遍历右子树(中左右)。遍历顺序为:1、2、4、5、3、6、7。
-
中序遍历:先中序遍历左子树,再访问根节点,最后中序遍历右子树(左中右)。遍历顺序为:4、2、5、1、6、3、7。
-
后序遍历:先后序遍历左子树,再后序遍历右子树,最后访问根节点(左右中)。遍历顺序为:4、5、2、6、7、3、1。
-
层序遍历:按照二叉树的层次从上到下、从左到右依次访问节点(一层层遍历)。遍历顺序为:1、2、3、4、5、6、7。
前序:当前结点第一个获取
中序:当前结点第二个获取
后序:当前结点第三个获取
代码示例:
class BinarySearchTree {
// 树的节点类,包含数据及左右子节点引用
private static class Node {
int data;
Node left, right;
Node(int data) {
this.data = data;
}
}
private Node root; // 根节点
// 插入新节点(对外接口)
public void insert(int data) {
root = insert(root, data);
}
// 递归插入逻辑(内部实现)
private Node insert(Node node, int data) {
// 若当前节点为空,创建新节点
if (node == null) return new Node(data);
// 根据数据大小决定插入左子树还是右子树
if (data < node.data) {
node.left = insert(node.left, data);
} else {
node.right = insert(node.right, data);
}
return node; // 返回当前节点(递归回溯时构建树结构)
}
// 中序遍历打印树(左-根-右,BST会按升序输出)
public void printInOrder() {
inOrder(root);
System.out.println();
}
// 递归实现中序遍历
private void inOrder(Node node) {
if (node != null) {
inOrder(node.left); // 遍历左子树
System.out.print(node.data + " "); // 访问当前节点
inOrder(node.right); // 遍历右子树
}
}
public static void main(String[] args) {
// 构建示例二叉树
BinarySearchTree tree = new BinarySearchTree();
// 按层次顺序插入节点
tree.insert(1);
tree.insert(2);
tree.insert(3);
tree.insert(4);
tree.insert(5);
tree.insert(6);
tree.insert(7);
// 输出中序遍历结果
System.out.print("中序遍历结果: ");
tree.printInOrder(); // 输出: 4 2 5 1 6 3 7
}
}
3.5 红黑树
红黑树是一种自平衡的二叉查找树,它通过节点着色和旋转操作维持平衡。红黑树在Java集合框架(特别是TreeMap和TreeSet)中有广泛应用,同时也在JDK 8后的HashMap中作为处理哈希冲突的辅助数据结构。
红黑树的规则:
- 每个节点要么是红色,要么是黑色
- 根节点必须是黑色
- 所有叶节点(NIL节点/空节点)都是黑色
- 如果一个节点是红色,则它的两个子节点都是黑色(不能有两个连续的红色节点)
- 对于每个节点,从该节点到其所有后代叶节点的简单路径上,均包含相同数目的黑色节点
发展历史:
- 1972年首次出现,当时被称为对称二叉B树
- 1978年被修改为现在的"红黑树"
红黑树与AVL树的比较:
- 红黑树的平衡条件较宽松(黑色节点平衡),插入/删除时旋转操作更少
- AVL树的平衡条件严格(高度差不超过1),查找性能可能略优
- 红黑树更适合插入/删除频繁的场景,AVL树更适合查询频繁的场景
红黑树的逻辑表示(B表示黑色节点,R表示红色节点):
┌───┐
│8 B│
└─┬─┘
┌───┴───┐
┌─┴─┐ ┌─┴─┐
│3 R│ │10B│
└─┬─┘ └─┬─┘
┌──┴──┐ └──┐
┌─┴─┐ ┌─┴─┐ ┌─┴─┐
│1 B│ │6 B│ │14R│
└───┘ └───┘ └───┘
红黑树节点添加过程:
- 按照二叉查找树的规则插入节点,新节点初始设为红色(不违反规则5)
- 如果新节点是根节点,则将其设为黑色(满足规则2)
- 如果新节点的父节点是黑色,不需要调整(满足所有规则)
- 如果新节点的父节点是红色(违反规则4),执行平衡操作:
- 情况1:叔叔节点是红色
- 将父节点和叔叔节点变为黑色
- 将祖父节点变为红色
- 以祖父节点为当前节点,递归处理
- 情况2:叔叔节点是黑色,当前节点是父节点的右子节点
- 以父节点为支点左旋
- 转换为情况3处理
- 情况3:叔叔节点是黑色,当前节点是父节点的左子节点
- 将父节点变为黑色
- 将祖父节点变为红色
- 以祖父节点为支点右旋
- 情况1:叔叔节点是红色
红黑树在Java集合中的应用:
- TreeMap和TreeSet的底层实现
- HashMap和LinkedHashMap在JDK 8后,当链表长度超过8且数组长度超过64时,链表会转换为红黑树
代码示例(基本结构):
// 红黑树的节点结构(简化示例)
class RedBlackNode {
int data;
RedBlackNode left, right, parent;
boolean isRed; // true表示红色,false表示黑色
RedBlackNode(int data) {
this.data = data;
this.isRed = true; // 新插入的节点默认为红色
this.left = this.right = this.parent = null;
}
}
// 完整的红黑树实现比较复杂,包含左旋、右旋、变色等操作
// 基本操作描述:
// 1. 插入节点时,始终将新节点着色为红色
// 2. 根据插入位置和周围节点颜色,执行旋转和变色操作
// 3. 最后确保根节点为黑色
3.6 迭代器模式
迭代器模式是一种行为设计模式,允许顺序访问集合的元素,而不暴露其内部结构。在Java集合框架中,Iterator接口是迭代器模式的核心实现。
主要接口方法:
- hasNext():检查是否还有更多元素
- next():获取下一个元素
- remove():从集合中移除当前元素
特点:
- 提供统一的遍历接口,无需关心底层实现
- 允许在遍历过程中安全地删除元素
- 支持快速失败(fail-fast)和安全失败(fail-safe)机制
代码示例:
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
// 迭代器使用示例
List<String> list = new ArrayList<>();
list.add("Java");
list.add("Python");
list.add("C++");
// 使用迭代器遍历
Iterator<String> it = list.iterator();
while (it.hasNext()) {
String element = it.next();
System.out.println(element);
// 安全地删除元素(如果需要)
if (element.equals("Python")) {
it.remove(); // 正确方式
}
}
// 错误示例:在增强for循环中修改集合
// 这可能会抛出ConcurrentModificationException
for (String element : list) {
if (element.equals("Java")) {
// list.remove(element); // 错误!不要这样做
}
}
4. 单列集合详解
4.1 Collection接口
Collection是所有单列集合的根接口,定义了单列集合的基本操作。
核心方法:
- 添加:
add(E e)
,addAll(Collection<? extends E> c)
- 删除:
remove(Object o)
,removeAll(Collection<?> c)
,clear()
- 查询:
contains(Object o)
,containsAll(Collection<?> c)
,isEmpty()
,size()
- 转换:
toArray()
,toArray(T[] a)
- 遍历:
iterator()
// Collection接口使用示例
Collection<String> collection = new ArrayList<>();
collection.add("Java"); // 添加元素
collection.add("Python");
System.out.println(collection.size()); // 输出元素数量:2
System.out.println(collection.contains("Java")); // 检查是否包含:true
collection.remove("Java"); // 删除元素
System.out.println(collection.isEmpty()); // 检查是否为空:false
collection.clear(); // 清空集合
System.out.println(collection.isEmpty()); // 检查是否为空:true
4.2 List接口及实现类
List接口扩展了Collection接口,表示有序、可重复的元素集合。List更像是数组的增强版,它有以下特点:
- 元素有序(按插入顺序排列)
- 元素可重复
- 可通过索引访问元素
4.2.1 ArrayList
ArrayList是基于动态数组实现的List集合,是最常用的List实现类。
实现原理:
- 内部使用一个Object类型的数组存储元素
- 初始容量为10
- 当元素数量超过容量时,自动扩容为原容量的1.5倍
- 扩容操作需要创建新数组并复制元素,开销较大
特点:
- 随机访问快:O(1)时间复杂度
- 末尾添加快:均摊O(1)时间复杂度
- 中间插入/删除慢:需要移动元素,O(n)时间复杂度
- 线程不安全:多线程环境下需要外部同步
源码剖析:
// ArrayList核心属性和方法
public class ArrayList<E> extends AbstractList<E> implements List<E>, ... {
// 存储元素的数组
transient Object[] elementData;
// 元素数量
private int size;
// 默认初始容量
private static final int DEFAULT_CAPACITY = 10;
// 构造函数
public ArrayList() {
// 使用默认空数组,延迟分配空间
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
// 添加元素
public boolean add(E e) {
// 确保容量足够
ensureCapacityInternal(size + 1);
// 添加元素并增加大小
elementData[size++] = e;
return true;
}
// 扩容核心方法
private void grow(int minCapacity) {
int oldCapacity = elementData.length;
// 新容量为旧容量的1.5倍
int newCapacity = oldCapacity + (oldCapacity >> 1);
// ...更多逻辑
// 创建新数组并复制元素
elementData = Arrays.copyOf(elementData, newCapacity);
}
// 获取元素
public E get(int index) {
// 检查索引
rangeCheck(index);
return elementData(index);
}
// 其他方法...
}
适用场景:
- 频繁随机访问元素
- 主要在末尾添加/删除元素
- 对查询性能要求高
代码示例:
// ArrayList基本用法
ArrayList<String> list = new ArrayList<>(); // 创建默认容量(10)的ArrayList
ArrayList<String> list2 = new ArrayList<>(20); // 创建指定初始容量的ArrayList
// 添加元素
list.add("Java"); // 添加到末尾
list.add("Python"); // 添加到末尾
list.add(1, "C++"); // 在指定位置插入
// 访问元素
String element = list.get(0); // 获取指定索引的元素:Java
int index = list.indexOf("Python"); // 查找元素的索引:2
// 修改元素
list.set(0, "JavaScript"); // 修改指定索引的元素
// 删除元素
list.remove(0); // 删除指定索引的元素
list.remove("Python"); // 删除指定元素
// 其他操作
int size = list.size(); // 获取元素数量
boolean isEmpty = list.isEmpty(); // 检查是否为空
list.clear(); // 清空列表
// 批量操作
List<String> otherList = Arrays.asList("Scala", "Go");
list.addAll(otherList); // 添加另一个集合的所有元素
// 遍历方式
// 1. 使用索引遍历
for (int i = 0; i < list.size(); i++) {
System.out.println(list.get(i));
}
// 2. 使用增强for循环遍历
for (String item : list) {
System.out.println(item);
}
// 3. 使用迭代器遍历
Iterator<String> it = list.iterator();
while (it.hasNext()) {
System.out.println(it.next());
}
// 4. 使用forEach方法(Java 8+)
list.forEach(System.out::println);
4.2.2 LinkedList
LinkedList是基于双向链表实现的List集合,同时也实现了Deque接口,可以作为队列或双端队列使用。
实现原理:
- 内部使用双向链表存储元素
- 每个节点包含元素、前驱节点引用和后继节点引用
- 维护first和last引用指向链表的首尾节点
源码剖析:
public class LinkedList<E> extends AbstractSequentialList<E> implements List<E>, Deque<E>, ... {
// 元素数量
transient int size = 0;
// 首节点
transient Node<E> first;
// 尾节点
transient Node<E> last;
// 节点类
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;
}
}
// 添加元素到末尾
public boolean add(E e) {
linkLast(e);
return true;
}
// 在链表尾部添加节点
void linkLast(E e) {
final Node<E> l = last;
final Node<E> newNode = new Node<>(l, e, null);
last = newNode;
if (l == null)
first = newNode; // 空链表
else
l.next = newNode;
size++;
}
// 获取元素
public E get(int index) {
// 检查索引
checkElementIndex(index);
return node(index).item;
}
// 查找指定索引的节点
Node<E> node(int index) {
// 从前或从后遍历,选择最短路径
if (index < (size >> 1)) {
Node<E> x = first;
for (int i = 0; i < index; i++)
x = x.next;
return x;
} else {
Node<E> x = last;
for (int i = size - 1; i > index; i--)
x = x.prev;
return x;
}
}
// 其他方法...
}
特点:
- 随机访问慢:需要从头或尾遍历,O(n)时间复杂度
- 插入/删除快:不需要移动元素,只需修改节点引用,O(1)时间复杂度(已知位置)
- 内存开销大:每个节点需要额外的引用
- 线程不安全:多线程环境下需要外部同步
适用场景:
- 频繁在两端或中间插入/删除元素
- 作为队列或栈使用
- 对随机访问性能要求不高
代码示例:
// LinkedList基本用法
LinkedList<String> list = new LinkedList<>();
// 添加元素
list.add("Java"); // 添加到末尾
list.addFirst("Python"); // 添加到开头
list.addLast("C++"); // 添加到末尾
// 访问元素
String first = list.getFirst(); // 获取第一个元素:Python
String last = list.getLast(); // 获取最后一个元素:C++
String element = list.get(1); // 获取指定索引的元素:Java
// 删除元素
list.removeFirst(); // 删除第一个元素
list.removeLast(); // 删除最后一个元素
list.remove(0); // 删除指定索引的元素
// 作为队列使用
list.offer("A"); // 入队(添加到末尾)
String head = list.poll(); // 出队(移除并返回队首元素)
// 作为栈使用
list.push("A"); // 入栈(添加到开头)
String top = list.pop(); // 出栈(移除并返回栈顶元素)
// 遍历
for (String item : list) {
System.out.println(item);
}
4.2.3 Vector和Stack
Vector是List接口的同步实现,几乎所有方法都被synchronized修饰,保证线程安全。Stack继承自Vector,实现了后进先出(LIFO)的栈数据结构。
特点:
- 线程安全,但性能较低(同步开销)
- 功能上与ArrayList类似,但扩容策略不同(默认扩容为原容量的2倍)
- 现代Java程序中较少使用,通常被ArrayList+Collections.synchronizedList或CopyOnWriteArrayList替代
代码示例:
// Vector使用
Vector<String> vector = new Vector<>(); // 默认容量10
vector.add("Java");
vector.add("Python");
// 线程安全操作
synchronized (vector) {
if (!vector.contains("C++")) {
vector.add("C++");
}
}
// Stack使用
Stack<String> stack = new Stack<>();
stack.push("A"); // 入栈
stack.push("B"); // 入栈
String top = stack.pop(); // 出栈:B
String peek = stack.peek(); // 查看栈顶元素而不移除:A
boolean empty = stack.empty(); // 检查栈是否为空
注意:Vector和Stack是Java早期类,现代开发中建议使用ArrayList和ArrayDeque替代。
// 推荐的替代方案
// 代替Vector的线程安全List
List<String> safeList = Collections.synchronizedList(new ArrayList<>());
// 或者
List<String> concurrentList = new CopyOnWriteArrayList<>();
// 代替Stack的栈实现
Deque<String> stack = new ArrayDeque<>();
stack.push("A");
stack.push("B");
String top = stack.pop();
4.2.4 List实现类的性能比较
操作 | ArrayList | LinkedList | 说明 |
---|---|---|---|
随机访问 | O(1) | O(n) | ArrayList直接使用数组索引访问 |
末尾添加/删除 | 均摊O(1) | O(1) | ArrayList可能触发扩容 |
中间添加/删除 | O(n) | O(1)+O(n) | LinkedList定位元素O(n),修改O(1) |
头部添加/删除 | O(n) | O(1) | ArrayList需要移动所有元素 |
内存占用 | 较少 | 较多 | LinkedList存储节点引用 |
迭代性能 | 较好 | 较好 | 都支持快速迭代 |
缓存局部性 | 好 | 差 | ArrayList元素存储连续 |
4.3 Set接口及实现类
Set接口扩展了Collection接口,表示不允许重复元素的集合,通常不保证有序性。主要特点是元素的唯一性,即不能包含重复元素。
4.3.1 HashSet
HashSet是基于HashMap实现的Set集合,不保证元素的顺序,允许null元素。
实现原理:
- 内部使用HashMap存储元素,将元素作为键,使用一个固定的Object对象作为值
- 判断元素是否重复基于hashCode和equals方法
- 元素的位置由哈希函数决定,所以是无序的
源码剖析:
public class HashSet<E> extends AbstractSet<E> implements Set<E>, ... {
// 内部使用HashMap存储元素
private transient HashMap<E,Object> map;
// 所有值对应的固定Object对象
private static final Object PRESENT = new Object();
// 构造函数
public HashSet() {
map = new HashMap<>();
}
// 添加元素
public boolean add(E e) {
// 将元素作为键,PRESENT作为值添加到内部HashMap
return map.put(e, PRESENT) == null;
}
// 判断是否包含元素
public boolean contains(Object o) {
return map.containsKey(o);
}
// 移除元素
public boolean remove(Object o) {
return map.remove(o) == PRESENT;
}
// 获取迭代器
public Iterator<E> iterator() {
return map.keySet().iterator();
}
// 其他方法...
}
特点:
- 查找/插入/删除效率高:O(1)时间复杂度
- 无序:不保证元素的顺序
- 线程不安全:多线程环境下需要外部同步
- 元素可以为null,但只能有一个null
适用场景:
- 需要快速查找/添加/删除元素
- 不关心元素顺序
- 需要元素唯一性(去重)
代码示例:
// HashSet基本用法
HashSet<String> set = new HashSet<>();
// 添加元素
set.add("Java");
set.add("Python");
set.add("Java"); // 重复元素不会被添加
set.add(null); // 可以添加null元素
// 检查元素
boolean hasJava = set.contains("Java"); // true
boolean hasCpp = set.contains("C++"); // false
// 删除元素
set.remove("Python");
// 遍历元素
for (String element : set) {
System.out.println(element); // 输出顺序不确定
}
// 集合操作
HashSet<String> set2 = new HashSet<>();
set2.add("Java");
set2.add("C++");
set.addAll(set2); // 并集
set.retainAll(set2); // 交集
set.removeAll(set2); // 差集
自定义类作为HashSet元素: 当使用自定义类的对象作为HashSet元素时,必须正确重写hashCode()和equals()方法,才能保证元素的唯一性。
public class Person {
private String name;
private int age;
// 构造函数、getter和setter省略
@Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (obj == null || getClass() != obj.getClass()) return false;
Person person = (Person) obj;
return age == person.age && Objects.equals(name, person.name);
}
@Override
public int hashCode() {
return Objects.hash(name, age);
}
}
// 使用自定义类作为HashSet元素
HashSet<Person> personSet = new HashSet<>();
personSet.add(new Person("张三", 25));
personSet.add(new Person("张三", 25)); // 由于equals返回true,不会被添加
personSet.add(new Person("李四", 30)); // 会被添加
4.3.2 LinkedHashSet
LinkedHashSet是HashSet的子类,它在HashSet的基础上,使用链表保持元素插入的顺序。
实现原理:
- 内部基于LinkedHashMap实现
- 除了使用哈希表存储元素外,还使用双向链表记录插入顺序
特点:
- 查找/插入/删除效率与HashSet相当
- 有序:保持元素的插入顺序
- 内存开销略大于HashSet
- 迭代效率高于HashSet
适用场景:
- 需要元素唯一性
- 需要保持插入顺序
- 频繁遍历集合
代码示例:
// LinkedHashSet基本用法
LinkedHashSet<String> set = new LinkedHashSet<>();
set.add("Java");
set.add("Python");
set.add("C++");
// 遍历元素(保持插入顺序)
for (String element : set) {
System.out.println(element); // 输出顺序:Java, Python, C++
}
// 添加重复元素
boolean added = set.add("Java"); // 返回false,不会添加
System.out.println(added); // false
// 移除元素
set.remove("Python");
4.3.3 TreeSet
TreeSet是基于TreeMap实现的Set集合,它保证元素按照特定顺序排序(自然顺序或者自定义顺序)。
实现原理:
- 内部使用TreeMap实现,将元素作为键,使用一个固定的Object对象作为值
- 基于红黑树数据结构,保证了较好的平衡性和性能
特点:
- 有序:元素按照自然顺序或自定义顺序排序
- 查找/插入/删除效率适中:O(log n)时间复杂度
- 不允许null元素(因为无法比较null)
- 线程不安全:多线程环境下需要外部同步
元素排序方式:
- 自然排序:元素类必须实现Comparable接口
- 自定义排序:创建TreeSet时提供Comparator比较器
适用场景:
- 需要元素唯一性
- 需要元素保持排序
- 需要范围查询操作
// 自然排序
TreeSet<String> treeSet = new TreeSet<>();
treeSet.add("Java");
treeSet.add("Python");
treeSet.add("C++");
// 遍历时会按照字母顺序:C++, Java, Python
// 自定义排序
TreeSet<Person> personSet = new TreeSet<>((p1, p2) -> p1.getAge() - p2.getAge());
personSet.add(new Person("张三", 25));
personSet.add(new Person("李四", 20));
// 遍历时会按照年龄升序排列
排序原理详解
在使用TreeSet或调用Collections.sort()进行排序时,需要理解比较器(Comparator)的排序原理:
自然排序(Comparable)
元素类实现Comparable接口的compareTo方法,定义默认排序规则:
class Student implements Comparable<Student> {
private String name;
private int age;
// 构造方法和getter/setter省略
@Override
public int compareTo(Student other) {
// 按年龄升序排列
return this.age - other.age;
}
}
定制排序(Comparator)
通过创建Comparator对象定义特定的排序规则:
// 按年龄降序排列
Comparator<Student> ageDescending = (s1, s2) -> s2.getAge() - s1.getAge();
TreeSet<Student> students = new TreeSet<>(ageDescending);
排序逻辑分析
当我们使用表达式o1.getAge() - o2.getAge()
作为比较逻辑时,结果决定了元素的排序顺序:
- 升序排列:
o1.getAge() - o2.getAge()
- 当o1年龄小于o2年龄:结果为负数,o1排在o2前面
- 当o1年龄大于o2年龄:结果为正数,o1排在o2后面
- 结果:年龄从小到大排序
- 降序排列:
o2.getAge() - o1.getAge()
- 当o1年龄小于o2年龄:结果为正数,o1排在o2后面
- 当o1年龄大于o2年龄:结果为负数,o1排在o2前面
- 结果:年龄从大到小排序
原理解析:
- 比较器返回负数,表示第一个参数应该排在第二个参数前面
- 比较器返回正数,表示第一个参数应该排在第二个参数后面
- 比较器返回零,表示两个参数相等,顺序无关紧要
记忆技巧:
- 想要升序(小到大):o1 - o2
- 想要降序(大到小):o2 - o1
4.3.4 Set实现类的比较
特性 | HashSet | LinkedHashSet | TreeSet |
---|---|---|---|
内部实现 | HashMap | LinkedHashMap | TreeMap (红黑树) |
有序性 | 无序 | 插入顺序 | 自然顺序或自定义顺序 |
性能(添加/删除/包含) | O(1) | O(1) | O(log n) |
是否允许null | 允许一个 | 允许一个 | 不允许 |
内存消耗 | 中等 | 较高 | 较高 |
适用场景 | 快速查找、不关心顺序 | 需要记住插入顺序 | 需要排序 |
4.4 Queue和Deque接口及实现类
Queue接口表示队列,遵循先进先出(FIFO)原则,而Deque接口表示双端队列,允许从两端添加或移除元素。
4.4.1 Queue接口
主要方法:
- offer(E e):添加元素到队尾,如果成功返回true,否则返回false
- poll():移除并返回队首元素,如果队列为空,返回null
- peek():返回队首元素但不移除,如果队列为空,返回null
抛出异常的等效方法:
- add(E e):等同于offer,但在失败时抛出异常
- remove():等同于poll,但在队列为空时抛出异常
- element():等同于peek,但在队列为空时抛出异常
代码示例:
// Queue基本用法
Queue<String> queue = new LinkedList<>();
// 添加元素
queue.offer("Java"); // 添加到队尾
queue.offer("Python");
queue.offer("C++");
// 查看队首元素但不移除
String head = queue.peek(); // 返回Java
// 移除并返回队首元素
String first = queue.poll(); // 返回并移除Java
String second = queue.poll(); // 返回并移除Python
// 检查队列是否为空
boolean isEmpty = queue.isEmpty(); // false(还有C++)
// 使用抛出异常的方法
try {
Queue<String> emptyQueue = new LinkedList<>();
String element = emptyQueue.element(); // 抛出NoSuchElementException
} catch (NoSuchElementException e) {
System.out.println("队列为空");
}
4.4.2 PriorityQueue
PriorityQueue是Queue接口的实现,提供了基于优先级的队列,元素按优先级顺序出队,而非先进先出。
实现原理:
- 基于小顶堆(最小元素在顶部)实现
- 插入和删除操作的时间复杂度为O(log n)
- 查看顶部元素的时间复杂度为O(1)
特点:
- 元素按照自然顺序或自定义比较器排序
- 不保证同优先级元素的顺序
- 不允许null元素
- 线程不安全(多线程环境使用PriorityBlockingQueue)
代码示例:
// PriorityQueue基本用法
// 1. 使用自然排序
PriorityQueue<Integer> pq = new PriorityQueue<>();
pq.offer(5);
pq.offer(2);
pq.offer(8);
pq.offer(1);
// 元素出队顺序
while (!pq.isEmpty()) {
System.out.print(pq.poll() + " "); // 输出: 1 2 5 8
}
// 2. 使用自定义比较器(降序)
PriorityQueue<Integer> pqDesc = new PriorityQueue<>((a, b) -> b - a);
pqDesc.offer(5);
pqDesc.offer(2);
pqDesc.offer(8);
pqDesc.offer(1);
// 元素出队顺序
while (!pqDesc.isEmpty()) {
System.out.print(pqDesc.poll() + " "); // 输出: 8 5 2 1
}
// 3. 自定义类与优先级
class Task implements Comparable<Task> {
private int priority;
private String name;
// 构造函数略
@Override
public int compareTo(Task other) {
// 优先级数字越小,任务优先级越高
return this.priority - other.priority;
}
@Override
public String toString() {
return name + "(" + priority + ")";
}
}
// 创建任务优先队列
PriorityQueue<Task> taskQueue = new PriorityQueue<>();
taskQueue.offer(new Task(3, "发送邮件"));
taskQueue.offer(new Task(1, "系统错误"));
taskQueue.offer(new Task(2, "用户请求"));
// 按优先级处理任务
while (!taskQueue.isEmpty()) {
Task task = taskQueue.poll();
System.out.println("处理:" + task);
// 输出:
// 处理:系统错误(1)
// 处理:用户请求(2)
// 处理:发送邮件(3)
}
4.4.3 Deque接口及ArrayDeque
Deque接口扩展了Queue,支持在队列两端进行操作,可以用作栈、队列或双端队列。ArrayDeque是Deque接口的高效实现。
主要方法:
队首操作:
- offerFirst(E e) / addFirst(E e):在队首添加元素
- pollFirst() / removeFirst():移除并返回队首元素
- peekFirst() / getFirst():返回队首元素但不移除
队尾操作:
- offerLast(E e) / addLast(E e):在队尾添加元素
- pollLast() / removeLast():移除并返回队尾元素
- peekLast() / getLast():返回队尾元素但不移除
栈操作:
- push(E e):入栈(等同于addFirst)
- pop():出栈(等同于removeFirst)
实现原理:
- ArrayDeque基于循环数组实现
- 不允许null元素
- 没有容量限制,自动扩容
- 作为栈比Stack更快,作为队列比LinkedList更快(除非有大量的remove(Object)操作)
代码示例:
// ArrayDeque基本用法
Deque<String> deque = new ArrayDeque<>();
// 队列操作(先进先出)
deque.offerLast("Java"); // 添加到队尾
deque.offerLast("Python");
deque.offerLast("C++");
String first = deque.pollFirst(); // 移除并返回队首元素:Java
// 栈操作(后进先出)
deque.clear(); // 清空队列
deque.push("Java"); // 添加到栈顶(队首)
deque.push("Python");
deque.push("C++");
String top = deque.pop(); // 移除并返回栈顶元素:C++
// 双端队列操作
deque.clear();
deque.offerFirst("Java"); // 添加到队首
deque.offerLast("Python"); // 添加到队尾
deque.offerFirst("C++"); // 添加到队首
// 此时顺序为:C++, Java, Python
String first = deque.peekFirst(); // 查看队首元素:C++
String last = deque.peekLast(); // 查看队尾元素:Python
// 遍历
for (String item : deque) {
System.out.println(item);
}
// 判断操作
boolean contains = deque.contains("Java"); // true
int size = deque.size(); // 3
4.4.4 Deque操作方法对比
操作 | 队首方法 | 队尾方法 |
---|---|---|
插入(不抛异常) | offerFirst(e) | offerLast(e) |
插入(抛异常) | addFirst(e) | addLast(e) |
移除(不抛异常) | pollFirst() | pollLast() |
移除(抛异常) | removeFirst() | removeLast() |
查看(不抛异常) | peekFirst() | peekLast() |
查看(抛异常) | getFirst() | getLast() |
4.5 Collections 工具类
一、核心功能概览
Java 提供的集合操作工具类,包含排序、查找、同步化、不可变集合等实用功能。所有方法均为static
,通过Collections.方法名()
调用。
// 典型使用场景示例
List<Integer> numbers = new ArrayList<>();
Collections.addAll(numbers, 5, 2, 9, 1); // 批量添加元素
Collections.sort(numbers); // 自然排序 → [1,2,5,9]
Collections.reverse(numbers); // 反转 → [9,5,2,1]
二、排序与交换操作
方法 | 说明 |
---|---|
sort(List<T> list) | 自然顺序排序(元素需实现Comparable ) |
sort(List<T> list, Comparator<T> c) | 自定义排序规则 |
shuffle(List<?> list) | 随机打乱顺序 |
swap(List<?> list, int i, int j) | 交换指定索引位置的元素 |
List<String> words = Arrays.asList("Banana", "Apple", "Cherry");
// 自定义排序:按字符串长度
Collections.sort(words, (s1, s2) -> s1.length() - s2.length());
// 结果 → [Apple, Banana, Cherry]
// 随机洗牌(常用于抽奖场景)
Collections.shuffle(words);
三、查找与统计操作
方法 | 说明 |
---|---|
binarySearch(List<T> list, T key) | 二分查找(列表必须先排序) |
max(Collection<T> coll) | 按自然顺序找最大值 |
frequency(Collection<?> c, Object o) | 统计元素出现次数 |
disjoint(Collection<?> c1, Collection<?> c2) | 判断两集合是否无共同元素 |
List<Integer> nums = Arrays.asList(10, 20, 30, 40, 50);
int index = Collections.binarySearch(nums, 30); // 返回2
int max = Collections.max(nums); // 50
int count = Collections.frequency(nums, 20); // 1
boolean noCommon = Collections.disjoint(nums, List.of(60,70)); // true
四、不可变集合(重点)
1. 创建方式对比
方法 | 来源 | 允许 null | 元素数量限制 |
---|---|---|---|
Collections.unmodifiableXxx | 包装现有集合 | ✅ | 无 |
List/Set/Map.of() | Java 9+ 工厂方法 | ❌ | Map 最多 10 对键值 |
// Java 9+ of()方法(推荐)
List<String> immutableList = List.of("A", "B", "C");
Set<Integer> immutableSet = Set.of(100, 200);
Map<String, Integer> immutableMap = Map.of("Math", 90, "Java", 95);
// 传统方法(可包装任何集合)
List<String> modifiable = new ArrayList<>(Arrays.asList("X", "Y"));
List<String> unmodifiable = Collections.unmodifiableList(modifiable);
2. 核心特点
- 不可修改性:任何修改操作会抛出
UnsupportedOperationException
- 线程安全:无需额外同步措施
- 内存优化:比普通集合更节省内存
- 防御性编程:防止外部修改内部数据
3. 典型应用场景
// 1. 全局常量
public static final List<String> COUNTRIES = List.of("China", "USA", "Japan");
// 2. API返回值
public List<String> getSystemConfig() {
return Collections.unmodifiableList(configData);
}
五、同步化集合
方法 | 说明 |
---|---|
synchronizedList(List<T> list) | 返回线程安全的 List |
synchronizedSet(Set<T> s) | 返回线程安全的 Set |
synchronizedMap(Map<K,V> m) | 返回线程安全的 Map |
// 创建同步集合
List<Integer> syncList = Collections.synchronizedList(new ArrayList<>());
Map<String, Object> syncMap = Collections.synchronizedMap(new HashMap<>());
// 手动同步迭代器
synchronized(syncList) {
Iterator<Integer> it = syncList.iterator();
while(it.hasNext()) { /*...*/ }
}
注意:优先使用
java.util.concurrent
包中的并发集合(如ConcurrentHashMap
)。
六、可变参数(Varargs)
1. 语法规则
- 定义:
类型... 参数名
- 规则:
- 只能有一个可变参数
- 必须是方法最后一个参数
- 内部作为数组处理
// 自定义可变参数方法
public static void logMessages(String prefix, String... messages) {
Arrays.stream(messages).forEach(msg -> System.out.println(prefix + msg));
}
// 调用示例
logMessages("[INFO] ", "Server started", "Port: 8080");
2. 在 Collections 中的应用
List<String> list = new ArrayList<>();
Collections.addAll(list, "A", "B", "C", "D"); // 可变参数添加元素
七、其他实用方法
方法 | 说明 |
---|---|
copy(List<T> dest, List<T> src) | 复制集合(需保证 dest 容量≥src) |
fill(List<T> list, T obj) | 用指定元素填充整个集合 |
replaceAll(List<T> list, T oldVal, T newVal) | 替换所有匹配元素 |
rotate(List<?> list, int distance) | 旋转列表元素(distance=1 时 [1,2,3]→[3,1,2]) |
// 复制示例
List<Integer> src = List.of(1, 2, 3);
List<Integer> dest = new ArrayList<>(Arrays.asList(0, 0, 0, 0));
Collections.copy(dest, src); // dest → [1,2,3,0]
// 填充与替换
Collections.fill(dest, 9); // [9,9,9,9]
Collections.replaceAll(dest, 9, 5); // [5,5,5,5]
4.6 集合的遍历方式
Java提供了多种遍历集合的方式,每种方式各有优缺点。
4.6.1 使用Iterator(迭代器)
Iterator是集合框架的核心接口,提供了统一的遍历方式。它的主要优点是可以在遍历过程中安全地删除元素。
List<String> list = new ArrayList<>();
list.add("Java");
list.add("Python");
list.add("C++");
// 使用迭代器遍历
Iterator<String> it = list.iterator();
while (it.hasNext()) {
String element = it.next();
System.out.println(element);
// 安全地删除元素
if (element.equals("Python")) {
it.remove();
}
}
4.6.2 使用增强for循环(for-each)
增强for循环是Java 5引入的语法糖,简化了集合遍历。它内部使用Iterator实现,但不能在遍历过程中修改集合结构。
List<String> list = new ArrayList<>();
list.add("Java");
list.add("Python");
list.add("C++");
// 使用增强for循环遍历
for (String element : list) {
System.out.println(element);
// 注意:不能在此处安全地删除元素
}
4.6.3 使用索引遍历(仅适用于List)
对于实现List接口的集合,可以使用索引遍历。这种方式允许修改元素,但不适用于Set等没有索引的集合。
List<String> list = new ArrayList<>();
list.add("Java");
list.add("Python");
list.add("C++");
// 使用索引遍历
for (int i = 0; i < list.size(); i++) {
String element = list.get(i);
System.out.println(element);
// 可以安全地修改元素
list.set(i, element.toUpperCase());
}
// 反向遍历(用于安全删除)
for (int i = list.size() - 1; i >= 0; i--) {
String element = list.get(i);
if (element.equals("PYTHON")) {
list.remove(i); // 从后往前删除,避免索引问题
}
}
4.6.4 使用forEach方法(Java 8+)
Java 8引入的forEach方法结合Lambda表达式或方法引用,提供了更简洁的遍历语法。
List<String> list = new ArrayList<>();
list.add("Java");
list.add("Python");
list.add("C++");
// 使用forEach方法遍历
list.forEach(element -> System.out.println(element));
// 或者更简洁地使用方法引用
list.forEach(System.out::println);
4.6.5 使用Stream API(Java 8+)
Java 8引入的Stream API提供了强大的数据处理能力,允许进行各种转换、过滤、映射等操作。
List<String> list = new ArrayList<>();
list.add("Java");
list.add("Python");
list.add("C++");
// 使用Stream API遍历并处理
list.stream()
.filter(s -> s.length() > 3) // 过滤
.map(String::toUpperCase) // 转换
.sorted() // 排序
.forEach(System.out::println);
// 收集处理结果
List<String> result = list.stream()
.filter(s -> s.length() > 3)
.map(String::toUpperCase)
.collect(Collectors.toList());
5. 双列集合详解
5.1 Map接口
Map接口定义了键值对映射的基本操作,每个键最多映射到一个值,不允许重复的键,但允许重复的值。
5.1.1 Map接口的核心方法
// 创建Map
Map<String, Integer> map = new HashMap<>();
// 添加和更新操作
map.put("Java", 95); // 添加键值对
map.put("Python", 90);
map.put("C++", 85);
map.put("Java", 97); // 更新已存在的键的值
// 获取操作
Integer javaScore = map.get("Java"); // 97
Integer goScore = map.get("Go"); // null(键不存在)
Integer cppScore = map.getOrDefault("Go", 0); // 0(默认值)
// 检查操作
boolean containsJava = map.containsKey("Java"); // true
boolean contains90 = map.containsValue(90); // true
boolean isEmpty = map.isEmpty(); // false
int size = map.size(); // 3
// 删除操作
map.remove("Python"); // 移除键为"Python"的键值对
map.remove("Java", 95); // 只有当值为95时才移除(这里不会移除,因为值是97)
// 遍历Map
// 1. 遍历键集
for (String key : map.keySet()) {
System.out.println(key + ": " + map.get(key));
}
// 2. 遍历键值对
for (Map.Entry<String, Integer> entry : map.entrySet()) {
System.out.println(entry.getKey() + ": " + entry.getValue());
}
// 3. Java 8+的forEach方法
map.forEach((key, value) -> System.out.println(key + ": " + value));
// 批量操作
Map<String, Integer> otherMap = new HashMap<>();
otherMap.put("Go", 80);
otherMap.put("JavaScript", 88);
map.putAll(otherMap); // 合并另一个Map
// 清空操作
map.clear(); // 移除所有键值对
5.1.2 Java 8+ Map新增方法
Map<String, Integer> map = new HashMap<>();
map.put("Java", 95);
map.put("Python", 90);
// 仅当键不存在时添加
map.putIfAbsent("Java", 100); // 不会改变值,仍为95
map.putIfAbsent("Scala", 75); // 会添加新键值对
// 基于当前值计算并更新
map.compute("Java", (k, v) -> v + 5); // Java值变为100
map.computeIfPresent("Python", (k, v) -> v + 10); // Python值变为100
map.computeIfAbsent("Ruby", k -> 80); // 添加新键值对
// 合并值
map.merge("Java", 2, (oldVal, newVal) -> oldVal + newVal); // Java值变为102
// 替换
map.replace("Java", 102, 95); // 如果值为102,则替换为95
map.replace("Python", 85); // 无条件替换
5.2 HashMap
HashMap是Map接口最常用的实现,基于哈希表数据结构,提供了近乎O(1)的增删改查性能。
5.2.1 实现原理
HashMap的内部结构是数组+链表+红黑树(JDK 8后):
- 内部维护一个Node<K,V>[]数组(哈希桶)
- 每个键值对存储在Node节点中
- 哈希冲突通过链表解决
- 当链表长度超过阈值(默认8)且数组长度超过最小树化容量(默认64)时,链表转换为红黑树
- 当红黑树节点数减少到6个时,红黑树转回链表
HashMap内部结构:
哈希桶数组:
┌───┬───┬───┬───┬───┬───┬───┬───┐
│ 0 │ 1 │ 2 │ 3 │ 4 │ 5 │ 6 │...│
└─┬─┴───┴───┴─┬─┴─┬─┴───┴───┴───┘
│ │ │
▼ ▼ ▼
null ┌───┐┌───┐ 链表结构
│ A ││ D │
└─┬─┘└─┬─┘
│ │
▼ ▼
┌───┐ null
│ B │
└─┬─┘
│
▼
┌───┐ 当链表过长时
│ C │ 转为红黑树结构
└───┘
关键属性:
- 初始容量:默认16,总是2的幂
- 负载因子:默认0.75,表示数组的使用率
- 阈值:容量 * 负载因子,超过此值触发扩容
5.2.2 HashMap的存取过程
put操作流程:
- 计算键的哈希值
- 确定桶位置(哈希值与数组长度-1进行与操作)
- 如果桶为空,直接放入
- 如果桶不为空,遍历链表或红黑树
- 找到相同的键,更新值
- 否则,添加到链表末尾或红黑树中
- 检查是否需要转换为红黑树
- 检查是否需要扩容
get操作流程:
- 计算键的哈希值
- 确定桶位置
- 遍历桶中的链表或红黑树,比较键是否相等
- 返回对应的值,或者null(如果键不存在)
5.2.3 HashMap的容量和负载因子
HashMap的容量(capacity)和负载因子(load factor)是影响其性能的重要参数。
容量:哈希桶数组的大小,总是2的幂,默认16。 负载因子:元素数量占容量的比例,超过此值将触发扩容,默认0.75。 扩容:创建一个容量为原来2倍的新数组,并重新分配所有元素。
// 容量和负载因子示例
// 如果预计存储1000个元素,理想的初始容量是多少?
// 计算所需容量
int expectedSize = 1000;
int capacity = (int) (expectedSize / 0.75f) + 1; // 约1334
// 找到大于等于capacity的最小2的幂
int powerOfTwo = 1;
while (powerOfTwo < capacity) {
powerOfTwo *= 2;
} // powerOfTwo = 2048
// 创建HashMap时指定初始容量
HashMap<String, Integer> map = new HashMap<>(powerOfTwo);
性能影响:
- 较高的负载因子节省空间但增加冲突概率
- 较低的负载因子减少冲突但浪费空间
- 恰当设置初始容量可减少扩容次数,提高性能
5.2.4 HashMap为什么使用红黑树
在JDK 8之前,HashMap使用数组+链表结构。当不同的键映射到同一个哈希桶时,通过链表解决冲突。但在极端情况下(多个键哈希值相同),链表会变得很长,查询性能降级为O(n)。
JDK 8引入了红黑树优化:当链表长度超过阈值(默认8)且数组长度超过最小树化容量(默认64)时,链表转换为红黑树,将最坏情况的查询复杂度从O(n)改善为O(log n)。
转换条件:
1. 链表长度 >= TREEIFY_THRESHOLD(8)
2. 数组长度 >= MIN_TREEIFY_CAPACITY(64)
如果只满足条件1但不满足条件2,HashMap会先扩容而不是转为红黑树。
为什么是链表长度为8?: 根据统计学分析,在哈希函数分布良好的情况下,链表长度超过8的概率小于百万分之一,这是一种非常极端的情况。设置为8是权衡了树化的开销与性能收益。
为什么是数组长度为64?: 如果数组太小,即使个别链表较长,扩容后的哈希分布会更均匀,可能不需要树化。只有当数组足够大且仍有长链表时,才值得付出树化的开销。
5.2.5 HashMap的哈希算法
HashMap使用键的hashCode经过扰动函数处理,减少哈希冲突。
// JDK 8 HashMap的hash方法
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
这个方法将hashCode的高16位与低16位进行异或,使得哈希值的高位也参与到索引计算中,减少冲突。
索引计算:
index = (n - 1) & hash // n是数组长度
由于n是2的幂,n-1的二进制表示全是1,按位与操作相当于取hash的低位,这也是为什么数组长度必须是2的幂。
5.2.6 HashMap的扩容机制
当HashMap中的元素数量超过容量 * 负载因子
时,会触发扩容操作。
扩容过程:
- 创建一个新的数组,容量为原来的2倍
- 重新计算每个元素的索引位置
- 将元素放入新数组
JDK 8的优化:扩容时不需要重新计算每个键的哈希值,只需判断原哈希值的新增参与位(最高位)是0还是1,决定元素在新数组中的位置是保持不变还是偏移原容量的距离。
例如,原容量16,扩容后32:
- 如果键的hash值第5位是0,位置不变
- 如果键的hash值第5位是1,新位置 = 原位置 + 16
5.2.7 HashMap与Hashtable的区别
特性 | HashMap | Hashtable |
---|---|---|
线程安全 | 否 | 是(方法同步) |
null键值 | 允许 | 不允许 |
性能 | 较高 | 较低(同步开销) |
迭代器 | fail-fast | fail-fast |
继承关系 | AbstractMap | Dictionary |
初始容量 | 16 | 11 |
扩容倍数 | 2倍 | 2倍+1 |
哈希方法 | 改进的哈希算法 | 直接使用hashCode |
推荐使用 | 一般场景 | 几乎不推荐,建议使用ConcurrentHashMap |
5.3 LinkedHashMap
LinkedHashMap是HashMap的子类,在保持HashMap高效查询特性的同时,通过双向链表维护元素的插入顺序或访问顺序。
5.3.1 实现原理
LinkedHashMap在HashMap的基础上,增加了一个双向链表,用于维护元素的插入顺序或访问顺序。
// LinkedHashMap的内部Entry节点结构(扩展了HashMap的Node)
static class Entry<K,V> extends HashMap.Node<K,V> {
Entry<K,V> before, after; // 链表的前后指针
// ...
}
5.3.2 两种顺序模式
插入顺序模式(默认):
- 元素的顺序与添加顺序一致
- 适合需要记住添加顺序的场景
访问顺序模式:
- 元素在被访问后移动到链表末尾
- 适合实现LRU(最近最少使用)缓存
// 1. 保持插入顺序(默认)
LinkedHashMap<String, Integer> map = new LinkedHashMap<>();
map.put("Java", 95);
map.put("Python", 90);
map.put("C++", 85);
// 遍历(按插入顺序)
for (Map.Entry<String, Integer> entry : map.entrySet()) {
System.out.println(entry.getKey() + ": " + entry.getValue());
}
// 输出顺序:Java: 95, Python: 90, C++: 85
// 2. 保持访问顺序(LRU顺序)
LinkedHashMap<String, Integer> accessMap = new LinkedHashMap<>(16, 0.75f, true); // 第三个参数为true表示按访问顺序
accessMap.put("Java", 95);
accessMap.put("Python", 90);
accessMap.put("C++", 85);
accessMap.get("Java"); // 访问Java,使其移动到链表尾部
// 遍历(按访问顺序)
for (Map.Entry<String, Integer> entry : accessMap.entrySet()) {
System.out.println(entry.getKey() + ": " + entry.getValue());
}
// 输出顺序:Python: 90, C++: 85, Java: 95(Java被访问过,移到了最后)
5.3.3 实现LRU缓存
LinkedHashMap可以通过重写removeEldestEntry方法轻松实现LRU(最近最少使用)缓存。
// 使用LinkedHashMap实现LRU缓存
public class LRUCache<K, V> extends LinkedHashMap<K, V> {
private final int capacity;
public LRUCache(int capacity) {
// 初始容量、负载因子、访问顺序
super(capacity, 0.75f, true);
this.capacity = capacity;
}
@Override
protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
// 当大小超过容量时,移除最老(最少使用)的元素
return size() > capacity;
}
public static void main(String[] args) {
LRUCache<String, Integer> cache = new LRUCache<>(3);
cache.put("A", 1);
cache.put("B", 2);
cache.put("C", 3);
// 访问A,使其成为最近使用的
cache.get("A");
// 添加D,此时B将被移除(最久未使用)
cache.put("D", 4);
System.out.println(cache.keySet()); // [C, A, D]
}
}
5.4 TreeMap
TreeMap是基于红黑树实现的Map集合,它保证了元素按照键的自然顺序或自定义顺序排序。
5.4.1 实现原理
TreeMap内部使用红黑树(自平衡二叉查找树)实现,保证了较好的平衡性和性能。
特点:
- 有序:元素按键的自然顺序或自定义顺序排序
- 性能:查找、插入、删除操作的平均和最坏时间复杂度为O(log n)
- 不允许null键:因为无法比较null的大小
- 允许null值
5.4.2 基本用法
// 1. 使用自然顺序
TreeMap<String, Integer> map = new TreeMap<>();
map.put("Java", 95);
map.put("Python", 90);
map.put("C++", 85);
// 遍历(按键字母顺序)
for (Map.Entry<String, Integer> entry : map.entrySet()) {
System.out.println(entry.getKey() + ": " + entry.getValue());
}
// 输出顺序:C++: 85, Java: 95, Python: 90
// 2. 使用自定义顺序
TreeMap<String, Integer> customMap = new TreeMap<>((s1, s2) -> s2.compareTo(s1)); // 逆序排序
customMap.put("Java", 95);
customMap.put("Python", 90);
customMap.put("C++", 85);
// 遍历(按键字母逆序)
for (Map.Entry<String, Integer> entry : customMap.entrySet()) {
System.out.println(entry.getKey() + ": " + entry.getValue());
}
// 输出顺序:Python: 90, Java: 95, C++: 85
5.4.3 导航方法
TreeMap提供了丰富的导航方法,可以方便地查找指定范围的键值对。
TreeMap<Integer, String> map = new TreeMap<>();
map.put(1, "One");
map.put(3, "Three");
map.put(5, "Five");
map.put(7, "Seven");
map.put(9, "Nine");
// 获取第一个和最后一个键值对
Map.Entry<Integer, String> first = map.firstEntry(); // 1=One
Map.Entry<Integer, String> last = map.lastEntry(); // 9=Nine
// 获取小于等于或大于等于指定键的键值对
Map.Entry<Integer, String> floor = map.floorEntry(4); // 3=Three(小于等于4的最大键)
Map.Entry<Integer, String> ceiling = map.ceilingEntry(4); // 5=Five(大于等于4的最小键)
// 获取小于或大于指定键的键值对
Map.Entry<Integer, String> lower = map.lowerEntry(5); // 3=Three(小于5的最大键)
Map.Entry<Integer, String> higher = map.higherEntry(5); // 7=Seven(大于5的最小键)
// 获取部分视图
SortedMap<Integer, String> headMap = map.headMap(5); // 小于5的部分:{1=One, 3=Three}
SortedMap<Integer, String> tailMap = map.tailMap(5); // 大于等于5的部分:{5=Five, 7=Seven, 9=Nine}
SortedMap<Integer, String> subMap = map.subMap(3, 8); // 3到8之间的部分(包含3,不包含8):{3=Three, 5=Five, 7=Seven}
// 轮询操作
Map.Entry<Integer, String> entry = map.pollFirstEntry(); // 移除并返回第一个元素:1=One
entry = map.pollLastEntry(); // 移除并返回最后一个元素:9=Nine
5.4.4 自定义排序
与TreeSet类似,TreeMap有两种方式定义键的排序规则:
- 自然排序:键类实现Comparable接口
- 自定义排序:创建TreeMap时提供Comparator比较器
// 1. 实现Comparable接口(自然排序)
class Person implements Comparable<Person> {
private String name;
private int age;
// 构造函数、getter和setter省略
@Override
public int compareTo(Person other) {
// 按年龄升序排序
return this.age - other.age;
}
@Override
public String toString() {
return name + "(" + age + ")";
}
}
// 使用自然排序
TreeMap<Person, String> map = new TreeMap<>();
map.put(new Person("张三", 20), "学生");
map.put(new Person("李四", 18), "学生");
map.put(new Person("王五", 22), "工程师");
// 按年龄升序:李四(18)=学生, 张三(20)=学生, 王五(22)=工程师
// 2. 使用Comparator(自定义排序)
TreeMap<Person, String> customMap = new TreeMap<>((p1, p2) -> p1.getName().compareTo(p2.getName()));
customMap.put(new Person("张三", 20), "学生");
customMap.put(new Person("李四", 18), "学生");
customMap.put(new Person("王五", 22), "工程师");
// 按姓名升序:李四(18)=学生, 王五(22)=工程师, 张三(20)=学生
5.5 Hashtable和Properties
Hashtable是早期Java提供的线程安全的Map实现,而Properties是Hashtable的子类,专门用于处理属性配置。
5.5.1 Hashtable
特点:
- 线程安全:所有方法都是同步的
- 不允许null键和null值
- 性能较差:同步带来开销
- 初始容量为11,负载因子为0.75
// Hashtable基本使用
Hashtable<String, Integer> table = new Hashtable<>();
table.put("Java", 95);
table.put("Python", 90);
table.put("C++", 85);
// table.put(null, 0); // 抛出NullPointerException
// table.put("Go", null); // 抛出NullPointerException
Integer value = table.get("Java"); // 95
table.remove("Python");
// 线程安全操作(不需要额外同步)
if (!table.containsKey("Go")) {
table.put("Go", 80);
}
注意:在现代Java应用中,很少直接使用Hashtable,通常使用ConcurrentHashMap代替。
5.5.2 Properties
Properties是Hashtable的子类,专门用于处理属性配置文件。主要特点是键和值都是字符串。
// Properties基本使用
Properties props = new Properties();
// 设置属性
props.setProperty("username", "admin");
props.setProperty("password", "123456");
props.setProperty("database.url", "jdbc:mysql://localhost:3306/mydb");
// 获取属性
String username = props.getProperty("username"); // admin
String timeout = props.getProperty("timeout", "30"); // 30(默认值)
// 遍历所有属性
for (String key : props.stringPropertyNames()) {
System.out.println(key + " = " + props.getProperty(key));
}
// 保存到文件
try (FileOutputStream out = new FileOutputStream("config.properties")) {
props.store(out, "Configuration Settings"); // 第二个参数是注释
} catch (IOException e) {
e.printStackTrace();
}
// 从文件加载
try (FileInputStream in = new FileInputStream("config.properties")) {
props.load(in);
} catch (IOException e) {
e.printStackTrace();
}
// 从XML文件加载
try (FileInputStream in = new FileInputStream("config.xml")) {
props.loadFromXML(in);
} catch (IOException e) {
e.printStackTrace();
}
// 保存为XML文件
try (FileOutputStream out = new FileOutputStream("config.xml")) {
props.storeToXML(out, "Configuration Settings");
} catch (IOException e) {
e.printStackTrace();
}
5.6 Map实现类的比较
特性 | HashMap | LinkedHashMap | TreeMap | Hashtable |
---|---|---|---|---|
内部实现 | 哈希表(数组+链表+红黑树) | HashMap + 双向链表 | 红黑树 | 哈希表 |
有序性 | 无序 | 插入顺序或访问顺序 | 键的自然顺序或自定义顺序 | 无序 |
键值限制 | 允许null键和值 | 允许null键和值 | 不允许null键 | 不允许null键和值 |
线程安全 | 否 | 否 | 否 | 是 |
性能 | 最佳 | 略低于HashMap | 中等 |
6. 集合的实际应用场景
6.1 数据缓存
使用LinkedHashMap实现简单的LRU缓存是很常见的应用。
// 实现LRU缓存(最近最少使用)
public class LRUCache<K, V> extends LinkedHashMap<K, V> {
private final int capacity;
public LRUCache(int capacity) {
super(capacity, 0.75f, true); // 访问顺序
this.capacity = capacity;
}
@Override
protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
return size() > capacity;
}
}
// 使用LRU缓存
public class DataService {
private final LRUCache<String, Object> cache;
public DataService(int cacheSize) {
this.cache = new LRUCache<>(cacheSize);
}
public Object getData(String key) {
// 先查缓存
if (cache.containsKey(key)) {
System.out.println("Cache hit: " + key);
return cache.get(key);
}
// 缓存未命中,执行昂贵的操作获取数据
System.out.println("Cache miss: " + key);
Object data = fetchDataFromDatabase(key); // 模拟从数据库获取
// 存入缓存
cache.put(key, data);
return data;
}
private Object fetchDataFromDatabase(String key) {
// 模拟昂贵的数据库操作
try {
Thread.sleep(100); // 模拟延迟
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
return "Data for " + key;
}
}
6.2 数据统计和处理
使用Map进行单词频率统计:
// 单词频率统计
public Map<String, Integer> countWords(String text) {
if (text == null || text.isEmpty()) {
return Collections.emptyMap();
}
Map<String, Integer> wordCount = new HashMap<>();
String[] words = text.split("\\s+");
for (String word : words) {
// 转为小写并移除标点符号
word = word.toLowerCase().replaceAll("[^a-zA-Z]", "");
if (!word.isEmpty()) {
// 如果词已存在,则计数+1;否则初始化为1
wordCount.put(word, wordCount.getOrDefault(word, 0) + 1);
}
}
return wordCount;
}
// 找出出现频率最高的单词
public Map.Entry<String, Integer> getMostFrequentWord(Map<String, Integer> wordCount) {
return wordCount.entrySet()
.stream()
.max(Map.Entry.comparingByValue())
.orElse(null);
}
// 使用示例
String text = "Java is a programming language. Java is also an island in Indonesia.";
Map<String, Integer> wordCount = countWords(text);
System.out.println(wordCount); // {programming=1, language=1, also=1, island=1, an=1, is=2, java=2, in=1, indonesia=1, a=1}
Map.Entry<String, Integer> mostFrequent = getMostFrequentWord(wordCount);
System.out.println("最常见的单词: " + mostFrequent.getKey() + ",出现 " + mostFrequent.getValue() + " 次");
// 最常见的单词: java,出现 2 次 (或 is,因为它们出现次数相同)
6.3 集合运算
使用Set进行并集、交集、差集等集合运算:
// 集合运算示例
Set<String> set1 = new HashSet<>(Arrays.asList("A", "B", "C"));
Set<String> set2 = new HashSet<>(Arrays.asList("B", "C", "D"));
// 并集
Set<String> union = new HashSet<>(set1);
union.addAll(set2);
System.out.println("并集: " + union); // [A, B, C, D]
// 交集
Set<String> intersection = new HashSet<>(set1);
intersection.retainAll(set2);
System.out.println("交集: " + intersection); // [B, C]
// 差集
Set<String> difference1 = new HashSet<>(set1);
difference1.removeAll(set2);
System.out.println("差集(set1 - set2): " + difference1); // [A]
Set<String> difference2 = new HashSet<>(set2);
difference2.removeAll(set1);
System.out.println("差集(set2 - set1): " + difference2); // [D]
// 对称差集(并集减去交集)
Set<String> symmetricDifference = new HashSet<>(union);
symmetricDifference.removeAll(intersection);
System.out.println("对称差集: " + symmetricDifference); // [A, D]
// Java 8+ Stream API
Set<String> unionStream = Stream.concat(set1.stream(), set2.stream())
.collect(Collectors.toSet());
6.4 多级排序
使用Collections.sort结合Comparator实现多维度排序:
// 学生类
class Student {
private String name;
private int age;
private double score;
public Student(String name, int age, double score) {
this.name = name;
this.age = age;
this.score = score;
}
// getter方法
public String getName() { return name; }
public int getAge() { return age; }
public double getScore() { return score; }
@Override
public String toString() {
return name + ", 年龄:" + age + ", 分数:" + score;
}
}
// 多级排序示例
List<Student> students = new ArrayList<>();
students.add(new Student("张三", 20, 85.5));
students.add(new Student("李四", 20, 90.0));
students.add(new Student("王五", 18, 85.5));
students.add(new Student("赵六", 22, 90.0));
// 1. 按分数降序,分数相同按年龄升序,年龄相同按姓名字母顺序
Collections.sort(students, (s1, s2) -> {
// 先按分数降序
int scoreCompare = Double.compare(s2.getScore(), s1.getScore());
if (scoreCompare != 0) {
return scoreCompare;
}
// 分数相同则按年龄升序
int ageCompare = Integer.compare(s1.getAge(), s2.getAge());
if (ageCompare != 0) {
return ageCompare;
}
// 年龄相同则按姓名升序
return s1.getName().compareTo(s2.getName());
});
// 2. Java 8+的更简洁写法
Comparator<Student> comparator = Comparator
.comparing(Student::getScore, Comparator.reverseOrder()) // 分数降序
.thenComparing(Student::getAge) // 年龄升序
.thenComparing(Student::getName); // 姓名升序
Collections.sort(students, comparator);
// 输出排序后的结果
for (Student student : students) {
System.out.println(student);
}
6.5 数据的去重和保序
使用LinkedHashSet实现数据去重并保持原顺序:
// 保持原顺序去重
public <T> List<T> removeDuplicatesPreserveOrder(List<T> list) {
return new ArrayList<>(new LinkedHashSet<>(list));
}
// 使用示例
List<String> withDuplicates = Arrays.asList("A", "B", "A", "C", "B", "D");
List<String> withoutDuplicates = removeDuplicatesPreserveOrder(withDuplicates);
System.out.println(withoutDuplicates); // [A, B, C, D]
// 使用Stream API(Java 8+)
List<String> distinctList = withDuplicates.stream()
.distinct()
.collect(Collectors.toList());
6.6 数据分组
使用Java 8的Stream API进行分组和聚合:
// 学生类
class Student {
private String name;
private int age;
private String grade;
private double score;
// 构造函数、getter方法略
}
// 数据准备
List<Student> students = new ArrayList<>();
students.add(new Student("张三", 18, "高一", 85.5));
students.add(new Student("李四", 17, "高一", 90.0));
students.add(new Student("王五", 18, "高二", 78.5));
students.add(new Student("赵六", 19, "高二", 88.0));
students.add(new Student("钱七", 17, "高三", 92.5));
// 1. 按年级分组
Map<String, List<Student>> byGrade = students.stream()
.collect(Collectors.groupingBy(Student::getGrade));
// 2. 按年级分组,进一步计算每个年级的平均成绩
Map<String, Double> gradeAvgScore = students.stream()
.collect(Collectors.groupingBy(
Student::getGrade,
Collectors.averagingDouble(Student::getScore)
));
// 3. 按年龄分组,并统计各年龄段人数
Map<Integer, Long> ageCount = students.stream()
.collect(Collectors.groupingBy(
Student::getAge,
Collectors.counting()
));
// 4. 找出每个年级分数最高的学生
Map<String, Student> topStudentByGrade = students.stream()
.collect(Collectors.toMap(
Student::getGrade,
student -> student,
(s1, s2) -> s1.getScore() > s2.getScore() ? s1 : s2
));
7. 集合的注意事项与最佳实践
7.1 线程安全
默认情况下,Java集合类不是线程安全的(除了Vector和Hashtable等旧版实现)。在多线程环境下,需要采取适当的同步措施。
7.1.1 同步包装器
使用Collections工具类的同步包装方法将普通集合包装成线程安全的:
// 创建同步集合
List<String> syncList = Collections.synchronizedList(new ArrayList<>());
Map<String, Integer> syncMap = Collections.synchronizedMap(new HashMap<>());
Set<String> syncSet = Collections.synchronizedSet(new HashSet<>());
// 注意:遍历同步集合时仍需要外部同步
synchronized (syncList) {
for (String item : syncList) {
// 处理item
}
}
7.1.2 并发集合
Java 5引入的java.util.concurrent包提供了多种并发集合,它们在多线程环境下更加高效。
// 并发集合示例
ConcurrentHashMap<String, Integer> concurrentMap = new ConcurrentHashMap<>();
CopyOnWriteArrayList<String> cowList = new CopyOnWriteArrayList<>();
CopyOnWriteArraySet<String> cowSet = new CopyOnWriteArraySet<>();
ConcurrentLinkedQueue<String> concurrentQueue = new ConcurrentLinkedQueue<>();
ConcurrentSkipListMap<String, Integer> skipListMap = new ConcurrentSkipListMap<>();
// 并发集合的迭代是线程安全的,无需额外同步
for (String item : cowList) {
// 安全地处理item
}
常见并发集合特点:
集合类 | 特点 | 适用场景 |
---|---|---|
ConcurrentHashMap | 分段锁实现,高并发读写 | 需要频繁读写的Map |
CopyOnWriteArrayList | 修改时复制整个数组,读多写少 | 读多写少的List |
CopyOnWriteArraySet | 基于CopyOnWriteArrayList实现 | 读多写少的Set |
ConcurrentLinkedQueue | 非阻塞队列,高并发 | 高并发队列操作 |
ConcurrentSkipListMap | 基于跳表实现的有序Map | 需要排序的并发Map |
7.1.3 不可变集合
不可变集合天然是线程安全的,因为它们不能被修改。
// 创建不可变集合
List<String> immutableList = List.of("A", "B", "C");
Set<String> immutableSet = Set.of("A", "B", "C");
Map<String, Integer> immutableMap = Map.of("A", 1, "B", 2, "C", 3);
// 在多线程环境下安全使用
for (String item : immutableList) {
// 处理item
}
7.2 性能优化
7.2.1 初始容量设置
合理设置集合的初始容量可以避免频繁扩容,提高性能。
// 预计存储1000个元素
List<String> list = new ArrayList<>(1000);
// HashMap容量计算(容量为2的幂,且考虑负载因子)
int expectedSize = 1000;
int capacity = (int) (expectedSize / 0.75) + 1; // 约1334
int powerOfTwo = Integer.highestOneBit(capacity - 1) << 1; // 找到>=capacity的最小2的幂:2048
HashMap<String, Integer> map = new HashMap<>(powerOfTwo);
7.2.2 选择合适的集合类型
根据具体场景选择最合适的集合类型,避免不必要的性能开销。
// 需要快速随机访问:ArrayList
List<String> randomAccessList = new ArrayList<>();
// 需要频繁插入/删除:LinkedList
List<String> frequentModList = new LinkedList<>();
// 需要快速查找和去重:HashSet
Set<String> uniqueSet = new HashSet<>();
// 需要有序且不重复:TreeSet
Set<String> orderedSet = new TreeSet<>();
// 需要键值对映射:HashMap
Map<String, Integer> fastMap = new HashMap<>();
// 需要按插入顺序:LinkedHashMap
Map<String, Integer> orderedMap = new LinkedHashMap<>();
// 需要按键排序:TreeMap
Map<String, Integer> sortedMap = new TreeMap<>();
7.2.3 避免频繁扩容
集合的扩容操作通常涉及创建新的内部数组和复制元素,开销较大。合理设置初始容量可以减少扩容次数。
// 不推荐:频繁扩容
ArrayList<String> badList = new ArrayList<>(); // 默认容量10
for (int i = 0; i < 1000; i++) {
badList.add("Item " + i); // 会多次扩容
}
// 推荐:一次性设置足够的容量
ArrayList<String> goodList = new ArrayList<>(1000);
for (int i = 0; i < 1000; i++) {
goodList.add("Item " + i); // 不会扩容
}
7.2.4 使用批量操作
尽量使用批量操作而不是逐个操作,可以提高性能。
// 不推荐:逐个添加
List<String> list1 = new ArrayList<>();
list1.add("A");
list1.add("B");
list1.add("C");
// 推荐:使用构造函数或批量添加
List<String> list2 = new ArrayList<>(Arrays.asList("A", "B", "C"));
// 或者
List<String> list3 = new ArrayList<>();
Collections.addAll(list3, "A", "B", "C");
7.2.5 使用Stream API进行并行处理
对于大量数据的处理,可以考虑使用Stream API的并行流提高性能。
// 串行处理
long count = list.stream()
.filter(item -> item.length() > 3)
.count();
// 并行处理
long parallelCount = list.parallelStream()
.filter(item -> item.length() > 3)
.count();
7.3 常见陷阱与错误
7.3.1 在foreach循环中修改集合
在增强for循环(foreach)中修改集合会导致ConcurrentModificationException。
// 错误写法
List<String> list = new ArrayList<>(Arrays.asList("A", "B", "C"));
for (String item : list) {
if ("B".equals(item)) {
list.remove(item); // 抛出ConcurrentModificationException
}
}
// 正确写法1:使用Iterator
List<String> list = new ArrayList<>(Arrays.asList("A", "B", "C"));
Iterator<String> it = list.iterator();
while (it.hasNext()) {
String item = it.next();
if ("B".equals(item)) {
it.remove(); // 安全地删除元素
}
}
// 正确写法2:使用removeIf(Java 8+)
List<String> list = new ArrayList<>(Arrays.asList("A", "B", "C"));
list.removeIf(item -> "B".equals(item));
// 正确写法3:使用索引遍历(仅适用于List)
List<String> list = new ArrayList<>(Arrays.asList("A", "B", "C"));
for (int i = list.size() - 1; i >= 0; i--) {
if ("B".equals(list.get(i))) {
list.remove(i);
}
}
// 正确写法4:先收集要删除的元素,然后一次性删除
List<String> list = new ArrayList<>(Arrays.asList("A", "B", "C"));
List<String> toRemove = new ArrayList<>();
for (String item : list) {
if ("B".equals(item)) {
toRemove.add(item);
}
}
list.removeAll(toRemove);
7.3.2 未重写equals和hashCode方法
自定义类作为HashMap/HashSet的键时,必须正确重写equals和hashCode方法,否则可能导致预期外的行为
7.3.3 忽略泛型的类型安全
不使用泛型或使用原始类型会丧失类型安全的好处,可能导致ClassCastException。
// 错误示例:使用原始类型
List list = new ArrayList();
list.add("字符串");
list.add(100); // 混合不同类型的元素
// 运行时错误
String s = (String) list.get(1); // ClassCastException
// 正确示例:使用泛型
List<String> stringList = new ArrayList<>();
stringList.add("字符串");
// stringList.add(100); // 编译错误,类型安全
// 类型安全的操作
String s = stringList.get(0); // 无需强制类型转换
7.3.4 忽略空值检查
使用集合时,应当进行适当的空值检查,避免NullPointerException。
// 潜在问题:未检查null
Map<String, Object> map = getMap(); // 方法可能返回null
Object value = map.get("key"); // 如果map为null,抛出NullPointerException
// 正确做法:检查null
Map<String, Object> map = getMap();
if (map != null) {
Object value = map.get("key");
// 处理value
}
// 或者使用Optional(Java 8+)
Optional.ofNullable(getMap())
.map(m -> m.get("key"))
.ifPresent(value -> {
// 处理value
});
7.3.5 误用线程不安全的集合
在多线程环境下使用线程不安全的集合可能导致数据不一致或异常。
// 错误示例:多线程环境下使用普通HashMap
Map<String, Integer> map = new HashMap<>();
// 在多个线程中并发操作map...
// 正确示例:使用线程安全的集合
Map<String, Integer> safeMap = new ConcurrentHashMap<>();
// 或者
Map<String, Integer> syncMap = Collections.synchronizedMap(new HashMap<>());
7.4 集合框架的最佳实践
7.4.1 恰当使用泛型
始终为集合指定合适的泛型类型,提高代码的类型安全性和可读性。
// 推荐:使用泛型
List<String> stringList = new ArrayList<>();
Map<String, Integer> scoreMap = new HashMap<>();
// 不推荐:使用原始类型
List rawList = new ArrayList();
Map rawMap = new HashMap();
7.4.2 使用接口类型而非实现类型
声明集合变量时,使用接口类型而不是具体实现类型,提高代码的灵活性。
// 推荐:使用接口类型
List<String> list = new ArrayList<>();
Map<String, Integer> map = new HashMap<>();
// 不推荐:使用实现类型
ArrayList<String> arrayList = new ArrayList<>();
HashMap<String, Integer> hashMap = new HashMap<>();
7.4.3 返回空集合而非null
方法返回集合时,如果没有元素,应返回空集合而不是null,避免调用者处理null的麻烦。
// 不推荐:返回null
public List<String> getNames() {
if (noNamesAvailable()) {
return null; // 调用者需要检查null
}
// ...
}
// 推荐:返回空集合
public List<String> getNames() {
if (noNamesAvailable()) {
return Collections.emptyList(); // 不会引发NullPointerException
}
// ...
}
7.4.4 优先使用Stream API进行集合操作
Java 8引入的Stream API提供了更简洁、更具表达力的集合操作方式。
// 传统方式
List<String> filtered = new ArrayList<>();
for (String str : strings) {
if (str.length() > 3) {
filtered.add(str.toUpperCase());
}
}
// Stream API方式
List<String> filtered = strings.stream()
.filter(str -> str.length() > 3)
.map(String::toUpperCase)
.collect(Collectors.toList());
7.4.5 适当使用不可变集合
对于不需要修改的集合,使用不可变集合可以提高安全性和性能。
// 创建不可变集合
List<String> constants = List.of("A", "B", "C");
Set<String> uniqueConstants = Set.of("X", "Y", "Z");
Map<String, Integer> codeMap = Map.of("A", 1, "B", 2, "C", 3);
// 或者使用Collections工具类(适用于JDK 9之前)
List<String> immutableList = Collections.unmodifiableList(new ArrayList<>(Arrays.asList("A", "B", "C")));
7.4.6 防御性复制
返回集合引用时,如果不希望调用者修改内部状态,应进行防御性复制。
// 错误示例:直接返回内部集合引用
private List<String> items = new ArrayList<>();
public List<String> getItems() {
return items; // 调用者可以修改内部状态
}
// 正确示例:返回防御性复制
public List<String> getItems() {
return new ArrayList<>(items); // 返回副本,调用者修改不会影响原集合
}
// 或者返回不可修改的视图
public List<String> getItems() {
return Collections.unmodifiableList(items); // 调用者尝试修改会抛出异常
}
8. 面试常见问题
8.1 ArrayList和LinkedList的区别?
答:ArrayList和LinkedList都实现了List接口,但内部实现和性能特点有很大差异:
- 内部实现:
- ArrayList基于动态数组实现
- LinkedList基于双向链表实现
- 性能特点:
- ArrayList随机访问元素的时间复杂度为O(1),但插入和删除的时间复杂度为O(n)(尤其是在列表中间)
- LinkedList随机访问元素的时间复杂度为O(n),但在已知位置插入和删除的时间复杂度为O(1)
- 内存占用:
- LinkedList由于需要存储前后引用,所以内存占用较ArrayList更大
- ArrayList在扩容时可能会浪费一些空间
- 应用场景:
- ArrayList适合需要频繁随机访问的场景
- LinkedList适合需要频繁在两端插入删除的场景,也可以用作队列或栈
8.2 HashMap的内部实现原理?
答:HashMap基于哈希表实现,在JDK 8中是数组+链表+红黑树的结构:
- 基本结构:HashMap内部维护一个Node<K,V>[]数组(哈希桶)
- 工作原理:
- 计算键的哈希值
- 通过哈希值确定在数组中的位置(桶位置)
- 如果桶为空,直接插入
- 如果桶不为空(哈希冲突),则遍历链表或红黑树,比较键是否相等
- 如果键相等,更新值
- 如果键不相等,添加到链表末尾或红黑树中
- JDK 8的优化:
- 当链表长度超过8且数组长度超过64时,链表会转换为红黑树,提高查询效率
- 当红黑树节点数减少到6时,红黑树会转回链表
- 负载因子:
- 默认负载因子是0.75
- 当元素数量超过容量*负载因子时,会触发扩容
- 扩容时容量翻倍,并重新计算所有键的位置
- 哈希函数优化:
- 通过(h = key.hashCode()) ^ (h >>> 16)的方式混合哈希值的高16位和低16位,减少哈希冲突
8.3 HashSet和HashMap的关系?
答:HashSet是基于HashMap实现的,两者有密切的关系:
- 内部实现:
- HashSet内部使用HashMap存储元素
- HashSet的元素作为HashMap的键
- HashMap的值是一个固定的Object对象(常量PRESENT)
- 功能映射:
- HashSet的add(E e)方法通过调用HashMap的put(K,V)实现
- HashSet的remove(Object o)方法通过调用HashMap的remove(Object key)实现
- HashSet的contains(Object o)方法通过调用HashMap的containsKey(Object key)实现
- 性能特性:
- 由于基于HashMap实现,HashSet的添加、删除、查找操作的时间复杂度都是O(1)
- HashSet不保证元素的顺序
- 源码关系:
public class HashSet<E> extends AbstractSet<E> implements Set<E> { private transient HashMap<E,Object> map; private static final Object PRESENT = new Object(); public HashSet() { map = new HashMap<>(); } public boolean add(E e) { return map.put(e, PRESENT) == null; } // 其他方法... }
8.4 ConcurrentHashMap的实现原理及与HashMap的区别?
答:ConcurrentHashMap是线程安全的HashMap实现,两者有以下区别:
- 线程安全性:
- HashMap不是线程安全的
- ConcurrentHashMap专门为并发环境设计,线程安全
- 实现方式:
- JDK 7中,ConcurrentHashMap采用分段锁(Segment)实现,将数据分为多个段,每段独立加锁
- JDK 8中,ConcurrentHashMap放弃了分段锁,采用CAS + synchronized来保证并发安全
- 空值支持:
- HashMap允许一个null键和多个null值
- ConcurrentHashMap不允许null键或null值
- 性能:
- 在单线程环境下,HashMap性能略高
- 在多线程环境下,ConcurrentHashMap性能远高于同步的HashMap
- 迭代器:
- HashMap的迭代器是fail-fast的(发现并发修改会抛出ConcurrentModificationException)
- ConcurrentHashMap的迭代器是弱一致性的(可以容忍并发修改)
8.5 TreeMap和HashMap的区别?
答:TreeMap和HashMap都实现了Map接口,但它们在内部实现和功能特点上有显著差异:
- 内部实现:
- HashMap基于哈希表实现(数组+链表+红黑树)
- TreeMap基于红黑树实现
- 有序性:
- HashMap不保证元素顺序
- TreeMap保证按照键的自然顺序或自定义比较器排序
- 性能:
- HashMap的添加、删除、查找操作时间复杂度为O(1)
- TreeMap的添加、删除、查找操作时间复杂度为O(log n)
- 键的要求:
- HashMap允许null键
- TreeMap不允许null键(因为无法比较)
- TreeMap的键必须实现Comparable接口或提供Comparator
- 导航方法:
- TreeMap提供了丰富的导航方法,如firstEntry()、lastEntry()、floorEntry()、ceilingEntry()等
- HashMap没有这类导航方法
8.6 Java集合框架中的fail-fast和fail-safe是什么?
答:fail-fast和fail-safe是Java集合框架中两种不同的迭代器行为模式:
- fail-fast(快速失败):
- 大多数Java集合(如ArrayList、HashMap)的迭代器都是fail-fast的
- 当迭代器创建后,如果集合结构被修改(添加、删除元素,而不是修改已有元素),会抛出ConcurrentModificationException
- 实现原理是通过modCount(修改计数器)来检测并发修改
- 目的是提醒用户检测并发问题
- 例如:ArrayList的迭代器在迭代过程中,如果调用ArrayList的add/remove方法修改结构,会抛出异常
- fail-safe(安全失败):
- java.util.concurrent包中的集合(如ConcurrentHashMap、CopyOnWriteArrayList)的迭代器是fail-safe的
- 这些迭代器操作的是集合的副本,因此允许在迭代过程中修改原集合
- 不会抛出ConcurrentModificationException
- 缺点是可能看不到最新修改,存在弱一致性
- 例如:CopyOnWriteArrayList在迭代过程中,可以安全地调用其add/remove方法
- 示例代码:
// fail-fast示例 List<String> list = new ArrayList<>(); list.add("A"); list.add("B"); // 错误方式:抛出ConcurrentModificationException for (String item : list) { if ("A".equals(item)) { list.remove(item); // 会抛出异常 } } // fail-safe示例 List<String> safeList = new CopyOnWriteArrayList<>(); safeList.add("A"); safeList.add("B"); // 安全方式:不会抛出异常 for (String item : safeList) { if ("A".equals(item)) { safeList.remove(item); // 安全 } }
8.7 HashMap和Hashtable的区别?
答:HashMap和Hashtable都实现了Map接口,但有以下重要区别:
- 线程安全性:
- HashMap不是线程安全的
- Hashtable是线程安全的,所有方法都使用synchronized修饰
- 性能:
- 由于没有同步开销,HashMap在单线程环境下性能更好
- Hashtable在多线程环境下安全,但性能较差(现代应用中通常使用ConcurrentHashMap代替)
- 空值支持:
- HashMap允许一个null键和多个null值
- Hashtable不允许null键或null值,否则抛出NullPointerException
- 继承关系:
- HashMap继承自AbstractMap
- Hashtable继承自Dictionary(一个过时的类)
- 初始容量和扩容:
- HashMap默认初始容量为16,扩容为原来的2倍
- Hashtable默认初始容量为11,扩容为原来的2倍+1
- 迭代器:
- HashMap使用Iterator迭代
- Hashtable除了Iterator外,还有Enumeration(遗留接口)
- 历史:
- Hashtable是Java 1.0就存在的遗留类
- HashMap是Java 1.2引入的,设计更优雅,是现代Java程序的推荐选择
8.8 如何选择合适的集合类?
答:选择合适的集合类需要考虑以下因素:
- 数据结构需求:
- 需要有序且可重复的元素序列 → List (ArrayList, LinkedList)
- 需要唯一元素 → Set (HashSet, LinkedHashSet, TreeSet)
- 需要键值对映射 → Map (HashMap, LinkedHashMap, TreeMap)
- 需要队列或栈 → Queue/Deque (LinkedList, ArrayDeque)
- 性能需求:
- 需要快速随机访问 → ArrayList, HashMap
- 需要快速插入/删除 → LinkedList
- 需要元素排序 → TreeSet, TreeMap
- 需要保持插入顺序 → LinkedHashSet, LinkedHashMap
- 需要处理大数据量 → HashMap, ArrayList (避免频繁扩容)
- 线程安全需求:
- 单线程环境 → HashMap, ArrayList 等非同步集合
- 多线程读多写少 → CopyOnWriteArrayList, CopyOnWriteArraySet
- 多线程读写频繁 → ConcurrentHashMap, ConcurrentSkipListMap
- 传统同步需求 → Collections.synchronizedXxx()
- 特殊功能需求:
- 需要操作头尾元素 → LinkedList, ArrayDeque
- 需要按优先级出队 → PriorityQueue
- 需要缓存实现 → LinkedHashMap (设置accessOrder=true)
- 需要范围查询 → TreeMap
- 选择指南:
集合选择速查表
如果你想要... 就使用... 最常用? 元素可重复 ArrayList ✓ 元素可重复,且增删操作明显多于查询 LinkedList 元素去重 HashSet ✓ 元素去重,且保持插入顺序 LinkedHashSet 元素去重,且需要排序 TreeSet 键值对映射 HashMap ✓ 键值对映射,且保持插入顺序 LinkedHashMap 键值对映射,且需要按键排序 TreeMap 线程安全的集合 ConcurrentHashMap, CopyOnWriteArrayList 不可变集合 List.of(), Set.of(), Map.of() 主要集合类的时间复杂度
集合类型 get/containsKey add/put remove 遍历 说明 ArrayList O(1) 均摊O(1) O(n) O(n) 随机访问快,中间插入删除慢 LinkedList O(n) O(1) O(1) O(n) 随机访问慢,两端插入删除快 HashSet O(1) O(1) O(1) O(n) 最快的Set,但无序 LinkedHashSet O(1) O(1) O(1) O(n) 比HashSet稍慢,但有序 TreeSet O(log n) O(log n) O(log n) O(n) 有序但较慢 HashMap O(1) O(1) O(1) O(n) 最快的Map,但无序 LinkedHashMap O(1) O(1) O(1) O(n) 比HashMap稍慢,但有序 TreeMap O(log n) O(log n) O(log n) O(n) 有序但较慢
9. 总结
Java集合框架是Java编程中不可或缺的部分,为管理和操作数据提供了强大而灵活的工具。本文详细介绍了集合框架的核心组件、基本用法和实际应用场景,以及优化技巧和常见陷阱。
9.1 核心知识点总结
- 集合框架的基本结构:
- 单列集合(Collection):List、Set、Queue
- 双列集合(Map):HashMap、LinkedHashMap、TreeMap等
- 主要实现类及其特点:
- ArrayList:基于数组实现,随机访问快,插入删除慢
- LinkedList:基于链表实现,插入删除快,随机访问慢
- HashSet:基于HashMap实现,无序,不重复
- LinkedHashSet:保持插入顺序的HashSet
- TreeSet:有序的Set,基于TreeMap实现
- HashMap:哈希表实现,键值对映射,无序
- LinkedHashMap:保持插入顺序或访问顺序的HashMap
- TreeMap:有序的Map,基于红黑树实现
- 集合选择准则:
- 需要元素唯一性?选择Set
- 需要保持插入顺序?选择LinkedHashXxx
- 需要元素排序?选择TreeXxx
- 需要频繁随机访问?选择ArrayList
- 需要频繁插入删除?选择LinkedList
- 需要键值对映射?选择Map族
- 性能优化关键点:
- 合理设置初始容量,避免频繁扩容
- 选择适合场景的集合类型
- 正确实现equals和hashCode方法
- 使用并发集合处理多线程场景
- 常见陷阱:
- 在forEach循环中修改集合结构
- 未重写equals和hashCode方法
- 忽略泛型的类型安全
- 在多线程环境中使用非线程安全的集合
9.2 学习和使用建议
- 循序渐进:
- 先掌握基础集合(ArrayList, HashMap)
- 再学习特殊功能集合(LinkedHashMap, TreeSet)
- 最后了解并发集合(ConcurrentHashMap, CopyOnWriteArrayList)
- 实战练习:
- 实现常见数据结构(如LRU缓存、计数器)
- 解决算法题(大多需要使用集合)
- 在实际项目中灵活运用
- 深入源码:
- 阅读集合框架的源代码,理解实现原理
- 关注扩容机制、哈希算法、红黑树等关键实现
- 性能测试:
- 对比不同集合在特定场景下的性能
- 了解集合操作的时间复杂度和空间复杂度
Java集合框架经过多年发展,已经非常成熟和强大。通过合理选择和正确使用集合类,可以显著提高程序的性能和可读性。无论是处理简单数据集还是构建复杂系统,Java集合框架都能提供有力支持。
掌握集合框架不仅是Java开发者的基本素养,也是进阶高级开发的必备技能。希望本文能帮助你更好地理解和使用Java集合框架,写出更高效、更优雅的代码。