Skip to content

Java 基础

Java 基础是面试第一关,涵盖语言特性、面向对象、核心 API、IO 与新特性。基础不牢,地动山摇。

Q1: 面向对象三大特性?Java 和 C++ 的区别? 「🟢 校招/初级」

考察点:OOP 基础概念是否扎实,是否理解 Java 语言的设计哲学,筛掉只会写业务代码、对语言本质缺乏思考的候选人。

参考答案

面向对象三大特性

  • 封装(Encapsulation):将数据和操作数据的方法绑定在一起,隐藏内部实现细节,仅对外暴露公共接口。目的是保护数据安全、降低耦合。

    • 实现方式:访问修饰符(private/protected/public)+ getter/setter。
    • 意义:高内聚、低耦合;内部修改不影响外部调用。
  • 继承(Inheritance):子类继承父类的属性和方法,实现代码复用。

    • Java 是单继承(一个类只能有一个直接父类),但可以多层继承。
    • 通过 extends 关键字实现,子类可以扩展(新增方法)或改造(重写方法)父类。
    • 构造方法不能继承,必须通过 super() 调用。
  • 多态(Polymorphism):同一方法调用,因对象不同而表现出不同行为。

    • 编译时多态(静态分派):方法重载(Overload),编译器根据参数列表决定调用哪个方法。
    • 运行时多态(动态绑定):方法重写(Override)+ 向上转型,JVM 在运行时根据实际对象类型决定调用哪个方法。
    • 实现前提:继承、重写、父类引用指向子类对象。

Java vs C++ 对比

特性JavaC++
指针无指针,引用更安全支持指针,灵活但易出错
内存管理自动 GC(垃圾回收)手动 new/delete,易内存泄漏
继承单继承(类)+ 多实现(接口)支持多继承(菱形继承问题)
运算符重载不支持(+ 对 String 是语法糖)支持运算符重载
模板/泛型泛型(类型擦除,编译期检查)模板(编译期实例化,功能更强)
跨平台JVM 实现"一次编写,到处运行"需重新编译,依赖平台
析构函数无(finalize 已废弃)有析构函数
全局函数/变量无(一切皆对象,除基本类型)支持全局函数和变量
异常处理强制 Checked Exception 处理异常可选,不强制捕获

追问延伸

  • 重载和重写的区别?它们分别属于哪种多态?
  • 抽象类和接口的区别?Java 为什么设计成单继承?
  • 多态的底层实现原理是什么?(虚方法表 vtable)
  • 封装除了用 private,还有哪些手段?(包访问权限、内部类等)

Q2: 重载(Overload)和重写(Override)的区别? 「🟢 校招/初级」

考察点:对多态的理解深度,是否清楚编译时多态和运行时多态的本质区别,筛掉概念模糊、分不清方法签名的人。

参考答案

核心区别

维度重载(Overload)重写(Override)
位置同一个类中(或父子类构造)子类重写父类方法
方法名必须相同必须相同
参数列表必须不同(数量/类型/顺序)必须相同
返回值类型无关必须相同或是其子类(协变返回)
访问修饰符无关不能比父类更严格
异常无关不能抛出更宽泛的异常
多态类型编译时多态(静态分派)运行时多态(动态绑定)
阶段编译期确定调用版本运行期确定调用版本

重载(Overload)详解

  • 定义:同一个类中,方法名相同但参数列表不同的多个方法。
  • 判断依据:只看方法名 + 参数列表,与返回值、修饰符、异常无关。
  • 典型例子System.out.println() 有十多个重载版本,支持不同类型参数。
java
public void print(String s) { ... }
public void print(int i) { ... }
public void print(double d) { ... }
// 这三个构成重载
  • 编译时多态:编译器根据实参的静态类型(声明类型)来匹配方法签名,确定调用哪个重载版本。

重写(Override)详解

  • 定义:子类重新实现父类中已有的方法,保持方法签名一致。
  • 规则("两同两小一大"):
    • 两同:方法名相同、参数列表相同
    • 两小:返回值类型更小或相等(协变)、抛出异常更小或相等
    • 一大:访问权限更大或相等
java
class Animal {
    protected Object eat() throws Exception {
        return "food";
    }
}

class Cat extends Animal {
    @Override
    public String eat() throws RuntimeException {  // 权限更大,返回值更小,异常更小
        return "fish";
    }
}
  • 运行时多态Animal a = new Cat(); a.eat(); —— 编译看左边,运行看右边。
  • @Override 注解:可选但推荐,编译器会检查是否真的重写了父类方法,防止手误。

不能重写的情况

  1. final 方法:final 修饰的方法不能被重写
  2. static 方法:静态方法不能被重写(父子类同名静态方法是"隐藏"而非重写)
  3. private 方法:private 方法对子类不可见,不能重写(子类定义同名方法算新方法)
  4. 构造方法:构造方法不能被继承,自然不能重写

代码示例 —— 静态方法不是重写

java
class Parent {
    public static void say() { System.out.println("Parent"); }
}
class Child extends Parent {
    public static void say() { System.out.println("Child"); }
}
// 调用:
Parent p = new Child();
p.say(); // 输出 "Parent",静态方法看引用类型,不是重写

追问延伸

  • 为什么重载不能仅凭返回值区分?(方法签名不包含返回值;调用时可能产生歧义)
  • 重写的底层实现原理?(虚方法表 vtable,子类覆盖父类方法条目)
  • 构造方法可以重载吗?可以重写吗?(可以重载,不能重写)
  • 什么是"编译看左边,运行看右边"?属性和静态方法适用吗?(属性和静态方法看左边,不适用)

Q3: 抽象类和接口的区别? 「🟢 校招/初级」

考察点:OOP 设计能力,是否理解抽象类和接口的设计意图与选型策略,筛掉只会背定义、不会在实际设计中做选择的人。

参考答案

核心对比

维度抽象类(abstract class)接口(interface)
关键字abstract classinterface
继承/实现子类用 extends单继承实现类用 implements多实现
构造方法可以有构造方法(供子类调用)不能有构造方法
成员变量可以有各种类型的变量只能有 public static final 常量
方法可以有抽象方法、非抽象方法、静态方法JDK 8 前只能有抽象方法;JDK 8+ 可有默认方法、静态方法;JDK 9+ 可有私有方法
访问修饰符可以是 public/protected/private(抽象方法不能 private)默认 public abstract(JDK 8 前);默认方法 public
设计意图is-a 关系,表征"是什么",代码复用has-a 能力,表征"能做什么",行为契约

抽象类详解

  • 定义:包含抽象方法的类叫抽象类(也可以没有抽象方法,但用 abstract 修饰)。
  • 特点
    • 不能实例化(不能 new)。
    • 子类继承后必须实现所有抽象方法,否则子类也得声明为抽象类。
    • 可以有成员变量、构造方法、普通方法、静态方法、final 方法等。
  • 使用场景:多个子类有共同的属性和方法,需要抽取到父类中复用。
java
abstract class Animal {
    protected String name;
    
    public Animal(String name) {
        this.name = name;
    }
    
    public abstract void eat();  // 抽象方法
    
    public void sleep() {        // 普通方法
        System.out.println(name + " is sleeping");
    }
}

接口详解

  • 定义:一组方法签名的集合,是一种行为契约。
  • JDK 版本演进
    • JDK 7 及之前:只能有 public abstract 抽象方法 + public static final 常量。
    • JDK 8:增加 默认方法(default method)和 静态方法(static method)。
      • 默认方法:接口可以有方法实现,实现类可直接继承或重写。解决接口演进问题(加新方法不破坏已有实现)。
      • 静态方法:接口的静态方法,只能通过接口名调用,不能被实现类继承。
    • JDK 9:增加 私有方法(private method),供默认方法内部复用代码。
java
interface Flyable {
    int MAX_SPEED = 100;  // 等价于 public static final int MAX_SPEED = 100;
    
    void fly();           // 等价于 public abstract void fly();
    
    default void takeOff() {   // JDK 8 默认方法
        System.out.println("Taking off...");
    }
    
    static void landingHelp() { // JDK 8 静态方法
        System.out.println("Landing assistance...");
    }
}

选型建议

  • 用抽象类:当多个类之间存在明确的 is-a 关系,且需要共享代码(属性或方法实现)时。
    • 例:AnimalCat / Dog,它们有共同的属性(name、age)和共同的方法实现(sleep())。
  • 用接口:当多个类之间是 has-a 能力 关系,只定义行为契约,不关心具体实现时。
    • 例:FlyableRunnableComparable,表征一种能力/契约。
  • 组合使用:抽象类定义骨架,接口定义能力。如 AbstractList 实现了 List 接口的骨架方法。

接口冲突问题

  • 一个类实现两个接口,两个接口有同名默认方法时,必须在类中重写该方法(否则编译错误)。
  • 可以通过 InterfaceName.super.method() 指定调用哪个接口的默认实现。

追问延伸

  • 为什么 Java 8 要给接口加默认方法?(接口演进、Lambda 表达式配合函数式接口)
  • 接口和抽象类都有抽象方法,那为什么还要两个东西?一个不够吗?(单继承限制、设计语义不同)
  • 接口中的变量为什么必须是 public static final 的?(接口是契约,不存储状态;final 保证不可变)
  • 函数式接口是什么?和普通接口有什么区别?(只有一个抽象方法,可以用 Lambda 表达式)

Q4: final、finally、finalize 的区别? 「🟢 校招/初级」

考察点:Java 关键字/方法的基础辨析,筛掉基础概念混淆的候选人。

参考答案

三者只是长得像,功能完全不同:

final —— 修饰符

final 是关键字,可修饰类、方法、变量,表示"不可变"。

修饰对象含义
final 类不能被继承。如 StringMathSystem 都是 final 类。
final 方法不能被重写(Override)。早期 JVM 还会将 final 方法转为内联调用提升性能,现代 JVM 已不需要。
final 变量变量赋值后不能再改。基本类型是值不可变,引用类型是引用不可变(对象内容可变)。
final 参数方法参数在方法体内不能被重新赋值。
final + static编译期常量,编译时会值替换。

代码示例 —— final 引用类型

java
final List<String> list = new ArrayList<>();
list.add("a");  // OK,对象内容可变
list = new LinkedList<>();  // ERROR,引用不能变

finally —— 异常处理

finally 是异常处理的一部分,保证代码块必定执行(即使 try/catch 中有 return 或异常)。

  • 作用:用于释放资源(关闭流、释放锁、关闭连接等)。
  • 不执行的唯一情况:调用了 System.exit(0)(JVM 退出)。
  • 经典坑:finally 中的 return 会覆盖 try 中的 return。
java
public int test() {
    try {
        return 1;
    } finally {
        return 2;  // 最终返回 2,覆盖了 try 中的 return
    }
}
  • 更好的替代:JDK 7 引入的 try-with-resources 语法,自动关闭实现了 AutoCloseable 接口的资源,比 finally 更优雅。
java
try (FileInputStream fis = new FileInputStream("a.txt")) {
    // 使用 fis
} catch (IOException e) {
    e.printStackTrace();
}
// 自动关闭 fis,无需 finally

finalize —— Object 的方法

finalize()Object 类的一个方法,在对象被 GC 回收前由垃圾回收器调用一次。

  • 设计初衷:给对象一个"临终遗言"的机会,清理非 Java 资源(如 native 资源)。
  • 问题多多
    • 不确定性:无法保证何时调用,甚至可能不调用(JVM 没 GC 就退出了)。
    • 性能差:finalizer 线程优先级低,可能导致大量对象堆积。
    • 复活对象:finalize 中可以把自己重新赋值给某个引用,让对象"复活"。
    • 内存泄漏风险:实现了 finalize 的对象至少经历两次 GC 才能被回收。
  • 废弃状态
    • JDK 9:标记为 @Deprecated(since="9")
    • JDK 18:标记为 @Deprecated(since="9", forRemoval=true),将在未来版本移除
  • 替代方案
    • try-with-resources / AutoCloseable
    • Cleaner 类(JDK 9 引入)
    • 资源用完手动释放

三者一句话总结

  • final:修饰符,表不可变
  • finally:异常处理,表必定执行
  • finalize:Object 方法,GC 前调用,已废弃

追问延伸

  • final 修饰的变量和普通变量有什么性能区别?(编译期常量会值替换)
  • finally 代码块一定会执行吗?有什么情况不会执行?(System.exit、JVM 崩溃、线程被杀死)
  • 为什么 finalize 被废弃?它有什么坑?(上面已答,可追问 Cleaner 的原理)
  • final 和 finally 在实际开发中你怎么用?能举项目中的例子吗?

Q5: == 和 equals 的区别?hashCode 有什么用? 「🟢 校招/初级」

考察点:对 Java 对象相等性的理解,是否清楚引用比较和值比较的区别,以及 hashCode 约定。筛掉只会用 == 比较字符串的人。

参考答案

== 运算符

  • 基本数据类型:比较的是是否相等。
    • int a = 1; int b = 1; a == b; // true
  • 引用数据类型:比较的是内存地址(引用是否指向同一个对象)。
    • new String("a") == new String("a"); // false,两个对象地址不同

equals 方法

  • equals()Object 类的方法,所有类都继承自 Object,所以所有引用类型都有 equals 方法。
  • Object 类的默认实现:就是 ==,比较引用地址。
java
// Object 类源码
public boolean equals(Object obj) {
    return (this == obj);
}
  • 重写 equals:很多类(如 String、Integer、Date 等)都重写了 equals,改为比较内容是否相等
    • new String("a").equals(new String("a")); // true,内容相同

equals 重写的规范

重写 equals 必须遵守以下约定(来自 Object 规范):

  1. 自反性x.equals(x) 必须返回 true
  2. 对称性x.equals(y) 为 true,则 y.equals(x) 也必须为 true
  3. 传递性x.equals(y) 为 true,y.equals(z) 为 true,则 x.equals(z) 也必须为 true
  4. 一致性:多次调用结果应一致
  5. 非空性x.equals(null) 必须返回 false

重写 equals 的标准模板

java
@Override
public boolean equals(Object o) {
    if (this == o) return true;                    // 同一对象,直接 true
    if (o == null || getClass() != o.getClass()) return false;  // 类型不同
    User user = (User) o;
    return Objects.equals(name, user.name) && age == user.age;  // 比较字段
}

