0.0 为什么这篇是第 0 篇
集合篇从
ArrayList讲起,多线程篇从synchronized讲起——它们都默认你已经懂==和equals的区别、懂值传递、懂泛型擦除。这篇就是把那些「默认」补上。
版本坐标(2026-10):
| JDK | 发布 | 定位 |
|---|---|---|
| 8 | 2014-03 | 存量项目基线,面试语法默认口径 |
| 11 | 2018-09 | LTS |
| 17 | 2021-09 | LTS,Spring Boot 3 / 4 的基线 |
| 21 | 2023-09 | LTS,虚拟线程 GA |
| 25 | 2025-09 | 当前 LTS |
| 26 / 27 | 2026-03 / 2026-09 | 非 LTS,尝鲜 |
面试口径:语法按 JDK 8 答,特性按 17 / 21 补。被问到「你们用的 JDK 多少」,答 17 或 21 都很常见。
0.1 基本类型与包装类
八种基本类型
| 类型 | 字节 | 范围 | 默认值 | 包装类 |
|---|---|---|---|---|
byte | 1 | -128 ~ 127 | 0 | Byte |
short | 2 | -32768 ~ 32767 | 0 | Short |
int | 4 | -2³¹ ~ 2³¹-1 | 0 | Integer |
long | 8 | -2⁶³ ~ 2⁶³-1 | 0L | Long |
float | 4 | ±3.4e-38 ~ 3.4e38 | 0.0f | Float |
double | 8 | ±4.9e-324 ~ 1.8e308 | 0.0d | Double |
char | 2 | 0 ~ 65535(UTF-16 码元) | '\u0000' | Character |
boolean | 1(JVM 实现相关) | true / false | false | Boolean |
自动装箱与拆箱
Integer a = 127, b = 127;
System.out.println(a == b); // true —— 命中缓存
Integer c = 128, d = 128;
System.out.println(c == d); // false —— 各自 new 了一个
System.out.println(c.equals(d)); // true
Integer内部有IntegerCache,默认缓存-128 ~ 127(下界可调,上界可用-XX:AutoBoxCacheMax改)- 比较值永远用
equals,==比的是引用 Integer与int比较时,Integer会自动拆箱,所以c == 128为true
空指针陷阱:
Integer x = null;
int y = x; // NullPointerException:自动拆箱时调用 x.intValue()
金额为什么不能用 double
float / double 用 IEEE 754 二进制表示,0.1 无法精确表示:
System.out.println(0.1 + 0.2); // 0.30000000000000004
System.out.println(new BigDecimal(0.1)); // 0.1000000000000000055511151231257827...
金额一律用
BigDecimal,并且必须用 String 构造(new BigDecimal("0.1")),用 double 构造会把误差原样带进来。比较用compareTo(),不要用equals()(后者连精度1.0vs1.00都不一样)。
为什么泛型不能用基本类型
List<int> list = new ArrayList<>(); // ❌ 编译错误
List<Integer> list = new ArrayList<>(); // ✅
泛型在编译期会被擦除成 Object,基本类型不是对象,无法参与。需要装箱——这也是 List<Integer> 存大量小整数时内存开销远大于 int[] 的原因。
0.2 String 与字符串池
为什么 String 不可变
public final class String {
private final byte[] value; // JDK 9+ Compact Strings:Latin-1 用 1 字节/字符
private int hash; // 懒计算并缓存
}
不可变带来三个好处:
- 线程安全:无状态可变,天然可共享
- 可缓存 hash:HashMap 的 key 不用每次重算
- 字符串常量池:同一字面量可以安全复用
JDK 8 及以前是
private final char[] value(2 字节/字符),JDK 9 的 Compact Strings(JEP 254) 改成byte[] + coder,纯 ASCII 字符串内存直接减半。
字符串常量池
String s1 = "abc"; // 字面量 → 池
String s2 = "abc"; // 复用池中同一个对象
String s3 = new String("abc"); // 强制在堆上新建
String s4 = s3.intern(); // 手动入池,返回池中引用
s1 == s2; // true
s1 == s3; // false
s1 == s4; // true
| 问题 | 答案 |
|---|---|
new String("abc") 创建几个对象? | 池里没有:2 个(池中字面量 + 堆对象);池里已有:1 个 |
String s = "a" + "b" 编译后是什么? | 编译期常量折叠,直接是 "ab" |
| 池在哪儿? | JDK 7 起从永久代移到堆(避免 PermGen OOM) |
字符串拼接的真实实现
String s = "a" + b + "c"; // 普通表达式
- JDK 8:反编译看到
new StringBuilder().append(a).append(b).append(c).toString() - JDK 9+:改用
invokedynamic+StringConcatFactory,由 JVM 在运行期选择最优策略
但循环里仍然是坑——每次迭代都会新建对象:
// ❌ O(n²)
String s = "";
for (String part : parts) s += part;
// ✅ O(n)
StringBuilder sb = new StringBuilder();
for (String part : parts) sb.append(part);
String / StringBuilder / StringBuffer
| String | StringBuilder | StringBuffer | |
|---|---|---|---|
| 可变性 | 不可变 | 可变 | 可变 |
| 线程安全 | 安全(不可变) | ❌ | ✅(方法加 synchronized) |
| 性能 | 拼接纳差 | 最快 | 比 StringBuilder 慢 |
| 场景 | 常量、key | 单线程拼接 | 多线程共享拼接(很少见) |
实际项目里
StringBuffer基本被淘汰:需要线程安全时,通常用局部StringBuilder就够了。
0.3 == / equals / hashCode
三者关系
| 作用 | 默认实现 | |
|---|---|---|
== | 基本类型比值,引用类型比地址 | — |
equals | 逻辑相等 | Object.equals 就是 == |
hashCode | 散列值,用于哈希表定位 | 对象地址衍生的整数(HotSpot 实现) |
equals 的契约
重写 equals 必须满足:
- 自反:
x.equals(x)为 true - 对称:
x.equals(y)与y.equals(x)一致 - 传递:
x=y、y=z⇒x=z - 一致:只要参与比较的状态没变,结果不变
- 非空:
x.equals(null)为 false
为什么重写 equals 必须重写 hashCode
HashMap 的查找逻辑是:
先比 hash → 落到桶 → 桶内再比 equals
如果两个对象 equals 相等但 hashCode 不同,它们会被分到不同的桶,map.get() 永远找不到——HashMap 直接坏了。
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
User user = (User) o;
return id == user.id && Objects.equals(name, user.name);
}
@Override
public int hashCode() {
return Objects.hash(id, name);
}
这是双向的:只重写
hashCode不重写equals同样会让集合行为错乱,规范要求两者一起改。
可变对象不能当 HashMap 的 key
List<String> key = new ArrayList<>(List.of("a"));
map.put(key, 1);
key.add("b"); // hashCode 变了
map.get(key); // null —— 按新 hash 找不到旧桶了
用
String、Integer、record(不可变)当 key 最稳。
0.4 面向对象
重载 vs 重写
| 重载 Overload | 重写 Override | |
|---|---|---|
| 发生位置 | 同一个类 | 父子类 |
| 方法签名 | 方法名同,参数列表不同 | 方法名 + 参数列表完全相同 |
| 绑定时机 | 编译期(静态绑定) | 运行期(动态绑定) |
| 返回值 | 可不同 | 必须相同或是子类(协变返回) |
| 访问修饰符 | 可不同 | 不能更严格 |
| 异常 | 可不同 | 不能抛更宽的检查异常 |
static / private / final | — | 不能重写(static 是隐藏,不是重写) |
抽象类 vs 接口
| 维度 | 抽象类 | 接口 |
|---|---|---|
| 继承 | 单继承 | 多实现 |
| 成员变量 | 任意 | public static final 常量 |
| 方法 | 抽象 + 具体 | 抽象 + default + static(JDK 8)+ private(JDK 9) |
| 构造器 | 有 | 无 |
| 设计语义 | is-a,共享状态与骨架 | can-do,定义能力契约 |
选择标准:需要复用状态/代码 → 抽象类;只需要约束行为 → 接口。现代 Java 里,接口 +
default已经能覆盖大部分”模板方法”的用法。
多态与动态绑定
Animal a = new Dog();
a.speak(); // 调用 Dog.speak() —— 运行期根据实际类型决定
- 向上转型自动完成,向下转型需要
instanceof检查 - 字段没有多态:
a.name取的是Animal里声明的那个(编译期绑定) static方法也没有多态,只有隐藏
// JDK 16+ 模式匹配,省掉强转
if (a instanceof Dog dog) {
dog.fetch();
}
Java 只有值传递
public static void swap(int a, int b) {
int t = a; a = b; b = t; // 只改了副本,外面纹丝不动
}
对象看起来”被修改了”,是因为传的是引用的副本——副本和原引用指向同一个对象,所以能改对象内容,但改不了引用本身。
初始化顺序
父类静态变量 / 静态块
→ 子类静态变量 / 静态块
→ 父类实例变量 / 实例块 → 父类构造器
→ 子类实例变量 / 实例块 → 子类构造器
静态部分只执行一次,在类加载的初始化阶段;实例部分每次
new都走一遍。
final / static / this / super
| 关键字 | 作用 |
|---|---|
final 变量 | 只能赋值一次(引用不可变,对象内容可变) |
final 方法 | 不可重写 |
final 类 | 不可继承(String、Integer) |
static | 属于类,所有实例共享;静态方法里不能用 this |
this | 当前对象引用;this() 调本类构造器 |
super | 父类引用;super() 调父类构造器,必须第一行 |
注意:
final字段的可见性依赖 JMM 的 final 语义——正确构造的对象,其它线程能看到构造器里赋的 final 值,不需要额外同步(详见多线程篇)。
0.5 异常体系
体系结构
Throwable
├── Error // 程序无法处理,别 catch
│ ├── OutOfMemoryError
│ ├── StackOverflowError
│ └── NoClassDefFoundError
└── Exception
├── RuntimeException // 非受检(unchecked)
│ ├── NullPointerException
│ ├── IllegalArgumentException
│ ├── ClassCastException
│ └── IndexOutOfBoundsException
└── 其它 // 受检(checked)
├── IOException
├── SQLException
└── InterruptedException
| 受检异常 | 非受检异常 | |
|---|---|---|
| 编译器强制处理 | ✅ try-catch 或 throws | ❌ |
| 代表 | IOException | NullPointerException |
| 语义 | 可预期的外部故障,调用方应恢复 | 编程错误,通常不该被捕获 |
现代实践:框架层(Spring)大量把受检异常包装成
RuntimeException(如DataAccessException),因为受检异常会污染整条调用链的签名。
try-with-resources
try (Connection conn = ds.getConnection();
PreparedStatement ps = conn.prepareStatement(sql)) {
// 自动关闭,按声明的**逆序**关闭
} catch (SQLException e) {
// 关闭时抛的异常会作为 suppressed 挂到主异常上
}
- 资源类必须实现
AutoCloseable(或Closeable) - 关闭时抛出的异常不会覆盖业务异常,而是通过
e.getSuppressed()取 - 手写
finally { close(); }的版本里,close()抛异常会吞掉原异常
finally 的三个坑
// 坑 1:finally 里 return,覆盖 try 的返回值
try { return 1; } finally { return 2; } // 返回 2
// 坑 2:finally 里抛异常,吞掉原异常
try { throw new A(); } finally { throw new B(); } // 只看到 B
// 坑 3:finally 修改基本类型返回值无效(副本已保存)
int x = 1;
try { return x; } finally { x = 2; } // 返回 1
异常的性能
fillInStackTrace() 会遍历调用栈,每 new 一个异常都是一次栈采集。所以:
- 不要用异常做流程控制(比如
for循环里catch (NumberFormatException)) - 频繁抛的异常可以复用(没有栈的异常)
- JIT 对反复抛出的隐式异常会省略栈信息(
OmitStackTraceInFastThrow),日志里出现无栈的 NPE 就是这个原因
0.6 泛型与类型擦除
什么是类型擦除
List<String> a = new ArrayList<>();
List<Integer> b = new ArrayList<>();
a.getClass() == b.getClass(); // true,都是 java.util.ArrayList
编译后泛型参数被擦成上界(默认 Object),运行期不存在泛型信息。
擦除带来的限制
| 写法 | 结果 | 原因 |
|---|---|---|
new T() | ❌ | 运行期不知道 T 是什么 |
new T[] | ❌ | 同上 |
instanceof List<String> | ❌ | 运行期没有泛型参数 |
catch (MyException<String> e) | ❌ | 同上 |
重载 f(List<String>) / f(List<Integer>) | ❌ | 擦除后签名相同 |
List<int> | ❌ | 基本类型不是 Object |
需要运行期类型时,用
Class<T>参数或TypeToken(Gson)/ParameterizedTypeReference(Spring)这类技巧。
通配符与 PECS
PECS:Producer Extends, Consumer Super。
// 只读(生产):能读成 Number,不能写
void readAll(List<? extends Number> list) {
Number n = list.get(0); // ✅
// list.add(1); // ❌ 不知道实际是 List<Integer> 还是 List<Double>
}
// 只写(消费):能写 Integer,读出来只能是 Object
void writeAll(List<? super Integer> list) {
list.add(1); // ✅
Object o = list.get(0); // ✅ 只能当 Object
}
经典应用:Collections.copy(List<? super T> dest, List<? extends T> src)。
桥方法
class Node { public Object get() { return null; } }
class IntNode extends Node {
@Override public Integer get() { return 1; } // 协变返回
}
编译器会生成一个合成桥方法 Object get() 来转发到 Integer get(),保证多态正确。反编译能看到 ACC_BRIDGE 标志。
0.7 反射与动态代理
反射
Class<?> clazz = Class.forName("com.example.User"); // 会触发类初始化
Object obj = clazz.getDeclaredConstructor().newInstance();
Method m = clazz.getDeclaredMethod("setName", String.class);
m.setAccessible(true);
m.invoke(obj, "Tom");
| 获取 Class 的方式 | 是否初始化 |
|---|---|
Class.forName("X") | ✅ 会 |
X.class | ❌ 不会 |
obj.getClass() | ❌ 不会 |
ClassLoader.loadClass("X") | ❌ 不会 |
性能:Method.invoke 涉及参数装箱、访问检查,比直接调用慢一个量级。优化手段:
- 缓存
Method对象(别在循环里getDeclaredMethod) MethodHandle/LambdaMetafactory(框架常用,接近直接调用)- JDK 18+ 的
MethodHandles与invokedynamic组合
JDK 17 起强封装(JEP 403):反射访问 JDK 内部 API(如
sun.misc.Unsafe之外的大量java.base私有成员)会直接抛InaccessibleObjectException,需要启动参数--add-opens。老框架升级到 17+ 报这个错,原因就在这里。
JDK 动态代理 vs CGLIB
| JDK 动态代理 | CGLIB | |
|---|---|---|
| 前提 | 目标必须实现接口 | 继承目标类,无需接口 |
| 生成方式 | Proxy.newProxyInstance + InvocationHandler | ASM 生成子类字节码 |
| 限制 | 只能代理接口方法 | 不能代理 final 类 / final 方法 / private 方法 |
| 性能 | 调用略慢(反射) | 生成慢,调用快(子类直接调用) |
Object proxy = Proxy.newProxyInstance(
target.getClass().getClassLoader(),
target.getClass().getInterfaces(),
(p, method, args) -> {
System.out.println("before " + method.getName());
return method.invoke(target, args);
});
Spring AOP 的选择:目标有接口时可用 JDK 代理;Spring Boot 2.x 起默认
proxyTargetClass = true,统一走 CGLIB。这也是为什么在 Spring 里对final方法加@Transactional不生效。
动态代理的经典用途
- Spring AOP / 事务 / 缓存
- MyBatis Mapper 接口(没有实现类,靠代理生成)
- RPC 框架的客户端存根
@Async、@Retryable等注解的运行时增强
0.8 注解与 SPI
注解
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
public @interface MyAnno {
String value() default "";
}
| 元注解 | 含义 |
|---|---|
@Target | 能标注在哪儿(类 / 方法 / 字段 / 参数…) |
@Retention | 保留到哪一阶段:SOURCE / CLASS / RUNTIME |
@Documented | 生成 Javadoc |
@Inherited | 子类是否继承父类上的注解 |
只有
RUNTIME的注解才能被反射读到。Spring 的「组合注解」(如@SpringBootApplication=@Configuration+@EnableAutoConfiguration+@ComponentScan)本质是注解上的注解 +@AliasFor。
SPI(Service Provider Interface)
JDK 原生 SPI:接口方定义规范,实现方在 META-INF/services/<接口全限定名> 文件里写实现类名,ServiceLoader 负责发现。
ServiceLoader<Driver> drivers = ServiceLoader.load(Driver.class);
for (Driver d : drivers) { /* ... */ }
- JDBC 4.0 起,
Class.forName("com.mysql.cj.jdbc.Driver")可以省略,就是 SPI 在起作用 - 缺点:一次性实例化所有实现、无法按名称获取、没有依赖注入
现代变体:
| 框架 | 机制 | 特点 |
|---|---|---|
| Spring Boot 3+ | META-INF/spring/...AutoConfiguration.imports | 取代了 2.x 的 spring.factories |
| Dubbo | META-INF/dubbo/<接口名> | 支持按名称获取、@Adaptive、@Activate |
| Java 9+ 模块 | provides ... with ... | 模块化声明式 SPI |
0.9 Java 8:函数式与 Stream
Lambda 与函数式接口
函数式接口 = 只有一个抽象方法的接口(
@FunctionalInterface只是编译期校验)。
| 接口 | 签名 | 用途 |
|---|---|---|
Function<T,R> | R apply(T) | 转换 |
Consumer<T> | void accept(T) | 消费 |
Supplier<T> | T get() | 生产 |
Predicate<T> | boolean test(T) | 判断 |
BiFunction<T,U,R> | R apply(T,U) | 双参转换 |
UnaryOperator<T> | T apply(T) | 同类型转换 |
底层:Lambda 不是匿名内部类,编译后没有独立的 .class,而是 invokedynamic + LambdaMetafactory 在运行期生成(首次调用时)。所以 Lambda 的 getClass() 是合成类,this 指向外层对象。
Stream
List<String> names = users.stream()
.filter(u -> u.getAge() > 18)
.map(User::getName)
.distinct()
.sorted()
.collect(Collectors.toList());
| 特性 | 说明 |
|---|---|
| 惰性求值 | 中间操作(filter/map/sorted)不执行,遇到终止操作(collect/forEach/count)才跑 |
| 短路 | findFirst、anyMatch、limit 可以提前结束 |
| 不可复用 | 一个 Stream 只能消费一次,重复用抛 IllegalStateException |
| 并行流 | 底层是 ForkJoinPool.commonPool |
⚠️ 并行流不是万能的:它的公共线程池大小 = CPU 核数 - 1,阻塞型任务(IO、数据库、HTTP)会把公共池占满,拖垮整个 JVM 里所有并行流。有阻塞就用线程池,JDK 21+ 更推荐虚拟线程(见多线程篇)。
Optional
// ✅ 推荐
user.map(User::getAddress).map(Address::getCity).orElse("未知");
user.orElseThrow(() -> new NotFoundException(id));
// ❌ 不推荐
if (opt.isPresent()) { ... } // 又回到了 if 判断
opt.get(); // 没值直接抛,等于没防住
- 不要用
Optional做字段类型和方法参数(不可序列化,且调用方被迫包装) orElse(x)无论有没有值都会执行x;需要懒加载用orElseGet(() -> x)
默认方法
interface Flyable {
default void fly() { System.out.println("fly"); }
static Flyable create() { return () -> {}; }
}
目的:给已有接口加方法而不破坏所有实现类(Collection.stream() 就是这么加的)。JDK 9 又加了 private 接口方法,用来给多个 default 方法抽公共逻辑。
0.10 JDK 9 → 25:现在的 Java 长什么样
语法特性年表
| 特性 | 版本 | 状态 | 说明 |
|---|---|---|---|
var 局部变量推断 | 10 | 正式 | 只是语法糖,不是动态类型 |
文本块 """ | 15 | 正式 | 多行字符串,自动处理缩进 |
record | 16 | 正式 | 不可变数据载体 |
instanceof 模式匹配 | 16 | 正式 | if (o instanceof User u) |
sealed 密封类 | 17 | 正式 | 限定谁能继承 |
switch 表达式 | 14 | 正式 | -> 语法 + 返回值 |
switch 模式匹配 | 21 | 正式 | 按类型/守卫条件分支 |
| 记录模式 | 21 | 正式 | case Point(int x, int y) |
| 有序集合 | 21 | 正式 | SequencedCollection(getFirst / getLast) |
| 虚拟线程 | 21 | 正式 | 见多线程篇 |
| 结构化并发 | 25 | 预览 | StructuredTaskScope,仍是第 5 轮预览 |
| ScopedValue | 25 | 预览 | ThreadLocal 的现代替代 |
| 灵活构造器体 | 25 | 正式 | super() 前可以写语句 |
紧凑源文件 / 实例 main | 25 | 正式 | 单文件跑脚本风格 |
| 字符串模板 | — | 已撤回 | 21/22 预览,JDK 23 正式撤回(JEP 459) |
record
public record Point(int x, int y) {
public Point { // 紧凑构造器,做校验
if (x < 0) throw new IllegalArgumentException();
}
public double distance() { return Math.hypot(x, y); }
}
编译器自动生成:private final 字段、全参构造器、访问器 x() / y()、equals / hashCode / toString。
record是浅不可变:字段final,但字段如果是List,里面的元素照样能改。
sealed + 模式匹配:穷尽性检查
sealed interface Shape permits Circle, Rect {}
record Circle(double r) implements Shape {}
record Rect(double w, double h) implements Shape {}
double area = switch (shape) {
case Circle c -> Math.PI * c.r() * c.r();
case Rect r -> r.w() * r.h();
// 没有 default 也能编译——sealed 保证了穷尽
};
这是 Java 近年最有价值的组合:代数数据类型 + 穷尽性检查,写业务分支时编译器帮你兜底。
两个”已死”的东西
- Security Manager:JDK 24 起永久禁用(JEP 486),
System.setSecurityManager()直接抛异常 sun.misc.Unsafe的内存访问方法:JDK 23 标记废弃、JDK 24 起使用会告警,替代品是VarHandle和MemorySegment(FFM API)
面试怎么答”你了解新版本 Java 吗”
一句话版:JDK 17 是 Spring Boot 的基线,JDK 21 带来虚拟线程,JDK 25 是当前 LTS。语法上 record / sealed / 模式匹配已经成熟;并发上虚拟线程 GA、结构化并发还在预览;字符串模板已经撤回,不要再用。
0.11 序列化
Java 原生序列化
public class User implements Serializable {
private static final long serialVersionUID = 1L;
private transient String password; // 不参与序列化
}
| 要点 | 说明 |
|---|---|
serialVersionUID | 不显式声明时,编译器按类结构生成;改一个字段就会变,导致 InvalidClassException |
transient | 跳过该字段 |
| 自定义逻辑 | writeObject / readObject / readResolve(单例防破坏) |
record | 自动支持,通过组件字段序列化 |
为什么现代项目不用它
- 安全问题:反序列化会执行
readObject里的任意逻辑,大量 RCE 漏洞(gadget chain)由此而来。JDK 9 加了ObjectInputFilter(JEP 290),但只是缓解 - 性能与体积:比 JSON / Protobuf 都差
- 跨语言:只有 Java 能读
| 场景 | 选择 |
|---|---|
| HTTP API | JSON(Jackson) |
| 内部 RPC / 高频 | Protobuf / Thrift |
| 消息队列 | JSON 或 Avro(带 schema 演进) |
| 缓存 | JSON 或 JDK 序列化(Redis 默认) |
如果非要用 Java 序列化(比如 Dubbo 的 hessian2、Redis 的 JDK 序列化),至少配上白名单过滤器,别反序列化不可信来源的数据。
0.12 面试速查
| 问题 | 一句话答案 |
|---|---|
== 和 equals 区别 | == 比值/地址,equals 比逻辑相等,默认实现就是 == |
| 为什么重写 equals 要重写 hashCode | 否则哈希表里 equals 相等的对象可能落在不同桶,get 不到 |
Integer a=127,b=127; a==b | true,命中 IntegerCache(-128~127);128 就是 false |
| String 为什么不可变 | final class + final byte[],换来线程安全、hash 缓存、常量池复用 |
new String("a") 几个对象 | 池中没有则 2 个,已有则 1 个 |
| String 拼接为什么用 StringBuilder | + 在循环里每次新建对象,O(n²) |
| Java 是值传递吗 | 是,对象传的是引用的副本 |
| 重载和重写的区别 | 编译期静态绑定 / 运行期动态绑定 |
| 抽象类和接口怎么选 | 共享状态和代码用抽象类,约束行为用接口 |
| 受检异常和非受检异常 | 编译器是否强制处理;前者是可预期外部故障,后者是编程错误 |
| try-with-resources 好处 | 逆序自动关闭,关闭异常作为 suppressed 不覆盖主异常 |
| 什么是类型擦除 | 泛型编译后擦成上界,运行期无泛型信息 |
| PECS 是什么 | Producer Extends, Consumer Super |
| JDK 动态代理和 CGLIB | 接口代理 / 子类代理;Spring Boot 默认 CGLIB |
| SPI 是什么 | META-INF/services + ServiceLoader,JDBC 驱动就是这么加载的 |
| Lambda 是匿名内部类吗 | 不是,invokedynamic + LambdaMetafactory |
| 并行流有什么坑 | 公共 ForkJoinPool 被阻塞任务占满,阻塞 IO 别用 |
| record 和普通类区别 | 不可变数据载体,自动生成构造器/访问器/equals/hashCode/toString |
| sealed 有什么用 | 限定子类,配合 switch 模式匹配做穷尽性检查 |
| 虚拟线程解决什么 | 阻塞 IO 场景下用少量平台线程承载海量任务(详见多线程篇) |
下一篇:JVM 篇——内存区域、垃圾回收、类加载与双亲委派、线上问题排查。