作者:没有四次元口袋的蓝胖
日期:2026-07-03
标签:Java, 集合框架, HashSet, 阻塞队列
Java集合(4)
一、HashSet 去重原理
1.1 HashSet 是什么?
HashSet 是 Set 接口的实现类,特点是元素无序、不可重复。
java
HashSet<String> set = new HashSet<>();
set.add("A");
set.add("B");
set.add("A"); // 添加失败,因为A已经存在
System.out.println(set.size()); // 2
HashSet 的底层就是 HashMap! 所有元素都存在 HashMap 的 key 上,value 是一个固定的 Object 对象(PRESENT)。
1.2 底层结构
java
public class HashSet<E> extends AbstractSet<E>
implements Set<E>, Cloneable, java.io.Serializable {
// 底层就是 HashMap
private transient HashMap<E,Object> map;
// 所有 key 共享的 value(一个虚拟对象)
private static final Object PRESENT = new Object();
public HashSet() {
map = new HashMap<>();
}
public boolean add(E e) {
return map.put(e, PRESENT) == null;
}
public boolean remove(Object o) {
return map.remove(o) == PRESENT;
}
public boolean contains(Object o) {
return map.containsKey(o);
}
}
一句话总结:HashSet 就是套了一层壳的 HashMap,只关心 key,value 是摆设。
1.3 去重原理
HashSet 去重依赖两个方法:hashCode() 和 equals()。
添加元素的过程:
add(新元素)
↓
计算新元素的 hashCode() → 定位到哪个桶
↓
这个桶里有元素吗?
├── 没有 → 直接添加,成功
└── 有 → 逐个和桶里的元素比较
├── hash 都不一样 → 不重复,添加
└── hash 相同 → 再调用 equals 比较
├── equals 返回 false → 不重复,添加
└── equals 返回 true → 重复,不添加
核心规则:
- hashCode 不同 → 两个对象一定不同(不用比 equals 了)
- hashCode 相同 → 两个对象不一定相同(哈希冲突,要继续比 equals)
- equals 相同 → 两个对象一定相同(判定重复,不添加)
1.4 为什么要同时用 hashCode 和 equals?
- 只用 hashCode:哈希冲突时,不同对象可能 hash 相同,会误判为重复
- 只用 equals:所有元素都要逐个 equals 比较,效率太低(O(n))
- 两者结合:先用 hashCode 快速定位到桶(O(1)),桶里元素少了再用 equals 精确比较,又快又准
1.5 为什么重写 equals 必须重写 hashCode?
面试必考题!
场景: 有一个 Person 类,重写了 equals(按 name 和 age 判断相等),但没重写 hashCode。
java
class Person {
String name;
int age;
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Person)) return false;
Person p = (Person) o;
return age == p.age && Objects.equals(name, p.name);
}
// ❌ 没重写 hashCode!用的是 Object 默认的(地址值)
}
问题:
java
HashSet<Person> set = new HashSet<>();
Person p1 = new Person("张三", 20);
Person p2 = new Person("张三", 20);
set.add(p1);
set.add(p2);
System.out.println(set.size()); // 2!两个"相同"的人都加进去了
原因:
- p1 和 p2 是不同对象,Object 默认的 hashCode 不一样
- hashCode 不同,直接被分到不同的桶里,根本不会触发 equals 比较
- 所以两个都加进去了,违反了 Set 的语义
正确做法: equals 和 hashCode 必须一起重写,保持一致。
java
class Person {
String name;
int age;
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Person)) return false;
Person p = (Person) o;
return age == p.age && Objects.equals(name, p.name);
}
@Override
public int hashCode() {
return Objects.hash(name, age); // Java 7+ 工具方法
}
}
规范约定(来自 Object 类的文档):
- 如果两个对象 equals 相等,那么 hashCode 必须相等
- 如果两个对象 hashCode 相等,equals 不一定相等(允许哈希冲突)
- 重写 equals 就必须重写 hashCode
1.6 TreeSet 去重(顺带了解)
TreeSet 是另一种 Set 实现,底层是红黑树,元素有序(按自然顺序或比较器排序)。
TreeSet 去重不依赖 equals 和 hashCode,依赖 Comparable/Comparator!
java
// 方式1:元素实现 Comparable 接口
class Person implements Comparable<Person> {
String name;
int age;
@Override
public int compareTo(Person o) {
// 返回 0 表示相等(去重)
// 返回负数表示 this < o
// 返回正数表示 this > o
int num = this.age - o.age;
return num == 0 ? this.name.compareTo(o.name) : num;
}
}
// 方式2:构造时传入 Comparator
TreeSet<Person> set = new TreeSet<>((p1, p2) -> p1.getAge() - p2.getAge());
TreeSet 去重看 compareTo/compare 返回 0,和 equals 没关系。
但规范上建议和 equals 保持一致,否则会违反 Set 接口的约定。
📌 常见面试题
Q:HashSet 怎么实现去重的?
底层是 HashMap,添加元素时先算 hashCode 定位桶,桶里有元素再用 equals 比较。hashCode 相同且 equals 为 true 就认为重复,不添加。
Q:重写 equals 为什么必须重写 hashCode?
如果只重写 equals 不重写 hashCode,两个 equals 相等的对象可能因为 hashCode 不同被放到不同的桶里,导致 HashSet 中出现重复元素,违反 Set 的语义。
Q:HashSet 和 TreeSet 的区别?
- 底层:HashSet 基于哈希表,TreeSet 基于红黑树
- 有序性:HashSet 无序,TreeSet 有序(排序)
- 去重方式:HashSet 用 hashCode+equals,TreeSet 用 Comparable/Comparator
- 性能:HashSet 增删查都是 O(1),TreeSet 是 O(log n)
- 适用:HashSet 用于去重,TreeSet 用于需要排序的场景
二、阻塞队列(BlockingQueue)基础 ⭐
2.1 什么是阻塞队列?
阻塞队列是一种特殊的队列,支持阻塞式的添加和移除操作。
- 队列满了:再往里加元素的线程会被阻塞,直到有元素被取走
- 队列空了:再往外取元素的线程会被阻塞,直到有元素放进去
非常适合生产者-消费者模式!
生产者 → [][][][][][] → 消费者
阻塞队列
满了就等 空了就等
2.2 四组操作方法
BlockingQueue 提供了 4 种不同风格的操作方式:
| 操作方式 | 抛出异常 | 返回特殊值 | 阻塞等待 | 超时等待 |
|---|---|---|---|---|
| 入队 | add(e) |
offer(e) |
put(e) |
offer(e, time, unit) |
| 出队 | remove() |
poll() |
take() |
poll(time, unit) |
| 检查队首 | element() |
peek() |
- | - |
怎么记:
- 抛异常:add/remove/element(极端情况直接抛)
- 返回值:offer/poll/peek(返回 boolean 或 null)
- 阻塞:put/take(没满/没空就一直等)
- 超时:offer(time)/poll(time)(等一段时间还不行就放弃)
2.3 代码示例
java
BlockingQueue<String> queue = new ArrayBlockingQueue<>(3); // 容量为3
// --- 抛出异常 ---
queue.add("A"); // 成功返回 true
queue.add("B");
queue.add("C");
queue.add("D"); // 队列满了,抛 IllegalStateException
queue.remove(); // 返回并移除队首
queue.element(); // 返回队首但不移除
// --- 返回特殊值 ---
queue.offer("A"); // 成功 true,失败 false
queue.poll(); // 有值返回,没值返回 null
queue.peek(); // 有值返回队首,没值返回 null
// --- 阻塞等待 ---
queue.put("A"); // 满了就阻塞等待
String s = queue.take(); // 空了就阻塞等待
// --- 超时等待 ---
queue.offer("A", 3, TimeUnit.SECONDS); // 等3秒还满就放弃
queue.poll(3, TimeUnit.SECONDS); // 等3秒还空就返回 null
2.4 常见实现类
| 实现类 | 底层结构 | 是否有界 | 特点 |
|---|---|---|---|
| ArrayBlockingQueue | 数组 | 有界(必须指定容量) | 经典有界队列,一把锁,读写不分离 |
| LinkedBlockingQueue | 链表 | 可选有界(默认 Integer.MAX_VALUE) | 两把锁,读写分离,并发性能更好 |
| PriorityBlockingQueue | 二叉堆 | 无界 | 优先级队列,按优先级出队 |
| DelayQueue | 二叉堆 | 无界 | 延迟队列,元素到期才能取出 |
| SynchronousQueue | 不存储 | - | 手递手,不存元素,put 必须等 take |
| LinkedTransferQueue | 链表 | 无界 | 增加了 transfer 方法,支持更灵活的传递 |
2.5 ArrayBlockingQueue vs LinkedBlockingQueue
| 对比项 | ArrayBlockingQueue | LinkedBlockingQueue |
|---|---|---|
| 底层 | 数组 | 链表 |
| 容量 | 必须指定(有界) | 可选(默认无界=Integer.MAX_VALUE) |
| 锁 | 一把锁(读写同一把) | 两把锁(读锁 + 写锁,读写分离) |
| 性能 | 稍低(锁竞争大) | 稍高(读写互不阻塞) |
| 内存 | 预分配数组,连续内存 | 节点按需创建,内存碎片 |
| GC 压力 | 小(数组复用) | 大(频繁创建销毁节点) |
2.6 生产者-消费者示例
阻塞队列最经典的应用:生产者-消费者模式
java
public class ProducerConsumerDemo {
public static void main(String[] args) {
BlockingQueue<Integer> queue = new ArrayBlockingQueue<>(10);
// 生产者线程
new Thread(() -> {
int i = 0;
while (true) {
try {
queue.put(i++);
System.out.println("生产了:" + (i-1) + ",队列大小:" + queue.size());
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "生产者").start();
// 消费者线程
new Thread(() -> {
while (true) {
try {
Integer take = queue.take();
System.out.println("消费了:" + take + ",队列大小:" + queue.size());
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "消费者").start();
}
}
好处:
- 生产者和消费者完全解耦,通过队列通信
- 生产快了就等队列有空间,消费快了就等队列有数据
- 不用自己写 wait/notify,代码简洁且不易出错
2.7 SynchronousQueue(特殊的队列)
java
SynchronousQueue<String> sq = new SynchronousQueue<>();
特点:
- 没有容量,不存储元素
- 每个 put 必须等一个 take,每个 take 必须等一个 put
- 相当于"手递手"传递
- CachedThreadPool 用的就是它
java
// 线程1:put
new Thread(() -> {
try {
sq.put("hello"); // 阻塞,等有人来 take
System.out.println("put 成功");
} catch (InterruptedException e) { }
}).start();
Thread.sleep(1000);
// 线程2:take
new Thread(() -> {
try {
String s = sq.take(); // 拿到了,put 那边也会解除阻塞
System.out.println("take 到:" + s);
} catch (InterruptedException e) { }
}).start();
📌 常见面试题
Q:ArrayBlockingQueue 和 LinkedBlockingQueue 的区别?
数组 vs 链表;有界 vs 可选有界;一把锁 vs 两把锁(读写分离);性能后者稍高。
Q:阻塞队列的实现原理?
基于 ReentrantLock + Condition 实现。以 ArrayBlockingQueue 为例,一把锁两个条件(notEmpty 和 notFull),队列为空就等 notEmpty,队列满了就等 notFull。
Q:什么场景用阻塞队列?
生产者-消费者模式、线程池(任务队列)、消息队列、异步任务处理等。
三、集合工具类
3.1 Collections 工具类
java.util.Collections 是 Collection 框架的工具类,提供了大量静态方法操作集合。
排序操作
java
List<Integer> list = new ArrayList<>(Arrays.asList(3, 1, 4, 2, 5));
// 自然排序(升序)
Collections.sort(list); // [1, 2, 3, 4, 5]
// 自定义排序
Collections.sort(list, (a, b) -> b - a); // 降序 [5, 4, 3, 2, 1]
// 反转
Collections.reverse(list);
// 随机打乱(洗牌)
Collections.shuffle(list);
// 交换位置
Collections.swap(list, 0, list.size() - 1);
// 旋转(元素向右移动 n 位,尾部绕到头部)
Collections.rotate(list, 2);
查找替换
java
List<Integer> list = Arrays.asList(3, 1, 4, 1, 5);
// 最大/最小值
Collections.max(list); // 5
Collections.min(list); // 1
// 自定义比较器找最大
Collections.max(list, Comparator.comparingInt(a -> a));
// 二分查找(必须先排序!)
Collections.sort(list);
int index = Collections.binarySearch(list, 4); // 返回下标 3
// 替换所有匹配的
Collections.replaceAll(list, 1, 100); // 所有1换成100
// 填充
Collections.fill(list, 0); // 全部填成 0
// 统计出现次数
int count = Collections.frequency(list, 1); // 1出现了几次
同步控制(线程安全包装)
java
// 包装成线程安全的集合
List<String> syncList = Collections.synchronizedList(new ArrayList<>());
Set<String> syncSet = Collections.synchronizedSet(new HashSet<>());
Map<String, String> syncMap = Collections.synchronizedMap(new HashMap<>());
原理: 在方法外包一层 synchronized 锁,锁的是包装对象本身。
缺点: 和 Hashtable 一样,全表锁,并发度低。读读也互斥。并发高的场景还是推荐用 ConcurrentHashMap、CopyOnWriteArrayList 等专用并发集合。
不可变集合
java
// 返回空的不可变集合
List<String> emptyList = Collections.emptyList();
Set<String> emptySet = Collections.emptySet();
Map<String, String> emptyMap = Collections.emptyMap();
// 返回只有一个元素的不可变集合
List<String> singletonList = Collections.singletonList("A");
Set<String> singleton = Collections.singleton("A");
// 将任意集合包装成不可变的
List<String> unmodifiable = Collections.unmodifiableList(originalList);
unmodifiable.add("X"); // 抛 UnsupportedOperationException
注意: 不可变集合只是不能修改引用,原集合如果被修改了,不可变视图也会跟着变(因为是同一个底层)。
其他常用
java
// 拷贝(目标列表长度必须 >= 源列表)
List<String> dest = new ArrayList<>(Arrays.asList("", "", ""));
Collections.copy(dest, src);
// 集合中是否没有交集
boolean disjoint = Collections.disjoint(list1, list2);
// 枚举类转集合
Enumeration<String> en = ...;
ArrayList<String> list = Collections.list(en);
3.2 Arrays 工具类
java.util.Arrays 是数组的工具类,也经常和集合配合使用。
数组转集合
java
// asList ------ 把数组转成 List
String[] arr = {"A", "B", "C"};
List<String> list = Arrays.asList(arr);
// ⚠️ 注意:asList 返回的是 Arrays 内部的 ArrayList,不是 java.util.ArrayList
// 它的大小是固定的,不能 add/remove!
list.add("D"); // 抛 UnsupportedOperationException
// 正确做法:用 new ArrayList 包一层
List<String> realList = new ArrayList<>(Arrays.asList(arr));
realList.add("D"); // 可以正常操作
常用方法
java
int[] arr = {3, 1, 4, 1, 5, 9, 2, 6};
// 排序
Arrays.sort(arr); // [1, 1, 2, 3, 4, 5, 6, 9]
// 二分查找(必须先排序)
int index = Arrays.binarySearch(arr, 5); // 返回下标 5
// 填充
Arrays.fill(arr, 0); // 全填成 0
// 复制(截取/扩容)
int[] newArr = Arrays.copyOf(arr, 10); // 复制到长度为10的新数组
int[] rangeArr = Arrays.copyOfRange(arr, 2, 5); // 截取下标2到5
// 比较数组内容
boolean equals = Arrays.equals(arr1, arr2);
// 打印数组(调试用,比直接 toString 好看)
System.out.println(Arrays.toString(arr));
// 二维数组用 deepToString
System.out.println(Arrays.deepToString(matrix));
// hashCode
int hash = Arrays.hashCode(arr);
// Java 8+ 流式操作
int sum = Arrays.stream(arr).sum();
int max = Arrays.stream(arr).max().getAsInt();
3.3 Java 9+ 新增:of() 方法
Java 9 开始,List/Set/Map 都有了 of() 静态方法,快速创建不可变集合:
java
// 不可变 List
List<String> list = List.of("A", "B", "C");
// 不可变 Set
Set<String> set = Set.of("A", "B", "C");
// 不可变 Map
Map<String, Integer> map = Map.of("key1", 1, "key2", 2);
Map<String, Integer> map2 = Map.ofEntries(
Map.entry("k1", 1),
Map.entry("k2", 2)
);
特点:
- 不可变(add/remove/put 都会抛异常)
- 不允许 null 元素
- 简洁高效
📌 常见面试题
Q:Collections.synchronizedList 和 ConcurrentHashMap 的区别?
synchronizedList 是把所有方法都加上 synchronized 锁,锁的是整个集合对象,读读也互斥,并发度低。ConcurrentHashMap 锁粒度更细(桶级别),读读不互斥,并发性能更好。
Q:Arrays.asList 有什么坑?
- 返回的不是 java.util.ArrayList,是 Arrays 内部类,大小固定,不能 add/remove
- 基本类型数组会被当成一个元素(因为泛型不能是基本类型)
- 原数组和返回的 list 共享同一个数组,修改一个另一个也变
Q:Collections 和 Collection 的区别?
Collection 是接口(List、Set 的父接口);Collections 是工具类,提供操作 Collection 的静态方法。
四、思维导图速览
集合补充知识
├── HashSet 去重原理
│ ├── 底层:HashMap(key存元素,value固定PRESENT)
│ ├── 去重逻辑:
│ │ ├── 先比 hashCode → 不同 = 不重复
│ │ └── hash 相同 → 再比 equals → true = 重复
│ ├── 重写 equals 必须重写 hashCode
│ └── TreeSet:红黑树,Comparable/Comparator 去重排序
├── 阻塞队列(BlockingQueue)
│ ├── 四组方法:抛异常/返回值/阻塞/超时
│ ├── 实现类:
│ │ ├── ArrayBlockingQueue(数组/有界/一把锁)
│ │ ├── LinkedBlockingQueue(链表/可选有界/两把锁)
│ │ ├── PriorityBlockingQueue(优先级/无界)
│ │ ├── DelayQueue(延迟队列)
│ │ └── SynchronousQueue(不存储/手递手)
│ ├── 原理:Lock + Condition
│ └── 经典应用:生产者-消费者模式
└── 集合工具类
├── Collections
│ ├── 排序:sort/reverse/shuffle/swap
│ ├── 查找:max/min/binarySearch/frequency
│ ├── 同步:synchronizedList/Set/Map
│ └── 不可变:emptyList/singleton/unmodifiableList
├── Arrays
│ ├── sort/binarySearch/copyOf/fill
│ ├── asList(注意坑:大小固定、共享数组)
│ └── toString/equals/hashCode
└── Java 9+ of() 方法
五、写在最后
学习建议
- HashSet 去重是必考题:hashCode + equals 的流程要能说清楚,"重写 equals 必须重写 hashCode"必须能讲出为什么
- 阻塞队列重点掌握 ArrayBlockingQueue 和 LinkedBlockingQueue:两者的区别、实现原理(Lock + Condition)、生产者-消费者模式
- 工具类当字典查:不用死记,知道有什么方法、用的时候查 API 就行,但几个坑要记住(asList 的坑、synchronizedList 的性能问题)
面试高频排序
- HashSet 去重原理 / 重写 equals 为什么要重写 hashCode(必问)
- ArrayBlockingQueue vs LinkedBlockingQueue
- Arrays.asList 的坑
- 阻塞队列的实现原理和应用场景
- Collections 和 Collection 的区别