← 全部材料 Java 核心
JAVA CORE · 17 个高频考点 · JDK 17

Java 的坑都在语义边界上

Java 语法本身不难,难的是几个"看起来一样、实际不一样"的边界:== 和 equals、可变的错觉、擦除后的类型、迭代器里的删除。这些地方答得含糊,说明只写过业务代码没踩过坑。

01先讲结论再举例每节都从一个能跑的例子开始,输出就是答案,不靠背诵。
02盯住三个边界相等性、可变性、类型信息——Java 的题八成出在这三处。
03对照前端经验泛型擦除像 TS,迭代器跳过像 forEach 里 splice,能对上就记住了。
1

String 为什么必须是不可变的★

不是设计洁癖,是三个能力的前提
String 内部是 private final byte[] value(JDK 9 起用 byte[],之前是 char[]),引用不可变、内容也不给你改。换字符串只能造新对象。这一个约束换来了三件事:常量池能安全共享、hashCode 能缓存、可以放心当 HashMap 的 key。
输出b = hello a = hello world a 还是同一个对象吗: false 字面量复用: true new 出来的同一个吗: false equals 呢: true intern 之后呢: true
面试怎么答String 不可变的根本原因不是"为了安全"这种空话,而是它支撑了三个具体能力:一是常量池共享,字面量相同的字符串指向同一块内存,如果可变就互相污染;二是 hashCode 缓存,String 里存了 hash 字段,算一次就复用,因为它是最高频的 Map key;三是天然线程安全,不需要任何同步。代价是拼接会不断造新对象,所以循环拼接要用 StringBuilder。JDK 9 之后内部从 char[] 换成了 byte[] 加一个 coder 字段,纯拉丁字符只用一半内存,这叫紧凑字符串。
2

循环里拼接字符串,慢在哪

编译器帮你优化的,只是单条表达式
很多人背过"编译器会把 + 优化成 StringBuilder"。这句话只对单条表达式成立。写在循环体里的 s += i,每次迭代都是一次完整的"新建 + 拷贝全部旧内容",量级是 O(n²)。JDK 9 起改用 invokedynamic 的字符串拼接,但每次迭代照样新建,循环累积的风险没变。
输出(这台机器上的实测,数字随机器波动,看量级)+= : 134 ms, 长度 88890 StringBuilder : 不到 1 ms, 长度 88890 结果相同: true
面试怎么答循环里用 += 拼字符串的复杂度是 O(n²),因为 String 不可变,每次拼接都要新建对象并把之前所有字符拷过去,第 k 次迭代拷 k 个字符,累加起来就是平方级。StringBuilder 内部是可扩容的数组,只在容量不足时翻倍拷贝,摊还是 O(n)。编译器确实会把加号优化成 StringBuilder,但只针对单条表达式,不会跨循环复用对象,所以循环累积它救不了。实测两万次迭代差两个数量级。实践上单条拼接直接用加号,循环里用 StringBuilder,能预估长度就先给初始容量。
3

== 和 equals 到底在比什么★

一个比地址,一个比内容,装箱还插了一脚
== 比的是引用是不是指向同一个对象(基本类型比数值),equals 默认也是比地址,只有重写之后才比内容。最容易翻车的是包装类:自动装箱会走缓存,-128 到 127 之间返回同一个对象,超出就新建。
输出127 == 127 : true 128 == 128 : false 128 equals : true Long 128 == : false new Integer(127) == 127 : false
面试怎么答== 对基本类型比数值,对引用类型比地址。equals 是 Object 的方法,默认实现就是 ==,只有重写后才比内容。包装类的坑在于自动装箱走 Integer.valueOf,-128 到 127 有缓存,返回同一对象,所以这个区间内 == 是 true,超出就是 false。这个范围上限还能通过 AutoBoxCacheMax 调,所以它根本不是语言保证。结论很简单:包装类一律用 equals,或者更稳的做法是先拆箱成基本类型再比。还要注意 Integer 和 Long 之间 equals 永远 false,因为 equals 里有 instanceof 判断。
4

重写 equals 就必须重写 hashCode

