Java基础与面试-每日必看(4)

前言

Java不秃,面试不慌!

欢迎来到这片 Java修炼场!这里没有枯燥的教科书,只有每日一更的 硬核知识+幽默吐槽,让你在欢笑中掌握 Java基础、算法、面试套路,摆脱“写代码如写诗、看代码如看天书”的困境。

记住: 代码会背叛你,但知识不会! 坚持积累,总有一天,HR会为你的八股文落泪,面试官会因你的算法沉默。

让我们一起加油,优雅不秃! 🚀


Java江湖之ConcurrentHashMap扩容:1.7 vs 1.8

扩容的路上,不是一个人在战斗!

1.7版:江湖门派「Segment」帮会制度

  1. 在 Java 1.7 时代,ConcurrentHashMap 还是一个 “分帮分派” 的江湖组织,它的核心思想是「分段锁」,即 Segment。可以理解为,一整个 ConcurrentHashMap 是一个 总帮派,而里面的 Segment 就像是 各大门派,各自管理一部分数据,互不干涉。
  2. 每个 Segment 就像是一个迷你版的 HashMap,自己管自己的一亩三分地,只有当自己的地盘快要装不下了,才会考虑扩容
  3. 扩容方式和传统 HashMap 差不多,也是先创建一个更大的数组,然后把老数组的成员一个个搬家到新家去(就像帮会升级,门派成员从旧驻地搬到更大的驻地)。
  4. 每个 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版:大一统帝国,众人搬砖

「一方有难,八方支援;扩容不再单打独斗!」

  1. 到了 Java 1.8 版本,Segment 这个帮派系统直接被解散了,ConcurrentHashMap 变成了一个整体的江湖势力,所有数据直接存入一个大数组里,不再分帮分派
  2. 当某个线程 put() 时,发现 ConcurrentHashMap 正在扩容,那么它就会主动加入扩容大军,一起干活,而不是等别人扩容完了再来插手(毕竟人多力量大)。
  3. 如果某个线程 put() 时,发现 还没开始扩容,那么就乖乖插入数据,然后检查是不是已经 超标 了,超标就触发扩容。
  4. 多个线程可以同时参与扩容,不像 1.7 版本一个 Segment 自己搬自己的家,而是多个线程一起分工搬家,提高效率。
  5. 扩容前,先新建一个更大的数组(这点和 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 其实是在后台默默操办了一场 "存储大作战",大致流程如下:

  1. 计算存放位置

    • 通过 哈希算法hash(key) & (table.length - 1))计算出 数组下标,找到该数据应该存放的位置。
    • 这个过程就像是在一个宿舍楼里,找到你的 “床位号”,但是这个床位可能已经有人住了,那就要看如何安顿你了。
  2. 检查该位置是否有人

    • 如果这个 "床位" 还空着,那就简单了,直接把 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 彻底改进了存储策略,主要有两个升级:

  1. 换成尾插法,链表顺序更稳定,避免死循环!
  2. 当链表长度超过 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); // ❌ 报错!不能往里面放数据

理解

  • 你能保证取出来的对象是 NumberNumber 的子类(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 取出

理解

  • 你能往里面放 IntegerInteger 的子类(但 Integer 没有子类)。
  • 但你不能保证取出来的类型,所以只能用 Object 类型接收。

? extends T vs ? super T:谁能放,谁能取?

通配符能不能放数据?能不能取数据?适用场景
? extends T❌ 不能放(除 null)✅ 可以取,类型是 T只读
? super T✅ 可以放 T 类型❌ 取出来是 Object只写


🚀 最后的话:点赞+关注,知识路上一起同行!

感谢你阅读到这里!🎉 Java修炼 是一场没有终点的旅程,每天进步一点点,面试不慌,成长不秃!💪

如果你觉得这篇文章对你有帮助,别忘了 点赞👍、收藏⭐、关注👀,这样我就能更加努力地输出更多优质内容啦!

💬 你的支持,就是我更新的最大动力!
📌 如果有任何问题,欢迎留言讨论,一起卷出天际! 🚀🚀🚀

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Starry-Walker

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值