Appearance
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++ 对比
| 特性 | Java | C++ |
|---|---|---|
| 指针 | 无指针,引用更安全 | 支持指针,灵活但易出错 |
| 内存管理 | 自动 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 注解:可选但推荐,编译器会检查是否真的重写了父类方法,防止手误。
不能重写的情况
- final 方法:final 修饰的方法不能被重写
- static 方法:静态方法不能被重写(父子类同名静态方法是"隐藏"而非重写)
- private 方法:private 方法对子类不可见,不能重写(子类定义同名方法算新方法)
- 构造方法:构造方法不能被继承,自然不能重写
代码示例 —— 静态方法不是重写:
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 class | interface |
| 继承/实现 | 子类用 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 关系,且需要共享代码(属性或方法实现)时。
- 例:
Animal→Cat/Dog,它们有共同的属性(name、age)和共同的方法实现(sleep())。
- 例:
- 用接口:当多个类之间是 has-a 能力 关系,只定义行为契约,不关心具体实现时。
- 例:
Flyable、Runnable、Comparable,表征一种能力/契约。
- 例:
- 组合使用:抽象类定义骨架,接口定义能力。如
AbstractList实现了List接口的骨架方法。
接口冲突问题
- 一个类实现两个接口,两个接口有同名默认方法时,必须在类中重写该方法(否则编译错误)。
- 可以通过
InterfaceName.super.method()指定调用哪个接口的默认实现。
追问延伸:
- 为什么 Java 8 要给接口加默认方法?(接口演进、Lambda 表达式配合函数式接口)
- 接口和抽象类都有抽象方法,那为什么还要两个东西?一个不够吗?(单继承限制、设计语义不同)
- 接口中的变量为什么必须是 public static final 的?(接口是契约,不存储状态;final 保证不可变)
- 函数式接口是什么?和普通接口有什么区别?(只有一个抽象方法,可以用 Lambda 表达式)
Q4: final、finally、finalize 的区别? 「🟢 校招/初级」
考察点:Java 关键字/方法的基础辨析,筛掉基础概念混淆的候选人。
参考答案:
三者只是长得像,功能完全不同:
final —— 修饰符
final 是关键字,可修饰类、方法、变量,表示"不可变"。
| 修饰对象 | 含义 |
|---|---|
| final 类 | 不能被继承。如 String、Math、System 都是 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,无需 finallyfinalize —— Object 的方法
finalize() 是 Object 类的一个方法,在对象被 GC 回收前由垃圾回收器调用一次。
- 设计初衷:给对象一个"临终遗言"的机会,清理非 Java 资源(如 native 资源)。
- 问题多多:
- 不确定性:无法保证何时调用,甚至可能不调用(JVM 没 GC 就退出了)。
- 性能差:finalizer 线程优先级低,可能导致大量对象堆积。
- 复活对象:finalize 中可以把自己重新赋值给某个引用,让对象"复活"。
- 内存泄漏风险:实现了 finalize 的对象至少经历两次 GC 才能被回收。
- 废弃状态:
- JDK 9:标记为
@Deprecated(since="9") - JDK 18:标记为
@Deprecated(since="9", forRemoval=true),将在未来版本移除
- JDK 9:标记为
- 替代方案:
try-with-resources/AutoCloseableCleaner类(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 规范):
- 自反性:
x.equals(x)必须返回 true - 对称性:
x.equals(y)为 true,则y.equals(x)也必须为 true - 传递性:
x.equals(y)为 true,y.equals(z)为 true,则x.equals(z)也必须为 true - 一致性:多次调用结果应一致
- 非空性:
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 的关系:
- 如果两个对象 equals 相等,那么它们的 hashCode 必须相等。
- 如果两个对象 hashCode 相等,它们的 equals 不一定相等(哈希碰撞)。
- 因此,重写 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 的区别? 「🟢 校招/初级」
考察点:字符串处理的核心知识,包括不可变设计、线程安全、性能对比、底层实现。筛掉只会用 + 拼接字符串的人。
参考答案:
三者对比
| 特性 | String | StringBuffer | StringBuilder |
|---|---|---|---|
| 可变性 | 不可变(final char[]/byte[]) | 可变 | 可变 |
| 线程安全 | 线程安全(不可变对象天然安全) | 线程安全(方法加 synchronized) | 非线程安全 |
| 性能 | 每次修改创建新对象,性能差 | 中等(有同步开销) | 最高(无同步开销) |
| 适用场景 | 字符串常量、少量修改 | 多线程环境下大量字符串操作 | 单线程环境下大量字符串操作 |
| 出现版本 | JDK 1.0 | JDK 1.0 | JDK 1.5 |
String 的不可变性
- 为什么不可变:
- JDK 9 之前:
private final char value[] - JDK 9 及之后:
private final byte[] value+private final byte coder(Latin-1 用 1 字节,UTF-16 用 2 字节,节省内存)
- value 数组是 final 的,引用不可变;且 String 类内部没有提供修改 value 的方法。
- JDK 9 之前:
- 不可变的好处:
- 安全:作为参数传递时不用担心被修改;作为 HashMap 的 key 更安全。
- 缓存 hash 值:String 的 hashCode 会被缓存(
private int hash;),因为不可变,只需计算一次。 - 字符串常量池:不可变是常量池的基础,可以复用字符串,节省内存。
- 线程安全:不可变对象天然线程安全,多线程下无需同步。
- 不可变的代价:每次修改(如
+、concat、substring)都会创建新对象,频繁修改性能差。
StringBuffer 和 StringBuilder
- 相同点:
- 都是可变的字符序列,底层是 char 数组(没有 final),可以扩容。
- 都继承自
AbstractStringBuilder。 - 常用方法相同:
append()、insert()、delete()、reverse()等。
- 不同点:
- StringBuffer 方法都加了
synchronized关键字,线程安全但性能低。 - StringBuilder 没有同步,非线程安全但性能高。
- StringBuffer 方法都加了
字符串拼接 + 原理
- 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 的区别
| 维度 | Error | Exception |
|---|---|---|
| 类型 | 系统级错误,JVM 层面 | 应用程序级异常 |
| 可恢复性 | 通常不可恢复,程序应终止 | 可以捕获并恢复 |
| 处理方式 | 一般不捕获,不处理 | 应该捕获并处理 |
| 举例 | OOM、StackOverflowError、LinkageError | IOException、NullPointerException |
Checked Exception vs Unchecked Exception
| 维度 | Checked Exception(受检异常) | Unchecked Exception(非受检异常) |
|---|---|---|
| 继承关系 | Exception 的子类(除 RuntimeException) | RuntimeException 及其子类 |
| 编译检查 | 编译期检查,必须处理(try-catch 或 throws) | 编译期不检查,不要求处理 |
| 代表含义 | 可预期、可恢复的异常情况 | 编程错误(bug),逻辑问题 |
| 处理方式 | 必须显式捕获或声明抛出 | 可以不处理,也可以捕获 |
| 举例 | IOException、SQLException、InterruptedException | NullPointerException、ArrayIndexOutOfBoundsException、IllegalArgumentException |
异常处理关键字
- try:监控可能发生异常的代码块
- catch:捕获异常并处理
- finally:必定执行的代码块(用于资源释放)
- throw:手动抛出异常对象
- throws:声明方法可能抛出的异常,交给调用方处理
最佳实践
- 不要捕获 Throwable/Error:Error 是系统级错误,程序无法处理,捕获也没意义。
- 具体异常优先:捕获最具体的异常,而不是宽泛的 Exception。
- 不要空吞异常:
catch (Exception e) {}是大忌,至少要打日志。 - 不要用异常做流程控制:异常是为了处理异常情况,用在正常流程中性能差且不清晰。
- finally 中不要 return:会覆盖 try 中的 return,也会丢失异常信息。
- 尽量使用 try-with-resources:JDK 7+,自动关闭资源,比 finally 更优雅安全。
- 异常信息要完整:捕获异常后打印堆栈或记录完整上下文,便于排查问题。
- 自定义异常时选择合适的父类:业务异常一般继承 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 种基本数据类型,每种都对应一个包装类:
| 基本类型 | 大小 | 默认值 | 包装类 | 缓存范围 |
|---|---|---|---|---|
| byte | 1字节 | 0 | Byte | -128 ~ 127 |
| short | 2字节 | 0 | Short | -128 ~ 127 |
| int | 4字节 | 0 | Integer | -128 ~ 127(可配置上限) |
| long | 8字节 | 0L | Long | -128 ~ 127 |
| float | 4字节 | 0.0f | Float | 无缓存 |
| double | 8字节 | 0.0d | Double | 无缓存 |
| char | 2字节 | '\u0000' | Character | 0 ~ 127 |
| boolean | 1位 | false | Boolean | true/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();
- 底层调用
触发场景:
- 赋值时(基本类型 ↔ 包装类)
- 方法调用时(参数是包装类传基本类型,或反之)
- 运算时(包装类参与算术运算会自动拆箱)
整数缓存机制
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()方法。
其他坑
- 空指针异常:包装类为 null 时自动拆箱会抛 NPE。
java
Integer i = null;
int j = i; // 抛 NullPointerException!- 三目运算符中的拆箱:
java
Integer a = null;
Integer b = 1;
Integer c = true ? a : b; // 抛 NPE!因为三目运算符会统一类型,a 拆箱为 int集合中只能用包装类:泛型不支持基本类型,必须用包装类。
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");反射的应用场景
- 框架设计:Spring IOC(依赖注入)、MyBatis ORM 映射、各种注解处理
- 动态代理:JDK 动态代理基于反射
- JDBC 加载驱动:
Class.forName("com.mysql.cj.jdbc.Driver") - 配置与解耦:通过配置文件加载类,不用硬编码
- 单元测试:JUnit、Mockito 等测试框架
反射的缺点
- 性能差:比直接调用慢很多,因为要做运行时类型解析、安全检查等
- 打破封装:可以访问 private 字段和方法,破坏面向对象封装性
- 维护成本高:反射代码难以 IDE 重构、难以追踪调用链
- 安全限制: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() { ... }注解的处理方式
编译时注解处理(APT):
- 注解处理器(Annotation Processor)在编译期扫描注解,生成代码或做校验。
- 如 Lombok(@Data 自动生成 getter/setter)、ButterKnife、Dagger 等。
- 保留策略是 SOURCE 或 CLASS。
运行时注解处理:
- 通过反射读取注解,根据注解执行特定逻辑。
- 如 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 的泛型是伪泛型,泛型信息只存在于编译期,编译后会被擦除,字节码中没有泛型类型参数。
擦除规则
- 无界泛型(
T):擦除为Object - 有界泛型(
T extends Number):擦除为边界类型Number - 通配符(
?):擦除为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));
}
}泛型的限制
- 不能用基本类型:
List<int>不行,必须List<Integer>(因为擦除后是 Object,基本类型不是对象) - 不能实例化泛型类型:
new T()不行(擦除后不知道类型) - 不能创建泛型数组:
new T[]不行(类型不安全) - 不能用泛型做类型判断:
obj instanceof T不行(运行时没有 T) - 静态方法不能使用类的泛型参数:静态方法在类加载时就存在,此时泛型还未确定
- 异常不能泛型:不能
catch (T e),但可以throws T
追问延伸:
- Java 的泛型为什么要用类型擦除?有什么优缺点?
- 什么是桥接方法?为什么需要桥接方法?
- 运行时能获取泛型的具体类型吗?怎么获取?(通过反射获取父类/接口的泛型参数)
List<Object>和List<?>有什么区别?(List<Object>可以 add 任意对象,List<?>除了 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 版本 | 特点 |
|---|---|---|---|---|
| BIO | Blocking I/O | 同步阻塞 | JDK 1.0 | 一连接一线程,简单但并发能力低 |
| NIO | Non-blocking I/O / New I/O | 同步非阻塞 | JDK 1.4 | 多路复用,单线程管理多连接,高并发 |
| AIO | Asynchronous I/O | 异步非阻塞 | JDK 1.7 | 真正异步,操作系统完成后回调,应用少 |
BIO(Blocking I/O)
- 模式:同步阻塞。
- 特点:每个连接需要一个独立线程处理,线程数 = 连接数。
- 工作流程:
- 服务端
ServerSocket.accept()阻塞等待连接。 - 客户端连接后,创建新线程处理读写。
- 读/写操作也是阻塞的(
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。
- 三大核心组件:Channel、Buffer、Selector。
三大组件
1. Channel(通道)
- 类似 BIO 的 Stream,但 Channel 是双向的(可读可写)。
- 主要实现:
SocketChannel、ServerSocketChannel、FileChannel、DatagramChannel。
2. Buffer(缓冲区)
- NIO 的数据读写都通过 Buffer。
- 核心属性:
capacity(容量)、position(当前位置)、limit(上界)、mark(标记)。 - 核心方法:
flip()(切换为读模式)、clear()(重置为写模式)、rewind()(重新读)。 - 常用实现:
ByteBuffer、CharBuffer、IntBuffer等。
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,完成后回调completed或failed方法。
- 现状:实际应用不如 NIO 广泛,Linux 上 AIO 支持不够成熟,Netty 也没有使用 AIO。
BIO vs NIO vs AIO 对比
| 维度 | BIO | NIO | AIO |
|---|---|---|---|
| 阻塞模式 | 同步阻塞 | 同步非阻塞(多路复用) | 异步非阻塞 |
| 线程模型 | 一连接一线程 | 单线程/少量线程管理多连接 | 线程池 + 回调 |
| 编程难度 | 简单 | 较复杂 | 复杂 |
| 并发能力 | 低 | 高 | 高 |
| 数据处理 | 面向流(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 操作分类
| 类型 | 方法 | 说明 |
|---|---|---|
| 中间操作 | filter、map、flatMap、sorted、distinct、limit、skip、peek | 返回新的 Stream,可链式调用,惰性求值 |
| 终止操作 | forEach、collect、count、reduce、max、min、findFirst、anyMatch、allMatch、toArray | 产生最终结果,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():创建 OptionalisPresent()/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
- 旧问题:
Date、Calendar可变、线程不安全、API 难用。 - 新 API(
java.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。 - 生成方式:
- 默认生成:不手动指定时,JVM 根据类名、接口、成员等自动计算(类似 hash 值)。
- 手动指定:
private static final long serialVersionUID = 1L;
为什么建议手动指定 serialVersionUID?
- 默认生成的 serialVersionUID 对类细节高度敏感,哪怕只是加了个空格、改了个方法名,serialVersionUID 都会变,导致反序列化失败。
- 手动指定可以在类升级时保持兼容(只要你保证兼容性)。
- IDE 可以自动生成。
transient 关键字
transient修饰的字段,序列化时会被忽略,反序列化后为默认值(null、0、false 等)。- 用途:
- 敏感信息(密码等)不希望被序列化传输。
- 不需要持久化的字段(如缓存、计算字段)。
- 不支持序列化的字段(如 Thread、Socket 等)。
java
private transient String password; // 密码不序列化序列化的注意事项
- 子类序列化:父类实现 Serializable,子类自动可序列化;子类实现 Serializable,父类需要无参构造(否则反序列化报错)。
- 静态变量:静态变量属于类,不属于对象,不会被序列化。
- 引用类型成员:成员变量也必须是可序列化的,否则抛
NotSerializableException。 - 单例模式:反序列化会创建新对象,破坏单例。解决方案:实现
readResolve()方法返回单例对象。 - 安全问题:Java 原生序列化存在安全风险(反序列化漏洞),应谨慎使用。
- 性能:Java 原生序列化性能差、字节体积大,一般不用于高性能场景。
常见序列化方式对比
| 序列化方式 | 特点 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|---|
| Java 原生序列化 | 实现 Serializable 接口 | Java 原生支持,无需第三方依赖 | 性能差、体积大、有安全风险、跨语言不行 | Java 内部系统、简单场景 |
| JSON(Jackson/Gson) | 文本格式,可读性好 | 可读性好、跨语言、使用广泛 | 体积较大、性能一般 | HTTP 接口、配置文件、调试 |
| XML | 文本格式,结构清晰 | 可读性好、Schema 验证 | 体积大、性能差 | 遗留系统、配置文件 |
| Protobuf(Protocol Buffers) | Google 的二进制格式 | 性能极高、体积小、跨语言、向后兼容 | 需要定义 .proto 文件、可读性差 | 高性能 RPC、微服务 |
| Kryo | Java 序列化框架 | 性能高、体积小、使用简单 | 跨语言不行、兼容性一般 | 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)
- 定义在方法或代码块内部。
- 作用域仅限于方法/代码块内。
- 可以访问外部类所有成员。
- 访问局部变量时,局部变量必须是 final 或 effectively 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 |
| this | this 指向匿名内部类对象 | this 指向外部类对象(没有自己的 this) |
| 适用接口 | 任意接口(只要有抽象方法) | 只能是函数式接口(一个抽象方法) |
| 性能 | 类加载开销大 | 首次调用慢一点,后续快,内存占用少 |
| 变量访问 | 可以访问外部类成员,局部变量需 final | 同左 |
追问延伸:
- 内部类的字节码文件名是什么样的?(Outer$Inner.class,匿名内部类是 Outer$1.class)
- 静态内部类和非静态内部类的区别?(持有外部类引用、访问权限、创建方式)
- 为什么局部内部类访问局部变量需要 final?JDK 8 的 effectively final 是什么?
- 匿名内部类和 Lambda 表达式有什么区别?
- 你在项目中用过哪些内部类?在什么场景下用的?
- 内部类可以继承吗?可以被继承吗?
- 成员内部类为什么不能有静态成员?(因为成员内部类依赖外部类实例,静态成员不依赖实例,语义矛盾)
Q15: Java 的优势和劣势?编译型语言和解释型语言的区别? 「🟢 校招/初级」
考察点:对 Java 语言定位的理解。
参考答案:
Java 既是编译型也是解释型语言:
- 编译:
.java→javac→.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 种基本数据类型:
| 类型 | 关键字 | 位数 | 字节 | 默认值 | 范围 |
|---|---|---|---|---|---|
| 字节型 | byte | 8 | 1 | 0 | -128 ~ 127 |
| 短整型 | short | 16 | 2 | 0 | -32768 ~ 32767 |
| 整型 | int | 32 | 4 | 0 | -2^31 ~ 2^31-1 |
| 长整型 | long | 64 | 8 | 0L | -2^63 ~ 2^63-1 |
| 单精度 | float | 32 | 4 | 0.0f | IEEE 754 |
| 双精度 | double | 64 | 8 | 0.0d | IEEE 754 |
| 字符型 | char | 16 | 2 | '\u0000' | 0 ~ 65535 |
| 布尔型 | boolean | - | - | false | true/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结果是int:10 / 3 = 3(不是 3.33)float到int的转换会截断小数部分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; // trueBigDecimal 运算注意:
- 除法要指定精度:
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 加载顺序:
- 父类静态代码块/静态变量(按代码顺序)
- 子类静态代码块/静态变量
- 父类实例代码块/实例变量
- 父类构造函数
- 子类实例代码块/实例变量
- 子类构造函数
追问延伸:
- 静态代码块和构造函数的区别?(静态代码块类加载时执行一次;构造函数每次创建对象都执行)
- 静态内部类和成员内部类的区别?(静态内部类不持有外部类引用)
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;
}
}深拷贝的三种实现方式:
- 实现 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;
}
}- 序列化/反序列化(最通用):
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();
}- 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常见注解:
| 注解 | 来源 | 说明 |
|---|---|---|
@Override | Java | 检查方法重写 |
@Deprecated | Java | 标记废弃 |
@SuppressWarnings | Java | 抑制警告 |
@Autowired | Spring | 依赖注入 |
@RequestMapping | Spring | 路由映射 |
@Transactional | Spring | 事务管理 |
@Component | Spring | 组件扫描 |
@Data | Lombok | 生成 getter/setter/toString |
追问延伸:
- Spring 的
@Autowired是怎么工作的?(BeanPostProcessor 在 bean 初始化时扫描注解并注入) - Lombok 是怎么在编译时生成代码的?(编译时注解处理器 AbstractProcessor)
Q25: throw 和 throws 的区别? 「🟢 校招/初级」
考察点:异常处理关键字的辨析。
参考答案:
| 维度 | throw | throws |
|---|---|---|
| 位置 | 方法体内 | 方法签名上 |
| 作用 | 抛出一个异常对象 | 声明方法可能抛出的异常类型 |
| 后跟 | 异常对象(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);函数式接口:
Runnable:void 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
追问延伸:
forEach和peek的区别?(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、toString4. 密封类(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: 多态的体现是什么?动态绑定的原理? 「🟢 校招/初级」
考察点:面向对象多态的深度理解。
参考答案:
多态的三个体现:
- 方法重写:子类重写父类方法,运行时根据实际类型调用
- 接口实现:不同实现类对同一接口方法有不同的实现
- 方法参数多态:父类引用指向子类对象
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:
| 维度 | BIO | NIO |
|---|---|---|
| 流向 | 单向(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 交互的方式:
- JNI(Java Native Interface):
java
// 声明 native 方法
public class System {
public static native long currentTimeMillis();
}
// JVM 通过 JNI 调用 C/C++ 本地代码,再调用 OS 系统调用- 系统调用(通过 JNI 间接调用):
- 文件操作:
File→FileInputStream→ JNI →read()系统调用 - 网络操作:
Socket→SocketChannel→ JNI →socket()/epoll_wait() - 线程操作:
Thread.start()→ JVM →pthread_create()(Linux) - 时间:
System.currentTimeMillis()→ JNI →gettimeofday()
- 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 的区别:
| 维度 | Java | JVM |
|---|---|---|
| 本质 | 编程语言 | 运行时虚拟机 |
| 产物 | .java → .class 字节码 | 加载并执行 .class 字节码 |
| 职责 | 语法规范、API 库 | 内存管理、GC、字节码执行 |
| 关系 | Java 编译后跑在 JVM 上 | JVM 可以跑任何编译为字节码的语言 |
- Java 是语言,JVM 是运行时平台
- JVM 不只跑 Java:Kotlin、Scala、Groovy、Clojure 都编译为
.class跑在 JVM 上
Java 和 Python 的区别:
| 维度 | Java | Python |
|---|---|---|
| 类型系统 | 静态类型 | 动态类型 |
| 执行方式 | 编译为字节码 → JVM 执行 | 解释执行(CPython) |
| 性能 | 快(JIT + 编译优化) | 慢(解释执行) |
| 语法 | 严格、样板代码多 | 简洁、灵活 |
| 应用领域 | 企业级后端、Android | 数据科学、AI、脚本 |
| 并发 | 多线程(1:1 内核线程) | GIL 限制多线程 |
| 内存管理 | JVM GC | 引用计数 + 分代 GC |
追问延伸:
- 为什么 Python 有 GIL?(CPython 的引用计数不是线程安全的,GIL 是简化实现)
- Kotlin 跑在 JVM 上和 Java 有什么区别?(语法更简洁,但最终都编译为
.class)
Q32: 有了包装类为什么还要保留 int 基本类型? 「🟡 中级」
考察点:基本类型和包装类共存的设计理解。
参考答案:
保留基本类型的原因:
- 性能:
- 基本类型直接存值(栈上),无对象开销
- 包装类是对象(堆上),有对象头(16-24 字节)+ 引用开销
int占 4 字节,Integer占 16+ 字节- 大量计算场景(数组、循环):基本类型快 5-10 倍
- 内存:
int[1000]= 4KB(连续内存)Integer[1000]= 16KB+(对象头 + 引用 + 数据分散在堆上)- 大数组和集合场景内存差异巨大
- 语义清晰:
- 基本类型有默认值 0,不会 null(避免 NPE)
- 包装类可以为 null,语义不同("没有值" vs "值为 0")
- 历史兼容:
- Java 1.0 只有基本类型,包装类是为了支持集合(
Object泛化) - 泛型只能用包装类(
List<int>不合法,List<Integer>合法)
- Java 1.0 只有基本类型,包装类是为了支持集合(
Java 的"最佳实践":
- 局部变量、计算 → 用基本类型(
int、long、double) - 集合元素、数据库映射、API 参数 → 用包装类(
Integer、Long) - 容易 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)); // 通过合成方法访问
}
}关键点:
this$0引用:非静态内部类持有外部类实例的引用 → 可以访问外部类所有成员(包括 private)- 合成方法(Bridge/Synthetic Method):编译器生成
access$xxx方法来绕过private访问限制 - 创建内部类必须有外部类实例: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 出的对象什么时候回收:
对象回收的触发条件:
- 没有任何引用指向该对象(不可达)→ GC 可回收
- GC 实际运行时才回收(不是立即,有延迟)
- finalize() 方法(Java 9 废弃):对象被回收前调用一次(不保证执行、不保证时序)
- 四种引用级别影响回收时机:
- 强引用(
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 |
|---|---|---|
| Error | OutOfMemoryError、StackOverflowError | 不需要 |
| RuntimeException 及子类 | NullPointerException、IllegalArgumentException、ArithmeticException | 不需要 |
| 其他 Exception 子类 | IOException、SQLException | 需要 |
设计原因:
- 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() 分三步:
- 分配内存
- 初始化对象
- 将引用指向内存
如果没有 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 的
Resource(ClassPathResource、FileSystemResource)
责任链模式:请求沿链条传递,每个节点决定处理或转发。
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、ServletFilter) - 日志级别链(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()); // 分数倒序对比:
| 维度 | Comparable | Comparator |
|---|---|---|
| 位置 | 类内部实现 | 类外部定义 |
| 方法 | 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)Comparator的thenComparing怎么实现多级排序?(链式调用,前一级相等才比下一级)