不是规范洁癖,是散列表的查找前提
HashMap 找 key 分两步:先按 hashCode 定位到桶,再在桶里用 equals 逐个比。只重写 equals 不重写 hashCode,两个"相等"的对象会算出不同的桶,于是 get 直接找不到——返回 null,还不报错。
输出只重写 equals,同 id 新对象去取: null 两个都重写,同 id 新对象去取: 张三 equals 相等: true hashCode 相等: false 最后两行就是病根:equals 说相等,hashCode 说不是一路人
面试怎么答HashMap 的查找是先用 hashCode 定位桶,再在桶内用 equals 比对,所以两个 equals 相等的对象如果 hashCode 不同,会落到不同桶,get 直接返回 null 且不报错。这就是必须成对重写的全部原因,本质是散列表的查找前提,不是风格规范。还有两个延伸考点:可变对象当 key 会导致 hashCode 变化后彻底找不到,实质是内存泄漏;Lombok 生成的两个方法是一致的,但子类继承时默认不含父类字段,要显式加 callSuper。
5

ArrayList 和 LinkedList 怎么选

链表在纸上更快,在 CPU 上更慢
教科书说"随机访问用 ArrayList,频繁插入删除用 LinkedList"。后半句在真实数据上基本是错的:链表插入前要先从头走到那个位置,本身是 O(n),而且节点散落在堆上,缓存命中率极差。实测中间插入 LinkedList 比 ArrayList 还慢。
输出(数字随机器波动,重点看倍数)中间位置 get x2000 : ArrayList 0 ms / LinkedList 68 ms 中间位置 add 一次 : ArrayList 11 us / LinkedList 65 us
面试怎么答教科书那句"随机访问用 ArrayList、频繁增删用 LinkedList"只对了一半。LinkedList 的插入本身是 O(1),但前提是你已经拿到了那个节点的引用;通过 List 接口按索引插入,得先遍历到位置,复杂度还是 O(n),而且节点在堆上分散,指针追逐的缓存命中率很低。实测 5 万元素中间插入,LinkedList 比 ArrayList 慢三四倍,因为 ArrayList 是 arraycopy 连续内存拷贝,速度快得多。所以实践结论是默认用 ArrayList,LinkedList 只在已经持有节点引用、或者当 Deque 用的时候才有价值。
6

HashMap 什么时候扩容,什么时候树化★

把反射打开看真实结构
HashMap 是数组 + 链表 + 红黑树。容量永远是 2 的幂,负载因子 0.75,元素数超过 容量 × 0.75 就翻倍扩容。链表长度到 8 并且容量到 64 才树化——不到 64 优先扩容而不是树化。
输出(JDK 17,需要 --add-opens 才读得到私有字段)刚创建 table 长度: 0 放 12 个后 table 长度: 16 放第 13 个后 table 长度: 32 放 25 个后 table 长度: 64 "Aa" 和 "BB" 的 hashCode 相等: true 两个都放进去后 size: 2
面试怎么答HashMap 是数组加链表加红黑树,容量恒为 2 的幂,负载因子 0.75,元素数超过容量乘 0.75 就翻倍扩容,因为索引用 n-1 与 hash 做位运算代替取模,容量是 2 的幂才能保证低位全 1、分布均匀。链表长度到 8 并且表容量到 64 才树化,不到 64 优先扩容,因为扩容会重新打散节点。hash 计算前会用高低 16 位异或做扰动,因为定位只用到低位。JDK 8 把扩容的头插改成尾插,避免了 1.7 并发扩容成环导致 CPU 打满,但 HashMap 依然不是线程安全的,并发场景要用 ConcurrentHashMap。
7

遍历时删除元素会怎样★

fail-fast 不是保证,跳过元素比抛异常更危险
用 for-each 遍历时调 list.remove(),可能抛 ConcurrentModificationException,也可能静默跳过元素——取决于删的是第几个。判断依据是内部 modCount 和迭代器手里的 expectedModCount 是否还相等。
输出删 a 之后继续走: ConcurrentModificationException 删 b 不抛异常,结果却是: [a, c],c 被跳过了 iterator.remove 之后: [a] Arrays.asList 删元素: UnsupportedOperationException
面试怎么答for-each 底层是迭代器,迭代器在 next 里比较 modCount 和 expectedModCount,不等就抛 ConcurrentModificationException。但删元素的位置会决定结果:删中间的,size 缩小后游标正好到达末尾,hasNext 返回 false,循环提前结束,不抛异常但后面的元素被跳过,这种静默数据丢失比抛异常危险得多。正确做法是三种:用 iterator.remove,它会同步 expectedModCount;用 removeIf;或者倒序遍历。还要补充 fail-fast 不是并发保证,JDK 文档只说它用于发现 bug,并发场景必须用并发容器。
8

