Skip to content

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 有什么关系?

回答要点

  1. equals 相等 → hashCode 必须相等
  2. hashCode 相等 → equals 不一定相等
  3. 重写 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. 很多方法返回新字符串,原字符串不变
}

为什么要设计成不可变?

  1. 字符串常量池需要:只有不可变才能共享
  2. 安全性:网络地址、文件路径、配置文件路径都依赖 String
  3. 线程安全:可以在多线程间安全共享
  4. hashCode 可缓存:String 的 hashCode 计算一次就缓存了

Q4: String、StringBuilder、StringBuffer 区别?

对比项StringStringBuilderStringBuffer
可变性不可变可变可变
线程安全-不安全安全(synchronized)
性能最慢(每次创建新对象)最快较慢(有同步开销)
引入版本JDK 1.0JDK 1.5JDK 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 的区别?

对比项intInteger
类型基本类型引用类型(对象)
默认值0null
存储位置
集合使用不能
泛型支持不能
性能更快有装箱开销

四、集合框架

Q8: ArrayList vs LinkedList?

操作ArrayListLinkedList
随机访问O(1)O(n)
头部插入/删除O(n)O(1)
尾部插入/删除O(1) amortizedO(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 流程

  1. 计算 key 的 hashCode
  2. 确定桶位置(hash & (length - 1))
  3. 桶为空:直接插入
  4. 桶不为空:equals 比较 key
  5. key 已存在:覆盖 value
  6. key 不存在:插入链表(长度 > 8 且容量 >= 64 → 转红黑树)
  7. 超过负载因子: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 表示成功
}

所以:

  1. 先用 hashCode 确定桶位置
  2. 再用 equals 精确比较
  3. 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(); }
finalizeObject 方法: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、线程安全

面试不是背答案,是理解原理。知道「是什么」不够,还要知道「为什么」。

基于 VitePress 构建