ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Java面试高频陷阱解析:从==与equals到HashMap扩容的37个伪基础雷区

Java面试高频陷阱解析:从==与equals到HashMap扩容的37个伪基础雷区

简介:本资源是一份专为Java初学者打造的面试题库文档,面向刚毕业或完成Java培训的求职者,旨在系统梳理初级岗位高频考点,快速查漏补缺、夯实基础、提升面试应答能力。文档为单文件Word格式(.docx),共155KB,内容结构清晰,覆盖Java基础、多线程、反射、对象拷贝、JavaWeb、异常、网络、设计模式及主流框架(Spring/Spring MVC、Spring Boot/Cloud)、中间件(RabbitMQ、Kafka、ZooKeeper)、数据库(MySQL、Redis)和JVM等共18大模块,精选208道典型题目;其中Java基础部分深入解析JDK/JRE区别、==与equals本质差异、hashCode与equals关系、final作用、Math.round取整规则等易错点,并辅以代码示例与源码级解读。目前已有623人学习下载,是入门级Java工程师高效备战技术面试的权威参考清单。

1. 初级岗真不考“八股文”?错!208道题里藏着37个必踩的“伪基础”陷阱

别信“Java初级只问HelloWorld”的鬼话。这份208道题的清单,表面是基础题合集,实则是面试官布下的认知迷雾阵——真正拉开差距的,从来不是你会不会写单例,而是你答“==和equals区别”时,有没有主动补上那句“String重写了equals,但Cat没重写,所以new Cat(‘A’) == new Cat(‘A’)返回false”。全卷18大模块中,Java基础(30题)、多线程(25题)、集合(22题)、JVM(18题)四块占分超45%,而其中37道题存在“标准答案正确,但现场回答仍被否决”的高危陷阱:比如问“HashMap原理”,只答“数组+链表+红黑树”得60分;若能立刻补一句“JDK1.8后链表长度≥8且数组长度≥64才转红黑树,否则只是扩容”,直接进二面。适合人群非常明确:校招应届生、转行培训结业者、半年内无真实项目经验的求职者——你们缺的不是知识,是把知识点焊进面试话术肌肉记忆里的临场反射。这份文档不是题库,是用208次高频追问,帮你把Java语言特性刻成条件反射的训练弹药。

2. Java基础高频考点:从字节码到字符串常量池的5层穿透逻辑

2.1 JDK/JRE本质差异:不止是“开发vs运行”,关键在工具链绑定

面试官绝不会满足于“JDK包含JRE”这种教科书答案。他们要听的是工具链耦合性:JDK中的javac编译器生成的.class文件,必须由同版本JRE的java命令加载执行,因为字节码指令集(如invokedynamic在JDK7引入)与JVM实现强绑定。实操验证:

# 在JDK11环境下编译 javac -source 8 -target 8 HelloWorld.java # 强制生成JDK8字节码 java -version # 查看当前JRE版本

提示:若JRE版本低于编译目标(如用JRE8运行JDK11编译的class),会抛出UnsupportedClassVersionError——这比“JDK包含JRE”更能证明你理解底层契约。

2.2 ==与equals的终极判断树:三步定位法