泛型是编译期的,运行时会被擦掉

擦除带来的三个后果,每个都是高频题
Java 泛型是编译期检查 + 运行时擦除,List<String> 和 List<Integer> 在运行时是同一个类。擦除带来三个后果:不能按泛型参数重载、不能 new T[]、能用反射塞进错误类型——而且编译期不报错,取出来赋值时才炸。
输出取出来是: java.lang.Integer 赋值时才抛 ClassCastException 运行时类型: java.util.ArrayList 两个泛型参数是同一个类: true
面试怎么答Java 泛型只在编译期做检查,运行时被擦除,List<String> 和 List<Integer> 是同一个类。擦除有三个直接后果:第一不能按泛型参数重载,因为擦完签名一样,编译报 same erasure;第二不能 new T 数组,运行时不知道 T 是什么,只能传 Class 对象用 Array.newInstance;第三可以通过原始类型绕过检查塞进错误元素,编译不报错,取出来赋值时才抛 ClassCastException,因为赋值点会插入 checkcast 指令。另外多态分派靠编译器生成的桥方法来兼容擦除后的签名。选擦除是为了兼容 Java 5 之前的老代码。
9

finally 里的 return 会吃掉异常★

写得越"稳妥",越容易埋雷
finally 一定会执行,而且它的 return 会覆盖 try 里的返回值、并丢弃 try 里正在抛的异常。所以"在 finally 里兜底返回"这种写法,等于把所有异常变成静默的正常返回,是线上最难查的一类 bug。
输出finally 里 return 覆盖返回值: 2 finally 里改局部变量: 1 finally 里 return 吞掉异常: 3
面试怎么答finally 一定执行,但它的 return 会覆盖 try 的返回值并且丢弃异常,因为 return 直接结束方法,待抛的异常被抛弃。所以生产代码里绝对不要在 finally 里 return。另一个细节是 return 表达式在进入 finally 之前就求值并压栈了,finally 里改局部变量不会影响已经算好的返回值。还要知道 finally 在 System.exit、JVM 崩溃、守护线程被杀这几种情况下不会执行。异常设计上,受检异常强制处理但会污染签名、在 lambda 里没法用,所以现在的主流做法是统一转成非受检异常,Spring 就是这么做的。
10

反射与动态代理:Spring 的地基★

理解这一节,Bean 和 AOP 就不用背了
反射是运行时按名字找类、找方法、改字段、造对象。动态代理是在反射之上再包一层:不写实现类,运行时生成一个新类替你转调。JDK 的代理只能代理接口,CGLIB 的代理靠继承生成子类。Spring 的 IoC 和 AOP 全都建立在这两件事上。
输出[前置] find 参数 7 [后置] 返回 用户7 结果: 用户7 代理类: $Proxy0 还是 UserService 吗: true
面试怎么答反射是运行时按名字获取类、方法、字段并调用,Class 对象是入口。动态代理是在运行时生成一个新类,把方法调用转发到 InvocationHandler,Spring 的事务、缓存、日志都是这么插进去的。两种实现:JDK 代理基于接口,因为只能 implements;CGLIB 用 ASM 生成目标类的子类,所以能代理普通类但代理不了 final 类和 final 方法。Spring Boot 2.x 默认 CGLIB。AOP 失效最常见的原因是同类内部 this 调用不经过代理。反射本身性能在现代 JVM 上已接近直接调用,但要缓存 Method 对象避免重复查找。JDK 9 之后模块系统会禁止反射访问其他模块的私有成员,需要 add-opens。
11

IO、NIO 与零拷贝

