Java集合(4)

作者:没有四次元口袋的蓝胖

日期: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 → 重复,不添加

核心规则:

  1. hashCode 不同 → 两个对象一定不同(不用比 equals 了)
  2. hashCode 相同 → 两个对象不一定相同(哈希冲突,要继续比 equals)
  3. 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 有什么坑?

  1. 返回的不是 java.util.ArrayList,是 Arrays 内部类,大小固定,不能 add/remove
  2. 基本类型数组会被当成一个元素(因为泛型不能是基本类型)
  3. 原数组和返回的 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() 方法

五、写在最后

学习建议

  1. HashSet 去重是必考题:hashCode + equals 的流程要能说清楚,"重写 equals 必须重写 hashCode"必须能讲出为什么
  2. 阻塞队列重点掌握 ArrayBlockingQueue 和 LinkedBlockingQueue:两者的区别、实现原理(Lock + Condition)、生产者-消费者模式
  3. 工具类当字典查:不用死记,知道有什么方法、用的时候查 API 就行,但几个坑要记住(asList 的坑、synchronizedList 的性能问题)

面试高频排序

  1. HashSet 去重原理 / 重写 equals 为什么要重写 hashCode(必问)
  2. ArrayBlockingQueue vs LinkedBlockingQueue
  3. Arrays.asList 的坑
  4. 阻塞队列的实现原理和应用场景
  5. Collections 和 Collection 的区别
相关推荐
2501_948106911 小时前
计算机毕业设计之基于jsp教科研信息共享系统
java·开发语言·信息可视化·spark·课程设计
TanYYF1 小时前
spring ai入门教程二
java·人工智能·spring
SeeYa-J2 小时前
Spring IOC(Inversion of Control)
java·spring·rpc
不会c+2 小时前
02-SpringBoot配置文件
java·spring boot·后端
AI 大模型学习不踩坑2 小时前
OpenClaw 完整教程:从安装到使用(官方脚本版)
java·人工智能·神经网络·机器学习·计算机视觉·自然语言处理·openclaw
Listen·Rain3 小时前
数据库流式查询
java·数据库
彦为君3 小时前
算法思维与经典智力题
java·前端·redis·算法
翔云 OCR API3 小时前
慧视扫描王-财务少加班
java·自动化
雨辰AI3 小时前
生产级实战:人大金仓 V9 标准化运维手册(日常巡检 + 监控告警 + 应急处置)
java·运维·数据库·后端