前言
Java不秃,面试不慌!
欢迎来到这片 Java修炼场!这里没有枯燥的教科书,只有每日一更的 硬核知识+幽默吐槽,让你在欢笑中掌握 Java基础、算法、面试套路,摆脱“写代码如写诗、看代码如看天书”的困境。
记住: 代码会背叛你,但知识不会! 坚持积累,总有一天,HR会为你的八股文落泪,面试官会因你的算法沉默。
让我们一起加油,优雅不秃! 🚀
Java江湖之ConcurrentHashMap扩容:1.7 vs 1.8
扩容的路上,不是一个人在战斗!
1.7版:江湖门派「Segment」帮会制度
- 在 Java 1.7 时代,ConcurrentHashMap 还是一个 “分帮分派” 的江湖组织,它的核心思想是「分段锁」,即 Segment。可以理解为,一整个 ConcurrentHashMap 是一个 总帮派,而里面的 Segment 就像是 各大门派,各自管理一部分数据,互不干涉。
- 每个 Segment 就像是一个迷你版的 HashMap,自己管自己的一亩三分地,只有当自己的地盘快要装不下了,才会考虑扩容。
- 扩容方式和传统 HashMap 差不多,也是先创建一个更大的数组,然后把老数组的成员一个个搬家到新家去(就像帮会升级,门派成员从旧驻地搬到更大的驻地)。
- 每个 Segment 只管自己扩容,不会管其他帮派的死活,这就意味着并不是整个 ConcurrentHashMap 一起扩容,而是某个门派觉得自己房子不够大,才会单独扩建。
// 1.7 版本的 ConcurrentHashMap 结构
class ConcurrentHashMap17<K, V> {
static class Segment<K, V> { // 门派组织
HashMap<K, V> map = new HashMap<>();
void put(K key, V value) {
if (map.size() >= threshold) { // 容量超标了
expand(); // 进行扩容
}
map.put(key, value);
}
void expand() { // 搬家逻辑
System.out.println("Segment扩容中...新建更大房子!");
// 省略HashMap扩容细节
}
}
}
1.8版:大一统帝国,众人搬砖
「一方有难,八方支援;扩容不再单打独斗!」
- 到了 Java 1.8 版本,Segment 这个帮派系统直接被解散了,ConcurrentHashMap 变成了一个整体的江湖势力,所有数据直接存入一个大数组里,不再分帮分派。
- 当某个线程
put()
时,发现 ConcurrentHashMap 正在扩容,那么它就会主动加入扩容大军,一起干活,而不是等别人扩容完了再来插手(毕竟人多力量大)。 - 如果某个线程
put()
时,发现 还没开始扩容,那么就乖乖插入数据,然后检查是不是已经 超标 了,超标就触发扩容。 - 多个线程可以同时参与扩容,不像 1.7 版本一个 Segment 自己搬自己的家,而是多个线程一起分工搬家,提高效率。
- 扩容前,先新建一个更大的数组(这点和 1.7 版一样),然后进入核心步骤——分工转移数据:
- 数组被分组,每个线程各搬一部分,不像以前一个人干到底,而是多个线程同时搬数据(就像是现代搬家公司,分区域搬运,提高效率)。
- 有些线程可能搬一组数据,有些可能搬多组,具体看抢到的任务。
📝 代码示例
// 1.8 版本的 ConcurrentHashMap 结构
class ConcurrentHashMap18<K, V> {
Node<K, V>[] table; // 大数组,所有数据在这里
void put(K key, V value) {
if (正在扩容) {
帮忙搬家(); // 发现搬家现场,直接加入帮忙
} else {
插入数据();
if (超过阈值) {
开启扩容();
}
}
}
void 帮忙搬家() {
System.out.println("搬家大队+1,开始干活!");
// 分组搬家逻辑
}
void 开启扩容() {
System.out.println("房子不够住了,全员搬家!");
// 多线程一起搬家
}
}
总结:1.7 vs 1.8
版本 | 扩容机制 | 线程参与度 | 数据结构 | 适用场景 |
---|---|---|---|---|
1.7 | 每个 Segment 独立扩容 | 只有 Segment 负责扩容 | 基于 Segment 分段锁 | 适合读多写少 |
1.8 | 整体 ConcurrentHashMap 扩容 | 多个线程协同扩容 | 基于 CAS+链表+红黑树 | 适合高并发写入 |
HashMap 的 put 方法:存数据 ≠ 简单插队,背后逻辑大有乾坤!
"你以为 put 只是找个空位放进去?其实,HashMap 在背后经历了一番生死较量!"
put 方法整体流程
当你执行 map.put(key, value)
时,HashMap 其实是在后台默默操办了一场 "存储大作战",大致流程如下:
-
计算存放位置
- 通过 哈希算法(
hash(key) & (table.length - 1)
)计算出 数组下标,找到该数据应该存放的位置。 - 这个过程就像是在一个宿舍楼里,找到你的 “床位号”,但是这个床位可能已经有人住了,那就要看如何安顿你了。
- 通过 哈希算法(
-
检查该位置是否有人
- 如果这个 "床位" 还空着,那就简单了,直接把
key-value
封装成一个对象,放进去,搞定! - 如果这个 "床位" 已经有人住,那 HashMap 需要进行一场 "合租协商",但 1.7 和 1.8 的处理方式可不一样!
- 如果这个 "床位" 还空着,那就简单了,直接把
JDK1.7:链表存储 + 头插法
"老 HashMap,快但容易乱,扩容容易死锁!"
如果发现该位置已经有人住?
- 先检查是否 需要扩容,如果容量太满,就会触发扩容(神秘的2倍增长法)。
- 如果不需要扩容,就把新的
Entry
对象 插入到链表头部(头插法)。 - 但是,头插法会导致链表反序,在并发环境下可能会出现 环形链表(死循环),这也是 1.7 版本的一个 坑。
📝 示例代码
static class Entry<K, V> { // 1.7 版本的 Entry 结构
final K key;
V value;
Entry<K, V> next; // 指向下一个节点(链表)
Entry(K key, V value, Entry<K, V> next) {
this.key = key;
this.value = value;
this.next = next;
}
}
1.7 版本:头插法
Entry<K, V> e = new Entry<>(key, value, table[index]);
table[index] = e; // 头插法,新的节点插在最前面
JDK1.8:红黑树 + 尾插法
"新 HashMap,稳但稍慢,扩容更聪明!"
到了 Java 1.8,HashMap 彻底改进了存储策略,主要有两个升级:
- 换成尾插法,链表顺序更稳定,避免死循环!
- 当链表长度超过 8,自动转换成红黑树,提高查找效率!
如果发现该位置已经有人住?
- 先看看 是不是红黑树:
- 如果是红黑树,就按 红黑树规则 插入,插入时会检查是否已经存在
key
,如果有就直接更新value
。
- 如果是红黑树,就按 红黑树规则 插入,插入时会检查是否已经存在
- 如果不是红黑树,而是 普通链表:
- 先遍历链表,如果发现已经有相同的
key
,那就直接更新value
。 - 如果没有找到相同
key
,就用尾插法 把key-value
插入链表最后(不同于 1.7 的头插法)。 - 插入后判断链表长度,如果 长度 ≥ 8,就把整个链表 转换成红黑树(提高查找速度)。
- 先遍历链表,如果发现已经有相同的
📝 示例代码
static class Node<K, V> { // 1.8 版本的 Node 结构
final K key;
V value;
Node<K, V> next; // 指向下一个节点(链表)
Node(K key, V value, Node<K, V> next) {
this.key = key;
this.value = value;
this.next = next;
}
}
1.8 版本:尾插法
Node<K, V> node = new Node<>(key, value, null);
if (tail == null) { // 如果链表是空的
table[index] = node;
} else {
tail.next = node; // 尾插法,保持链表顺序
}
JDK1.7 vs JDK1.8 版本对比
版本 | 存储方式 | 插入方式 | 超长链表处理 | 并发问题 |
---|---|---|---|---|
JDK1.7 | 数组 + 链表 | 头插法 | 还是链表(低效) | 可能出现环形链表死锁 |
JDK1.8 | 数组 + 链表 + 红黑树 | 尾插法 | 转成红黑树(高效) | 避免了死锁问题,效率更稳定 |
Java 泛型:神秘的“类型变量”世界
"用泛型,让代码告别类型转换的烦恼!"
在 Java 中,泛型(Generics) 就像是 “模板”,让我们的代码更加灵活和安全。例如,List<String>
表示只能存储 String 类型的 List,而 List<Integer>
则只能存储 Integer,避免了强制类型转换带来的麻烦。
泛型的核心概念
1. 泛型的基本语法
泛型的基本使用方式如下:
class Box<T> {
private T value;
public void set(T value) {
this.value = value;
}
public T get() {
return value;
}
}
🔹 T
代表一个类型参数,在使用时可以指定具体的类型:
Box<String> stringBox = new Box<>();
stringBox.set("Hello");
String str = stringBox.get(); // 不需要强制类型转换
? extends T
和 ? super T
的区别
“到底是往里放,还是往外拿?”
1. ? extends T
—— 上界通配符
“你可以装子类,但取出来时只能当成 T”
? extends T
代表:“我是 T 及其子类,但你不能确定我是具体的哪个子类”。- 常用于只读操作,你可以从中取出
T
类型的对象,但不能往里面放对象(除了null
)。
🔹 示例
List<? extends Number> list = new ArrayList<Integer>(); // 允许
list.add(null); // ✅ 只允许放 null
Number num = list.get(0); // ✅ 取出来的都是 Number 或其子类
list.add(10); // ❌ 报错!不能往里面放数据
理解:
- 你能保证取出来的对象是
Number
或Number
的子类(Integer
,Double
)。 - 但你不能确定具体是哪个子类,所以不能随意放数据进去。
2. ? super T
—— 下界通配符
“你可以装父类,但取出来时只能当成 Object”
? super T
代表:“我是 T 及其父类,但你不能确定我是具体的哪个父类”。- 常用于写入操作,你可以往里面放
T
类型的对象,但取出来时只能当作Object
。
🔹 示例
List<? super Integer> list = new ArrayList<Number>(); // 允许
list.add(10); // ✅ 可以放 Integer
Object obj = list.get(0); // ✅ 只能用 Object 接收
Integer i = list.get(0); // ❌ 报错!不能直接当 Integer 取出
理解:
- 你能往里面放
Integer
或Integer
的子类(但Integer
没有子类)。 - 但你不能保证取出来的类型,所以只能用
Object
类型接收。
? extends T
vs ? super T
:谁能放,谁能取?
通配符 | 能不能放数据? | 能不能取数据? | 适用场景 |
---|---|---|---|
? extends T | ❌ 不能放(除 null) | ✅ 可以取,类型是 T | 只读 |
? super T | ✅ 可以放 T 类型 | ❌ 取出来是 Object | 只写 |
🚀 最后的话:点赞+关注,知识路上一起同行!
感谢你阅读到这里!🎉 Java修炼 是一场没有终点的旅程,每天进步一点点,面试不慌,成长不秃!💪
如果你觉得这篇文章对你有帮助,别忘了 点赞👍、收藏⭐、关注👀,这样我就能更加努力地输出更多优质内容啦!
💬 你的支持,就是我更新的最大动力!
📌 如果有任何问题,欢迎留言讨论,一起卷出天际! 🚀🚀🚀