四次拷贝变两次,省的是 CPU 和上下文切换
传统"读文件再写 socket"要经过四次拷贝、四次用户态/内核态切换。零拷贝(sendfile)让数据不进用户态,在内核里直接搬运,降成两次拷贝。Java 的 FileChannel.transferTo 底层用的就是它。
两次拷贝差在哪【传统 read + write】4 次拷贝 磁盘 → 内核页缓存 (DMA 拷贝) 内核页缓存 → 用户缓冲区(CPU 拷贝) ← 这次是白干的 用户缓冲区 → socket 缓冲区(CPU 拷贝) ← 这次也是白干的 socket 缓冲区 → 网卡 (DMA 拷贝) 外加 4 次用户态/内核态切换 【sendfile】2 次拷贝 磁盘 → 内核页缓存 (DMA 拷贝) 内核页缓存 → 网卡 (DMA 拷贝,带 scatter-gather 时 CPU 完全不管数据) 切换:2 次
面试怎么答传统 read 加 write 要四次拷贝四次切换:磁盘到页缓存、页缓存到用户缓冲区、用户缓冲区到 socket 缓冲区、socket 缓冲区到网卡,中间两次 CPU 拷贝在纯转发场景下是白干的。sendfile 让数据不进用户态,内核里直接从页缓存搬到网卡,配 scatter-gather DMA 后 CPU 完全不参与数据搬运,降到两次拷贝两次切换。Java 里对应 FileChannel.transferTo。但需要在数据中间做加密压缩的场景用不了,数据必须进用户态。注意零拷贝解决的是数据搬几次,BIO/NIO/AIO 解决的是谁来等,两回事。
12

Java 序列化:能不用就别用

它在生产环境里主要是负债
Java 原生序列化把对象转成字节流,只序列化非静态、非 transient 的字段,靠 serialVersionUID 判定版本是否兼容。它在生产环境的问题比价值多:不可读、跨语言不通、反序列化是经典漏洞入口。现在基本用 JSON 或 Protobuf 替代。
输出序列化后字节数: 68 字节里能找到密码吗: false name = 张三 password = null company = B 公司
面试怎么答Java 原生序列化只序列化非静态、非 transient 的字段,静态字段属于类不跟着对象走。serialVersionUID 决定版本兼容性,不显式声明时由编译器按类结构自动计算,改一个字段就会变,老数据反序列化会抛 InvalidClassException,所以必须显式写死。生产环境的问题有三个:一是反序列化是经典漏洞入口,readObject 可以被利用执行任意代码,外部输入绝对不能直接反序列化;二是体积大速度慢,因为它要写完整的类元信息;三是跨语言不通。现在主流用 JSON 做通用交换,Protobuf 做高性能 RPC。
13

static 嵌套类和内部类差在哪

一个持有外部引用,一个不持有
static class 叫静态嵌套类,和外部类只是命名空间关系;不加 static 的才是内部类,每个实例都隐式持有一个 this$0 指向外部对象。这个隐藏引用是内存泄漏的常见源头——内部类活得久,外部对象就跟着活。
输出 内部类看到外部字段: 占内存的大对象 内部类有外部引用吗: true 静态嵌套类有 this$0 吗: false
面试怎么答加了 static 的是静态嵌套类,和外部类只是命名空间关系;不加的才是内部类,每个实例都隐含一个 this$0 字段指向外部对象,创建时要写 outer.new Inner()。这个隐藏引用是内存泄漏的常见原因:内部类被长生命周期对象持有时,外部对象也回收不了,Android 的 Handler 泄漏就是典型。所以除非真要访问外部实例状态,一律加 static,这是 Effective Java 的建议。另外 final 修饰字段只是引用不可变,对象内部状态照样能改,final List 还能 add。
14

重载看编译期类型,重写看运行期类型

一句话区分静态分派和动态分派
重载(overload)在编译期就定死了,编译器按参数的静态类型选方法;重写(override)在运行期按实际类型分派,走虚方法表。所以把 String 赋给 Object 变量再传给重载方法,走的是 Object 那个版本。
输出静态类型是 Object: -> Object 版本 静态类型是 String: -> String 版本 传 null: -> String 版本 重写看实际类型: -> Sub.hi
面试怎么答重载是编译期按参数的静态类型选方法,叫静态分派;重写是运行期按对象的实际类型选实现,叫动态分派,靠虚方法表。所以把 String 赋给 Object 变量去调重载方法,走的是 Object 版本。返回值不能作为重载依据,因为调用点无法区分。传 null 时编译器挑最具体的参数类型,如果有两个平级的重载就会报歧义。重写的规则是两同两小一大,private、static、final 方法不能被重写。多态的实现就是对象头里的类型指针找到方法表再取槽位。
15

接口的 default 方法冲突怎么解