hashCode 的作用

  • hashCode() 也是 Object 的方法,返回对象的哈希码(int 值),用于在哈希表(HashMap、HashSet、HashTable)中快速定位桶(bucket)。
  • 核心约定 —— equals 与 hashCode 的关系
    1. 如果两个对象 equals 相等,那么它们的 hashCode 必须相等
    2. 如果两个对象 hashCode 相等,它们的 equals 不一定相等(哈希碰撞)。
    3. 因此,重写 equals 必须同时重写 hashCode,否则在 HashMap/HashSet 中会出问题。

为什么 hashCode 相等 equals 不一定相等?

  • 因为 hashCode 是 int 类型,范围有限(2^32),而对象数量可以无限多,根据鸽巢原理,必然存在不同对象哈希值相同的情况(哈希碰撞)。
  • 哈希碰撞的解决:链表/红黑树(在 HashMap 中)。

经典面试题

问题:两个对象 hashCode 相同,equals 一定为 true 吗? :不一定。hashCode 相同只是说明它们在哈希表中落在同一个桶,equals 比较内容可能不同。反过来,equals 相同则 hashCode 必须相同。

问题:为什么重写 equals 必须重写 hashCode? :如果只重写 equals 不重写 hashCode,会导致两个逻辑上相等的对象(equals 返回 true)hashCode 不同,那么在 HashMap/HashSet 中会被认为是两个不同的 key/元素,破坏了集合的语义。

代码示例 —— 只重写 equals 不重写 hashCode 的坑

java
class Person {
    private String name;
    
    public Person(String name) { this.name = name; }
    
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof Person)) return false;
        Person person = (Person) o;
        return Objects.equals(name, person.name);
    }
    // 没有重写 hashCode!
}

Set<Person> set = new HashSet<>();
set.add(new Person("Alice"));
set.add(new Person("Alice"));
System.out.println(set.size()); // 输出 2,而不是 1!两个对象逻辑相等但被当成不同元素

追问延伸

  • String 类的 hashCode 是怎么计算的?(s[0]*31^(n-1) + s[1]*31^(n-2) + ... + s[n-1],选 31 是因为奇素数,乘法可优化为位移和减法:31*i = (i<<5) - i
  • 两个对象不相等,hashCode 可能相同吗?这种现象叫什么?(哈希碰撞)
  • HashMap 中如何解决哈希碰撞?(链地址法 + 红黑树)
  • 你在项目中什么时候需要重写 equals 和 hashCode?(作为 HashMap 的 key、放入 Set 等场景)

Q6: String、StringBuffer、StringBuilder 的区别? 「🟢 校招/初级」

考察点:字符串处理的核心知识,包括不可变设计、线程安全、性能对比、底层实现。筛掉只会用 + 拼接字符串的人。

参考答案

三者对比

特性StringStringBufferStringBuilder
可变性不可变(final char[]/byte[])可变可变
线程安全线程安全(不可变对象天然安全)线程安全(方法加 synchronized)非线程安全
性能每次修改创建新对象,性能差中等(有同步开销)最高(无同步开销)
适用场景字符串常量、少量修改多线程环境下大量字符串操作单线程环境下大量字符串操作
出现版本JDK 1.0JDK 1.0JDK 1.5

String 的不可变性

  • 为什么不可变
    1. JDK 9 之前:private final char value[]
    2. JDK 9 及之后:private final byte[] value + private final byte coder(Latin-1 用 1 字节,UTF-16 用 2 字节,节省内存)
    • value 数组是 final 的,引用不可变;且 String 类内部没有提供修改 value 的方法。
  • 不可变的好处
    1. 安全:作为参数传递时不用担心被修改;作为 HashMap 的 key 更安全。
    2. 缓存 hash 值:String 的 hashCode 会被缓存(private int hash;),因为不可变,只需计算一次。
    3. 字符串常量池:不可变是常量池的基础,可以复用字符串,节省内存。
    4. 线程安全:不可变对象天然线程安全,多线程下无需同步。
  • 不可变的代价:每次修改(如 +concatsubstring)都会创建新对象,频繁修改性能差。

StringBuffer 和 StringBuilder

  • 相同点
    • 都是可变的字符序列,底层是 char 数组(没有 final),可以扩容。
    • 都继承自 AbstractStringBuilder
    • 常用方法相同:append()insert()delete()reverse() 等。
  • 不同点
    • StringBuffer 方法都加了 synchronized 关键字,线程安全但性能低。
    • StringBuilder 没有同步,非线程安全但性能高。

字符串拼接 + 原理

  • Java 中使用 + 拼接字符串,编译器会优化为 StringBuilder 的 append 操作。
  • 但在循环中用 + 拼接有坑:每次循环都会创建新的 StringBuilder 对象,性能极差。
java
// 反编译前
String result = "";
for (int i = 0; i < 100; i++) {
    result += i;  // 每次循环 new 一个 StringBuilder,性能差
}

// 反编译后等价于
String result = "";
for (int i = 0; i < 100; i++) {
    result = new StringBuilder(result).append(i).toString();
}

正确做法:循环外手动创建 StringBuilder。

java
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 100; i++) {
    sb.append(i);
}
String result = sb.toString();

字符串常量池

  • 位置:JDK 7 及之后在堆中(之前在永久代)。
  • 作用:复用字符串字面量,节省内存。
  • 入池时机
    • 字面量直接赋值:String s = "abc"; —— 编译期入池。
    • new String("abc") —— 只在堆中创建对象,不入池(除非字面量首次出现)。
    • s.intern() —— 手动入池,若池中已有则返回池引用,否则将字符串加入池中并返回引用。

经典问题String s = new String("abc"); 创建了几个对象?

  • :1 或 2 个。
    • 如果常量池中已有 "abc":只创建 1 个堆中的对象。
    • 如果常量池中没有 "abc":创建 2 个对象(1 个在堆中,1 个在常量池中)。

JDK 9 String 优化

  • JDK 9 之前 String 底层是 char[],每个 char 占 2 字节(UTF-16)。
  • JDK 9 之后改为 byte[] + coder
    • 如果字符串全是 Latin-1 字符(ASCII 等),用 1 字节存一个字符(LATIN1 编码)。
    • 如果有中文等非 Latin-1 字符,用 2 字节存一个字符(UTF-16 编码)。
    • coder 字段标识使用哪种编码(0 = LATIN1,1 = UTF16)。
  • 效果:大部分英文场景内存占用减少约一半。

追问延伸

  • 为什么 String 设计成不可变?有什么利弊?
  • String.intern() 的作用和原理?在 JDK 6 和 JDK 7+ 有什么区别?(JDK 6 是复制对象到永久代,JDK 7+ 是把引用移到堆中的常量池)
  • StringBuilder 初始容量是多少?扩容机制?(默认 16,扩容 2 倍 + 2,Arrays.copyOf)
  • String、StringBuffer、StringBuilder 在项目中你怎么选型?
  • String 对 "abc" + "def" 的优化?(编译期就合并成 "abcdef")

Q7: Java 的异常体系?Checked vs Unchecked? 「🟢 校招/初级」

考察点:异常处理机制的理解,是否清楚异常分类和处理原则,筛掉只会 try-catch 一切的人。

参考答案

异常体系结构

Throwable
├── Error              (系统级错误,程序无法处理)
│   ├── OutOfMemoryError
│   ├── StackOverflowError
│   ├── NoClassDefFoundError
│   └── ...
└── Exception          (程序可以处理的异常)
    ├── RuntimeException  (Unchecked Exception,非受检异常)
    │   ├── NullPointerException
    │   ├── ArrayIndexOutOfBoundsException
    │   ├── ClassCastException
    │   ├── IllegalArgumentException
    │   └── ...
    └── 其他 Exception 子类  (Checked Exception,受检异常)
        ├── IOException
        ├── SQLException
        ├── FileNotFoundException
        ├── InterruptedException
        └── ...

Error 和 Exception 的区别

维度ErrorException
类型系统级错误,JVM 层面应用程序级异常
可恢复性通常不可恢复,程序应终止可以捕获并恢复
处理方式一般不捕获,不处理应该捕获并处理
举例OOM、StackOverflowError、LinkageErrorIOException、NullPointerException

Checked Exception vs Unchecked Exception

维度Checked Exception(受检异常)Unchecked Exception(非受检异常)
继承关系Exception 的子类(除 RuntimeException)RuntimeException 及其子类
编译检查编译期检查,必须处理(try-catch 或 throws)编译期不检查,不要求处理
代表含义可预期、可恢复的异常情况编程错误(bug),逻辑问题
处理方式必须显式捕获或声明抛出可以不处理,也可以捕获
举例IOException、SQLException、InterruptedExceptionNullPointerException、ArrayIndexOutOfBoundsException、IllegalArgumentException

异常处理关键字

  • try:监控可能发生异常的代码块
  • catch:捕获异常并处理
  • finally:必定执行的代码块(用于资源释放)
  • throw:手动抛出异常对象
  • throws:声明方法可能抛出的异常,交给调用方处理

最佳实践

  1. 不要捕获 Throwable/Error:Error 是系统级错误,程序无法处理,捕获也没意义。
  2. 具体异常优先:捕获最具体的异常,而不是宽泛的 Exception。
  3. 不要空吞异常catch (Exception e) {} 是大忌,至少要打日志。
  4. 不要用异常做流程控制:异常是为了处理异常情况,用在正常流程中性能差且不清晰。
  5. finally 中不要 return:会覆盖 try 中的 return,也会丢失异常信息。
  6. 尽量使用 try-with-resources:JDK 7+,自动关闭资源,比 finally 更优雅安全。
  7. 异常信息要完整:捕获异常后打印堆栈或记录完整上下文,便于排查问题。
  8. 自定义异常时选择合适的父类:业务异常一般继承 Exception(Checked)或 RuntimeException(Unchecked),根据是否强制调用方处理来决定。

try-with-resources

java
// 旧写法(finally 关闭资源)
FileInputStream fis = null;
try {
    fis = new FileInputStream("test.txt");
    // 使用 fis
} catch (IOException e) {
    e.printStackTrace();
} finally {
    if (fis != null) {
        try { fis.close(); } catch (IOException e) { e.printStackTrace(); }
    }
}

// 新写法(try-with-resources,自动关闭)
try (FileInputStream fis = new FileInputStream("test.txt")) {
    // 使用 fis
} catch (IOException e) {
    e.printStackTrace();
}
// 自动关闭 fis,代码更简洁,更安全

异常丢失问题

  • 如果 try 块抛出异常 A,finally 块又抛出异常 B,那么异常 A 会丢失,只传播异常 B。
  • 解决方案:使用 try-with-resources(会把被抑制的异常 addSuppressed),或手动记录原始异常。

追问延伸

  • 你在项目中怎么处理异常?自定义异常一般继承 Exception 还是 RuntimeException?为什么?
  • throw 和 throws 的区别?
  • final、finally、finalize 的区别?(联动题)
  • try-catch-finally 中,try 里有 return,finally 还执行吗?在 return 之前还是之后?(执行,在 return 之前;但 finally 修改基本类型返回值无效,修改引用类型内容有效)
  • NoClassDefFoundError 和 ClassNotFoundException 的区别?(一个是 Error,运行时找不到类;一个是 Exception,加载时找不到类)

Q8: 基本类型和包装类的区别?自动装箱和拆箱? 「🟢 校招/初级」

考察点:对 Java 类型系统的理解,自动装箱拆箱的原理和坑,整数缓存机制。筛掉对基本类型和包装类混用有隐患的人。

参考答案

基本类型和包装类

Java 有 8 种基本数据类型,每种都对应一个包装类:

基本类型大小默认值包装类缓存范围
byte1字节0Byte-128 ~ 127
short2字节0Short-128 ~ 127
int4字节0Integer-128 ~ 127(可配置上限)
long8字节0LLong-128 ~ 127
float4字节0.0fFloat无缓存
double8字节0.0dDouble无缓存
char2字节'\u0000'Character0 ~ 127
boolean1位falseBooleantrue/false

基本类型 vs 包装类

维度基本类型包装类
存储位置栈中(局部变量)或对象头中(成员变量)堆中对象,栈中存引用
默认值有默认值(int=0, boolean=false 等)null
内存占用大(对象头 + 数据 + 对齐)
性能高(直接操作值)低(对象开销、GC 压力)
可 null不能为 null可以为 null
泛型不能用(泛型擦除后是 Object)可以用
参与运算直接运算需要拆箱

自动装箱和拆箱

  • 自动装箱(Autoboxing):基本类型自动转换为包装类对象。
    • 底层调用 valueOf() 方法。
    • Integer i = 10; → 编译后:Integer i = Integer.valueOf(10);
  • 自动拆箱(Unboxing):包装类自动转换为基本类型。
    • 底层调用 xxxValue() 方法(如 intValue())。
    • int n = i; → 编译后:int n = i.intValue();

触发场景

  1. 赋值时(基本类型 ↔ 包装类)
  2. 方法调用时(参数是包装类传基本类型,或反之)
  3. 运算时(包装类参与算术运算会自动拆箱)

整数缓存机制

  • Integer.valueOf() 内部维护了一个缓存数组,缓存 -128 ~ 127 之间的 Integer 对象。
  • 在这个范围内的值,valueOf() 直接返回缓存对象,不创建新对象。
  • 超出范围才 new 新对象。
java
// Integer.valueOf 源码(简化)
public static Integer valueOf(int i) {
    if (i >= IntegerCache.low && i <= IntegerCache.high)
        return IntegerCache.cache[i + (-IntegerCache.low)];
    return new Integer(i);
}
  • 缓存上限可配置:通过 JVM 参数 -XX:AutoBoxCacheMax=<size> 调整 Integer 缓存上限。
  • 其他包装类:Byte、Short、Long、Character 也有缓存,但范围固定,不可配置。

经典坑:== 比较

java
Integer a = 127;
Integer b = 127;
System.out.println(a == b);  // true,缓存范围内,同一个对象

Integer c = 128;
Integer d = 128;
System.out.println(c == d);  // false,超出缓存范围,不同对象

int e = 128;
System.out.println(c == e);  // true,c 自动拆箱为 int,值比较