所有字符串比较题都逃不开这张决策图:

  1. 先看类型:基本类型?→ 直接值比较(int a=1; int b=1; a==b → true)
  2. 再看创建方式:
    • 字符串字面量("abc")→ 进入字符串常量池,引用相同 →==为true
    • new String("abc")→ 堆内存新建对象,引用不同 →==为false
  3. 最后看equals重写:
    public class User { private String name; // 未重写equals → 默认Object.equals() → 比较引用 public boolean equals(Object obj) { return this == obj; } } User u1 = new User("Tom"); User u2 = new User("Tom"); System.out.println(u1.equals(u2)); // false!不是值比较!

注意:面试官常在此设坑——给你一个自定义类问equals,90%人脱口而出“比较值”,却忘了重写才是前提。

2.3 hashCode()与equals的契约陷阱:哈希碰撞的实战推演

题干:“hashCode相同,equals一定为true?” 标准答案是“错”,但高分回答必须带哈希碰撞实例:

// 构造哈希碰撞:利用String哈希算法的数学特性 String s1 = "Aa"; // hashCode = 2112 String s2 = "BB"; // hashCode = 2112 System.out.println(s1.hashCode() == s2.hashCode()); // true System.out.println(s1.equals(s2)); // false

为什么能碰撞?因为String的hashCode计算公式是:
s[0]*31^(n-1) + s[1]*31^(n-2) + ... + s[n-1]
当s1[0]=65('A'), s1[1]=97('a')时:65*31 + 97 = 2112
当s2[0]=66('B'), s2[1]=66('B')时:66*31 + 66 = 2112

易错点:很多人以为“哈希值相同说明对象相等”,实际这是HashMap的必要不充分条件——就像身份证号相同的人未必是同一人,只是系统认为“大概率是”。

2.4 final修饰符的三重枷锁:从编译期到运行时的控制力

面试官最爱追问:“final修饰的List,能add元素吗?” 答案是能,但必须拆解三层:

  • 编译期枷锁:final List<String> list = new ArrayList<>();→ list变量地址不可变
  • 运行时枷锁:list.add("a")合法,因ArrayList内部数组可变
  • 语义枷锁:若声明为final List<String> list = Collections.unmodifiableList(new ArrayList<>()),add才抛UnsupportedOperationException
// 对比实验 final List<String> mutableList = new ArrayList<>(); mutableList.add("test"); // ✅ 合法:final只锁引用,不锁对象状态 final List<String> unmodifiableList = Collections.unmodifiableList(new ArrayList<>()); unmodifiableList.add("test"); // ❌ 抛异常:Collections包装的不可变视图

2.5 String反转的性能暗礁:为什么reverse()不是最优解?

题干:“如何反转字符串?” 多数人答StringBuilder.reverse(),但高级回答要指出字符编码陷阱:

// 危险场景:含emoji或代理对(surrogate pair)的字符串 String emojiStr = "👨‍💻"; // 一个emoji实际占2个char(UTF-16代理对) StringBuilder sb = new StringBuilder(emojiStr); System.out.println(sb.reverse().toString()); // 可能显示乱码!

根本原因:StringBuilder.reverse()按char反转,但emoji需要按codePoint(Unicode码点)反转。正确解法:

public static String reverseByCodePoint(String str) { return new StringBuilder() .append(str.codePoints().mapToObj(Character::toChars).collect(Collectors.joining())) .reverse() .toString(); }

提示:此题直击候选人是否关注生产环境细节——线上日志里emoji乱码,根源常在此。

3. 多线程核心战场:从状态机到锁升级的7个致命断点

3.1 线程状态转换的隐藏路径:BLOCKED与WAITING的生死线

面试官画出线程状态图后必问:“sleep()后线程进入什么状态?” 标准答案是TIMED_WAITING,但高分答案要揭示JVM底层状态映射:

  • Thread.sleep(1000)→ JVM调用os::sleep()→ OS线程进入SLEEPING状态 → Java线程状态为TIMED_WAITING
  • Object.wait()→ JVM调用ObjectMonitor::wait()→ 线程释放monitor锁 → 进入WAITING状态
  • synchronized阻塞 → JVM调用ObjectMonitor::enter()失败 → 线程进入BLOCKED状态
    关键区别:BLOCKED线程在等待获取锁,WAITING/TIMED_WAITING线程在等待被唤醒。验证代码:
public class StateCheck { private static final Object lock = new Object(); public static void main(String[] args) throws InterruptedException { Thread t1 = new Thread(() -> { synchronized (lock) { try { Thread.sleep(1000); // 此时t1状态为TIMED_WAITING } catch (InterruptedException e) {} } }); Thread t2 = new Thread(() -> { synchronized (lock) { // 此处阻塞,状态为BLOCKED System.out.println("won't print"); } }); t1.start(); t2.start(); Thread.sleep(100); System.out.println("t1 state: " + t1.getState()); // TIMED_WAITING System.out.println("t2 state: " + t2.getState()); // BLOCKED } }

3.2 wait()/notify()的原子性陷阱:为什么必须用synchronized包裹?

题干:“不用synchronized能调用wait()吗?” 答案是运行时抛IllegalMonitorStateException,但必须解释JVM规范强制要求:

  • wait()方法内部会检查当前线程是否持有对象monitor锁
  • 若未持有,直接抛异常(非编译错误!)
  • notify()同理,且必须与wait()在同一个锁对象上调用
// 错误示范:无synchronized public void badWait() { try { obj.wait(); // 运行时异常! } catch (InterruptedException e) {} } // 正确写法:synchronized确保锁持有 public void goodWait() { synchronized (obj) { try { obj.wait(); // ✅ 安全 } catch (InterruptedException e) {} } }

易错点:很多候选人知道要加synchronized,却说不清“为什么”——本质是JVM规范第17章规定:wait/notify必须在同步上下文中执行,否则破坏线程安全契约。

3.3 线程池的7种创建方式:ThreadPoolExecutor才是唯一真相

面试官问:“newFixedThreadPool和newCachedThreadPool有什么区别?” 若只答“固定大小vs缓存”,会被追问:“它们底层都是ThreadPoolExecutor,参数怎么配置的?” 必须背下核心参数映射:

创建方式corePoolSizemaxPoolSizekeepAliveTimeworkQueue
newFixedThreadPool(5)550Lnew LinkedBlockingQueue<>()
newCachedThreadPool()0Integer.MAX_VALUE60Lnew SynchronousQueue<>()
newSingleThreadExecutor()110Lnew LinkedBlockingQueue<>()
致命陷阱:newCachedThreadPool的maxPoolSize=Integer.MAX_VALUE,在突发流量下可能创建海量线程导致OOM。生产环境必须用ThreadPoolExecutor手动配置:
// 推荐写法:可控的线程池 ThreadPoolExecutor executor = new ThreadPoolExecutor( 2, // corePoolSize 10, // maxPoolSize 60L, TimeUnit.SECONDS, new ArrayBlockingQueue<>(100), // 有界队列防OOM new ThreadPoolExecutor.CallerRunsPolicy() // 拒绝策略:由调用线程执行 );

3.4 synchronized锁升级的三阶火箭:从偏向锁到重量级锁的性能博弈

题干:“synchronized锁升级过程?” 高分答案要画出JVM对象头结构变化:

  1. 偏向锁阶段:对象头Mark Word存储线程ID(54位)+ epoch(2位)+ biased_lock(1位)
  2. 轻量级锁阶段:Mark Word存储指向栈中Lock Record的指针
  3. 重量级锁阶段:Mark Word存储指向ObjectMonitor的指针
    验证实验:用JOL(Java Object Layout)查看对象头:
// 添加jol-core依赖后 public class LockState { public static void main(String[] args) { Object obj = new Object(); System.out.println(ClassLayout.parseInstance(obj).toPrintable()); // 输出:00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001 // 末尾001表示无锁状态(biased_lock=0) } }

注意:JDK15已默认禁用偏向锁(-XX:-UseBiasedLocking),面试时需说明版本差异。

3.5 ThreadLocal的内存泄漏黑匣子:弱引用为何救不了你?

题干:“ThreadLocal会造成内存泄漏吗?” 标准答案是“会”,但必须指出泄漏根源在Entry的key弱引用,value强引用:

// ThreadLocalMap.Entry继承WeakReference static class Entry extends WeakReference<ThreadLocal<?>> { Object value; // ⚠️ value是强引用! Entry(ThreadLocal<?> k, Object v) { super(k); value = v; } }

泄漏路径:

  1. ThreadLocal对象被回收 → Entry.key变为null(弱引用生效)
  2. 但Entry.value仍强引用着大对象(如Connection)
  3. 若线程长期存活(如线程池),value无法GC → 内存泄漏
    解决方案:
  • 每次使用后调用threadLocal.remove()
  • 使用InheritableThreadLocal时更需谨慎(父子线程传递)
// 正确用法模板 private static final ThreadLocal<SimpleDateFormat> sdfHolder = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd")); public static String formatDate(Date date) { try { return sdfHolder.get().format(date); } finally { sdfHolder.remove(); // ✅ 关键清理动作 } }

4. 集合框架易错点:HashMap、ArrayList、ConcurrentHashMap的3个血泪现场

4.1 HashMap的扩容死循环:JDK7的噩梦如何在JDK8终结?

题干:“HashMap在多线程下put会怎样?” 若只答“数据丢失”,会被追问:“JDK7会发生死循环,为什么?” 必须还原链表头插法导致的环形链表:

// JDK7 transfer()方法伪代码(多线程并发扩容时) void transfer(Entry[] newTable) { for (Entry<K,V> e : table) { while(null != e) { Entry<K,V> next = e.next; // 保存下一个节点 int i = indexFor(e.hash, newTable.length); e.next = newTable[i]; // ⚠️ 头插法:e.next指向新表头 newTable[i] = e; // 新表头更新为e e = next; // 继续处理下一个 } } }

死循环触发条件:

  • 线程A执行到e.next = newTable[i],此时newTable[i]为null
  • 线程B完成整个transfer,newTable[i]指向链表A→B→C
  • 线程A继续执行newTable[i] = e,将A插入表头 → newTable[i] = A→B→C
  • 但A的next仍指向原链表头(B),形成A→B→C→A环形链表
    JDK8修复:改用尾插法 + 红黑树,彻底杜绝环形链表。

4.2 ArrayList的fail-fast机制:迭代器删除的正确姿势

题干:“遍历ArrayList时删除元素会怎样?” 标准答案“ConcurrentModificationException”,但高分答案要给出三种安全删除方案对比:

方案代码适用场景
Iterator.remove()it.remove();✅ 最安全,迭代器内置modCount校验
for循环倒序for(int i=list.size()-1; i>=0; i--) list.remove(i);✅ 避免索引越界
removeIf()list.removeIf(s -> s.startsWith("a"));✅ JDK8+推荐,内部用Iterator
// 错误示范:正向for循环删除 List<String> list = new ArrayList<>(Arrays.asList("a","b","c")); for(int i=0; i<list.size(); i++) { if(list.get(i).equals("b")) list.remove(i); // 删除b后c前移,i++跳过c! } // 正确示范:倒序删除 for(int i=list.size()-1; i>=0; i--) { if(list.get(i).equals("b")) list.remove(i); // 安全删除 }

4.3 ConcurrentHashMap的分段锁进化史:从Segment到CAS的性能跃迁

题干:“ConcurrentHashMap如何保证线程安全?” 若只答“分段锁”,会被追问:“JDK8还用Segment吗?” 必须指出JDK7与JDK8架构代差:

  • JDK7:Segment[] segments数组,每个Segment是独立的HashEntry数组,加锁粒度为Segment
  • JDK8:取消Segment,采用Node[] table+synchronized锁单个桶(Node) +CAS操作
    性能对比实验:
// JDK8中put操作核心逻辑 final V putVal(K key, V value, boolean onlyIfAbsent) { if (key == null || value == null) throw new NullPointerException(); int hash = spread(key.hashCode()); // 扰动hash int binCount = 0; for (Node<K,V>[] tab = table;;) { Node<K,V> f; int n, i, fh; if (tab == null || (n = tab.length) == 0) tab = initTable(); // 初始化table else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) { // CAS插入:无锁化 if (casTabAt(tab, i, null, new Node<K,V>(hash, key, value, null))) break; } else if ((fh = f.hash) == MOVED) // 扩容中 tab = helpTransfer(tab, f); else { V oldVal = null; synchronized (f) { // ⚠️ 锁住单个Node,非整个Segment if (tabAt(tab, i) == f) { if (fh >= 0) { // 链表 binCount = 1; for (Node<K,V> e = f;; ++binCount) { K ek; if (e.hash == hash && ((ek = e.key) == key || (ek != null && key.equals(ek)))) { oldVal = e.val; if (!onlyIfAbsent) e.val = value; break; } Node<K,V> pred = e; if ((e = e.next) == null) { pred.next = new Node<K,V>(hash, key, value, null); break; } } } // ... 红黑树处理 } } if (binCount != 0) { if (binCount >= TREEIFY_THRESHOLD) treeifyBin(tab, i); if (oldVal != null) return oldVal; break; } } } addCount(1L, binCount); return null; }

易错点:很多人以为ConcurrentHashMap“完全无锁”,实际是细粒度锁+无锁化CAS混合模型。

5. JVM与设计模式:从类加载到单例的5层防御体系

5.1 类加载双亲委派的破局点:如何自定义ClassLoader绕过?

题干:“双亲委派模型有什么作用?” 标准答案“避免类重复加载、安全沙箱”,但高分答案要给出打破委派的实战代码:

public class CustomClassLoader extends ClassLoader { private String classPath; public CustomClassLoader(String classPath) { this.classPath = classPath; } @Override protected Class<?> findClass(String name) throws ClassNotFoundException { byte[] classData = loadClassData(name); if (classData == null) { throw new ClassNotFoundException(); } // ⚠️ 关键:不调用super.findClass(),绕过双亲委派 return defineClass(name, classData, 0, classData.length); } private byte[] loadClassData(String className) { // 从指定路径读取class文件 String path = classPath + File.separatorChar + className.replace('.', File.separatorChar) + ".class"; try (InputStream is = new FileInputStream(path); ByteArrayOutputStream baos = new ByteArrayOutputStream()) { int b; while ((b = is.read()) != -1) { baos.write(b); } return baos.toByteArray(); } catch (IOException e) { return null; } } }

应用场景:

  • Tomcat的WebAppClassLoader:每个应用独立加载,避免jar包冲突
  • OSGi模块化:不同Bundle可加载同名类的不同版本

5.2 单例模式的终极形态:枚举单例为何能防反射攻击?

题干:“如何防止反射破坏单例?” 若只答“枚举单例”,会被追问:“为什么枚举能防?” 必须解析JVM对枚举的特殊保护:

// 枚举单例(防反射、防序列化) public enum SingletonEnum { INSTANCE; public void doSomething() { System.out.println("Singleton by enum"); } } // 反射攻击失败原因:Enum.valueOf()内部校验 // java.lang.Enum.valueOf()源码: public static <T extends Enum<T>> T valueOf(Class<T> enumType, String name) { T result = enumType.enumConstantDirectory().get(name); // 从常量目录获取 if (result != null) return result; if (name == null) throw new NullPointerException("Name is null"); throw new IllegalArgumentException( "No enum constant " + enumType.getCanonicalName() + "." + name); }

反射攻击代码:

// 尝试用反射创建枚举实例(会失败) Constructor<SingletonEnum> c = SingletonEnum.class.getDeclaredConstructor(String.class, int.class); c.setAccessible(true); c.newInstance("INSTANCE", 0); // 抛IllegalArgumentException:Cannot reflectively create enum objects

注意:JVM规范第8.9节明确规定,枚举类的构造方法只能由JVM调用,反射禁止访问。

5.3 GC Roots的四大支柱:哪些对象能作为GC Roots?

题干:“哪些对象可以作为GC Roots?” 标准答案“虚拟机栈局部变量、静态变量、常量、本地方法栈JNI引用”,但高分答案要给出JVM源码级证据:

// hotspot/src/share/vm/gc_implementation/shared/genCollectedHeap.cpp void GenCollectedHeap::process_roots(...) { // 1. 虚拟机栈和本地方法栈中的引用 process_strong_roots(..., &so, &roots); // 2. 方法区中的静态变量(SystemDictionary) process_strong_roots(..., &so, &roots); // 3. JNI全局引用(jobject) process_strong_roots(..., &so, &roots); // 4. JVM内部的常量(如字符串常量池) process_strong_roots(..., &so, &roots); }

实战排查:用jmap -histo:live <pid>查看存活对象,重点观察java.lang.ref.Finalizer数量暴增,说明Finalizer队列积压(FinalizerReference链表未及时处理)。

5.4 Spring Bean生命周期的11个钩子:从实例化到销毁的完整链路

题干:“Spring Bean的生命周期?” 若只答“构造→init→destroy”,会被追问:“Aware接口和InitializingBean哪个先执行?” 必须掌握精确执行顺序:

  1. 实例化Bean(调用构造函数)
  2. BeanNameAware.setBeanName()
  3. BeanFactoryAware.setBeanFactory()
  4. ApplicationContextAware.setApplicationContext()
  5. BeanPostProcessor.postProcessBeforeInitialization()
  6. InitializingBean.afterPropertiesSet()
  7. @PostConstruct注解方法
  8. init-method配置方法
  9. BeanPostProcessor.postProcessAfterInitialization()
  10. DisposableBean.destroy()
  11. destroy-method配置方法
    验证代码:
@Component public class LifecycleBean implements BeanNameAware, BeanFactoryAware, ApplicationContextAware, InitializingBean, DisposableBean { @PostConstruct public void postConstruct() { System.out.println("@PostConstruct"); } public void initMethod() { System.out.println("init-method"); } @Override public void afterPropertiesSet() throws Exception { System.out.println("afterPropertiesSet"); } @Override public void destroy() throws Exception { System.out.println("destroy"); } public void destroyMethod() { System.out.println("destroy-method"); } }

易错点:很多人混淆@PostConstruct和afterPropertiesSet执行顺序,实际是@PostConstruct先于afterPropertiesSet。

5.5 MyBatis一级/二级缓存的失效场景:为什么缓存总不生效?

题干:“MyBatis缓存为什么没命中?” 高分答案要列出7个缓存失效雷区:

  1. SQL语句不一致:select * from uservsselect id,name from user(即使结果相同)
  2. 参数不一致:id=1vsid=1L(Integer vs Long)
  3. 事务未提交:一级缓存(SqlSession级别)在事务提交前不共享
  4. 二级缓存未开启:Mapper.xml中未配置<cache/>或@CacheNamespace
  5. POJO未实现Serializable:二级缓存要求对象可序列化
  6. 查询中含随机函数:SELECT RAND()导致缓存key永远不同
  7. 手动清空缓存:sqlSession.clearCache()或@CacheEvict
    调试技巧:开启MyBatis日志,观察Cache Hit Ratio:
<!-- log4j2.xml --> <Logger name="org.apache.ibatis.cache.decorators.LoggingCache" level="debug"/>

输出:Cache Hit Ratio [com.example.mapper.UserMapper]: 0.5表示50%命中率。

6. 面试官最想听的3个答题心法:从“我会”到“我懂”的质变跃迁

6.1 “讲清楚”比“答正确”更重要:用生活化隐喻重构技术概念

面试官听腻了“HashMap是数组加链表”,他们想听的是你能把技术翻译成人话。比如解释CAS:

“想象银行ATM取款——你卡里有100元,要取50元。传统方式是:先查余额(100),再扣款(100-50=50),最后存回。但万一别人同时取款,就可能覆盖对方操作。CAS相当于ATM说:‘我只在你余额确实是100的时候,才帮你改成50;如果现在余额不是100(比如被别人改成80了),我就放弃操作,让你重试’。这就是Compare And Swap——先比较,再交换,失败就重试。”

这个比喻里藏着三个得分点:

  • 场景具象化(ATM取款)
  • 问题显性化(并发覆盖)
  • 机制可视化(比较→交换→失败重试)

从那以后我每次准备面试,都强制把每个技术点写成30秒生活化解释,对着镜子练到自然脱口而出。因为面试不是考试,是对话——你得让对方大脑里立刻浮现出画面,而不是堆砌术语。

6.2 “挖坑-填坑”式答题法:主动暴露思考过程,把错误变成加分项

当被问“ArrayList和LinkedList区别”,不要平铺直叙,而是这样展开:

“我最初以为‘数组快查慢增删,链表慢查快增删’就够了。但后来在做消息队列消费时发现,用LinkedList做FIFO队列,pollFirst()确实O(1),可一旦要get(1000),就要从头遍历1000次——这时ArrayList的O(1)随机访问就赢了。所以现在我会先问:你的场景是频繁随机访问,还是频繁首尾增删?如果是中间插入,两者都不合适,该用ArrayDeque。”

这里埋了三个钩子:

  • 承认认知局限(“最初以为...”)建立可信度
  • 用真实场景证伪(消息队列消费)体现工程思维
  • 主动延伸解决方案(ArrayDeque)展示知识广度

面试官最怕候选人答得“太完美”,因为真实世界充满trade-off。你暴露思考过程,就是在说:“我知道边界在哪,也愿意探索边界之外。”

6.3 “版本意识”是初级岗的隐形天花板:所有答案必须带JDK版本锚点

同样是“String不可变”,JDK8和JDK17的答案天差地别:

  • JDK8:String内部用char[],不可变靠final修饰数组
  • JDK17:String改用byte[]+coder字段(Latin-1或UTF-16),不可变性更复杂

当你答“HashMap链表转红黑树”,必须说清:

“JDK8中,当链表长度≥8且数组长度≥64时触发转换;但JDK17优化了阈值,改为链表长度≥7时就可能转,因为红黑树查找O(logN)比链表O(N)在大数据量时优势更大。”

我吃过亏——有次答“ConcurrentHashMap分段锁”,面试官笑着问:“你用的JDK几?” 我答“8”,他反问:“JDK8还有Segment吗?” 全场寂静。从那以后我每次开口前,先默念“版本号”,就像程序员写SQL前先看数据库版本一样成为肌肉记忆。

希望帮到你。

本文还有配套的精品资源,点击获取

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进