API 常见面试题
面试官问:「Integer a = 100; Integer b = 100; a == b 输出什么?」
你脱口而出:「true!」
面试官笑了:「那 Integer a = 200; Integer b = 200; a == b 呢?」
你沉默了...
这不是刁难,这是 Java 基础中的基础。包装类的比较,是每个 Java 工程师必须掌握的知识点。
一、equals 和 hashCode 的契约
Q1: equals 和 hashCode 有什么关系?
回答要点:
- equals 相等 → hashCode 必须相等
- hashCode 相等 → equals 不一定相等
- 重写 equals 必须重写 hashCode
java
// 反例:重写了 equals 但没重写 hashCode
class Person {
private String name;
private int age;
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Person person = (Person) o;
return age == person.age && Objects.equals(name, person.name);
}
// hashCode 没重写!违背契约!
// ✅ 正确做法
@Override
public int hashCode() {
return Objects.hash(name, age);
}
}为什么必须有这个契约? 因为 HashMap 先用 hashCode 定位桶,再用 equals 精确匹配。
如果两个对象 equals 为 true 但 hashCode 不同,HashMap 会认为是不同的 key,导致查找失败。
Q2: hashCode 约定是怎么来的?
hashCode 的约定定义在 Object 类的 Javadoc 中:
java
// hashCode 必须满足:
// 1. 同一个对象多次调用,返回值必须相同(程序执行期间)
// 2. equals 相等的两个对象,hashCode 必须相等
// 3. equals 不等的两个对象,hashCode 可以相等(但会影响 HashMap 性能)二、String 专题
Q3: String 为什么不可变?
四层防护:
java
// 1. String 类被 final 修饰,不能被继承
public final class String {
// 2. 存储数据的 char[] 也是 final
private final char[] value;
// 3. 没有公开的 setter
// 4. 很多方法返回新字符串,原字符串不变
}为什么要设计成不可变?
- 字符串常量池需要:只有不可变才能共享
- 安全性:网络地址、文件路径、配置文件路径都依赖 String
- 线程安全:可以在多线程间安全共享
- hashCode 可缓存:String 的 hashCode 计算一次就缓存了
Q4: String、StringBuilder、StringBuffer 区别?
| 对比项 | String | StringBuilder | StringBuffer |
|---|---|---|---|
| 可变性 | 不可变 | 可变 | 可变 |
| 线程安全 | - | 不安全 | 安全(synchronized) |
| 性能 | 最慢(每次创建新对象) | 最快 | 较慢(有同步开销) |
| 引入版本 | JDK 1.0 | JDK 1.5 | JDK 1.0 |
java
public class StringBuilderVsBuffer {
public static void main(String[] args) {
// StringBuilder:非同步,推荐单线程使用
StringBuilder sb = new StringBuilder();
sb.append("hello");
sb.append(" world");
// StringBuffer:同步,线程安全
// JDK 5+ 被 StringBuilder 替代,保留是为了兼容旧代码
StringBuffer sbf = new StringBuffer();
sbf.append("hello");
sbf.append(" world"); // 有 synchronized 关键字
// 结论:直接用 StringBuilder,除非你的代码真的需要线程安全
}
}Q5: String s = new String("abc") 创建了几个对象?
答案:1 个或 2 个
java
String s1 = "abc"; // 1 个:在常量池
String s2 = new String("abc"); // 2 个:常量池 + 堆
String s3 = new String("abc").intern(); // 1 个:intern 返回常量池引用解析:
"abc"是字符串字面量,编译时存入字符串常量池new String("abc")在堆中创建一个新 String 对象,内容引用常量池的 "abc"- 所以最多创建 2 个对象(如果常量池已有,则只创建 1 个堆对象)
三、包装类专题
Q6: 自动装箱的坑?
java
// 缓存范围:-128 ~ 127
Integer a = 127;
Integer b = 127;
System.out.println(a == b); // true(缓存复用)
Integer c = 128;
Integer d = 128;
System.out.println(c == d); // false(超出缓存,每次 new)
// 根本原因
Integer.valueOf(100); // 内部有 IntegerCache,复用对象
Integer.valueOf(200); // 超出范围,new Integer(200)最佳实践:
java
// ❌ 永远不要用 == 比较包装类
if (a == b) { ... }
// ✅ 始终用 equals
if (a.equals(b)) { ... }Q7: Integer 和 int 的区别?
| 对比项 | int | Integer |
|---|---|---|
| 类型 | 基本类型 | 引用类型(对象) |
| 默认值 | 0 | null |
| 存储位置 | 栈 | 堆 |
| 集合使用 | 不能 | 能 |
| 泛型支持 | 不能 | 能 |
| 性能 | 更快 | 有装箱开销 |
四、集合框架
Q8: ArrayList vs LinkedList?
| 操作 | ArrayList | LinkedList |
|---|---|---|
| 随机访问 | O(1) | O(n) |
| 头部插入/删除 | O(n) | O(1) |
| 尾部插入/删除 | O(1) amortized | O(1) |
| 内存占用 | 小(连续空间) | 大(节点 + 指针) |
| CPU 缓存 | 友好 | 不友好 |
java
public class ListSelection {
public static void main(String[] args) {
// 需要频繁随机访问 → ArrayList
List<Integer> array = new ArrayList<>();
for (int i = 0; i < 1000; i++) {
array.add(i);
}
int value = array.get(500); // O(1)
// 需要频繁在中间插入删除 → LinkedList
List<Integer> linked = new LinkedList<>();
for (int i = 0; i < 1000; i++) {
linked.add(0, i); // 头部插入 O(1)
}
}
}Q9: HashMap 底层实现?
JDK 8+:数组 + 链表 + 红黑树
java
// 关键参数
initialCapacity = 16 // 初始容量
loadFactor = 0.75 // 负载因子
TREEIFY_THRESHOLD = 8 // 链表转红黑树阈值
UNTREEIFY_THRESHOLD = 6 // 红黑树退链表阈值
MIN_TREEIFY_CAPACITY = 64 // 树化最小容量put 流程:
- 计算 key 的 hashCode
- 确定桶位置(hash & (length - 1))
- 桶为空:直接插入
- 桶不为空:equals 比较 key
- key 已存在:覆盖 value
- key 不存在:插入链表(长度 > 8 且容量 >= 64 → 转红黑树)
- 超过负载因子:resize 扩容
Q10: HashSet 如何保证不重复?
java
// HashSet 底层是 HashMap,value 用同一个常量对象
private static final Object PRESENT = new Object();
public boolean add(E e) {
return map.put(e, PRESENT) == null; // put 返回 null 表示成功
}所以:
- 先用 hashCode 确定桶位置
- 再用 equals 精确比较
- hashCode 相同且 equals 为 true → 认为是重复元素
五、日期时间
Q11: JDK 8 日期时间 API 的改进?
JDK 8 之前的 Date 问题:
java
// 1. 非线程安全
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
// 多线程下会出问题!
// 2. 设计混乱
Date date = new Date();
int year = date.getYear(); // 1900 年至今的年数,不是 4 位年份!JDK 8+ 的改进:
java
public class NewDateTimeAPI {
public static void main(String[] args) {
// 线程安全:所有类都是不可变的
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");
LocalDate date = LocalDate.now();
String formatted = date.format(formatter);
// 设计清晰:分离了概念
LocalDate localDate = LocalDate.of(2024, 3, 22); // 日期
LocalTime localTime = LocalTime.of(14, 30); // 时间
LocalDateTime dateTime = LocalDateTime.of(date, time); // 日期时间
// 时区支持
ZonedDateTime zoned = ZonedDateTime.now(ZoneId.of("Asia/Shanghai"));
// Duration vs Period
Duration duration = Duration.ofHours(2); // 时间段
Period period = Period.ofDays(3); // 日期段
}
}六、其他高频问题
Q12: try-with-resources 原理?
java
// 原理:编译器自动生成 finally 块
try (BufferedReader br = new BufferedReader(
new FileReader("file.txt"))) {
String line = br.readLine();
} // 自动调用 br.close()
// 编译器翻译后
BufferedReader br = null;
try {
br = new BufferedReader(new FileReader("file.txt"));
String line = br.readLine();
} finally {
if (br != null) {
if (br instanceof AutoCloseable) {
((AutoCloseable) br).close(); // 逆序关闭
}
}
}Q13: final、finally、finalize 区别?
| 关键字 | 作用 | 示例 |
|---|---|---|
| final | 修饰符:变量不可变/方法不可重写/类不可继承 | final int X = 1 |
| finally | 异常处理:try-catch 块的一部分,保证执行 | finally { close(); } |
| finalize | Object 方法:GC 前调用,JDK 9+ 已废弃 | @Deprecated |
Q14: Object 类的常用方法?
java
public class ObjectMethods {
public static void main(String[] args) {
Object obj = new Object();
// toString:对象的字符串表示
obj.toString(); // 默认返回 类名@哈希码
// equals:对象相等性判断
obj.equals(obj); // 默认是 == 比较
// hashCode:对象的哈希值
obj.hashCode(); // 默认是内存地址的哈希
// getClass:获取类信息
obj.getClass(); // 返回 Class 对象
// notify/notifyAll/wait:线程通信
obj.notify();
obj.wait();
// clone:对象拷贝
obj.clone(); // 需要实现 Cloneable 接口
// finalize:垃圾回收前调用
// JDK 9+ 已废弃,不建议使用
}
}面试要点总结
| 主题 | 必须掌握 |
|---|---|
| equals/hashCode | 契约、三条规则、重写时机 |
| String | 不可变原因、StringBuilder vs StringBuffer |
| 包装类 | 缓存范围、自动装箱陷阱、Integer vs int |
| 集合 | ArrayList vs LinkedList、HashMap 原理 |
| 日期时间 | JDK 8+ 新 API、线程安全 |
| 异常 | try-with-resources 原理、checked vs unchecked |
要点回顾
- equals 和 hashCode:equals 相等必须 hashCode 相等
- String:不可变、字符串常量池、StringBuilder 单线程首选
- 包装类:缓存 -128~127、永远用 equals 比较
- HashMap:数组 + 链表 + 红黑树、负载因子 0.75
- JDK 8 日期:LocalDate/LocalTime/LocalDateTime、线程安全
面试不是背答案,是理解原理。知道「是什么」不够,还要知道「为什么」。