规则总结

  • 两个包装类用 ==:比较的是引用地址(是否同一个对象)。
  • 基本类型和包装类用 ==:包装类自动拆箱,比较值。
  • 包装类比较值:用 equals() 方法。

其他坑

  1. 空指针异常:包装类为 null 时自动拆箱会抛 NPE。
java
Integer i = null;
int j = i;  // 抛 NullPointerException!
  1. 三目运算符中的拆箱
java
Integer a = null;
Integer b = 1;
Integer c = true ? a : b;  // 抛 NPE!因为三目运算符会统一类型,a 拆箱为 int
  1. 集合中只能用包装类:泛型不支持基本类型,必须用包装类。

  2. new Integer(127) vs Integer.valueOf(127)

    • new Integer(127) 每次都创建新对象
    • Integer.valueOf(127) 复用缓存对象

追问延伸

  • 为什么要有包装类?基本类型不够用吗?(泛型需要、可以为 null、面向对象特性、丰富的方法)
  • Integer 缓存的范围是多少?可以修改吗?怎么修改?
  • 自动装箱和拆箱的底层原理?编译期做了什么?
  • == 和 equals 比较包装类有什么区别?
  • 你在项目中遇到过自动装箱拆箱导致的问题吗?
  • 为什么 float 和 double 没有缓存?(浮点数范围太大,离散的,缓存没有意义)

Q9: 反射和注解的原理及应用? 「🟡 中级」

考察点:对 Java 元编程能力的理解,反射和注解的原理、应用场景、性能问题。筛掉只会用框架、不懂底层原理的人。

参考答案

反射(Reflection)

什么是反射

反射是 Java 提供的一种机制,允许程序在运行时获取任意类的信息(属性、方法、构造器等),并可以操作对象的属性和方法。

  • 正常方式:new 类名() → 编译期确定类型
  • 反射方式:Class.forName("类名").newInstance() → 运行期动态加载

反射的核心类

作用
java.lang.Class代表类的字节码对象,反射的入口
java.lang.reflect.Constructor类的构造方法
java.lang.reflect.Method类的方法
java.lang.reflect.Field类的字段

获取 Class 对象的三种方式

java
// 1. 类名.class(编译期确定,最安全)
Class<?> clazz1 = String.class;

// 2. 对象.getClass()
String s = "abc";
Class<?> clazz2 = s.getClass();

// 3. Class.forName("全限定类名")(最常用,运行时动态加载)
Class<?> clazz3 = Class.forName("java.lang.String");

反射的基本操作

java
// 获取 Class 对象
Class<?> clazz = Class.forName("com.example.User");

// 创建对象(调用无参构造)
Object obj = clazz.newInstance();  // JDK 9 废弃,推荐用 getConstructor().newInstance()

// 调用有参构造
Constructor<?> constructor = clazz.getConstructor(String.class, int.class);
Object user = constructor.newInstance("Alice", 20);

// 获取并调用方法
Method method = clazz.getMethod("setName", String.class);
method.invoke(user, "Bob");  // 等价于 user.setName("Bob")

// 获取并修改字段(包括 private)
Field field = clazz.getDeclaredField("name");
field.setAccessible(true);  // 打破封装
field.set(user, "Charlie");

反射的应用场景

  1. 框架设计:Spring IOC(依赖注入)、MyBatis ORM 映射、各种注解处理
  2. 动态代理:JDK 动态代理基于反射
  3. JDBC 加载驱动Class.forName("com.mysql.cj.jdbc.Driver")
  4. 配置与解耦:通过配置文件加载类,不用硬编码
  5. 单元测试:JUnit、Mockito 等测试框架

反射的缺点

  1. 性能差:比直接调用慢很多,因为要做运行时类型解析、安全检查等
  2. 打破封装:可以访问 private 字段和方法,破坏面向对象封装性
  3. 维护成本高:反射代码难以 IDE 重构、难以追踪调用链
  4. 安全限制:SecurityManager 下可能被禁止

注解(Annotation)

什么是注解

注解是 JDK 5 引入的一种元数据机制,用于给代码添加标记/说明,可以被编译器或运行时框架读取处理。

  • 注解不会直接影响代码逻辑,但可以被工具用来生成代码、检查约束、实现特定功能。

元注解(Meta-Annotation)

元注解是用来定义注解的注解:

元注解作用
@Target注解可以用在什么地方(类、方法、字段、参数等)
@Retention注解保留策略(SOURCE/CLASS/RUNTIME)
@Documented注解是否包含在 Javadoc 中
@Inherited子类是否继承父类的注解
@Repeatable注解是否可重复标注(JDK 8)

@Retention 三种策略

  • SOURCE:只在源码中,编译时丢弃(如 @Override
  • CLASS:编译到 class 文件中,运行时不加载(默认值)
  • RUNTIME:运行时保留,可通过反射读取(框架最常用)

自定义注解

java
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Log {
    String value() default "";   // 注解属性
    boolean enable() default true;
}

// 使用
@Log("用户登录")
public void login() { ... }

注解的处理方式

  1. 编译时注解处理(APT)

    • 注解处理器(Annotation Processor)在编译期扫描注解,生成代码或做校验。
    • 如 Lombok(@Data 自动生成 getter/setter)、ButterKnife、Dagger 等。
    • 保留策略是 SOURCE 或 CLASS。
  2. 运行时注解处理

    • 通过反射读取注解,根据注解执行特定逻辑。
    • 如 Spring 的 @Autowired@RequestMapping,JUnit 的 @Test
    • 保留策略必须是 RUNTIME。

动态代理

动态代理是反射的重要应用,也是 Spring AOP 的核心。

JDK 动态代理

  • 基于接口实现,通过 Proxy.newProxyInstance() 创建代理对象。
  • 被代理类必须实现至少一个接口。
java
// 接口
interface UserService {
    void addUser();
}

// 实现类
class UserServiceImpl implements UserService {
    public void addUser() { System.out.println("添加用户"); }
}

// 调用处理器
class MyInvocationHandler implements InvocationHandler {
    private Object target;
    
    public MyInvocationHandler(Object target) { this.target = target; }
    
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("前置增强");
        Object result = method.invoke(target, args);  // 反射调用目标方法
        System.out.println("后置增强");
        return result;
    }
}

// 创建代理
UserService proxy = (UserService) Proxy.newProxyInstance(
    UserServiceImpl.class.getClassLoader(),
    UserServiceImpl.class.getInterfaces(),
    new MyInvocationHandler(new UserServiceImpl())
);
proxy.addUser();  // 调用代理方法

CGLIB 动态代理

  • 基于继承实现,通过字节码技术生成被代理类的子类。
  • 被代理类不需要实现接口,但不能是 final 类,final 方法也不能被代理。
  • Spring AOP 默认策略:有接口用 JDK 代理,没接口用 CGLIB。

JDK 代理 vs CGLIB 代理

维度JDK 动态代理CGLIB 代理
实现方式基于接口基于继承(字节码生成子类)
被代理类要求必须实现接口不能是 final 类
性能创建快,调用稍慢(反射调用)创建慢,调用快(直接调用子类方法)
场景有接口的情况无接口的情况

追问延伸

  • 反射为什么慢?可以怎么优化?(缓存 Class/Method 对象、setAccessible(true) 跳过安全检查、使用 MethodHandle)
  • 注解的本质是什么?(实现了 Annotation 接口的动态代理类)
  • Spring AOP 的实现原理?(JDK 动态代理 + CGLIB)
  • 你在项目中用过反射吗?在什么场景下用的?
  • Lombok 的原理是什么?(编译时注解处理器 APT,修改 AST)
  • 为什么 JDK 动态代理只能代理接口?(生成的代理类已经继承了 Proxy 类,Java 单继承)

Q10: 泛型的实现原理?类型擦除?PECS原则? 「🟡 中级」

考察点:对 Java 泛型底层机制的理解,类型擦除的影响,通配符和 PECS 原则的运用。筛掉只会用泛型集合、不懂原理的人。

参考答案

什么是泛型

泛型(Generics)是 JDK 5 引入的特性,允许在定义类、接口、方法时使用类型参数,使用时再指定具体类型。

  • 核心目的:类型安全 + 代码复用。
  • 类型安全:编译期检查类型,避免运行时 ClassCastException。
  • 代码复用:一套代码支持多种类型。
java
// 泛型类
class Box<T> {
    private T content;
    public T get() { return content; }
    public void set(T content) { this.content = content; }
}

// 使用
Box<String> box = new Box<>();
box.set("hello");
String s = box.get();  // 不需要强转,编译期保证类型安全

类型擦除(Type Erasure)

Java 的泛型是伪泛型,泛型信息只存在于编译期,编译后会被擦除,字节码中没有泛型类型参数。