Java 8 加它,是为了不破坏已有实现
两个接口有同名 default 方法,一个类同时实现它们时编译直接报错,必须显式覆盖,并用 接口名.super.方法() 指定要调哪个。Java 8 引入 default 的唯一动机是:给老接口加方法,不破坏所有已有实现类。
输出A+B 如果把 @Override 那段删掉,编译直接报: class DefaultConflict inherits unrelated defaults for hello() from types A and B
面试怎么答两个接口有同名 default 方法时,实现类必须显式覆盖,否则编译报错说是继承了不相关的默认方法,解法是在实现里用接口名点 super 调用指定版本。优先级是类中的实现最高,然后是子接口,最后是父接口;如果父类和接口有同名方法,父类的具体方法优先,这条叫类优先原则。default 方法是 Java 8 为了向后兼容加的,目的是给 Collection 这类老接口加 stream 方法而不让全世界的实现类编译失败。接口和抽象类的分工是,接口定义能力且能多实现,抽象类能持有实例状态但只能单继承。
16

try-with-resources 的关闭顺序

后开的先关,关闭失败的异常会被压制
多个资源写在同一个 try 括号里,关闭顺序和打开顺序相反(后开先关,像栈)。如果业务代码和 close 都抛异常,业务异常是主异常,close 的异常挂在 suppressed 里,不会覆盖主异常——这正是它比手写 finally 强的地方。
输出-- 正常退出 -- 打开 A 打开 B 业务代码 关闭 B 关闭 A -- 业务和 close 都抛 -- 打开 C 关闭 C 捕获: 业务异常 被压制的: C 关闭失败
面试怎么答多个资源写在同一个 try 括号里,关闭顺序是后开先关,因为后开的资源可能依赖先开的,比如包装流。它比手写 finally 强的核心不是省代码,而是异常处理:业务异常和 close 抛的异常同时出现时,业务异常作为主异常抛出,close 的异常挂在 getSuppressed 里,不会覆盖主异常。手写 finally 最常见的 bug 就是 close 抛异常把原始业务异常吞掉,日志里只剩关闭失败。资源变量是隐式 final。AutoCloseable 是基接口,Closeable 是它的子接口,只多一个 IOException 约束。
17

金额为什么必须用 BigDecimal★

double 存不下 0.1,这不是 Java 的问题
float/double 是二进制浮点,0.1 在二进制里是无限循环小数,只能近似存。所以 0.1 + 0.2 != 0.3。金额场景必须用 BigDecimal,而且要用字符串构造、用 compareTo 比较、divide 必须指定精度。
输出0.1 + 0.2 = 0.30000000000000004 0.1 + 0.2 == 0.3 ? false new BigDecimal(0.1) = 0.1000000000000000055511151231257827021181583404541015625 BigDecimal.valueOf(0.1) = 0.1 new BigDecimal("0.1") = 0.1 equals : false compareTo : true divide 不整除: Non-terminating decimal expansion; no exact representable decimal result. 给精度: 0.3333
面试怎么答float 和 double 是二进制浮点,0.1 在二进制里是无限循环小数只能近似表示,所以 0.1 加 0.2 不等于 0.3。金额必须用 BigDecimal,而且有三个必须注意的点:一是不能用 double 构造,new BigDecimal(0.1) 会把 double 的误差一比一搬进来,要用字符串或者 valueOf;二是比较不能用 equals,因为 equals 比数值加精度,1.0 和 1.00 是 false,要用 compareTo;三是 divide 遇到无限小数会抛 ArithmeticException,必须显式指定 scale 和 RoundingMode。另外这个精度问题不是 Java 独有的,JS 里完全一样,因为都是 IEEE 754。
18

Java 8:Lambda、Stream 与 Optional

lambda is syntax sugar, the stream is the point
Lambda 只是把匿名内部类写短了,真正的变化是 Stream 把"遍历"变成了"声明式流水线"。它有两个必须知道的特性:中间操作是惰性的(不写终止操作就一行都不跑)、一个流只能消费一次。Optional 则是给返回值用的容器,不是"用来判空"的工具。
实测输出(JDK 17)--- 流已经建好,但什么都没发生 --- filter 处理了 amy filter 处理了 bob filter 处理了 cathy filter 处理了 david filter 处理了 eve count = 2(到这里才真的开始跑) 链式结果: CATHY, DAVID findFirst 命中: david findFirst 落空: (没有) map 之后: -1

✕ Optional 的典型误用

  • 把 Optional 当参数类型 —— 参数应该直接要求非空,或者用重载
  • 把 Optional 当字段 —— 它不实现 Serializable,而且多一层包装
  • optional.get() 前先 isPresent() —— 白用了,和 if (x != null) 一样
  • orElse(compute()) —— 无论命不命中都会算一遍

