基础题最容易答成背诵。下面每题都按「结论 → 原理 → 追问」组织,重点是把 JLS 里的规则和日常踩过的坑对应起来。
1. == 和 equals 有什么区别?
== 比较值本身:基本类型比较数值,引用类型比较是否指向同一个对象。equals 是 Object 的方法,默认实现就是 ==,但 String、包装类、集合等重写了它,改成比较内容。
重写 equals 必须同时重写 hashCode,否则两个相等的对象可能算进不同的桶。契约是:equals 为真则 hashCode 必须相等;反过来不要求。违反契约会让 HashMap、HashSet 出现「放进去却查不到」的诡异现象。
追问常见于浮点数:0.1 + 0.2 == 0.3 为 false,因为二进制无法精确表示十进制小数。金额应使用 BigDecimal,并且用字符串构造,否则从 double 转入时误差已经带入。
2. 为什么 String 设计成不可变?三种字符串怎么选?
String 类是 final 的,内部字符存储在私有字段中(JDK 9 起改为 byte[] 加编码标记,以压缩拉丁字符占用),所有看似修改的方法都返回新对象。
不可变换来四件事:可以安全共享、可以缓存哈希值(所以适合当 HashMap 的 key)、天然线程安全、传参后不会被调用方篡改。代价是频繁拼接会产生大量临时对象。
选型上:常量或少量拼接用 String;循环内拼接用 StringBuilder;确实需要多线程共享同一个可变缓冲区时才用 StringBuffer。绝大多数场景里,把 StringBuilder 限制在方法内部比用同步版本更有效。
3. new String("abc") 会创建几个对象?
分情况:若常量池中还没有 "abc",类加载解析字面量时先创建池中对象,执行 new 时再在堆上创建实例,共两个;若池中已有,则只创建堆上那一个。面试时把两种口径讲清楚,比背「两个」更稳妥。
intern() 的语义是:池中有相等字符串就返回池中引用,否则把当前字符串的引用登记进去并返回。JDK 7 起常量池移到堆中,所以 new String("a").intern() == "a" 为 true。
编译期能折叠的只有字面量拼接,例如 "a" + "b" 会直接变成 "ab"。而 new String("a") + new String("b") 是运行期拼接,结果不会自动进入常量池。
4. 包装类的缓存与自动拆箱有哪些坑?
自动装箱调用 Integer.valueOf,默认缓存 -128 到 127,可用 -XX:AutoBoxCacheMax 调大上界。因此两个值为 127 的 Integer 用 == 比较为 true,128 则为 false;比较值一律用 equals。
最常见的坑是空指针:Integer x = null; x == 1 会拆箱,直接抛 NPE。Map.get() 返回 null 后赋给 int、三目表达式两边类型不一致导致整体提升,也都会引发同类问题。
Long 的缓存范围固定为 -128 到 127,不受上述参数影响。金额、订单号这类业务标识不要依赖缓存行为,统一用值比较或明确的基本类型。
5. Java 是值传递还是引用传递?
Java 只有值传递。基本类型传的是数值副本;引用类型传的是引用的副本,也就是对象地址的拷贝。
所以方法内可以通过这个副本修改对象内容,但把形参重新指向新对象,不会影响调用方的引用。典型例子是:给 StringBuilder 追加内容能生效,而在方法里 sb = new StringBuilder() 之后的操作与外部无关。
追问常考「为什么交换两个对象不生效」:交换的只是两个引用副本。交换数组元素则能生效,因为修改的是数组对象内部的数据。
6. 泛型擦除带来了哪些限制?PECS 怎么理解?
Java 泛型是编译期特性,编译后类型参数被擦除到上界(无界时为 Object),运行时拿不到 List<String> 的具体类型实参。
由此产生几条限制:不能 new T()、不能直接创建泛型数组、不能写 instanceof List<String>、静态成员不能引用类上的类型参数、基本类型不能作为类型实参。为了维持多态,编译器会生成桥方法。
通配符遵循 PECS:生产者用 ? extends T,只能读不能写;消费者用 ? super T,可以写但读出来只能是 Object。List<?> 不能 add 任何非 null 元素。
还要区分:List<Integer> 不能赋给 List<Number>,这不是擦除造成的,而是泛型不协变。数组是协变的,所以 Object[] arr = new String[1] 能通过编译,但写入其他类型会在运行期抛 ArrayStoreException。
7. 重载和重写有什么区别?多态靠什么实现?
重载在编译期按静态类型和参数列表选定,返回值不同不构成重载;重写在运行期按对象实际类型分派。重写要求签名相同、返回类型协变、访问权限不能收窄、不能抛出更宽的受检异常。
多态依赖对象头里的类型元数据指针和方法表:调用虚方法时按实际类型找到实现。private、static、final 方法不参与动态分派。
两个容易忽略的点:一是在构造器里调用可重写方法,子类字段尚未初始化,可能读到默认值;二是重载在编译期就固定,父类静态类型看不到子类新增的重载版本。
8. 从 Java 8 到 21,哪些语言特性值得在面试里讲?
JDK 8 带来 Lambda、Stream、Optional、接口默认方法和新的日期时间 API。JDK 10 的 var 只做局部变量类型推断,不改变类型系统。JDK 11 标准化了 HttpClient。JDK 17 是主流 LTS,包含 record、密封类、instanceof 模式匹配、文本块和 switch 表达式。
JDK 21 值得关注 switch 模式匹配(含 record 解构)、虚拟线程、分代 ZGC 和有序集合接口。
回答时最好落到场景:record 适合 DTO 和值对象,不适合需要可变状态或依赖无参构造的框架实体;虚拟线程适合大量阻塞在 IO 上的任务,不适合 CPU 密集任务,也不适合长时间持有 synchronized 的临界区——JDK 21 下会钉住载体线程,JDK 24 才改善这一限制。
易错点与自测
- 不要只背「String 不可变所以线程安全」,要能说出共享、哈希缓存、常量池三个具体收益。
- 包装类比较、
null拆箱、BigDecimal构造方式,是笔试里最高频的失分点。 - 泛型擦除不等于「运行时完全没有泛型信息」,声明处的信息仍可通过反射读到。
自测:① 解释 Integer a = 128; Integer b = 128; a == b 的结果;② 说明为什么泛型不能协变而数组可以;③ 说出 JDK 21 虚拟线程不适合哪两类任务。
参考资料
- JLS SE 21:类型、转换与泛型 — 泛型擦除、装箱转换的规范定义
- java.lang.Object API —
equals与hashCode的契约原文 - JEP 444:Virtual Threads — 虚拟线程的适用场景与钉住问题
- JEP 441:Pattern Matching for switch — switch 模式匹配的正式语义