擦除规则

  1. 无界泛型(T:擦除为 Object
  2. 有界泛型(T extends Number:擦除为边界类型 Number
  3. 通配符(?:擦除为 Object(或上界)
java
// 源码
class Box<T extends Number> {
    private T value;
    public T get() { return value; }
}

// 编译擦除后(等价代码)
class Box {
    private Number value;
    public Number get() { return value; }
}

为什么要类型擦除

  • 向后兼容:JDK 5 之前没有泛型,擦除保证旧代码(raw type)能和新代码共存。
  • 这是一种权衡:为了兼容性牺牲了一些特性(如基本类型不能做泛型参数、运行时无法获取泛型类型等)。

类型擦除的补偿机制

  • 桥接方法(Bridge Method):当子类实现泛型接口或重写泛型方法时,编译器会生成桥接方法来保持多态性。
java
// 源码
interface Comparable<T> {
    int compareTo(T o);
}

class User implements Comparable<User> {
    @Override
    public int compareTo(User o) { ... }
}

// 编译后,编译器生成桥接方法
class User implements Comparable {
    public int compareTo(User o) { ... }
    
    // 桥接方法,保持接口契约
    public int compareTo(Object o) {
        return compareTo((User) o);
    }
}
  • 类型擦除后怎么获取泛型信息
    • 通过反射 getGenericSuperclass() / getGenericInterfaces() 可以获取父类/接口的泛型参数(ParameterizedType)。
    • 这是很多框架(如 Spring、MyBatis、Gson)获取泛型信息的方式。

通配符(Wildcard)

三种通配符

通配符含义说明
?无界通配符任意类型,等价于 ? extends Object
? extends T上界通配符T 及其子类
? super T下界通配符T 及其父类

上界通配符 ? extends T

  • 表示"是 T 的子类"。
  • 可以读(get 出来的是 T 类型)。
  • 不能写(除了 null,因为不知道具体是什么子类型)。
java
List<? extends Number> list = new ArrayList<Integer>();
Number n = list.get(0);  // OK,读出来是 Number
list.add(1);             // ERROR!不能写,不知道具体类型是 Integer 还是 Double
list.add(null);          // OK,null 可以

下界通配符 ? super T

  • 表示"是 T 的父类"。
  • 可以写(可以添加 T 及其子类)。
  • 读受限(get 出来只能当 Object 用)。
java
List<? super Integer> list = new ArrayList<Number>();
list.add(1);              // OK,Integer 是 Integer 本身
list.add(2);              // OK
Object o = list.get(0);   // 只能用 Object 接收

PECS 原则

Producer Extends, Consumer Super —— 生产者使用 extends,消费者使用 super。

  • 生产者(Producer):频繁读取,几乎不写入 → 用 ? extends T
    • 因为读取出来的都是 T 类型,安全。
  • 消费者(Consumer):频繁写入,几乎不读取 → 用 ? super T
    • 因为可以添加 T 及其子类,安全。
  • 既读又写:不要用通配符,用具体类型。

经典例子 —— Collections.copy

java
public static <T> void copy(List<? super T> dest, List<? extends T> src) {
    // src 是生产者(读数据),用 extends
    // dest 是消费者(写数据),用 super
    for (int i = 0; i < src.size(); i++) {
        dest.set(i, src.get(i));
    }
}

泛型的限制

  1. 不能用基本类型List<int> 不行,必须 List<Integer>(因为擦除后是 Object,基本类型不是对象)
  2. 不能实例化泛型类型new T() 不行(擦除后不知道类型)
  3. 不能创建泛型数组new T[] 不行(类型不安全)
  4. 不能用泛型做类型判断obj instanceof T 不行(运行时没有 T)
  5. 静态方法不能使用类的泛型参数:静态方法在类加载时就存在,此时泛型还未确定
  6. 异常不能泛型:不能 catch (T e),但可以 throws T

追问延伸

  • Java 的泛型为什么要用类型擦除?有什么优缺点?
  • 什么是桥接方法?为什么需要桥接方法?
  • 运行时能获取泛型的具体类型吗?怎么获取?(通过反射获取父类/接口的泛型参数)
  • List&lt;Object&gt;List&lt;?&gt; 有什么区别?(List&lt;Object&gt; 可以 add 任意对象,List&lt;?&gt; 除了 null 不能 add)
  • 你能说一下 PECS 原则吗?在什么场景下用?
  • 泛型数组为什么不安全?能举个例子吗?(String[] 可以赋值给 Object[],但 List<String> 不能赋值给 List<Object>,数组是协变的,泛型是不变的)

Q11: Java 的 I/O 模型?BIO vs NIO vs AIO? 「🟡 中级」

考察点:对 Java I/O 模型的理解,同步/异步、阻塞/非阻塞的概念,NIO 核心组件。筛掉对 IO 模型概念模糊的人。

参考答案

I/O 模型概述

Java 提供了三种 I/O 模型:

模型全称模式JDK 版本特点
BIOBlocking I/O同步阻塞JDK 1.0一连接一线程,简单但并发能力低
NIONon-blocking I/O / New I/O同步非阻塞JDK 1.4多路复用,单线程管理多连接,高并发
AIOAsynchronous I/O异步非阻塞JDK 1.7真正异步,操作系统完成后回调,应用少

BIO(Blocking I/O)

  • 模式:同步阻塞。
  • 特点:每个连接需要一个独立线程处理,线程数 = 连接数。
  • 工作流程
    1. 服务端 ServerSocket.accept() 阻塞等待连接。
    2. 客户端连接后,创建新线程处理读写。
    3. 读/写操作也是阻塞的(read() 阻塞直到有数据)。
  • 优点:编程简单,易于理解。
  • 缺点:线程数受限,高并发下线程切换开销大,不适合高并发场景。
  • 适用场景:连接数少、连接时间长的场景。
java
// BIO 示例
ServerSocket serverSocket = new ServerSocket(8080);
while (true) {
    Socket socket = serverSocket.accept();  // 阻塞等待连接
    new Thread(() -> {
        try (BufferedReader reader = new BufferedReader(
                new InputStreamReader(socket.getInputStream()))) {
            String line;
            while ((line = reader.readLine()) != null) {  // 阻塞读
                System.out.println(line);
            }
        } catch (IOException e) { e.printStackTrace(); }
    }).start();
}

NIO(Non-blocking I/O / New I/O)

  • 模式:同步非阻塞(IO 多路复用)。
  • 核心思想:一个线程管理多个连接,通过 Selector 轮询就绪的 Channel。
  • 三大核心组件ChannelBufferSelector

三大组件

1. Channel(通道)

  • 类似 BIO 的 Stream,但 Channel 是双向的(可读可写)。
  • 主要实现:SocketChannelServerSocketChannelFileChannelDatagramChannel

2. Buffer(缓冲区)

  • NIO 的数据读写都通过 Buffer。
  • 核心属性:capacity(容量)、position(当前位置)、limit(上界)、mark(标记)。
  • 核心方法:flip()(切换为读模式)、clear()(重置为写模式)、rewind()(重新读)。
  • 常用实现:ByteBufferCharBufferIntBuffer 等。

3. Selector(选择器/多路复用器)

  • 一个 Selector 可以管理多个 Channel。
  • Channel 注册到 Selector 上,并指定感兴趣的事件(读/写/连接/接受)。
  • Selector 通过 select() 方法阻塞等待就绪的 Channel,然后处理。
  • 底层依赖操作系统的多路复用机制:Linux 上是 epoll,Windows 上是 IOCP,macOS 上是 kqueue。

NIO 工作流程

客户端 ──→ Channel ──→ Selector ←── Channel ←── 客户端
                     (单线程轮询)

                     处理就绪事件

为什么 NIO 性能高

  • 线程少:一个线程可以处理成千上万连接,减少线程创建和上下文切换开销。
  • 非阻塞:没有数据时不阻塞,可以处理其他连接。
  • 零拷贝FileChannel.transferTo() / transferFrom() 可以实现零拷贝。

AIO(Asynchronous I/O)

  • 模式:异步非阻塞。
  • 特点:应用程序发起 IO 请求后立即返回,操作系统完成 IO 后通过回调或 Future 通知应用程序。
  • 真正的异步,应用线程不需要轮询。
  • 实现方式
    • Future 方式:调用返回 Future<V>,通过 get() 获取结果。
    • 回调方式:传入 CompletionHandler,完成后回调 completedfailed 方法。
  • 现状:实际应用不如 NIO 广泛,Linux 上 AIO 支持不够成熟,Netty 也没有使用 AIO。

BIO vs NIO vs AIO 对比

维度BIONIOAIO
阻塞模式同步阻塞同步非阻塞(多路复用)异步非阻塞
线程模型一连接一线程单线程/少量线程管理多连接线程池 + 回调
编程难度简单较复杂复杂
并发能力
数据处理面向流(Stream)面向缓冲区(Buffer)面向缓冲区 + 回调
适用场景连接数少、连接时间长连接数多、连接时间短(高并发)连接数多、连接时间长(应用少)

零拷贝

  • 传统 IO:磁盘 → 内核缓冲区 → 用户缓冲区 → Socket 缓冲区 → 网卡,4 次拷贝 + 2 次用户态内核态切换。
  • 零拷贝(mmap + sendfile)
    • mmap:内存映射,文件直接映射到用户空间,减少一次拷贝。
    • sendfile:数据直接从内核缓冲区传到 Socket 缓冲区,不经过用户态。
    • Linux 2.4+ sendfile 还支持 DMA 直接从文件系统缓存到网卡,真正的零拷贝。
  • NIO 中实现FileChannel.transferTo() / transferFrom()
  • 应用:Kafka、Netty、RocketMQ 等高性能框架都用到了零拷贝。

Netty 为什么选 NIO

  • Netty 基于 NIO 封装,提供更易用的 API。
  • 不选 BIO:并发性能差。
  • 不选 AIO:Linux 上 AIO 不够成熟,性能提升不明显,编程模型更复杂。
  • Netty 对 NIO 做了很多优化:
    • 基于 Reactor 模式(主从 Reactor 多线程模型)
    • 零拷贝增强(CompositeByteBuf 等)
    • 内存池(PooledByteBufAllocator)
    • 解决了 JDK NIO 的空轮询 bug

追问延伸

  • NIO 和 IO 的区别?(面向流 vs 面向缓冲、阻塞 vs 非阻塞、Selector 多路复用)
  • 什么是零拷贝?有哪些实现方式?
  • 同步和异步、阻塞和非阻塞的区别?(同步/异步是消息通知机制,阻塞/非阻塞是线程状态)
  • Selector 的底层原理?Linux 下用什么实现?(epoll)
  • select、poll、epoll 的区别?(水平触发/边缘触发、性能差异)
  • Netty 的线程模型?Reactor 模式的三种实现?

Q12: Java 8 新特性有哪些? 「🟡 中级」

考察点:对 Java 8 核心新特性的掌握程度,Lambda、Stream、Optional 等是否真的理解并会用。筛掉还停留在 Java 7 思维的人。

参考答案

Java 8 是 Java 历史上最重要的版本之一,引入了函数式编程思想,主要新特性如下:

1. Lambda 表达式

  • 作用:匿名函数的简写,把函数作为参数传递。
  • 语法(参数列表) -> { 方法体 }
  • 前提:必须是函数式接口(只有一个抽象方法的接口)。
java
// 旧写法(匿名内部类)
new Thread(new Runnable() {
    @Override
    public void run() {
        System.out.println("hello");
    }
}).start();

// Lambda 写法
new Thread(() -> System.out.println("hello")).start();
  • 方法引用:Lambda 的进一步简化。
    • 静态方法引用:类名::静态方法名
    • 实例方法引用:对象::实例方法名
    • 构造方法引用:类名::new
java
list.forEach(System.out::println);  // 实例方法引用

2. 函数式接口

  • 定义:只有一个抽象方法的接口。
  • @FunctionalInterface:注解标记,编译器会检查是否只有一个抽象方法。
  • 四大核心函数式接口
接口方法用途
Consumer<T>void accept(T t)消费型:有入参,无返回值
Supplier<T>T get()供给型:无入参,有返回值
Function<T, R>R apply(T t)函数型:有入参,有返回值
Predicate<T>boolean test(T t)断言型:有入参,返回 boolean

3. Stream API

  • 作用:对集合进行流式操作,支持过滤、映射、聚合等操作,声明式编程,代码更简洁。
  • 特点
    • 不存储数据,只是计算
    • 不改变源数据
    • 延迟执行(中间操作不立即执行,终止操作时才执行)
    • 可并行(parallelStream()

Stream 操作分类

类型方法说明
中间操作filtermapflatMapsorteddistinctlimitskippeek返回新的 Stream,可链式调用,惰性求值
终止操作forEachcollectcountreducemaxminfindFirstanyMatchallMatchtoArray产生最终结果,Stream 关闭

常用操作示例

java
List<User> users = ...;

// 过滤 + 映射 + 收集
List<String> names = users.stream()
    .filter(u -> u.getAge() > 18)         // 过滤年龄 > 18
    .map(User::getName)                   // 提取名字
    .sorted()                             // 排序
    .limit(10)                            // 取前10
    .collect(Collectors.toList());        // 收集为 List

// 分组
Map<Integer, List<User>> groupByAge = users.stream()
    .collect(Collectors.groupingBy(User::getAge));

// 规约
int totalAge = users.stream()
    .mapToInt(User::getAge)
    .sum();

4. Optional 类

  • 作用:优雅地处理空指针异常(NPE),用对象封装可能为 null 的值。
  • 核心思想:明确表示"值可能不存在",让编译器强制你处理空的情况。
java
// 旧写法
String name = user.getName();
if (name != null) {
    return name.toUpperCase();
} else {
    return "UNKNOWN";
}

// Optional 写法
return Optional.ofNullable(user.getName())
    .map(String::toUpperCase)
    .orElse("UNKNOWN");
  • 常用方法
    • of() / ofNullable():创建 Optional
    • isPresent() / ifPresent():判断是否有值
    • map() / flatMap():转换值
    • orElse() / orElseGet() / orElseThrow():值为空时的备选
    • filter():过滤

5. 接口新特性

默认方法(Default Method)

  • 接口中可以有默认实现的方法,用 default 关键字修饰。
  • 作用:接口演进时不破坏已有实现类(如 Collection 接口加 stream() 方法)。
  • 实现类可以直接使用,也可以重写。
java
interface MyInterface {
    default void defaultMethod() {
        System.out.println("这是默认方法");
    }
}

静态方法(Static Method)

  • 接口中可以有静态方法。
  • 只能通过接口名调用,不能被实现类继承。
java
interface MyInterface {
    static void staticMethod() {
        System.out.println("这是静态方法");
    }
}

6. 新日期时间 API

  • 旧问题DateCalendar 可变、线程不安全、API 难用。
  • 新 APIjava.time 包):
    • LocalDate:日期(年月日)
    • LocalTime:时间(时分秒)
    • LocalDateTime:日期时间
    • Instant:时间戳
    • Duration / Period:时间段
    • ZoneId / ZonedDateTime:时区
    • DateTimeFormatter:格式化(线程安全)
  • 特点:不可变、线程安全、API 清晰、遵循 ISO 标准。
java
// 创建
LocalDate today = LocalDate.now();
LocalDate birthday = LocalDate.of(2000, 1, 1);

// 计算
LocalDate nextWeek = today.plusWeeks(1);
Period period = Period.between(birthday, today);

// 格式化
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
String str = LocalDateTime.now().format(formatter);
LocalDateTime dt = LocalDateTime.parse("2024-01-01 12:00:00", formatter);

7. 其他新特性

  • 重复注解(@Repeatable):同一个注解可以在同一位置多次使用。
  • 类型注解:注解可以用在任何使用类型的地方(如 @NonNull String)。
  • 方法参数名反射:编译时加 -parameters 参数,可以通过反射获取方法参数名。
  • StampedLock:新的读写锁实现,乐观读,性能比 ReentrantReadWriteLock 更好。
  • CompletableFuture:增强的 Future,支持函数式编程、链式调用、组合异步任务。
  • 并行数组排序Arrays.parallelSort(),利用多线程加速大数组排序。

追问延伸

  • 你在项目中常用哪些 Java 8 特性?能举几个例子吗?
  • Stream 的中间操作和终止操作有什么区别?你知道哪些中间/终止操作?
  • Optional 可以完全避免 NPE 吗?你在项目中怎么用 Optional?
  • 函数式接口有什么特点?为什么必须只有一个抽象方法?
  • 新日期 API 和旧的 Date/Calendar 比有什么优势?
  • Lambda 表达式的底层原理是什么?(invokedynamic + 方法句柄)
  • Stream 的 parallelStream 用的是什么线程池?(ForkJoinPool.commonPool())

Q13: 序列化和反序列化?Serializable接口? 「🟡 中级」

考察点:对序列化机制的理解,Serializable 原理、serialVersionUID、transient 关键字,以及常见序列化方案的对比。筛掉对序列化只有模糊概念的人。

参考答案

基本概念

  • 序列化(Serialization):将 Java 对象转换为字节序列(或其他格式),以便存储或传输。
  • 反序列化(Deserialization):将字节序列还原为 Java 对象。
  • 目的:对象持久化(存文件/数据库)、网络传输(RPC 调用)。

Serializable 接口

  • java.io.Serializable 是一个标记接口(没有任何方法),表示该类可以被序列化。
  • 一个类只要实现了 Serializable 接口,就可以被 ObjectOutputStream / ObjectInputStream 序列化/反序列化。
java
public class User implements Serializable {
    private String name;
    private int age;
    // ...
}

// 序列化
User user = new User("Alice", 20);
try (ObjectOutputStream oos = new ObjectOutputStream(
        new FileOutputStream("user.dat"))) {
    oos.writeObject(user);
}

// 反序列化
try (ObjectInputStream ois = new ObjectInputStream(
        new FileInputStream("user.dat"))) {
    User user = (User) ois.readObject();
}

serialVersionUID

  • 作用:序列化版本号,用于验证序列化和反序列化的类版本是否一致。
  • 原理:序列化时写入 serialVersionUID,反序列化时读取并与当前类的 serialVersionUID 比较,如果不同则抛 InvalidClassException
  • 生成方式
    1. 默认生成:不手动指定时,JVM 根据类名、接口、成员等自动计算(类似 hash 值)。
    2. 手动指定private static final long serialVersionUID = 1L;

为什么建议手动指定 serialVersionUID?

  • 默认生成的 serialVersionUID 对类细节高度敏感,哪怕只是加了个空格、改了个方法名,serialVersionUID 都会变,导致反序列化失败。
  • 手动指定可以在类升级时保持兼容(只要你保证兼容性)。
  • IDE 可以自动生成。

transient 关键字

  • transient 修饰的字段,序列化时会被忽略,反序列化后为默认值(null、0、false 等)。
  • 用途
    • 敏感信息(密码等)不希望被序列化传输。
    • 不需要持久化的字段(如缓存、计算字段)。
    • 不支持序列化的字段(如 Thread、Socket 等)。
java
private transient String password;  // 密码不序列化

序列化的注意事项

  1. 子类序列化:父类实现 Serializable,子类自动可序列化;子类实现 Serializable,父类需要无参构造(否则反序列化报错)。
  2. 静态变量:静态变量属于类,不属于对象,不会被序列化。
  3. 引用类型成员:成员变量也必须是可序列化的,否则抛 NotSerializableException
  4. 单例模式:反序列化会创建新对象,破坏单例。解决方案:实现 readResolve() 方法返回单例对象。
  5. 安全问题:Java 原生序列化存在安全风险(反序列化漏洞),应谨慎使用。
  6. 性能:Java 原生序列化性能差、字节体积大,一般不用于高性能场景。

常见序列化方式对比

序列化方式特点优点缺点适用场景
Java 原生序列化实现 Serializable 接口Java 原生支持,无需第三方依赖性能差、体积大、有安全风险、跨语言不行Java 内部系统、简单场景
JSON(Jackson/Gson)文本格式,可读性好可读性好、跨语言、使用广泛体积较大、性能一般HTTP 接口、配置文件、调试
XML文本格式,结构清晰可读性好、Schema 验证体积大、性能差遗留系统、配置文件
Protobuf(Protocol Buffers)Google 的二进制格式性能极高、体积小、跨语言、向后兼容需要定义 .proto 文件、可读性差高性能 RPC、微服务
KryoJava 序列化框架性能高、体积小、使用简单跨语言不行、兼容性一般Java 内部高性能场景
Hessian二进制 Web 服务协议跨语言、自带 RPC 支持性能不如 Protobuf跨语言 RPC

选型建议

  • 对外 HTTP 接口:JSON(Jackson),可读性好,调试方便。
  • 内部 RPC 高性能:Protobuf,性能高体积小。
  • Java 内部系统:Kryo / Protobuf,看团队熟悉度。
  • 简单 Java 内部场景:Java 原生序列化(但要注意安全和性能)。

追问延伸

  • 什么是 serialVersionUID?有什么用?不指定会怎样?
  • transient 关键字有什么用?静态变量会被序列化吗?
  • 一个类实现了 Serializable,它的所有属性都可以被序列化吗?(属性类型也要实现 Serializable)
  • 你在项目中用什么序列化?为什么选它?
  • Java 反序列化漏洞是什么原理?怎么防范?(反序列化时执行恶意代码;不信任的输入不反序列化、使用白名单、升级 JDK)
  • Protobuf 为什么性能好体积小?(Varint 编码、Tag 机制、二进制格式)
  • 单例类如何防止反序列化破坏单例?(readResolve 方法)

Q14: 内部类有哪几种?各有什么特点? 「🟢 校招/初级」

考察点:对 Java 内部类体系的掌握,各种内部类的特点和使用场景。筛掉对内部类概念模糊的人。

参考答案

内部类是定义在另一个类内部的类。Java 中有 4 种内部类:

内部类类型定义位置访问外部类成员静态成员创建方式
成员内部类类的成员位置可以访问所有成员(包括 private)不能有静态成员(除了 static final 常量)外部类对象.new 内部类()
静态内部类类的成员位置 + static只能访问外部类的静态成员可以有静态成员new 外部类.内部类()
局部内部类方法/代码块内部可以访问外部类所有成员 + final/effectively final 局部变量不能有静态成员方法内创建
匿名内部类方法/代码块内部,无类名同上不能有静态成员new 接口/父类()

1. 成员内部类(Member Inner Class)

  • 定义在外部类的成员位置(和属性、方法平级)。
  • 可以访问外部类的所有成员(包括 private)。
  • 不能有静态成员(除了静态常量)。
  • 成员内部类持有外部类的引用(外部类名.this)。
java
class Outer {
    private int x = 10;
    
    class Inner {
        public void print() {
            System.out.println(x);  // 可以直接访问外部类私有成员
            System.out.println(Outer.this.x);  // 等价,显式引用
        }
    }
}

// 创建方式
Outer outer = new Outer();
Outer.Inner inner = outer.new Inner();
inner.print();

2. 静态内部类(Static Inner Class / Nested Class)

  • 成员内部类加 static 修饰。
  • 只能访问外部类的静态成员。
  • 不持有外部类的引用,不依赖外部类对象。
  • 可以有静态成员。
java
class Outer {
    private static int x = 10;
    private int y = 20;
    
    static class StaticInner {
        public void print() {
            System.out.println(x);  // OK,静态变量
            // System.out.println(y);  // ERROR,不能访问非静态成员
        }
    }
}

// 创建方式(不需要外部类对象)
Outer.StaticInner inner = new Outer.StaticInner();

典型应用:Builder 模式、双检锁单例中的 Holder 模式。

java
// 静态内部类实现单例(Holder 模式)
public class Singleton {
    private Singleton() {}
    
    private static class SingletonHolder {
        private static final Singleton INSTANCE = new Singleton();
    }
    
    public static Singleton getInstance() {
        return SingletonHolder.INSTANCE;
    }
}

3. 局部内部类(Local Inner Class)

  • 定义在方法或代码块内部。
  • 作用域仅限于方法/代码块内。
  • 可以访问外部类所有成员。
  • 访问局部变量时,局部变量必须是 finaleffectively final(JDK 8+)。
  • 不能有静态成员。
java
class Outer {
    private int x = 10;
    
    public void method() {
        final int y = 20;  // 必须 final 或 effectively final
        // JDK 8+ 不加 final 也可以,但不能修改
        
        class LocalInner {
            public void print() {
                System.out.println(x);  // 外部类成员
                System.out.println(y);  // 局部变量
            }
        }
        
        LocalInner inner = new LocalInner();
        inner.print();
    }
}

4. 匿名内部类(Anonymous Inner Class)

  • 没有名字的内部类,同时创建对象。
  • 必须继承一个父类或实现一个接口。
  • 只能使用一次(创建一个对象)。
  • 其他限制和局部内部类相同。
java
// 实现 Runnable 接口的匿名内部类
new Thread(new Runnable() {
    @Override
    public void run() {
        System.out.println("hello");
    }
}).start();

// 继承类的匿名内部类
Button button = new Button();
button.setOnClickListener(new OnClickListener() {
    @Override
    public void onClick(View v) {
        // 处理点击
    }
});

为什么局部内部类和匿名内部类访问局部变量需要 final?

  • 原因:生命周期不同。局部变量在方法结束后就销毁了,但内部类对象可能还活着(比如异步回调)。
  • 实现原理:编译器会把局部变量的值复制一份到内部类中。如果变量可以修改,就会出现数据不一致的问题。
  • 所以 Java 规定必须是 final(不可修改),保证内外一致。
  • JDK 8 引入了 effectively final 的概念:虽然没写 final,但实际上没有修改过,也可以。

Lambda 和匿名内部类的区别

维度匿名内部类Lambda 表达式
本质类,编译后生成 .class 文件不是类,基于 invokedynamic
数量每个匿名内部类一个 class 文件不会生成额外 class
thisthis 指向匿名内部类对象this 指向外部类对象(没有自己的 this)
适用接口任意接口(只要有抽象方法)只能是函数式接口(一个抽象方法)
性能类加载开销大首次调用慢一点,后续快,内存占用少
变量访问可以访问外部类成员,局部变量需 final同左

追问延伸

  • 内部类的字节码文件名是什么样的?(Outer$Inner.class,匿名内部类是 Outer$1.class)
  • 静态内部类和非静态内部类的区别?(持有外部类引用、访问权限、创建方式)
  • 为什么局部内部类访问局部变量需要 final?JDK 8 的 effectively final 是什么?
  • 匿名内部类和 Lambda 表达式有什么区别?
  • 你在项目中用过哪些内部类?在什么场景下用的?
  • 内部类可以继承吗?可以被继承吗?
  • 成员内部类为什么不能有静态成员?(因为成员内部类依赖外部类实例,静态成员不依赖实例,语义矛盾)

Q15: Java 的优势和劣势?编译型语言和解释型语言的区别? 「🟢 校招/初级」

考察点:对 Java 语言定位的理解。

参考答案

Java 既是编译型也是解释型语言:

  • 编译.javajavac.class(字节码)
  • 解释:JVM 类加载器加载 .class → 解释器/ JIT 编译器 → 机器码执行
维度编译型(C/C++)解释型(Python)Java
执行方式源码→机器码直接执行源码逐行解释执行源码→字节码→JVM解释/JIT编译
跨平台否(需重新编译)是(一次编译到处运行)
性能最快较慢中(JIT 优化后接近 native)
启动速度慢(JVM 启动 + 类加载)

Java 的优势:

  • 跨平台(Write Once Run Anywhere)
  • 自动内存管理(GC)
  • 生态成熟(Spring/Hadoop/Android)
  • 强类型、面向对象

Java 的劣势:

  • 启动慢(JVM 预热)
  • 内存占用大
  • 语法冗余(比 Python/Kotlin)
  • 不适合底层系统编程

追问延伸

  • JIT 是什么?和 AOT 有什么区别?(JIT 运行时编译热点代码;AOT 编译时直接生成机器码,如 GraalVM)
  • 为什么 Java 21 引入虚拟线程后启动慢的问题缓解了?

Q16: Java 是值传递还是引用传递? 「🟡 中级」

考察点:参数传递机制的深度理解。

参考答案

Java 只有值传递,没有引用传递。

  • 基本类型:传递值的副本
  • 引用类型:传递引用地址的副本(不是引用本身)
java
// 基本类型 - 修改不影响原变量
void modify(int x) { x = 100; }
int a = 10;
modify(a);  // a 仍然是 10

// 引用类型 - 可以修改对象内部,但不能改变引用指向
void modifyList(List<String> list) {
    list.add("new");      // 修改对象内部 → 原列表变了
    list = new ArrayList<>(); // 改变引用指向 → 原列表不变
}
List<String> myList = new ArrayList<>();
modifyList(myList);  // myList 有一个 "new" 元素

值传递 vs 引用传递:

  • 值传递:传递实参的副本,修改形参不影响实参
  • 引用传递:传递实参本身,修改形参直接影响实参
  • Java 中方法参数传递的永远是"值"(基本类型是值本身,引用类型是地址值)

追问延伸

  • String 作为参数传递后为什么修改不了?(String 不可变,"修改"实际是创建新对象)
  • C++ 的引用传递 void foo(int& x) 和 Java 有什么区别?

Q17: Java 基本数据类型有哪些?自动类型转换和强制转换的规则? 「🟢 校招/初级」

考察点:数据类型基础。

参考答案

8 种基本数据类型:

类型关键字位数字节默认值范围
字节型byte810-128 ~ 127
短整型short1620-32768 ~ 32767
整型int3240-2^31 ~ 2^31-1
长整型long6480L-2^63 ~ 2^63-1
单精度float3240.0fIEEE 754
双精度double6480.0dIEEE 754
字符型char162'\u0000'0 ~ 65535
布尔型boolean--falsetrue/false

自动类型转换(隐式,小→大):

java
byte b = 10;
int i = b;        // byte → int(自动)
long l = i;       // int → long(自动)
float f = l;      // long → float(自动,但可能丢精度)
double d = f;     // float → double(自动)

强制类型转换(显式,大→小,可能丢精度/溢出):

java
int i = 128;
byte b = (byte) i;  // 溢出:b = -128

double d = 3.99;
int n = (int) d;     // 截断:n = 3

类型转换陷阱:

  • int / int 结果是 int10 / 3 = 3(不是 3.33)
  • floatint 的转换会截断小数部分
  • char 可以自动转 int'A' + 1 = 66

追问延伸

  • int(1)int(10) 在 MySQL 中有什么不同?(MySQL 中括号是显示宽度,不影响存储范围)
  • boolean 占多少字节?(JVM 规范未定义,HotSpot 中单个 boolean 用 int 存储占 4 字节,数组中用 byte 占 1 字节)

Q18: 为什么用 BigDecimal 不用 double?浮点数精度问题? 「🟡 中级」

考察点:浮点数精度问题的理解。

参考答案

double 精度问题:

java
System.out.println(0.1 + 0.2);  // 输出 0.30000000000000004
System.out.println(0.1 + 0.2 == 0.3);  // false

原因:double 用 IEEE 754 二进制浮点数表示,0.1 在二进制中是无限循环小数,截断后有精度损失。

BigDecimal 解决方案:

java
BigDecimal a = new BigDecimal("0.1");  // 用字符串构造!
BigDecimal b = new BigDecimal("0.2");
System.out.println(a.add(b));  // 精确输出 0.3

// 注意:不要用 double 构造
BigDecimal bad = new BigDecimal(0.1);  // 仍然有精度问题!

BigDecimal 比较:

java
// 错误:equals 会比较精度
new BigDecimal("1.0").equals(new BigDecimal("1.00"));  // false

// 正确:用 compareTo
new BigDecimal("1.0").compareTo(new BigDecimal("1.00")) == 0;  // true

BigDecimal 运算注意:

  • 除法要指定精度:a.divide(b, 2, RoundingMode.HALF_UP)
  • 不指定精度遇到无限循环小数会抛 ArithmeticException

适用场景:金融计算、价格计算、金额汇总。

追问延伸

  • double 和 float 的精度分别是多少?(float 约 7 位有效数字,double 约 15-16 位)
  • 怎么优化 BigDecimal 的性能?(用 long 存储分/分为单位,避免 BigDecimal 开销)

Q19: 自动装箱和拆箱的原理?Integer 缓存池? 「🟢 校招/初级」

考察点:包装类的底层机制。

参考答案

自动装箱(Autoboxing):基本类型 → 包装类 自动拆箱(Unboxing):包装类 → 基本类型

java
// 装箱:实际调用 Integer.valueOf(int)
Integer i = 100;  // 编译后:Integer.valueOf(100)

// 拆箱:实际调用 Integer.intValue()
int n = i;        // 编译后:i.intValue()

Integer 缓存池(Integer Cache):

java
Integer a = 127;
Integer b = 127;
System.out.println(a == b);   // true(缓存池,同一个对象)

Integer c = 128;
Integer d = 128;
System.out.println(c == d);   // false(超出缓存范围,新对象)

// 缓存范围:-128 ~ 127(可通过 -XX:AutoBoxCacheMax 调整上限)

缓存池的类:

  • Integer:-128 ~ 127(默认)
  • Byte:全部缓存(-128 ~ 127)
  • Short:-128 ~ 127
  • Long:-128 ~ 127
  • Character:0 ~ 127
  • Boolean:TRUE / FALSE(两个实例)
  • Float / Double:没有缓存

注意事项:

  • Integer == int 时会自动拆箱,比较的是值(不会有缓存问题)
  • Integer == Integer 比较的是对象地址(需用 equals
  • 自动装箱/拆箱在循环中有性能开销

追问延伸

  • 为什么 Integer 要设计缓存池?(常用值复用,减少对象创建开销)
  • Integer.valueOf(127)new Integer(127) 有什么区别?(前者返回缓存对象,后者总是创建新对象)

Q20: static 关键字的作用?静态变量/方法/代码块/内部类的区别? 「🟢 校招/初级」

考察点:static 的全面理解。

参考答案

static 修饰的成员属于类级别(不属于实例),在类加载时初始化。

修饰对象特点使用场景
静态变量类级别共享,所有实例共用一份计数器、常量、配置
静态方法通过类名调用,不能访问实例变量工具方法、工厂方法
静态代码块类加载时执行一次,只执行一次初始化静态资源
静态内部类不依赖外部类实例,可独立创建建造者模式、工具类
java
public class Counter {
    private static int count = 0;           // 静态变量
    private static final int MAX = 100;      // 静态常量

    static {
        // 静态代码块:类加载时执行一次
        System.out.println("类加载中...");
    }

    public static Counter getInstance() {   // 静态方法
        return new Counter();
    }

    static class Builder {                  // 静态内部类
        private String name;
        public Builder name(String n) { this.name = n; return this; }
    }
}

static 的限制:

  • 静态方法不能访问实例变量和实例方法(没有 this
  • 静态方法不能被 @Override(但可以被隐藏/hide)
  • 静态内部类不能访问外部类的实例变量(只能访问外部类的静态成员)

static 加载顺序:

  1. 父类静态代码块/静态变量(按代码顺序)
  2. 子类静态代码块/静态变量
  3. 父类实例代码块/实例变量
  4. 父类构造函数
  5. 子类实例代码块/实例变量
  6. 子类构造函数

追问延伸

  • 静态代码块和构造函数的区别?(静态代码块类加载时执行一次;构造函数每次创建对象都执行)
  • 静态内部类和成员内部类的区别?(静态内部类不持有外部类引用)

Q21: 深拷贝和浅拷贝的区别?如何实现深拷贝? 「🟡 中级」

考察点:对象拷贝的深度理解。

参考答案

类型说明引用对象
浅拷贝只复制对象本身,不复制引用对象共享同一个引用对象
深拷贝复制对象本身,递归复制所有引用对象各自独立
java
class Person implements Cloneable {
    String name;
    Address address;  // 引用类型

    @Override
    protected Object clone() throws CloneNotSupportedException {
        Person p = (Person) super.clone();  // 浅拷贝
        p.address = (Address) address.clone(); // 手动深拷贝
        return p;
    }
}

深拷贝的三种实现方式:

  1. 实现 Cloneable + 递归 clone
java
class Address implements Cloneable {
    String city;
    public Address clone() throws CloneNotSupportedException {
        return (Address) super.clone();
    }
}
class Person implements Cloneable {
    Address address;
    public Person clone() throws CloneNotSupportedException {
        Person p = (Person) super.clone();
        p.address = address.clone();
        return p;
    }
}
  1. 序列化/反序列化(最通用):
java
@SuppressWarnings("unchecked")
public static <T extends Serializable> T deepCopy(T obj) throws Exception {
    ByteArrayOutputStream bos = new ByteArrayOutputStream();
    ObjectOutputStream oos = new ObjectOutputStream(bos);
    oos.writeObject(obj);

    ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray());
    ObjectInputStream ois = new ObjectInputStream(bis);
    return (T) ois.readObject();
}
  1. JSON 序列化(跨语言,但有性能开销):
java
String json = JSON.toJSONString(obj);
Person copy = JSON.parseObject(json, Person.class);

追问延伸

  • Object.clone() 是浅拷贝还是深拷贝?(浅拷贝)
  • 为什么推荐用序列化实现深拷贝而不是 Cloneable?(Cloneable 容易遗漏嵌套对象,序列化自动处理全部字段)

Q22: Java 创建对象有哪些方式? 「🟢 校招/初级」

考察点:对象创建的全面了解。

参考答案

5 种创建对象的方式:

java
// 1. new 关键字(最常用)
Person p1 = new Person();

// 2. 反射(Class.newInstance / Constructor.newInstance)
Person p2 = Person.class.getDeclaredConstructor().newInstance();
// JDK 9+ 推荐 Constructor.newInstance,class.newInstance 已废弃

// 3. Clone(实现 Cloneable)
Person p3 = (Person) p1.clone();

// 4. 反序列化
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("person.dat"));
Person p4 = (Person) ois.readObject();

// 5. Unsafe.allocateInstance(不走构造函数)
Unsafe unsafe = getUnsafe();
Person p5 = (Person) unsafe.allocateInstance(Person.class);

区别:

方式调用构造函数静态代码块
new
Constructor.newInstance是(类加载时)
clone
反序列化
Unsafe

常见场景:

  • new:日常开发
  • 反射:框架内部(Spring IoC 创建 Bean)
  • clone:原型模式
  • 反序列化:RPC 传输、缓存恢复
  • Unsafe:底层框架(Netty、JVM 内部)

追问延伸

  • Class.newInstance()Constructor.newInstance() 的区别?(后者可以调用私有构造函数,可以传参数)
  • Spring 创建 Bean 用的是哪种方式?(Constructor.newInstance 反射创建)

Q23: Object 类有哪些方法? 「🟡 中级」

考察点:Java 根类的了解程度。

参考答案

Object 类的 11 个方法(Java 8):

方法说明
equals(Object obj)判断两个对象是否"相等"
hashCode()返回对象的哈希码
toString()返回对象的字符串表示
getClass()返回运行时类对象
clone()创建并返回对象副本(protected)
finalize()GC 回收前调用(已废弃,Java 9+ 用 Cleaner)
wait()线程等待(获取锁的线程释放锁)
wait(long timeout)带超时的等待
wait(long timeout, int nanos)精确超时等待
notify()唤醒一个等待的线程
notifyAll()唤醒所有等待的线程

重点方法约定:

  • equals 和 hashCode 的契约:equals 相等的两个对象,hashCode 必须相等;hashCode 相等,equals 不一定相等
  • toString:默认返回 类名@十六进制哈希码,建议重写
  • clone:浅拷贝,需实现 Cloneable 接口,否则抛 CloneNotSupportedException

wait/notify 的使用约束:

  • 必须在 synchronized 块内调用
  • 调用 wait 时必须持有对象的锁
  • wait 会释放锁,sleep 不会释放锁
java
synchronized (obj) {
    while (!condition) {
        obj.wait();  // 释放锁,等待被 notify 唤醒
    }
    // 条件满足,继续执行
}

追问延伸

  • 为什么重写 equals 必须重写 hashCode?(HashMap/Put 时先 hashCode 再 equals,不一致会导致 HashSet 存入"相等"的对象)
  • finalize 为什么要废弃?(不可预测、性能差、可能导致内存泄漏,用 java.lang.ref.Cleaner 替代)

Q24: Java 注解的原理是什么?注解的作用域? 「🟡 中级」

考察点:注解的底层理解。

参考答案

注解(Annotation)本质是一个继承 java.lang.annotation.Annotation 的接口,运行时由 JVM 生成代理对象。

自定义注解:

java
@Retention(RetentionPolicy.RUNTIME)  // 保留策略
@Target(ElementType.METHOD)           // 作用目标
public @interface MyAnnotation {
    String value() default "";
    int priority() default 0;
}

元注解(修饰注解的注解):

  • @Retention:保留策略
    • SOURCE:仅源码中存在,编译后丢弃(如 @Override
    • CLASS:编译后存在于 class 文件中,运行时不存在(默认值)
    • RUNTIME:运行时可通过反射读取(如 @Autowired
  • @Target:作用目标(TYPE/FIELD/METHOD/PARAMETER/CONSTRUCTOR 等)
  • @Documented:出现在 Javadoc 中
  • @Inherited:子类可继承父类的注解

注解的解析方式:

java
// 1. 运行时反射解析
Method method = clazz.getMethod("doSomething");
if (method.isAnnotationPresent(MyAnnotation.class)) {
    MyAnnotation ann = method.getAnnotation(MyAnnotation.class);
    String value = ann.value();
}

// 2. 编译时注解处理器(APT)
// 继承 AbstractProcessor,在编译时扫描处理注解
// 如 Lombok、ButterKnife

常见注解:

注解来源说明
@OverrideJava检查方法重写
@DeprecatedJava标记废弃
@SuppressWarningsJava抑制警告
@AutowiredSpring依赖注入
@RequestMappingSpring路由映射
@TransactionalSpring事务管理
@ComponentSpring组件扫描
@DataLombok生成 getter/setter/toString

追问延伸

  • Spring 的 @Autowired 是怎么工作的?(BeanPostProcessor 在 bean 初始化时扫描注解并注入)
  • Lombok 是怎么在编译时生成代码的?(编译时注解处理器 AbstractProcessor)

Q25: throw 和 throws 的区别? 「🟢 校招/初级」

考察点:异常处理关键字的辨析。

参考答案

维度throwthrows
位置方法体内方法签名上
作用抛出一个异常对象声明方法可能抛出的异常类型
后跟异常对象(new XXXException()异常类名(可多个,逗号分隔)
执行立即抛出,后续代码不执行只是声明,不主动抛出
java
// throw:手动抛出异常
public void withdraw(int amount) {
    if (amount > balance) {
        throw new IllegalArgumentException("余额不足");
    }
    balance -= amount;
}

// throws:声明可能抛出的异常
public void readFile(String path) throws IOException, ClassNotFoundException {
    // 方法体内可能抛出 IOException 或 ClassNotFoundException
    // 调用者必须 try-catch 或继续 throws
}

使用场景:

  • throw:业务逻辑中主动抛出异常(参数校验、业务规则)
  • throws:方法可能产生 Checked Exception 时,声明给调用者处理

最佳实践:

  • 业务异常用 throw 抛出自定义异常
  • Checked Exception 用 throws 声明,或内部 try-catch
  • 不要用 throws Exception 笼统声明(无法知道具体异常类型)
  • 异常要有明确的语义(不要用异常控制流程)

追问延伸

  • Checked Exception 和 Unchecked Exception 的区别?(Checked 必须处理,Unchecked 可不处理)
  • 为什么有些人建议只用 RuntimeException?(Checked Exception 污染接口,Lambda 不能抛 Checked Exception)

Q26: Java 8 的 Lambda 表达式和 Stream API 怎么用? 「🟡 中级」

考察点:Java 8 函数式编程。

参考答案

Lambda 表达式:函数式接口(只有一个抽象方法的接口)的简洁实现。

java
// 匿名内部类
Runnable r1 = new Runnable() {
    @Override
    public void run() { System.out.println("hello"); }
};

// Lambda
Runnable r2 = () -> System.out.println("hello");

// 带参数
Comparator<String> c1 = (a, b) -> a.length() - b.length();
// 方法引用
Comparator<String> c2 = Comparator.comparingInt(String::length);

函数式接口:

  • Runnablevoid run()
  • Supplier<T>T get()(生产者,无参有返回)
  • Consumer<T>void accept(T t)(消费者,有参无返回)
  • Function<T, R>R apply(T t)(转换)
  • Predicate<T>boolean test(T t)(判断)

Stream API:

java
List<String> names = Arrays.asList("Alice", "Bob", "Charlie", "David");

// 过滤 + 映射 + 收集
List<String> result = names.stream()
    .filter(n -> n.length() > 3)        // 过滤
    .map(String::toUpperCase)           // 映射
    .sorted()                            // 排序
    .collect(Collectors.toList());      // 收集

// 统计
long count = names.stream().filter(n -> n.startsWith("A")).count();

// 归约
int totalLength = names.stream().mapToInt(String::length).sum();

// 分组
Map<Character, List<String>> grouped = names.stream()
    .collect(Collectors.groupingBy(n -> n.charAt(0)));

// 并行流(多线程处理)
List<String> parallelResult = names.parallelStream()
    .filter(n -> n.length() > 3)
    .collect(Collectors.toList());

Stream 的特点:

  • 不修改源数据(返回新 Stream/集合)
  • 惰性执行(终端操作才触发计算)
  • 可链式调用
  • parallelStream 底层用 ForkJoinPool

追问延伸

  • forEachpeek 的区别?(forEach 是终端操作,peek 是中间操作)
  • 并行 Stream 有什么坑?(线程安全、顺序不保证、不适合 IO 密集型)

Q27: Java 21 新特性有哪些?虚拟线程是什么? 「🟡 中级」

考察点:对 Java 新版本特性的了解。

参考答案

Java 21 (LTS) 重要新特性:

1. 虚拟线程(Virtual Threads,JEP 444)

java
// 创建虚拟线程
Thread.startVirtualThread(() -> {
    System.out.println("虚拟线程: " + Thread.currentThread());
});

// 虚拟线程执行器
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
    executor.submit(() -> { /* IO 操作 */ });
}

虚拟线程 vs 平台线程:

维度平台线程虚拟线程
映射1:1 映射 OS 线程M:N 映射(多个虚拟线程映射少量 OS 线程)
内存~1-2 MB/线程~几 KB/线程
数量几百到几千百万级
阻塞阻塞 OS 线程不阻塞 OS 线程(JVM 管理)
适用CPU 密集型IO 密集型

虚拟线程的原理:

  • 虚拟线程在平台线程上运行,IO 阻塞时自动让出(unmount),IO 完成后自动恢复(mount)
  • 底层使用 ForkJoinPool 作为载体线程池
  • 不需要池化管理(每个任务一个虚拟线程)

2. 模式匹配(Pattern Matching,JEP 441)

java
// instanceof 模式匹配
if (obj instanceof String s) {
    System.out.println(s.length()); // s 已自动转型
}

// switch 模式匹配
switch (obj) {
    case Integer i -> System.out.println("int: " + i);
    case String s -> System.out.println("string: " + s);
    case null -> System.out.println("null");
    default -> System.out.println("unknown");
}

3. 记录类(Record,JEP 395 正式版)

java
public record Point(int x, int y) {}
// 自动生成:构造器、getter、equals、hashCode、toString

4. 密封类(Sealed,JEP 409 正式版)

java
public sealed interface Shape permits Circle, Square, Triangle {}
record Circle(double radius) implements Shape {}
record Square(double side) implements Shape {}

追问延伸

  • 虚拟线程和 Go 的协程有什么区别?(虚拟线程是 JVM 级别的,Go goroutine 是运行时级别的;虚拟线程不抢占,goroutine 抢占式调度)
  • 虚拟线程能完全替代线程池吗?(IO 密集型可以,CPU 密集型仍需平台线程池)

Q28: 多态的体现是什么?动态绑定的原理? 「🟢 校招/初级」

考察点:面向对象多态的深度理解。

参考答案

多态的三个体现:

  1. 方法重写:子类重写父类方法,运行时根据实际类型调用
  2. 接口实现:不同实现类对同一接口方法有不同的实现
  3. 方法参数多态:父类引用指向子类对象
java
Animal a = new Dog();
a.speak();  // 输出 "汪汪"(动态绑定到 Dog 的 speak)

Animal b = new Cat();
b.speak();  // 输出 "喵喵"(动态绑定到 Cat 的 speak)

动态绑定(运行时绑定)原理:

  • 编译时:编译器只知道引用类型是 Animal,检查 Animal 是否有 speak 方法
  • 运行时:JVM 根据对象的实际类型(Dog/Cat)从方法表(vtable)中找到对应的方法

方法表(vtable)机制:

  • 每个类有一个方法表,记录所有虚方法的实际入口
  • 子类的方法表继承父类,重写的方法指向子类实现
  • 调用时通过对象头中的类指针找到方法表,定位到正确的方法
java
// 静态绑定(编译时确定)
class A {
    static void hello() { }  // 静态方法:静态绑定
    void world() { }         // 实例方法:动态绑定
    final void foo() { }     // final 方法:静态绑定(不可重写)
}

多态解决的问题:

  • 代码复用:同一接口,不同实现
  • 扩展性:新增子类不影响已有代码
  • 开闭原则:对扩展开放,对修改关闭

追问延伸

  • final 方法、private 方法、static 方法是动态绑定吗?(都不是,它们是静态绑定)
  • 字段是多态的吗?(不是,字段访问看引用类型,不看实际类型)

Q29: Java 怎么实现网络 IO 高并发编程?NIO 的原理? 「🟡 中级」

考察点:Java NIO 的理解。

参考答案

Java 三种 IO 模型:

  • BIO(同步阻塞):一个连接一个线程,连接不做事也占线程
  • NIO(同步非阻塞):一个线程管理多个连接,基于 Selector 多路复用
  • AIO(异步非阻塞):OS 完成 IO 后回调通知(Windows IOCP,Linux 用 epoll 模拟)

NIO 三大核心组件:

java
// 1. Buffer:数据容器
ByteBuffer buf = ByteBuffer.allocate(1024);
buf.put("hello".getBytes());
buf.flip();  // 切换为读模式
socketChannel.write(buf);

// 2. Channel:双向通道(可读可写)
SocketChannel channel = SocketChannel.open(new InetSocketAddress("host", 8080));
channel.configureBlocking(false);  // 非阻塞模式

// 3. Selector:多路复用器
Selector selector = Selector.open();
channel.register(selector, SelectionKey.OP_READ);

while (true) {
    selector.select();  // 阻塞直到有事件就绪
    Set<SelectionKey> keys = selector.selectedKeys();
    for (SelectionKey key : keys) {
        if (key.isReadable()) { /* 处理读事件 */ }
        if (key.isWritable()) { /* 处理写事件 */ }
    }
}

NIO vs BIO:

维度BIONIO
流向单向(InputStream/OutputStream)双向(Channel)
阻塞阻塞非阻塞(可配置)
数据流(逐字节)Buffer(块)
模型一连接一线程多路复用

NIO 的底层依赖:

  • Linux:epoll(IO 多路复用)
  • macOS:kqueue
  • Windows:IOCP

使用 NIO 的框架:

  • Netty(Java 生态最流行的 NIO 框架)
  • Mina(Apache)
  • Vert.x(基于 Netty 的响应式框架)

追问延伸

  • Netty 相比原生 NIO 有什么优势?(零拷贝、内存池、解决 epoll 空轮询 bug、易用 API)
  • AIO 在 Linux 上为什么没有真正实现?(Linux 的 AIO 不成熟,Netty 曾支持又移除了 AIO)

Q30: Java 进程是怎么跟操作系统交互的? 「🔴 高级」

考察点:Java 与底层操作系统的关系。

参考答案

Java 进程与 OS 交互的方式:

  1. JNI(Java Native Interface)
java
// 声明 native 方法
public class System {
    public static native long currentTimeMillis();
}
// JVM 通过 JNI 调用 C/C++ 本地代码,再调用 OS 系统调用
  1. 系统调用(通过 JNI 间接调用)
  • 文件操作:FileFileInputStream → JNI → read() 系统调用
  • 网络操作:SocketSocketChannel → JNI → socket() / epoll_wait()
  • 线程操作:Thread.start() → JVM → pthread_create()(Linux)
  • 时间:System.currentTimeMillis() → JNI → gettimeofday()
  1. JVM 直接调用 OS API
  • 内存分配:new 对象 → JVM 堆 → mmap() / malloc()
  • GC:System.gc() → JVM → madvise() / mprotect()
  • 锁:synchronized → JVM → futex()(Linux 用户态锁)

Java 线程和 OS 线程的关系:

  • Java 线程在 HotSpot JVM 中 1:1 映射到 OS 内核线程
  • Thread.start() → 调用 pthread_create() → OS 创建线程
  • Java 21 虚拟线程打破了 1:1 映射,变为 M:N

Java 进程的内存布局:

+------------------+
|   JVM 进程        |
|  +------------+  |
|  | 堆          |  | ← Java 对象
|  +------------+  |
|  | 方法区/元空间 |  | ← 类元数据
|  +------------+  |
|  | Java 栈    |  | ← 每个线程一份
|  +------------+  |
|  | 本地方法栈  |  | ← JNI 调用
|  +------------+  |
|  | JVM 内部   |  | ← Code Cache, GC 等
|  +------------+  |
|  | 共享库     |  | ← libc, libpthread
+------------------+

追问延伸

  • 为什么 Java 线程在 Linux 上是用户级线程还是内核级线程?(内核级,1:1 模型,由 OS 调度)
  • StackOverflowError 是 JVM 抛的还是 OS 抛的?(JVM 捕获 OS 的段中断后转换为 StackOverflowError)

Q31: JVM 和 Java 有什么区别?Java 和 Python 有什么区别? 「🟢 校招/初级」

考察点:语言和运行时的概念区分。

参考答案

JVM 和 Java 的区别

维度JavaJVM
本质编程语言运行时虚拟机
产物.java.class 字节码加载并执行 .class 字节码
职责语法规范、API 库内存管理、GC、字节码执行
关系Java 编译后跑在 JVM 上JVM 可以跑任何编译为字节码的语言
  • Java 是语言,JVM 是运行时平台
  • JVM 不只跑 Java:Kotlin、Scala、Groovy、Clojure 都编译为 .class 跑在 JVM 上

Java 和 Python 的区别

维度JavaPython
类型系统静态类型动态类型
执行方式编译为字节码 → JVM 执行解释执行(CPython)
性能快(JIT + 编译优化)慢(解释执行)
语法严格、样板代码多简洁、灵活
应用领域企业级后端、Android数据科学、AI、脚本
并发多线程(1:1 内核线程)GIL 限制多线程
内存管理JVM GC引用计数 + 分代 GC

追问延伸

  • 为什么 Python 有 GIL?(CPython 的引用计数不是线程安全的,GIL 是简化实现)
  • Kotlin 跑在 JVM 上和 Java 有什么区别?(语法更简洁,但最终都编译为 .class

Q32: 有了包装类为什么还要保留 int 基本类型? 「🟡 中级」

考察点:基本类型和包装类共存的设计理解。

参考答案

保留基本类型的原因:

  1. 性能
    • 基本类型直接存值(栈上),无对象开销
    • 包装类是对象(堆上),有对象头(16-24 字节)+ 引用开销
    • int 占 4 字节,Integer 占 16+ 字节
    • 大量计算场景(数组、循环):基本类型快 5-10 倍
  2. 内存
    • int[1000] = 4KB(连续内存)
    • Integer[1000] = 16KB+(对象头 + 引用 + 数据分散在堆上)
    • 大数组和集合场景内存差异巨大
  3. 语义清晰
    • 基本类型有默认值 0,不会 null(避免 NPE)
    • 包装类可以为 null,语义不同("没有值" vs "值为 0")
  4. 历史兼容
    • Java 1.0 只有基本类型,包装类是为了支持集合(Object 泛化)
    • 泛型只能用包装类(List<int> 不合法,List<Integer> 合法)

Java 的"最佳实践":

  • 局部变量、计算 → 用基本类型(intlongdouble
  • 集合元素、数据库映射、API 参数 → 用包装类(IntegerLong
  • 容易 NPE 的场景注意拆箱:Integer a = null; int b = a; → NPE

Project Valhalla(Java 未来):

  • 引入值类型(Value Type),兼具基本类型的性能和对象的语义
  • inline class Point { int x; int y; } → 无对象头、栈上分配

追问延伸

  • Integer 缓存池的范围是多少?(-128 到 127,可通过 -XX:AutoBoxCacheMax 调整)
  • 自动拆箱时如果包装类为 null 会怎样?(抛 NullPointerException)

Q33: 面向对象的设计原则有哪些? 「🟡 中级」

考察点:SOLID 原则和设计思想的了解。

参考答案

SOLID 五大原则

原则全称含义示例
S单一职责(SRP)一个类只做一件事UserService 只管用户,不管发邮件
O开闭原则(OCP)对扩展开放,对修改关闭用策略模式加新算法,不改老代码
L里氏替换(LSP)子类必须能替换父类Rectangle 的子类 Square 不能破坏 setWidth 的语义
I接口隔离(ISP)不要强迫依赖不需要的接口拆分大接口为多个小接口
D依赖倒置(DIP)依赖抽象,不依赖具体面向接口编程,UserService 依赖 UserRepository 接口

其他常用原则:

  • KISS(Keep It Simple, Stupid):保持简单,不过度设计
  • DRY(Don't Repeat Yourself):消除重复,抽取公共逻辑
  • YAGNI(You Aren't Gonna Need It):不要提前设计不需要的功能
  • 合成复用原则:优先用组合而非继承(has-a 优于 is-a
  • 迪米特法则(最少知识原则):一个对象只与直接朋友通信
java
// 开闭原则示例
// 不好的设计:加新形状要改 GraphicEditor
class GraphicEditor {
    void drawShape(Shape s) {
        if (s.type == 1) drawRectangle();
        else if (s.type == 2) drawCircle();
    }
}

// 好的设计:加新形状只需新增类,不改老代码
interface Shape { void draw(); }
class Rectangle implements Shape { void draw() { ... } }
class Circle implements Shape { void draw() { ... } }
class GraphicEditor { void drawShape(Shape s) { s.draw(); } }

追问延伸

  • 里氏替换原则的典型违反场景?(子类改变了父类方法的行为契约)
  • 合成 vs 继承什么时候用哪个?(复用:组合 > 继承;多态:接口 > 继承)

Q34: 抽象类能加 final 吗?抽象类能被实例化吗?接口能有构造函数吗? 「🟡 中级」

考察点:抽象类和接口的语法细节。

参考答案

抽象类能加 final 吗?

  • 不能final 修饰类意味着不能被继承,而抽象类必须被继承才能实现其抽象方法。
  • final abstract class Animal {} → 编译错误:illegal combination of modifiers
  • 语义矛盾:抽象类就是设计来被继承的,final 禁止继承

抽象类能被实例化吗?

  • 不能直接实例化new Animal() → 编译错误
  • 间接可以:通过匿名内部类实现抽象方法后实例化:
    java
    abstract class Animal {
        abstract void speak();
    }
    // 匿名内部类
    Animal a = new Animal() {
        @override
        void speak() { System.out.println("..."); }
    };
    // 这里实际创建的是 Animal 的匿名子类实例,不是 Animal 本身

接口能有构造函数吗?

  • 不能。接口没有构造函数(没有状态需要初始化)
  • 接口的方法默认是 public abstract(Java 8 前只有抽象方法)
  • Java 8+ 接口可以有 default 方法和 static 方法,但仍不能有构造函数
  • 接口可以有常量(public static final),但不允许实例字段
java
interface MyInterface {
    // 编译错误:接口不能有构造函数
    // MyInterface() {}
    
    int CONSTANT = 42;           // public static final
    default void defaultMethod() { System.out.println("default"); }
    static void staticMethod() { System.out.println("static"); }
}

追问延伸

  • 接口可以有 main 方法吗?(可以,static void main 合法)
  • 抽象类和接口在 Java 8 后的界限是不是模糊了?(确实更模糊了,但接口仍不能有状态字段和构造函数)

Q35: 非静态内部类为什么能直接访问外部类的成员?编译器是怎么实现的? 「🔴 高级」

考察点:内部类的底层实现原理。

参考答案

非静态内部类(成员内部类)能访问外部类成员的原因:编译器在内部类中自动添加了一个外部类引用

java
// 源代码
class Outer {
    private int x = 10;
    
    class Inner {
        void show() {
            System.out.println(x);  // 直接访问外部类的 private 变量
        }
    }
}

编译后的实际代码(反编译可见):

java
class Outer {
    private int x = 10;
    
    // 编译器生成的访问器(合成方法)
    static int access$000(Outer outer) {
        return outer.x;  // 绕过 private 限制
    }
}

class Outer$Inner {
    // 编译器自动添加的外部类引用
    final Outer this$0;
    
    Outer$Inner(Outer outer) {
        this.this$0 = outer;
    }
    
    void show() {
        System.out.println(Outer.access$000(this.this$0));  // 通过合成方法访问
    }
}

关键点:

  1. this$0 引用:非静态内部类持有外部类实例的引用 → 可以访问外部类所有成员(包括 private)
  2. 合成方法(Bridge/Synthetic Method):编译器生成 access$xxx 方法来绕过 private 访问限制
  3. 创建内部类必须有外部类实例
    java
    Outer outer = new Outer();
    Outer.Inner inner = outer.new Inner();  // 依赖外部类实例

静态内部类 vs 非静态内部类:

维度非静态内部类静态内部类
持有外部引用是(this$0
访问外部成员可以(含 private)只能访问静态成员
创建方式outer.new Inner()new Outer.Inner()
内存开销额外引用开销
使用场景需要访问外部实例独立逻辑、Builder 模式

追问延伸

  • 匿名内部类也是非静态的吗?(是,编译后也有 this$0 指向外部类实例)
  • 为什么静态内部类不会导致内存泄漏?(不持有外部类引用)

Q36: new 出的对象什么时候被回收?如何获取私有对象的值? 「🟡 中级」

考察点:对象生命周期和反射访问。

参考答案

new 出的对象什么时候回收

对象回收的触发条件:

  1. 没有任何引用指向该对象(不可达)→ GC 可回收
  2. GC 实际运行时才回收(不是立即,有延迟)
  3. finalize() 方法(Java 9 废弃):对象被回收前调用一次(不保证执行、不保证时序)
  4. 四种引用级别影响回收时机:
    • 强引用(Object o = new Object()):永不回收(除非设为 null)
    • 软引用(SoftReference):内存不足时回收
    • 弱引用(WeakReference):下一次 GC 就回收
    • 虚引用(PhantomReference):不影响生命周期,仅跟踪回收事件
java
Object o = new Object();  // 强引用
o = null;  // 对象不可达 → 下次 GC 可回收
// 但具体什么时候 GC 取决于 JVM(可能是 Full GC 时)

如何获取私有对象的值

通过反射可以绕过 private 访问限制:

java
class User {
    private String name = "张三";
    private int age = 25;
}

// 通过反射获取私有字段
User user = new User();
Class<?> clazz = user.getClass();

// 获取 private 字段
Field nameField = clazz.getDeclaredField("name");
nameField.setAccessible(true);  // 突破 private 限制
String name = (String) nameField.get(user);  // "张三"

// 调用 private 方法
Method method = clazz.getDeclaredMethod("privateMethod");
method.setAccessible(true);
method.invoke(user);

setAccessible(true) 的原理:

  • 绕过 Java 语言层面的访问检查
  • 底层通过 unsafe 或 JNI 直接操作内存
  • 需要 ReflectPermission 权限(安全管理器开启时可能被拒)

注意:

  • 反射破坏封装性,生产代码慎用
  • 框架(Spring、MyBatis)大量使用反射来注入依赖
  • Java 9+ 模块系统限制了反射跨模块访问

追问延伸

  • 对象在 finalize() 中"自救"会怎样?(复活一次,但下次 GC 不再调用 finalize())
  • setAccessible(true) 在 Java 9+ 模块系统中还可靠吗?(需要 --add-opens 参数开放模块)

Q37: 抛出异常为什么有些不用 throws 声明? 「🟡 中级」

考察点:Checked 异常和 Unchecked 异常的 throws 规则。

参考答案

Java 异常分两类,throws 声明规则不同:

java
// Checked Exception:必须 throws 或 try-catch
public void readFile() throws IOException {
    throw new IOException("file not found");
}

// Unchecked Exception(RuntimeException):不需要 throws
public void divide(int a, int b) {
    if (b == 0) throw new IllegalArgumentException("除数不能为0");  // 不用 throws
}

不需要 throws 的异常类型:

类型示例需要 throws
ErrorOutOfMemoryErrorStackOverflowError不需要
RuntimeException 及子类NullPointerExceptionIllegalArgumentExceptionArithmeticException不需要
其他 Exception 子类IOExceptionSQLException需要

设计原因:

  • Checked Exception:编译器强制处理 → 程序员不能忽略(如 IO 异常、SQL 异常)
  • Unchecked Exception:编程错误 → 处理了也没意义(如 NPE、数组越界),应该修复代码

RuntimeException 虽然不强制 throws,但也可以声明(作为文档提示调用方可能发生):

java
// 合法但不强制
public String process(String input) throws IllegalArgumentException {
    ...
}

最佳实践:

  • 业务异常通常继承 RuntimeException(不强制调用方处理,减少样板代码)
  • Spring 的 DataAccessException 就是 RuntimeException(把 Checked 的 SQLException 包装为 Unchecked)
  • 只有需要调用方"必须处理"的异常才用 Checked(如自定义业务校验异常)

追问延伸

  • 为什么很多人讨厌 Checked Exception?(样板代码多、接口污染、Lambda 不支持 Checked)
  • Error 需要 throws 吗?(不需要,Error 不受编译器检查)

Q38: String 有哪些常用方法? 「🟢 校招/初级」

考察点:Java 基础 API 的熟悉度。

参考答案

String 常用方法分类:

1. 查询与判断

方法说明
length()字符串长度
isEmpty()是否为空(""
isBlank()是否空白(全空格,Java 11+)
charAt(int i)取指定位置的字符
indexOf(String s)查找子串位置(返回 -1 表示没找到)
lastIndexOf(String s)从后往前查找
contains(CharSequence s)是否包含子串
startsWith(String s)是否以指定前缀开头
endsWith(String s)是否以指定后缀结尾
equals(Object o)比较内容
equalsIgnoreCase(String s)忽略大小写比较
compareTo(String s)字典序比较

2. 转换与操作

方法说明
substring(int begin, int end)截取子串
trim()去首尾空格
strip()去首尾 Unicode 空白(Java 11+)
toLowerCase() / toUpperCase()大小写转换
replace(old, new)替换所有匹配
replaceAll(regex, new)正则替换
split(String regex)按正则分割为数组
toCharArray()转为 char 数组
concat(String s)拼接
repeat(int n)重复 n 次(Java 11+)

3. 静态方法

方法说明
String.valueOf(x)将基本类型/对象转为字符串
String.format(fmt, args)格式化字符串
String.join(delim, list)用分隔符拼接列表
String.copyValueOf(char[])char 数组转字符串

4. Java 8+ 方法

java
// chars() - 返回 IntStream
"abc123".chars().filter(c -> c >= '0' && c <= '9').count();

// lines() - 按行分割
"a\nb\nc".lines().forEach(System.out::println);

// indent() - 添加缩进
"text".indent(4);  // "    text"

// transform() - 函数式转换
"123".transform(Integer::parseInt);  // 123

追问延伸

  • String.format()+ 拼接性能比较?
  • Java 15+ 的 formatted() 方法怎么用?("Hello %s".formatted("World")

Q39: 设计模式:单例模式怎么实现?volatile 在单例中有什么作用? 「🔴 高级」

考察点:单例模式的实现方式和并发安全。

参考答案

单例模式五种实现方式:

1. 饿汉式(线程安全)

java
public class Singleton {
    private static final Singleton INSTANCE = new Singleton();
    private Singleton() {}
    public static Singleton getInstance() { return INSTANCE; }
}
  • 类加载时创建,天然线程安全
  • 缺点:无法懒加载、序列化/反射可破坏

2. 懒汉式(需同步)

java
public class Singleton {
    private static Singleton instance;
    private Singleton() {}
    public static synchronized Singleton getInstance() {
        if (instance == null) instance = new Singleton();
        return instance;
    }
}
  • 缺点:每次获取都加锁 → 性能差

3. 双重检查锁(DCL)

java
public class Singleton {
    // volatile 关键!防止指令重排序
    private static volatile Singleton instance;
    private Singleton() {}
    public static Singleton getInstance() {
        if (instance == null) {                  // 第一次检查(无锁)
            synchronized (Singleton.class) {
                if (instance == null) {          // 第二次检查
                    instance = new Singleton();  // new 操作不是原子的
                }
            }
        }
        return instance;
    }
}

volatile 在 DCL 中的作用new Singleton() 分三步:

  1. 分配内存
  2. 初始化对象
  3. 将引用指向内存

如果没有 volatile,编译器/CPU 可能重排序为 1→3→2:

  • 线程 A 执行到 3(引用已指向,但对象未初始化)
  • 线程 B 在第一次检查时发现 instance != null → 直接返回 → 拿到未初始化的对象 → NPE
  • volatile 禁止重排序,保证 2 在 3 之前完成

4. 静态内部类(推荐)

java
public class Singleton {
    private Singleton() {}
    private static class Holder {
        static final Singleton INSTANCE = new Singleton();
    }
    public static Singleton getInstance() { return Holder.INSTANCE; }
}
  • 利用类加载机制保证线程安全 + 懒加载
  • 推荐:无锁、安全、简洁

5. 枚举(最安全)

java
public enum Singleton {
    INSTANCE;
    public void doSomething() { ... }
}
  • 天然防止反射攻击和序列化破坏
  • Effective Java 推荐

破坏单例的方式及防御:

  • 反射:Constructor.setAccessible(true) → 枚举天然防御
  • 序列化/反序列化:readResolve() 方法返回同一实例
  • 多线程:volatile + 同步

追问延伸

  • 为什么枚举单例最安全?(枚举天然单例,JVM 保证,反射无法创建枚举)
  • DCL 为什么需要两次 if?(第一次减少锁竞争,第二次防止重复创建)

Q40: 代理模式和适配器模式有什么区别? 「🟡 中级」

考察点:设计模式的区分理解。

参考答案

维度代理模式适配器模式
意图控制访问(增强/拦截)接口转换(让不兼容的接口能一起用)
关系代理和被代理实现同一接口适配器实现目标接口,持有被适配者
场景AOP、远程代理、缓存代理旧接口适配新接口、第三方库适配
对原对象增强但不改变接口接口不同需要转换

代理模式示例:

java
interface Service { void execute(); }
class RealService implements Service {
    public void execute() { System.out.println("执行"); }
}
// 代理:增强但不改变接口
class ServiceProxy implements Service {
    private RealService real;
    public void execute() {
        System.out.println("前置日志");   // 增强
        real.execute();
        System.out.println("后置日志");   // 增强
    }
}
// Spring AOP 就是动态代理

适配器模式示例:

java
// 旧接口(被适配者)
class OldPrinter { void printOld(String text) { ... } }
// 新接口(目标)
interface NewPrinter { void print(String text); }
// 适配器:转换接口
class PrinterAdapter implements NewPrinter {
    private OldPrinter old;
    public void print(String text) { old.printOld(text); }  // 接口转换
}

相关模式对比:

  • 装饰器模式:增强功能,不改接口(和代理很像,但装饰器强调"递归叠加")
  • 外观模式:简化复杂子系统的访问(一个接口对多个子系统)

追问延伸

  • JDK 动态代理和 CGLIB 代理的区别?(JDK 需要接口,CGLIB 继承类生成子类)
  • Spring AOP 用的是代理还是适配器?(代理,JDK 或 CGLIB 动态代理)

Q41: 策略模式和责任链模式分别用在什么场景? 「🟡 中级」

考察点:设计模式的实际应用。

参考答案

策略模式:定义一系列算法,封装为独立类,可互换使用。

java
// 策略接口
interface PaymentStrategy { void pay(double amount); }
// 具体策略
class Alipay implements PaymentStrategy { public void pay(double amount) { ... } }
class WechatPay implements PaymentStrategy { public void pay(double amount) { ... } }
// 上下文
class PaymentContext {
    private PaymentStrategy strategy;
    public void setStrategy(PaymentStrategy s) { this.strategy = s; }
    public void pay(double amount) { strategy.pay(amount); }
}

应用场景:

  • 支付方式选择(支付宝/微信/银行卡)
  • 排序算法选择(快排/归并/插入)
  • 促销策略(满减/折扣/返券)
  • Spring 的 ResourceClassPathResourceFileSystemResource

责任链模式:请求沿链条传递,每个节点决定处理或转发。

java
abstract class Handler {
    protected Handler next;
    public Handler setNext(Handler next) { this.next = next; return next; }
    abstract void handle(Request req);
}
class AuthHandler extends Handler {
    void handle(Request req) {
        if (!authed) { reject(); return; }
        if (next != null) next.handle(req);
    }
}
class RateLimitHandler extends Handler { ... }
class BusinessHandler extends Handler { ... }

// 组装链
Handler chain = new AuthHandler();
chain.setNext(new RateLimitHandler()).setNext(new BusinessHandler());
chain.handle(req);

应用场景:

  • 过滤器链(Spring Security FilterChain、Servlet Filter
  • 日志级别链(DEBUG → INFO → ERROR)
  • OA 审批流(组长 → 经理 → CEO)
  • Spring Interceptor

策略模式 vs 责任链模式:

维度策略模式责任链模式
选择方式外部主动选择策略请求自动沿链传递
处理者一个策略处理链上一个或多个处理
关注点算法可互换处理流程编排
场景同一问题多种解法一个请求多步处理

追问延伸

  • Spring 的 HandlerInterceptor 是责任链吗?(是,InterceptorRegistry 组装链)
  • 策略模式怎么消除 if-else?(用 Map 存策略 + 工厂模式按 key 取策略)

Q42: 对象排序怎么做?Comparator 和 Comparable 的区别? 「🟡 中级」

考察点:Java 排序机制的实践。

参考答案

Comparable(内部比较器):类实现接口,定义自然排序。

java
class Student implements Comparable<Student> {
    int score;
    int id;
    public int compareTo(Student other) {
        return Integer.compare(this.score, other.score);  // 按分数排
    }
}
// 使用
Collections.sort(list);  // 按 Comparable 的自然顺序排序

Comparator(外部比较器):独立定义比较规则,不修改类本身。

java
// 先按分数排,分数相同按学号排
Comparator<Student> comparator = Comparator
    .comparingInt(Student::getScore)
    .thenComparingInt(Student::getId);
Collections.sort(list, comparator);  // 按 Comparator 排序

// 或用 list.sort
list.sort(Comparator.comparing(Student::getScore).reversed());  // 分数倒序

对比:

维度ComparableComparator
位置类内部实现类外部定义
方法compareTo(T o)compare(T o1, T o2)
排序方式自然顺序(唯一)可定义多种排序
灵活性低(需改类源码)高(运行时切换)
使用Collections.sort(list)Collections.sort(list, comparator)

Java 排序算法:

  • Collections.sort() / Arrays.sort()
    • 对象排序:TimSort(归并排序 + 插入排序的混合,O(N logN))
    • 基本类型排序:Dual-Pivot QuickSort(快速排序变种)

追问延伸

  • compareTo 返回值的含义?(负数:this < other;0:相等;正数:this > other)
  • ComparatorthenComparing 怎么实现多级排序?(链式调用,前一级相等才比下一级)