✓ 正确的用法

  • 只用在返回值上,表示"这里可能没有结果",强迫调用方处理
  • 用 orElse / orElseGet / orElseThrow 收尾
  • 需要惰性求值用 orElseGet(() -> 默认值),别用 orElse
  • 用 map / filter 链式处理,避免嵌套判空
面试怎么答Lambda 本质是函数式接口的实例,编译后是一个 invokedynamic 指令,不是匿名内部类,所以不会生成额外的 class 文件。Stream 是惰性的:filter、map 这些中间操作只做登记,只有遇到 count、collect、findFirst 这类终止操作才会真正开始遍历,所以一条流水线可以一次遍历完成多次转换。要注意流只能消费一次,终止操作之后流就关闭了。parallelStream 用的是 ForkJoinPool 的公共池,在 Web 应用里可能和业务抢线程,数据量小或者有 IO 的时候反而更慢。Optional 应该只用在返回值上表示可能没有结果,不要当参数或字段,取值用 orElse 或者 orElseThrow,而不是 get 之前先 isPresent,那样和判空没区别。
19

速答清单

面试前一小时只看这页
01Stream 的中间操作和终止操作有什么区别?
中间操作(filter、map、sorted)只登记不执行,是惰性的;终止操作(count、collect、findFirst、forEach)一出现整条流水线才开始跑。所以不写终止操作,一个元素都不会被处理。
02一个 Stream 能用两次吗?
不能。终止操作执行后流就关闭了,再调用抛 IllegalStateException: stream has already been operated upon or closed。要复用就把 stream() 的调用包成方法,每次重新生成。
03parallelStream 有什么坑?
它用的是 ForkJoinPool 的公共池,线程数是 CPU 核数减一。Web 应用里会和业务抢同一个池,一个慢任务能把池占满。数据量小或者操作里有 IO 时,并行流反而比串行慢。
04Lambda 里为什么不能改外部变量?
捕获的是值的副本,不是引用。改了会让副本和原变量不一致,所以要求 effectively final——不是完全不能改,而是不能在捕获之后再赋值。要改就用单元素数组或 AtomicInteger。
05Optional 该怎么用?
只用在返回值上,表示"这里可能没有结果"。取值用 orElse / orElseGet / orElseThrow,不要 get 之前先 isPresent,那和判空没区别。不要当参数类型和字段,它不实现 Serializable,而且多一层包装。
06orElse 和 orElseGet 有什么区别?
orElse 的参数是值,无论 Optional 有没有值都会先算出来;orElseGet 的参数是 Supplier,只有真的为空时才调用。默认值需要计算(比如查库)时用 orElseGet。
07String 为什么不可变?
内部是 final 的字节数组,引用和内容都不给改。换来常量池可共享、hashCode 可缓存、天然线程安全。
08String、StringBuilder、StringBuffer 怎么选?
少量拼接用 +;循环累积用 StringBuilder;需要同步才用 StringBuffer,实际很少需要。
09字符串常量相加和变量相加有什么区别?
常量在编译期直接折叠成一个字面量存进常量池,零运行时开销;变量必须运行时拼接,每次新建对象。
10== 和 equals 的区别?
== 比引用(基本类型比数值),equals 默认也是比引用,重写后才比内容。包装类一律用 equals。
11Integer 128 == Integer 128 为什么是 false?
自动装箱走 Integer.valueOf,-128 到 127 有缓存返回同一对象,超出就新建。上限可用 AutoBoxCacheMax 调,所以不是语言保证。
12为什么重写 equals 必须重写 hashCode?
HashMap 先用 hashCode 定桶再用 equals 比对,不重写会落到不同桶,get 返回 null 且不报错。
13可变对象为什么不能当 HashMap 的 key?
放进去后改字段会改变 hashCode,桶位置变了,get 和 remove 都找不到,对象永久留在表里,是内存泄漏。
14ArrayList 默认容量多少,怎么扩容?
无参构造是空数组,首次 add 分配 10。扩容是 old + old>>1,即 1.5 倍,用 arraycopy 搬数据。
15为什么 LinkedList 插入反而不快?
按索引插入要先遍历定位,本身 O(n),而且节点分散导致缓存命中率差。只有已持有节点引用时才快。
16HashMap 的容量为什么必须是 2 的幂?
索引用 (n-1) & hash 代替取模,位运算更快,且 n-1 低位全 1 时分布更均匀。new HashMap(100) 会被取整到 128。
17HashMap 什么时候树化?
链表长度到 8 并且 table 长度到 64 才转红黑树;不到 64 优先扩容。退化阈值是 6,留 7 作缓冲。
18JDK 8 为什么把头插改成尾插?
1.7 并发扩容头插会反转链表并可能成环,导致 CPU 100%。改成尾插规避了这个问题,但 HashMap 依然不是线程安全的。
19for-each 里删元素为什么有时不报错?
删倒数第二个时 size 缩小后 hasNext 直接返回 false,循环提前结束,异常没机会触发,但后面的元素被静默跳过。
20正确删除集合元素的三种写法?
iterator.remove()、removeIf()、倒序遍历按下标删。前两个最稳。
21泛型擦除带来哪些限制?
不能按泛型参数重载(签名相同)、不能 new T[]、不能 instanceof List<String>、能用原始类型绕过检查。
22桥方法是什么?
泛型类实现泛型接口时,编译器生成的额外方法,参数是擦除后的类型,内部强转后调真实方法,用于保证多态分派。
23finally 里的 return 有什么后果?
覆盖 try 的返回值,并丢弃正在抛的异常。生产代码里 finally 有 return 基本等同于 bug。
24finally 什么情况下不执行?
System.exit、Runtime.halt、JVM 崩溃、守护线程被强制结束。
25受检异常和非受检异常怎么选?
受检强制处理但污染签名、lambda 里没法用,所以主流是把受检异常转成非受检(Spring 就是这么做的)。
26JDK 动态代理和 CGLIB 的区别?
JDK 基于接口,只能代理由接口定义的方法;CGLIB 生成子类,能代理普通类但不能代理 final 类和 final 方法。Spring Boot 2.x 默认 CGLIB。
27AOP 什么时候会失效?
同类内部 this 调用不经过代理。解法是自注入、AopContext.currentProxy()、或把方法拆到另一个 Bean。
28零拷贝省掉了什么?
省掉内核页缓存到用户缓冲区、用户缓冲区到 socket 缓冲区这两次 CPU 拷贝,以及两次上下文切换。sendfile 底层靠 DMA 搬运。
29为什么需要加密压缩就不能用零拷贝?
数据必须进用户态才能被应用处理,一旦进用户态就退化成传统的四次拷贝。
30Java 序列化有哪些问题?
不可读、体积大、跨语言不通、反序列化是经典漏洞入口。生产上用 JSON 或 Protobuf。
31serialVersionUID 不写会怎样?
编译器按类结构自动算,改一个字段值就变,老数据反序列化抛 InvalidClassException。必须显式写死。
32transient 和 static 字段会被序列化吗?
都不会。transient 是明确排除,static 属于类不跟着对象走。
33内部类和静态嵌套类的区别?
内部类隐含 this$0 指向外部实例,创建要写 outer.new Inner(),会拖住外部对象导致泄漏;静态嵌套类不持有。
34final 修饰字段等于不可变吗?
不等于。只是引用不能变,对象内部状态照样能改,final List 还能 add。
35重载和重写分别什么时候确定?
重载编译期按静态类型定(静态分派),重写运行期按实际类型定(动态分派,走虚方法表)。
36为什么返回值不能作为重载依据?
调用点无法从方法名和参数区分要调哪个,编译器没法生成确定的调用指令。
37两个接口有同名 default 方法怎么办?
必须显式覆盖,用 接口名.super.方法() 指定。父类和接口同名时父类优先。
38try-with-resources 的关闭顺序?
后开先关。业务异常和 close 异常同时出现时,业务异常是主异常,close 的挂到 suppressed 里。
39BigDecimal 有哪些坑?
别用 double 构造(要用字符串或 valueOf)、比较用 compareTo 不用 equals、divide 必须指定 scale 和 RoundingMode。
40为什么 0.1 + 0.2 不等于 0.3?
IEEE 754 二进制浮点表示不了 0.1,只能近似。这不是 Java 的问题,JS 里一模一样。
41金额计算还有别的做法吗?
存整数分,用 long 运算,展示时再除以 100。高频计费场景比 BigDecimal 更快,但要自己管好精度。