写在前面:这是JavaSE系列的第13篇。上一篇讲了List家族,今天来讲Set和Map。HashMap是面试中问得最多的集合类,底层原理必须搞懂。建议收藏,反复看。

文章目录
-
- 一、Set集合:不可重复
-
- [1.1 Set的特点](#1.1 Set的特点)
- [1.2 HashSet](#1.2 HashSet)
- [1.3 LinkedHashSet](#1.3 LinkedHashSet)
- [1.4 TreeSet](#1.4 TreeSet)
- [1.5 Set对比](#1.5 Set对比)
- 二、Map集合:键值对
-
- [2.1 Map的基本使用](#2.1 Map的基本使用)
- [2.2 HashMap的常用方法](#2.2 HashMap的常用方法)
- [2.3 HashMap的底层原理(面试重点)](#2.3 HashMap的底层原理(面试重点))
- [2.4 HashMap的面试高频问题](#2.4 HashMap的面试高频问题)
- [2.5 LinkedHashMap](#2.5 LinkedHashMap)
- [2.6 TreeMap](#2.6 TreeMap)
- [2.7 Map对比](#2.7 Map对比)
- 参考资料
- 三、总结
一、Set集合:不可重复
1.1 Set的特点
Set:无序、不可重复
├── HashSet → 基于HashMap,无序
├── LinkedHashSet → 基于LinkedHashMap,保持插入顺序
└── TreeSet → 基于TreeMap,自动排序
1.2 HashSet
实际场景:在开发登录注册功能时,需要判断用户名是否已存在。用List需要遍历查找,效率低;用HashSet只需O(1)就能判断。
java
import java.util.HashSet;
import java.util.Set;
Set<String> set = new HashSet<>();
// 添加元素(自动去重)
set.add("Java");
set.add("Python");
set.add("Java"); // 重复,不会添加
set.add("C++");
System.out.println(set); // [Java, C++, Python](顺序不固定)
// 常用方法
set.size(); // 3
set.contains("Java"); // true
set.remove("Python"); // 删除
set.isEmpty(); // false
set.clear(); // 清空
// 遍历(和List一样)
for (String s : set) {
System.out.println(s);
}
set.forEach(s -> System.out.println(s));
HashSet去重原理:
踩坑提醒:自定义对象存入HashSet时,如果不重写hashCode和equals,去重会失效!因为默认的hashCode是对象地址,每个new出来的对象地址都不同。
java
// HashSet底层是HashMap
// 添加元素时,先计算hashCode
// 如果hashCode相同,再调用equals比较
// hashCode和equals都相同,才认为是重复元素
class Student {
String name;
int age;
// 必须重写hashCode和equals才能正确去重
@Override
public 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 && Objects.equals(name, student.name);
}
@Override
public int hashCode() {
return Objects.hash(name, age);
}
}
Set<Student> set = new HashSet<>();
set.add(new Student("张三", 20));
set.add(new Student("张三", 20)); // 重复,不会添加
set.add(new Student("李四", 21));
System.out.println(set.size()); // 2
1.3 LinkedHashSet
java
// LinkedHashSet:保持插入顺序
Set<String> set = new LinkedHashSet<>();
set.add("C");
set.add("A");
set.add("B");
System.out.println(set); // [C, A, B](按插入顺序)
1.4 TreeSet
java
import java.util.TreeSet;
// TreeSet:自动排序(自然排序)
TreeSet<Integer> set = new TreeSet<>();
set.add(30);
set.add(10);
set.add(20);
set.add(10); // 重复,不添加
System.out.println(set); // [10, 20, 30](自动排序)
// 自定义排序
TreeSet<String> set2 = new TreeSet<>(Comparator.reverseOrder());
set2.add("A");
set2.add("C");
set2.add("B");
System.out.println(set2); // [C, B, A](降序)
// TreeSet的特有方法
TreeSet<Integer> nums = new TreeSet<>(Arrays.asList(1, 3, 5, 7, 9));
nums.first(); // 1(最小值)
nums.last(); // 9(最大值)
nums.lower(5); // 3(严格小于5的最大值)
nums.floor(5); // 5(小于等于5的最大值)
nums.higher(5); // 7(严格大于5的最小值)
nums.ceiling(5); // 5(大于等于5的最小值)
nums.subSet(3, 7); // [3, 5](3到7之间,不含7)
nums.headSet(5); // [1, 3](小于5)
nums.tailSet(5); // [5, 7, 9](大于等于5)
1.5 Set对比
| 特性 | HashSet | LinkedHashSet | TreeSet |
|---|---|---|---|
| 底层 | HashMap | LinkedHashMap | TreeMap |
| 有序性 | 无序 | 插入顺序 | 排序 |
| null值 | 允许 | 允许 | 不允许 |
| 性能 | O(1) | O(1) | O(log n) |
| 去重依据 | hashCode+equals | hashCode+equals | compareTo |
二、Map集合:键值对
2.1 Map的基本使用
java
import java.util.HashMap;
import java.util.Map;
Map<String, Integer> map = new HashMap<>();
// 添加键值对
map.put("Java", 1);
map.put("Python", 2);
map.put("C++", 3);
map.put("Java", 10); // key重复,value覆盖
// 获取值
Integer value = map.get("Java"); // 10
Integer def = map.get("Go"); // null(key不存在)
// 安全获取
Integer v2 = map.getOrDefault("Go", 0); // 0(key不存在返回默认值)
// 判断
map.containsKey("Java"); // true
map.containsValue(1); // false(1已被覆盖为10)
// 删除
map.remove("C++"); // 删除指定key
// 遍历
// 方式1:entrySet(推荐)
for (Map.Entry<String, Integer> entry : map.entrySet()) {
System.out.println(entry.getKey() + " → " + entry.getValue());
}
// 方式2:keySet
for (String key : map.keySet()) {
System.out.println(key + " → " + map.get(key));
}
// 方式3:forEach(Java 8+)
map.forEach((key, val) -> System.out.println(key + " → " + val));
// 方式4:values
for (Integer v : map.values()) {
System.out.println(v);
}
// 大小
map.size(); // 2
map.isEmpty(); // false
map.clear(); // 清空
2.2 HashMap的常用方法
java
Map<String, Integer> map = new HashMap<>();
// putIfAbsent:key不存在才放入
map.putIfAbsent("Java", 1); // 放入
map.putIfAbsent("Java", 10); // 不放入(key已存在)
// computeIfAbsent:key不存在才计算
map.computeIfAbsent("Python", k -> k.length()); // Python长度6
// computeIfPresent:key存在才计算
map.computeIfPresent("Java", (k, v) -> v + 1); // Java的value+1
// merge:合并值
map.merge("Java", 1, Integer::sum); // Java的value+1
// replace:替换
map.replace("Java", 100); // 替换Java的value为100
// getOrDefault
map.getOrDefault("Go", 0); // Go不存在,返回0
2.3 HashMap的底层原理(面试重点)
面试必问:HashMap的底层原理几乎是Java面试的"送分题",但很多人答不全。下面从数据结构、put过程、扩容机制三个维度彻底讲透。
java
// HashMap底层:数组 + 链表 + 红黑树(JDK 8+)
// 数据结构
// 数组的每个位置叫一个"桶"(bucket)
// 每个桶可以存一个链表或红黑树
transient Node<K,V>[] table; // 哈希桶数组
static class Node<K,V> {
final int hash; // 哈希值
final K key; // 键
V value; // 值
Node<K,V> next; // 下一个节点(链表)
}
// JDK 8之前:数组 + 链表
// JDK 8之后:数组 + 链表 + 红黑树(链表长度>=8时转红黑树)
put过程图解:
1. 计算key的hashCode
2. 对hashCode进行扰动处理(减少冲突)
3. 用(n-1) & hash计算桶的位置
4. 如果桶为空,直接放入
5. 如果桶不为空:
a. 如果key相同,覆盖value
b. 如果key不同,加入链表尾部
c. 如果链表长度>=8且数组长度>=64,转红黑树
桶数组(table)
┌─────┬─────┬─────┬─────┬─────┬─────┬─────┬─────┐
│ 0 │ 1 │ 2 │ 3 │ 4 │ 5 │ 6 │ 7 │
├─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┤
│null │ [A] │null │ [B] │null │ [C] │null │null │
│ │ ↓ │ │ ↓ │ │ ↓ │ │ │
│ │ [D] │ │ [E] │ │ [F] │ │ │
│ │ ↓ │ │ │ │ ↓ │ │ │
│ │ [G] │ │ │ │ [H] │ │ │
└─────┴─────┴─────┴─────┴─────┴─────┴─────┴─────┘
扩容机制:
经验之谈 :如果知道数据量大小,一定要在创建HashMap时指定初始容量,避免频繁扩容影响性能。比如要存1000个元素,可以new HashMap<>(2048)(2的幂次方)。
java
// 默认初始容量:16
// 默认负载因子:0.75
// 扩容阈值:16 * 0.75 = 12
// 当元素数量超过阈值时,扩容为原来的2倍
// 16 → 32 → 64 → 128 → ...
// 为什么负载因子是0.75?
// 太小:空间浪费
// 太大:哈希冲突增多,查询效率降低
// 0.75是时间和空间的平衡点
踩坑提醒:HashMap扩容时需要重新计算所有元素的哈希位置(rehash),这是一个非常耗时的操作。在性能敏感的场景下,避免HashMap频繁扩容。
2.4 HashMap的面试高频问题
问题1:hashCode和equals的关系?
java
// 1. 两个对象equals为true,hashCode必须相同
// 2. 两个对象hashCode相同,equals不一定为true(哈希冲突)
// 3. 重写equals必须重写hashCode
// 正确的hashCode实现
@Override
public int hashCode() {
return Objects.hash(name, age);
}
@Override
public 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 && Objects.equals(name, student.name);
}
问题2:HashMap为什么线程不安全?
java
// JDK 7:多线程扩容时可能产生环形链表,导致死循环
// JDK 8:多线程put可能导致数据丢失
// 解决方案:
// 1. ConcurrentHashMap(推荐)
// 2. Collections.synchronizedMap()
// 3. Hashtable(不推荐,锁粒度太大)
经验之谈:实际开发中,如果需要线程安全的Map,直接用ConcurrentHashMap。它使用分段锁(JDK 7)或CAS+synchronized(JDK 8),性能远优于Hashtable。
问题3:HashMap的key可以为null吗?
java
// HashMap:key可以为null(只有一个,放在桶0)
// TreeMap:key不能为null(需要比较)
// ConcurrentHashMap:key和value都不能为null
2.5 LinkedHashMap
实际应用:LinkedHashMap的访问顺序模式是实现LRU缓存的基础。很多缓存框架(如Guava Cache、LruCache)都是基于它实现的。
java
// LinkedHashMap:保持插入顺序
Map<String, Integer> map = new LinkedHashMap<>();
map.put("C", 3);
map.put("A", 1);
map.put("B", 2);
System.out.println(map); // {C=3, A=1, B=2}(按插入顺序)
// 访问顺序(LRU缓存)
Map<String, Integer> lru = new LinkedHashMap<>(16, 0.75f, true);
lru.put("A", 1);
lru.put("B", 2);
lru.get("A"); // 访问A,A移到最后
lru.put("C", 3);
System.out.println(lru.keySet()); // [B, A, C](A被访问过,移到后面)
2.6 TreeMap
java
// TreeMap:按key排序
TreeMap<String, Integer> map = new TreeMap<>();
map.put("C", 3);
map.put("A", 1);
map.put("B", 2);
System.out.println(map); // {A=1, B=2, C=3}(按key排序)
// 自定义排序
TreeMap<String, Integer> map2 = new TreeMap<>(Comparator.reverseOrder());
map2.put("C", 3);
map2.put("A", 1);
System.out.println(map2); // {C=3, A=1}(降序)
// TreeMap的特有方法
TreeMap<Integer, String> treeMap = new TreeMap<>();
treeMap.put(1, "A");
treeMap.put(3, "C");
treeMap.put(5, "E");
treeMap.firstKey(); // 1
treeMap.lastKey(); // 5
treeMap.lowerKey(3); // 1
treeMap.higherKey(3); // 5
treeMap.subMap(1, 5); // {1=A, 3=C}
2.7 Map对比
| 特性 | HashMap | LinkedHashMap | TreeMap | Hashtable |
|---|---|---|---|---|
| 底层 | 数组+链表+红黑树 | 数组+链表+红黑树 | 红黑树 | 数组+链表 |
| 有序性 | 无序 | 插入/访问顺序 | 按key排序 | 无序 |
| null key | 允许1个 | 允许1个 | 不允许 | 不允许 |
| null value | 允许 | 允许 | 不允许 | 不允许 |
| 线程安全 | 不安全 | 不安全 | 不安全 | 安全 |
| 性能 | O(1) | O(1) | O(log n) | O(1) |
参考资料
三、总结
今天我们学习了:
- ✅ Set集合的使用和去重原理
- ✅ Map集合的使用
- ✅ HashMap的底层原理(数组+链表+红黑树)
- ✅ HashMap的扩容机制
- ✅ LinkedHashMap和TreeMap的特点
重点记忆:
- HashSet去重依赖hashCode和equals
- HashMap底层是数组+链表+红黑树
- 负载因子0.75,扩容为2倍
- 重写equals必须重写hashCode
- ConcurrentHashMap是线程安全的HashMap
下一步预告 :
Day14我们将学习IO流与文件操作------字节流、字符流、缓冲流、序列化等。
互动话题:HashMap是面试必问题,你被问过哪些HashMap的面试题?欢迎在评论区分享!
如果这篇文章对你有帮助,欢迎点赞、收藏 !这是【JavaSE全面教学】系列的第13篇,关注我看完整套教程 👇
本文为【JavaSE全面教学】系列第13篇,持续更新中...