Java 基础面试八股文(50题)

Java 基础面试八股文(50 题)

📚 面试高频:本文覆盖 Java 基础核心知识点,从基础概念到高级特性,帮你系统掌握 Java 基础面试要点。

🎯 面试加分:每个问题都包含深度解析和面试加分回答,让你在面试中脱颖而出。

快速掌握:所有问题都附有通俗易懂的解释和实战经验分享。


📖 学习指南

🎯 学习目标:通过本文,你将系统掌握 Java 基础的核心概念、底层原理和实战技巧,能够自信地应对任何 Java 基础相关的面试问题。

适合人群

  • 🔰 初学者:想系统学习 Java 基础的开发者
  • 🚀 有经验者:想深入理解 Java 原理的高级开发者
  • 💼 面试准备者:想刷 Java 基础面试题的求职者

学习建议

  1. 先理解概念,再深入原理:先搞懂”是什么”,再搞懂”为什么”
  2. 结合实战场景:不要死记硬背,要理解实际应用场景
  3. 动手实践:在自己电脑上编写本文的示例代码
  4. 反复复习:面试前一周,每天复习 10 个问题

学习时间估算

  • ⏱️ 快速复习(只看一句话总结):2 小时
  • 📚 系统学习(看深度解析):5 天
  • 💪 深入理解(研究源码):1 个月+

🗺️ 知识图谱

mindmap
  root((Java 基础))
    基础概念
      JDK JRE JVM
      编译 vs 解释
      面向对象
    数据类型
      基础类型 vs 包装类型
      自动装箱拆箱
      String 不可变
    集合框架
      List
      Map
      Set
      Queue
    面向对象
      封装 继承 多态
      抽象类 vs 接口
      重载 vs 重写
    异常
      Checked vs Unchecked
      try-catch-finally
    反射
      Class.forName
      反射调用方法
    注解
     内置注解
      自定义注解
    泛型
      泛型擦除
      通配符
    IO
      BIO NIO AIO
      序列化和反序列化
    其他
      equals vs hashCode
      static final
      Integer 缓存

⚠️ 常见陷阱与误区

陷阱 1:String 不可变的理解

错误理解

  • 以为 String s = "abc"; s = "def"; 修改了字符串内容

正确认知

  • String 是不可变的,s = "def" 是让 s 指向了新的字符串对象
  • 字符串内容修改会创建新对象(内存浪费)

陷阱 2:== 和 equals 的区别

错误理解

  • 以为 == 比较值,equals 也比较值

正确认知

  • == 比较基本类型是值,比较引用类型是内存地址
  • equals 默认比较内存地址,但 String、Integer 等重写了 equals,比较内容

陷阱 3:HashMap 和 Hashtable 的区别

错误理解

  • 以为 HashMap 和 Hashtable 只是线程安全不同

正确认知

  • HashMap:线程不安全,允许 null 键值,效率更高
  • Hashtable:线程安全,不允许 null 键值,效率较低(已过时,不推荐使用)

陷阱 4:重载(Overload)和重写(Override)的区别

错误理解

  • 以为重载是子类重写父类方法

正确认知

  • 重载:同一个类中,方法名相同,参数不同(编译时多态)
  • 重写:子类中,方法签名相同,方法体不同(运行时多态)

💡 面试技巧

🎯 面试技巧:面试时遇到 Java 基础问题,不要只背概念,要结合实际项目经验回答。

回答思路

  1. 先说一句话总结(让面试官快速理解)
  2. 再深入解析(底层原理、源码分析)
  3. 最后面试加分回答(实际项目经验、踩过的坑)

举例说明

问题:String、StringBuffer、StringBuilder 的区别?

一句话总结

  • String 不可变(线程安全,适合字符串不常修改的场景)
  • StringBuffer 可变、线程安全(适合多线程环境)
  • StringBuilder 可变、线程不安全(适合单线程环境,效率最高)

面试加分回答

“在实际项目中,字符串拼接用 StringBuilder(单线程效率高),多线程环境用 StringBuffer。不要直接用 String 拼接(会创建大量对象,内存浪费)。”



Q1: JDK、JRE、JVM 的区别?

一句话总结:JDK = JRE + 开发工具;JRE = JVM + 核心类库;JVM 是 Java 跨平台的基石。

深度解析

三者的关系

1
2
3
4
JDK(Java Development Kit)
└── JRE(Java Runtime Environment)
└── JVM(Java Virtual Machine)
└── 字节码指令集、寄存器、栈、垃圾回收器
组件 全称 作用 面向人群
JDK Java Development Kit 开发工具包(编译器 javac、调试工具 jdb、jar 打包工具) 开发者
JRE Java Runtime Environment 运行环境(JVM + 核心类库 rt.jar) 只需要运行 Java 程序的人
JVM Java Virtual Machine 虚拟机(负责执行字节码 .class 文件) 所有 Java 程序的运行基础

JVM 的跨平台原理

  • 源代码(.java)→ 编译器(javac)→ 字节码(.class)
  • 字节码(.class)→ JVM(解释执行 / JIT 编译)→ 机器码
  • 不同操作系统安装不同的 JVM,但执行的字节码相同 → 一次编写,到处运行

实际项目经验

  • 开发环境装 JDK(需要编译代码)
  • 生产环境只需要 JDK 或 JRE(只需要运行代码)
  • Docker 镜像中使用 eclipse-temurin:17-jrejdk 镜像小 100MB+

面试加分回答

“在实际项目中,我们对 JDK/JRE/JVM 的理解:

  1. 开发环境:安装 JDK(需要 javac 编译器)
  2. 生产环境:只需要 JRE(不需要编译器,减小镜像体积)
  3. Docker 镜像优化:使用 eclipse-temurin:17-jre 基础镜像,比 JDK 镜像小 100MB+
  4. JVM 调优:生产环境需要根据服务器内存调整 JVM 参数(-Xms-Xmx)”

Q2: Java 是编译型语言还是解释型语言?

一句话总结:Java 是半编译半解释型语言(先编译成字节码,再由 JVM 解释执行,热点代码由 JIT 编译成本地机器码)。

深度解析

编译型语言 vs 解释型语言

类型 代表语言 优点 缺点
编译型 C、C++ 执行速度快(直接编译成机器码) 跨平台需要重新编译
解释型 Python、JavaScript 跨平台(解释器直接执行源码) 执行速度慢

Java 的编译 + 解释过程

1
2
3
4
5
6
7
8
9
源代码(.java)
javac 编译
字节码(.class)
JVM 类加载器加载
字节码指令
↓ 解释器解释执行(逐行解释)
机器码 → 执行
JIT 编译器(热点代码)
本地机器码(缓存起来,下次直接执行)

JIT(Just-In-Time)编译器

  • JVM 内置JIT 编译器(C1、C2)
  • 热点代码(执行超过 1 万次的方法)会被 JIT 编译成本地机器码
  • 下次执行时直接运行本地机器码(速度接近 C++)

实际项目经验

  • 服务刚启动时响应慢(解释执行阶段)
  • 运行一段时间后响应变快(JIT 编译完成)

面试加分回答

“在实际项目中,Java 的半编译半解释特性带来了跨平台能力(编译后的字节码可以在任何安装了 JVM 的系统上运行),同时通过 JIT 编译器保证了执行效率(热点代码编译成本地机器码)。这也是为什么 Java 适合企业级应用 —— 兼顾跨平台和高性能。”


Q3: Java 中的 8 种基础数据类型是什么?各自占多少字节?

一句话总结:Java 有 8 种基础数据类型(4 种整型、2 种浮点型、1 种字符型、1 种布尔型),每种类型占用固定的字节数。

深度解析

8 种基础数据类型

数据类型 大小 取值范围 默认值 包装类型
byte 1 字节(8 bit) -128 ~ 127 0 Byte
short 2 字节(16 bit) -32768 ~ 32767 0 Short
int 4 字节(32 bit) -2^31 ~ 2^31-1 0 Integer
long 8 字节(64 bit) -2^63 ~ 2^63-1 0L Long
float 4 字节(32 bit) ±3.4E38(约 7 位有效数字) 0.0f Float
double 8 字节(64 bit) ±1.8E308(约 15 位有效数字) 0.0d Double
char 2 字节(16 bit) 0 ~ 65535(Unicode 字符) \u0000 Character
boolean 未精确定义(通常 1 字节) true / false false Boolean

注意事项

  1. boolean 的大小:JVM 规范没有精确定义,通常 1 字节(Oracle JVM 实现)
  2. 整型字面量默认是 intlong l = 10000000000L; 需要加 L 后缀
  3. 浮点数字面量默认是 doublefloat f = 3.14f; 需要加 f 后缀
  4. char 可以存储中文:因为 Java 使用 Unicode 编码(2 字节)

实际项目经验

  • 数据库 ID 用 Long(避免超过 int 上限)
  • 金钱计算用 BigDecimal(不要用 float / double,精度丢失)
  • 布尔值用 boolean(不要用 int 的 0/1 表示)

面试加分回答

“在实际项目中,我们对基础数据类型的选择:

  1. 数据库 ID:用 Longint 上限 21 亿,不够用)
  2. 金钱计算:用 BigDecimal(不要用 float/double,会有精度丢失问题)
  3. 布尔值:用 boolean(不要用 int 的 0/1 表示,降低代码可读性)
  4. 字符存储:用 charString(不要用 byte,中文会乱码)”

Q4: 基础数据类型和包装类型的区别?

一句话总结:基础数据类型是值,包装类型是对象;基础数据类型存在栈中,包装类型存在堆中。

深度解析

主要区别

对比项 基础数据类型 包装类型
存储位置 栈(局部变量) 堆(对象)
是否有 null 否(有默认值) 是(可以为 null
是否包含方法 是(如 Integer.parseInt()
比较方式 == 比较值 == 比较地址,equals() 比较值
效率 高(直接操作栈) 低(需要堆内存分配、垃圾回收)

自动装箱(Boxing)和自动拆箱(Unboxing)

1
2
3
4
5
// 自动装箱:int → Integer(编译器自动调用 Integer.valueOf())
Integer i1 = 100; // 等价于 Integer i1 = Integer.valueOf(100);

// 自动拆箱:Integer → int(编译器自动调用 i1.intValue())
int i2 = i1; // 等价于 int i2 = i1.intValue();

Integer 缓存机制(重要!面试高频):

1
2
3
4
5
6
7
8
// Integer 缓存了 -128 ~ 127 的整数
Integer a = 100;
Integer b = 100;
System.out.println(a == b); // true(都指向缓存中的同一个对象)

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

实际项目经验

  • 数据库查询的 NULL 值需要用包装类型接收(如 IntegerLong
  • 不要用 == 比较包装类型,要用 equals()
  • 优先使用基础数据类型(效率高),除非需要表示 null

面试加分回答

“在实际项目中,我们对基础数据类型和包装类型的选择:

  1. POJO 类的属性:用包装类型(如 Integer age,可以表示 null
  2. 局部变量:用基础数据类型(效率高)
  3. 集合中的元素:只能用包装类型(集合不能存储基础数据类型)
  4. Integer 缓存-128 ~ 127 的整数会被缓存,比较时用 equals() 而不是 ==

Q5: 什么是自动装箱和自动拆箱?

一句话总结:自动装箱是编译器自动将基础数据类型转换成包装类型(intInteger);自动拆箱是编译器自动将包装类型转换成基础数据类型(Integerint)。

深度解析

自动装箱(Autoboxing)

1
2
3
4
5
// 手动装箱(JDK 1.5 之前)
Integer i1 = Integer.valueOf(100);

// 自动装箱(JDK 1.5 之后,编译器自动调用 Integer.valueOf())
Integer i2 = 100; // 编译器会自动转换成 Integer.valueOf(100)

自动拆箱(Unboxing)

1
2
3
4
5
// 手动拆箱(JDK 1.5 之前)
int i1 = i2.intValue();

// 自动拆箱(JDK 1.5 之后,编译器自动调用 i2.intValue())
int i3 = i2; // 编译器会自动转换成 i2.intValue()

自动装箱/拆箱的使用场景

1
2
3
4
5
6
7
8
9
10
11
12
// 1. 集合中使用
List<Integer> list = new ArrayList<>();
list.add(100); // 自动装箱:100 → Integer.valueOf(100)
int value = list.get(0); // 自动拆箱:Integer → int

// 2. 运算中使用
Integer i = 100;
int result = i + 200; // 自动拆箱:i → int,然后运算

// 3. 方法参数中使用
public void print(Integer i) { ... }
print(100); // 自动装箱:100 → Integer.valueOf(100)

NullPointerException 陷阱(重要!):

1
2
Integer i = null;
int value = i; // NullPointerException!自动拆箱时,i 为 null

实际项目经验

  • 避免 Integerint 混用(容易 NullPointerException
  • 数据库查询返回 null 时,自动拆箱会抛异常

面试加分回答

“在实际项目中,自动装箱/拆箱带来的 NullPointerException 陷阱需要注意:

1
2
Integer i = null;  // 数据库查询返回 null
int value = i; // NullPointerException!自动拆箱时,i 为 null

解决方案:先判断是否为 null,再拆箱:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
Integer i = ...;
int value = (i != null) ? i : 0; // 安全拆箱
```"

---

### Q6: `==` 和 `equals()` 的区别?

**一句话总结**:`==` 比较基础类型是值,比较引用类型是内存地址;`equals()` 默认比较内存地址,但 `String`、`Integer` 等重写了 `equals()` 比较内容。

#### 深度解析

**`==` 运算符**:
- **比较基础数据类型**:比较值是否相等
- **比较引用数据类型**:比较内存地址是否相同(是否为同一个对象)

**`equals()` 方法**:
- **默认实现**(`Object.equals()`):等价于 `==`(比较内存地址)
- **重写后**(`String`、`Integer` 等):比较内容是否相同

**`String` 的 `==` 和 `equals()` 对比**:
```java
String s1 = "abc";
String s2 = "abc";
System.out.println(s1 == s2); // true(都指向字符串常量池中的同一个对象)
System.out.println(s1.equals(s2)); // true(内容相同)

String s3 = new String("abc");
System.out.println(s1 == s3); // false(s1 在字符串常量池,s3 在堆中)
System.out.println(s1.equals(s3)); // true(内容相同)

Integer==equals() 对比

1
2
3
4
5
6
7
8
9
Integer i1 = 100;
Integer i2 = 100;
System.out.println(i1 == i2); // true(Integer 缓存 -128~127)
System.out.println(i1.equals(i2)); // true(内容相同)

Integer i3 = 200;
Integer i4 = 200;
System.out.println(i3 == i4); // false(超出缓存范围,新建对象)
System.out.println(i3.equals(i4)); // true(内容相同)

重写 equals() 的规则(重要!):

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

实际项目经验

  • 比较字符串内容用 equals(),不要用 ==
  • 比较包装类型内容用 equals(),不要用 ==
  • 重写 equals() 必须重写 hashCode()(否则 HashMap、HashSet 会出问题)

面试加分回答

“在实际项目中,关于 ==equals()最佳实践

  1. 比较字符串内容:用 equals()(不要用 ==
  2. 比较包装类型内容:用 equals()(不要用 ==
  3. 重写 equals() 必须重写 hashCode()
    • 如果两个对象 equals() 返回 true,则 hashCode() 必须相同
    • 如果 hashCode() 相同,equals() 不一定返回 true(哈希冲突)
    • 不重写 hashCode() 会导致 HashMap、HashSet 无法正常工作”

Q7: 为什么重写 equals() 必须重写 hashCode()

一句话总结:因为 HashMap、HashSet 先比较 hashCode(),再比较 equals();如果只重写 equals() 不重写 hashCode(),会导致两个相等的对象被当作不相等的(存入 HashMap 时产生重复键)。

深度解析

hashCode() 的契约(Contract)

  1. 如果两个对象 equals() 返回 true,则 hashCode() 必须相同
  2. 如果 hashCode() 相同,equals() 不一定返回 true(哈希冲突)
  3. 多次调用同一个对象的 hashCode() 必须返回相同的值(前提:对象未被修改)**

HashMap 的 put() 流程(重要!):

1
2
3
4
5
6
7
8
1. 计算 key 的 hashCode()
2. 根据 hashCode() 找到桶(bucket)位置
3. 如果桶为空,直接存入
4. 如果桶不为空,遍历桶中的元素:
a. 如果 hashCode() 不同,继续遍历
b. 如果 hashCode() 相同,再用 equals() 比较:
- equals() 返回 true:认为是同一个 key,覆盖 value
- equals() 返回 false:链表追加(或红黑树插入)

只重写 equals() 不重写 hashCode() 的后果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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 == null || getClass() != o.getClass()) return false;
Person person = (Person) o;
return Objects.equals(name, person.name);
}
// 没有重写 hashCode()
}

Person p1 = new Person("张三");
Person p2 = new Person("张三");

System.out.println(p1.equals(p2)); // true(内容相同)

HashMap<Person, String> map = new HashMap<>();
map.put(p1, "学生");
System.out.println(map.get(p2)); // null!!因为 p1 和 p2 的 hashCode() 不同

正确的重写方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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 == null || getClass() != o.getClass()) return false;
Person person = (Person) o;
return Objects.equals(name, person.name);
}

@Override
public int hashCode() {
return Objects.hash(name); // 使用相同的字段计算 hashCode
}
}

面试加分回答

“在实际项目中,关于 equals()hashCode()最佳实践

  1. 使用 IDE 自动生成:IntelliJ IDEA 可以自动生成 equals()hashCode()(Alt + Insert)
  2. 使用相同的字段equals()hashCode() 使用相同的字段计算(否则会违反契约)
  3. 使用 Objects.hash() 计算 hashCode():避免手动计算哈希值(容易出错)
  4. 不可变对象:如果对象是不可变的(所有字段是 final),可以缓存 hashCode() 值(提高性能)”

Q8: final 关键字的作用?

一句话总结final 修饰变量表示常量(不可修改),修饰方法表示不可重写,修饰类表示不可继承。

深度解析

final 修饰变量

1
2
3
4
5
6
7
8
// 1. 修饰基础数据类型变量:值不可修改
final int x = 10;
x = 20; // 编译错误!

// 2. 修饰引用数据类型变量:引用不可修改(但对象内容可以修改)
final List<String> list = new ArrayList<>();
list.add("abc"); // 允许!修改的是对象内容
list = new ArrayList<>(); // 编译错误!不能修改引用

final 修饰方法

1
2
3
4
5
6
7
8
9
// 子类不能重写 final 方法
class Parent {
public final void method() { ... }
}

class Child extends Parent {
@Override
public void method() { ... } // 编译错误!不能重写 final 方法
}

final 修饰类

1
2
// 类不能被继承(如 String、Integer 都是 final 类)
public final class String { ... }

final 的好处

  1. 安全性:防止子类修改父类的关键方法(如 String 不能被继承,防止绕过字符串常量池)
  2. 性能优化:JVM 可以对 final 方法进行内联优化(减少方法调用开销)
  3. 线程安全final 字段在构造函数中初始化后,其他线程可以看到(无需 volatile

实际项目经验

  • 工具类(如 MathArrays)应该声明为 final(防止被继承)
  • 模板方法模式中的模板方法应该声明为 final(防止子类破坏模板流程)

面试加分回答

“在实际项目中,我们对 final 的使用:

  1. 工具类声明为 final:如 MathArrays(防止被继承)
  2. 模板方法声明为 final:模板方法模式中的模板方法(防止子类破坏模板流程)
  3. final 提高性能:JVM 可以对 final 方法进行内联优化(减少方法调用开销)
  4. Stringfinal:防止绕过字符串常量池(保证字符串常量池的安全性)”

Q9: finalfinallyfinalize 的区别?

一句话总结final 修饰符(常量、不可重写、不可继承);finally 异常处理的 finally 块(无论是否抛异常都会执行);finalize() 是 Object 类的方法(垃圾回收前调用,已废弃)。

深度解析

final(修饰符):

  • 修饰变量:常量(不可修改)
  • 修饰方法:不可重写
  • 修饰类:不可继承

finally(异常处理):

1
2
3
4
5
6
7
try {
// 可能抛出异常的代码
} catch (Exception e) {
// 异常处理
} finally {
// 无论是否抛异常,都会执行(常用于释放资源)
}

finally 的执行时机(重要!面试高频):

1
2
3
4
5
6
7
try {
System.out.println("try");
return 1; // 先执行 finally,再 return
} finally {
System.out.println("finally");
}
// 输出:try → finally → return 1

finalize()(方法,已废弃):

  • Object 类的方法,垃圾回收前调用
  • 已废弃(JDK 9+):不确定何时调用,性能差,推荐使用 CleanerPhantomReference

finally 不执行的情况(重要!):

  1. System.exit() 退出 JVM
  2. 程序所在线程死亡
  3. 关闭 CPU(物理关机)

面试加分回答

“在实际项目中,关于 finally最佳实践

  1. 释放资源finally 块中释放资源(如关闭文件流、关闭数据库连接)
  2. JDK 7+ 使用 try-with-resources:自动释放资源(不需要手写 finally
    1
    2
    3
    4
    5
    6
    try (FileInputStream fis = new FileInputStream("file.txt")) {
    // 使用 fis
    } catch (IOException e) {
    // 异常处理
    }
    // 不需要 finally,fis 会自动关闭
  3. finalize() 已废弃:不要重写 finalize() 方法(不确定何时调用,性能差)”

Q10: static 关键字的作用?

一句话总结static 修饰的字段或方法属于类,而不是属于对象;静态字段在类加载时初始化,静态方法可以直接通过类名调用。

深度解析

static 修饰字段

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Person {
private static int count = 0; // 静态字段(属于类,所有对象共享)
private String name; // 实例字段(属于对象,每个对象独立)

public Person(String name) {
this.name = name;
count++; // 每创建一个对象,count + 1
}

public static int getCount() {
return count; // 静态方法访问静态字段
}
}

Person p1 = new Person("张三");
Person p2 = new Person("李四");
System.out.println(Person.getCount()); // 2(静态字段被所有对象共享)

static 修饰方法

  • 静态方法可以直接通过类名调用(不需要创建对象)
  • 静态方法中不能使用 this(因为 this 代表当前对象,而静态方法属于类)
  • 静态方法中只能访问静态字段和静态方法

static 修饰代码块

1
2
3
4
5
6
7
8
9
10
11
class Person {
// 静态代码块(类加载时执行,只执行一次)
static {
System.out.println("Person 类被加载了");
}

// 实例代码块(每次创建对象时执行,在构造函数之前执行)
{
System.out.println("Person 对象被创建了");
}
}

static 的使用场景

  1. 工具类的方法:如 Math.random()Arrays.sort()
  2. 常量public static final int MAX_SIZE = 100;
  3. 单例模式:私有构造函数 + 静态方法返回实例
  4. 静态工厂方法:如 Integer.valueOf()LocalDate.now()

面试加分回答

“在实际项目中,关于 static最佳实践

  1. 工具类的方法声明为 static:如 MathArrays(不需要创建对象,直接通过类名调用)
  2. 常量声明为 static finalpublic static final String APP_NAME = "MyApp";
  3. 单例模式使用 static:私有构造函数 + 静态方法返回实例
  4. 静态代码块初始化静态字段:类加载时执行(只执行一次)”


Q11: 为什么 String 是不可变的?

一句话总结:String 不可变是因为内部使用 final char[](JDK 8 及之前)或 final byte[](JDK 9+)存储字符串,且没有提供修改内容的方法。

深度解析

String 不可变的底层实现(JDK 8):

1
2
3
4
5
6
public final class String {
private final char value[]; // final 数组,引用不可变

// 没有提供修改 value[] 内容的方法
// substring()、concat()、replace() 都返回新的 String 对象
}

String 不可变的好处

  1. 字符串常量池:相同的字符串字面量指向同一个对象(节省内存)
  2. 线程安全:不可变对象天生线程安全(不需要同步)
  3. 哈希码缓存hashCode() 的结果可以缓存(提高 HashMap 性能)
  4. 安全性:防止被篡改(如数据库用户名、密码)

字符串常量池(String Pool)

1
2
3
4
5
6
String s1 = "abc";  // 字符串常量池
String s2 = "abc"; // 指向常量池中的同一个对象
System.out.println(s1 == s2); // true

String s3 = new String("abc"); // 强制创建新对象(在堆中)
System.out.println(s1 == s3); // false

JDK 9+ 的改进

  • JDK 8:char value[](每个字符 2 字节)
  • JDK 9+:byte value[](根据编码选择 1 或 2 字节,节省空间)

面试加分回答

“在实际项目中,String 不可变的好处:

  1. 字符串常量池:相同的字符串字面量指向同一个对象(节省内存)
  2. 线程安全:不可变对象天生线程安全(不需要同步)
  3. 哈希码缓存hashCode() 的结果可以缓存(提高 HashMap 性能)
  4. JDK 9+ 的改进char[] 改成 byte[](节省空间,根据编码选择 1 或 2 字节)”

Q12: String、StringBuffer、StringBuilder 的区别?

一句话总结:String 不可变(线程安全);StringBuffer 可变、线程安全(方法加 synchronized);StringBuilder 可变、线程不安全(效率高)。

深度解析

对比项 String StringBuffer StringBuilder
可变性 不可变 可变 可变
线程安全 是(不可变天生安全) 是(方法加 synchronized
效率 低(每次修改创建新对象) 中(同步开销) 高(无同步开销)
适用场景 字符串不常修改 多线程环境 单线程环境

使用场景

1
2
3
4
5
6
7
8
9
10
11
12
13
// 1. String:字符串不常修改(如常量、配置项)
String name = "张三";
String url = "https://example.com";

// 2. StringBuffer:多线程环境(如全局配置拼接)
StringBuffer sb1 = new StringBuffer();
Runnable task = () -> sb1.append("abc"); // 多线程安全

// 3. StringBuilder:单线程环境(如 SQL 拼接、JSON 拼接)
StringBuilder sb2 = new StringBuilder();
sb2.append("SELECT * FROM users WHERE ");
sb2.append("age > ").append(18);
String sql = sb2.toString(); // 效率高

toString() 方法中的 StringBuilder

1
2
3
4
// 编译器会将字符串拼接转换成 StringBuilder
String s = "abc" + 123 + true;
// 等价于:
String s = new StringBuilder().append("abc").append(123).append(true).toString();

面试加分回答

“在实际项目中,我们对三者的选择:

  1. 字符串不常修改:用 String(如常量、配置项)
  2. 单线程环境字符串拼接:用 StringBuilder(效率高)
  3. 多线程环境字符串拼接:用 StringBuffer(线程安全)
  4. 不要直接用 String 拼接:在循环中使用 String 拼接会创建大量对象(内存浪费)”

Q13: String s = new String(“abc”) 创建了几个对象?

一句话总结1 个或 2 个对象(如果字符串常量池中已有 “abc”,则创建 1 个;如果没有,则创建 2 个)。

深度解析

创建过程分析

1
2
3
4
5
1 步:检查字符串常量池
- 如果常量池中有 "abc",不创建新对象
- 如果常量池中没有 "abc",在常量池中创建 "abc" 对象

2 步:在堆中创建 String 对象(new String("abc"))

示例代码

1
2
3
4
5
6
7
// 情况 1:常量池中已有 "abc"
String s1 = "abc"; // 在常量池中创建 "abc"
String s2 = new String("abc"); // 只在堆中创建 1 个对象
System.out.println(s1 == s2); // false(s1 在常量池,s2 在堆中)

// 情况 2:常量池中没有 "abc"
String s3 = new String("def"); // 在常量池中创建 "def" + 在堆中创建 String 对象(共 2 个)

面试高频追问

  • Q: String.intern() 方法的作用?
    • A: 将字符串对象放入字符串常量池(如果常量池中已有,则返回常量池中的引用;如果没有,则放入常量池并返回引用)

面试加分回答

“在实际项目中,关于 new String("abc")最佳实践

  1. 不要使用 new String("abc"):优先使用字面量 String s = "abc"(利用字符串常量池,节省内存)
  2. String.intern() 的使用场景:从文件/网络读取的大量重复字符串,可以用 intern() 去重(节省内存)”

Q14: Object 类的常见方法有哪些?

一句话总结:Object 是所有类的父类,常见方法有 equals()hashCode()toString()clone()finalize()(已废弃)、wait()notify()notifyAll()

深度解析

Object 类的常见方法

方法 作用 说明
equals(Object obj) 判断两个对象是否相等 默认实现是 ==(比较地址),通常需要重写
hashCode() 返回对象的哈希码 重写 equals() 必须重写 hashCode()
toString() 返回对象的字符串表示 默认格式:类名@哈希码,通常需要重写
clone() 克隆对象 需要实现 Cloneable 接口,重写成 public
finalize() 垃圾回收前调用 已废弃(JDK 9+),不推荐使用
getClass() 返回对象的 Class 对象 用于反射
wait() 让当前线程等待 必须在 synchronized 块中调用
notify() 唤醒一个等待的线程 必须在 synchronized 块中调用
notifyAll() 唤醒所有等待的线程 必须在 synchronized 块中调用

clone() 方法的使用

1
2
3
4
5
6
7
8
class Person implements Cloneable {
private String name;

@Override
public Object clone() throws CloneNotSupportedException {
return super.clone(); // 浅拷贝
}
}

wait()notify() 的使用

1
2
3
4
5
6
7
synchronized (lock) {
while (condition) {
lock.wait(); // 等待
}
// 业务逻辑
lock.notifyAll(); // 唤醒其他线程
}

面试加分回答

“在实际项目中,关于 Object 类方法的 最佳实践

  1. 重写 equals() 必须重写 hashCode():否则 HashMap、HashSet 会出问题
  2. 重写 toString():便于日志输出和调试
  3. clone() 不推荐使用:推荐使用拷贝构造函数或拷贝工厂方法
    1
    2
    3
    4
    class Person {
    private String name;
    public Person(Person other) { this.name = other.name; } // 拷贝构造函数
    }
  4. finalize() 已废弃:不推荐使用(不确定何时调用,性能差)”

Q15: Java 的 4 种访问修饰符?

一句话总结private(当前类)→ default(同包)→ protected(同包 + 子类)→ public(任意地方)。

深度解析

4 种访问修饰符的访问范围

修饰符 当前类 同包 子类 任意地方
private
default(默认)
protected
public

使用建议

  1. 字段尽量用 private:封装性(外部不能直接访问)
  2. 提供 getter/setter:控制字段的读写权限
  3. 方法尽量用 public:对外提供的 API
  4. 工具类构造器用 private:防止被实例化(如 MathArrays

实际项目经验

  • 实体类(POJO)的字段用 private,提供 getter/setter
  • 工具类的构造器用 private(如 MathArrays
  • 单例模式的构造器用 private(防止被实例化)

面试加分回答

“在实际项目中,关于访问修饰符的 最佳实践

  1. 字段尽量用 private:封装性(外部不能直接访问)
  2. 提供 getter/setter:控制字段的读写权限
  3. 工具类构造器用 private:防止被实例化(如 MathArrays
  4. 单例模式的构造器用 private:防止被实例化”

Q16: 接口和抽象类的区别?

一句话总结:抽象类只能单继承,可以有非抽象方法和字段;接口可以多实现,JDK 8+ 可以有 default 方法和静态方法,JDK 9+ 可以有 private 方法。

深度解析

对比项 抽象类 接口
继承/实现 单继承(extends 多实现(implements
字段 可以有任意字段 只能是 public static final 常量
方法 可以有抽象方法和非抽象方法 JDK 8+ 可以有 default 方法和静态方法
构造器 有构造器(但不能实例化) 没有构造器
适用场景 “is-a” 关系(如 AnimalDog “can-do” 关系(如 RunnableSerializable

JDK 8+ 接口的改进

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
interface MyInterface {
void abstractMethod(); // 抽象方法(默认 public abstract)

// JDK 8+:default 方法(有方法体,子类可以不重写)
default void defaultMethod() {
System.out.println("default method");
}

// JDK 8+:静态方法(有方法体)
static void staticMethod() {
System.out.println("static method");
}

// JDK 9+:private 方法(有方法体,只能在接口内部调用)
private void privateMethod() {
System.out.println("private method");
}
}

实际项目经验

  • 抽象类:用于”模板方法模式”(定义算法骨架,子类实现具体步骤)
  • 接口:用于定义规范(如 Service 层接口、DAO 层接口)

面试加分回答

“在实际项目中,我们对抽象类和接口的选择:

  1. “is-a” 关系用抽象类:如 AnimalDog(狗是动物)
  2. “can-do” 关系用接口:如 Runnable(可以运行)、Serializable(可以序列化)
  3. 需要定义规范用接口:如 Service 层接口、DAO 层接口
  4. 需要提供默认实现用 default 方法:JDK 8+ 接口可以有 default 方法(有方法体,子类可以不重写)”

Q17: 重载(Overload)和重写(Override)的区别?

一句话总结:重载是同一个类中方法名相同、参数不同(编译时多态);重写是子类中方法签名相同、方法体不同(运行时多态)。

深度解析

重载(Overload)

  • 发生在同一个类中
  • 方法名相同,参数列表不同(参数类型、参数个数、参数顺序)
  • 返回类型可以不同
  • 访问修饰符可以不同
  • 编译时多态(编译器根据方法签名确定调用哪个方法)
1
2
3
4
5
class Calculator {
public int add(int a, int b) { return a + b; }
public double add(double a, double b) { return a + b; } // 重载
public int add(int a, int b, int c) { return a + b + c; } // 重载
}

重写(Override)

  • 发生在父子类之间
  • 方法签名相同(方法名、参数列表、返回类型)
  • 访问修饰符不能比父类更严格(如父类是 public,子类不能是 protected
  • 异常不能比父类更广泛(如父类抛出 IOException,子类不能抛出 Exception
  • 运行时多态(JVM 根据对象实际类型调用对应方法)
1
2
3
4
5
6
7
8
class Parent {
public void method() { System.out.println("parent"); }
}

class Child extends Parent {
@Override
public void method() { System.out.println("child"); } // 重写
}

@Override 注解的作用

  • 编译器检查是否真的重写了父类方法(防止拼写错误)
  • 提高代码可读性(明确这是重写方法)

面试加分回答

“在实际项目中,关于重载和重写的 最佳实践

  1. 重写方法加 @Override 注解:编译器检查是否真的重写了父类方法(防止拼写错误)
  2. 重载的用途:构造函数重载(提供多个构造函数)、方法重载(提供多个参数列表)
  3. 重写的用途:模板方法模式(父类定义算法骨架,子类实现具体步骤)”

Q18: 构造器(Constructor)是否可以被重写?

一句话总结:构造器不能被重写(因为构造器名称必须与类名相同,子类和父类类名不同),但可以被重载

深度解析

为什么构造器不能被重写?

  • 重写要求方法签名相同(方法名、参数列表、返回类型)
  • 构造器名称必须与类名相同
  • 子类和父类的类名不同 → 构造器名称不同 → 不满足重写条件

构造器可以被重载

1
2
3
4
5
6
7
8
9
class Person {
private String name;
private int age;

// 构造器重载
public Person() { this.name = "未知"; }
public Person(String name) { this.name = name; }
public Person(String name, int age) { this.name = name; this.age = age; }
}

子类构造器默认调用父类无参构造器

1
2
3
4
5
6
7
8
9
10
11
class Parent {
public Parent() { System.out.println("parent"); }
}

class Child extends Parent {
public Child() {
super(); // 默认调用父类无参构造器(可以省略)
System.out.println("child");
}
}
// 输出:parent → child

如果父类没有无参构造器,子类必须显式调用 super(参数)

1
2
3
4
5
6
7
8
9
class Parent {
public Parent(String name) { ... } // 没有无参构造器
}

class Child extends Parent {
public Child() {
super("张三"); // 必须显式调用,否则编译错误
}
}

面试加分回答

“在实际项目中,关于构造器的 最佳实践

  1. 提供无参构造器:防止子类构造器编译错误(如果父类没有无参构造器,子类必须显式调用 super(参数)
  2. 构造器中不要调用可被重写的方法:因为子类对象初始化时,父类构造器先执行,此时子类重写的方法会被调用(容易导致空指针异常)
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
       class Parent {
    public Parent() { method(); } // 不要这样做!
    public void method() {}
    }
    ```"

    ---

    ### Q19: this 和 super 关键字的作用?

    **一句话总结**:`this` 指向当前对象(访问当前类的字段、方法、构造器);`super` 指向父类对象(访问父类的字段、方法、构造器)。

    #### 深度解析

    **`this` 关键字的作用**:
    ```java
    class Person {
    private String name;

    public Person(String name) {
    this.name = name; // this 指向当前对象(区分字段和参数)
    }

    public void method() {
    this.anotherMethod(); // this 可以省略
    }

    public Person() {
    this("默认名称"); // this() 调用当前类的其他构造器(必须在第一行)
    }
    }

super 关键字的作用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Parent {
protected String name;

public Parent(String name) { this.name = name; }
public void method() { System.out.println("parent"); }
}

class Child extends Parent {
public Child(String name) {
super(name); // super() 调用父类构造器(必须在第一行)
super.name = "张三"; // super 访问父类字段
}

@Override
public void method() {
super.method(); // super 调用父类方法
System.out.println("child");
}
}

this()super() 不能同时出现

  • 因为 this()super() 都必须在构造器的第一行
  • 如果同时出现,编译器不知道哪一个是第一行

面试加分回答

“在实际项目中,关于 thissuper最佳实践

  1. this()super() 不能同时出现:因为都必须在构造器的第一行
  2. 使用 this 区分字段和参数:当字段和参数同名时,用 this.field 区分
  3. 重写方法中使用 super 调用父类方法:如 toString() 中调用 super.toString()

Q20: 面向对象的三大特性?

一句话总结:封装(隐藏实现细节,提供公共访问方式);继承(子类复用父类代码,单继承);多态(父类引用指向子类对象,运行时动态绑定)。

深度解析

1. 封装(Encapsulation)

  • 核心思想:隐藏实现细节,提供公共访问方式
  • 实现方式:字段用 private 修饰,提供 publicgetter/setter
  • 好处:提高安全性(外部不能直接访问字段)、提高代码复用性
1
2
3
4
5
6
class Person {
private String name; // 封装:字段用 private 修饰

public String getName() { return name; } // 提供公共访问方式
public void setName(String name) { this.name = name; }
}

2. 继承(Inheritance)

  • 核心思想:子类复用父类代码
  • 实现方式class Child extends Parent
  • 限制:Java 只支持单继承(一个类只能有一个直接父类)
  • 好处:代码复用、方法重写(运行时多态)
1
2
class Animal { public void eat() { System.out.println("动物吃东西"); } }
class Dog extends Animal { @Override public void eat() { System.out.println("狗吃骨头"); } }

3. 多态(Polymorphism)

  • 核心思想:父类引用指向子类对象,运行时动态绑定
  • 前提条件:继承 + 重写 + 父类引用指向子类对象
  • 好处:提高代码扩展性(新增子类不需要修改父类代码)
1
2
3
4
5
6
7
8
9
class Animal { public void eat() { System.out.println("动物吃东西"); } }
class Dog extends Animal { @Override public void eat() { System.out.println("狗吃骨头"); } }
class Cat extends Animal { @Override public void eat() { System.out.println("猫吃鱼"); } }

// 多态:父类引用指向子类对象
Animal a1 = new Dog();
Animal a2 = new Cat();
a1.eat(); // 输出:狗吃骨头(运行时动态绑定)
a2.eat(); // 输出:猫吃鱼(运行时动态绑定)

面试加分回答

“在实际项目中,面向对象三大特性的 实际运用

  1. 封装:实体类(POJO)的字段用 private,提供 getter/setter
  2. 继承BaseController(基础控制器)、BaseService(基础服务层)
  3. 多态:策略模式(不同的策略实现同一个接口,运行时动态选择策略)”


Q21: ArrayList 和 LinkedList 的区别?

一句话总结:ArrayList 基于动态数组(随机访问快,增删慢);LinkedList 基于双向链表(随机访问慢,增删快)。

深度解析

对比项 ArrayList LinkedList
底层结构 动态数组 双向链表
随机访问 O(1)(数组索引) O(n)(需要遍历)
头部插入 O(n)(需要移动元素) O(1)(只需要修改指针)
尾部插入 O(1)(均摊) O(1)
中间插入 O(n)(需要移动元素) O(n)(需要遍历找到位置)
内存占用 少(只存储元素) 多(需要存储前驱/后继指针)

ArrayList 的动态扩容

1
2
3
4
5
6
7
8
// ArrayList 默认初始容量 10,扩容为原来的 1.5 倍
transient Object[] elementData; // 底层数组

private void grow(int minCapacity) {
int oldCapacity = elementData.length;
int newCapacity = oldCapacity + (oldCapacity >> 1); // 1.5 倍
elementData = Arrays.copyOf(elementData, newCapacity);
}

LinkedList 的双向链表结构

1
2
3
4
5
6
7
8
9
// LinkedList 每个节点有前驱和后继指针
transient Node<E> first; // 头节点
transient Node<E> last; // 尾节点

private static class Node<E> {
E item;
Node<E> next; // 后继指针
Node<E> prev; // 前驱指针
}

使用场景

  • ArrayList:需要频繁随机访问(如 get(i)),增删不多
  • LinkedList:需要频繁在头部/中部增删,随机访问不多

面试加分回答

“在实际项目中,我们对 ArrayList 和 LinkedList 的选择:

  1. 需要随机访问:用 ArrayList(如根据索引获取元素)
  2. 需要频繁在头部增删:用 LinkedList(如实现队列、栈)
  3. 需要频繁在尾部增删:两者都可以(ArrayList 更高效,均摊 O(1))
  4. 大坑提醒:ArrayList 扩容会复制数组(耗时),初始化时尽量指定容量(new ArrayList<>(1000))”

Q22: HashMap 的底层原理(JDK 7 vs JDK 8)?

一句话总结:JDK 7 使用数组 + 链表(头插法,多线程扩容可能死循环);JDK 8 使用数组 + 链表 + 红黑树(尾插法,链表长度 > 8 且数组长度 ≥ 64 时转红黑树)。

深度解析

JDK 7 的 HashMap

1
2
3
4
数组(Entry[])
├── 索引 0 → 链表(头插法)
├── 索引 1 → null
└── 索引 n → 链表
  • 扩容死循环问题:多线程同时扩容,头插法可能导致链表成环(CPU 100%)

JDK 8 的 HashMap

1
2
3
4
数组(Node[])
├── 索引 0 → 链表(尾插法,长度 ≤ 8
├── 索引 1 → 红黑树(长度 > 8 且数组长度 ≥ 64
└── 索引 n → null
  • 负载因子 0.75:平衡空间和时间成本
  • 扩容阈值 = 容量 × 0.75(如容量 16,阈值 12)
  • 链表 → 红黑树条件:链表长度 > 8 数组长度 ≥ 64
  • 红黑树 → 链表条件:红黑树节点数 < 6

put() 流程(JDK 8)

1
2
3
4
5
6
7
8
9
10
1. 计算 key 的 hashCode(),高位参与运算(减少哈希冲突)
2. 根据 (n - 1) & hash 找到桶位置
3. 如果桶为空,直接插入
4. 如果桶不为空:
a. 如果是红黑树,按红黑树插入
b. 如果是链表,遍历链表:
- 如果 key 已存在,覆盖 value
- 如果 key 不存在,尾插法插入链表
- 如果链表长度 > 8,转为红黑树
5. 如果 size > 阈值,扩容(2 倍)

面试加分回答

“在实际项目中,关于 HashMap 的 最佳实践

  1. 初始化时指定容量new HashMap<>(1024)(避免频繁扩容)
  2. 不要用可变对象作为 key:如 new HashMap<List, String>()(List 内容变了,hashCode() 也变,找不到 value)
  3. 多线程环境用 ConcurrentHashMap:HashMap 线程不安全
  4. 为什么负载因子是 0.75?:在数学上,0.75 是空间和时间的平衡点(泊松分布)”

Q23: HashMap 和 Hashtable 的区别?

一句话总结:HashMap 线程不安全、允许 null 键值;Hashtable 线程安全、不允许 null 键值(已过时,不推荐使用)。

深度解析

对比项 HashMap Hashtable
线程安全 是(synchronized 修饰方法)
null 键值 允许(1 个 null 键,多个 null 值) 不允许(会抛 NullPointerException
初始容量 16 11
扩容方式 2 倍 2 倍 + 1
哈希算法 高位参与运算(减少冲突) 直接 hash & 0x7FFFFFFF
效率 低(synchronized 锁住整个表)
是否推荐 否(已过时,推荐用 ConcurrentHashMap)

Hashtable 为什么过时?

  • 使用 synchronized 修饰方法,锁住整个表(效率低)
  • 已有更好的替代方案:ConcurrentHashMap(分段锁 / CAS + synchronized)

面试加分回答

“在实际项目中,关于 HashMap 和 Hashtable 的选择:

  1. 单线程环境:用 HashMap
  2. 多线程环境:用 ConcurrentHashMap(不要用 Hashtable,已过时)
  3. 需要线程安全的 Map:用 ConcurrentHashMap 或 Collections.synchronizedMap()
  4. Hashtable 已过时:不推荐使用(效率低,已有更好的替代方案)”

Q24: ConcurrentHashMap 的底层原理(JDK 7 vs JDK 8)?

一句话总结:JDK 7 使用分段锁(Segment);JDK 8 使用 CAS + synchronized(锁住桶的头节点,并发度更高)。

深度解析

JDK 7 的 ConcurrentHashMap

1
2
3
4
5
ConcurrentHashMap
└── Segment[](默认 16 个段,每个段是一把锁)
├── Segment[0] → HashEntry[](数组 + 链表)
├── Segment[1] → HashEntry[](数组 + 链表)
└── ...
  • 并发度:默认 16(可以同时有 16 个线程操作不同的段)
  • 缺点:段数量固定,扩容时需要重新计算哈希(耗时)

JDK 8 的 ConcurrentHashMap

1
2
3
4
5
ConcurrentHashMap
└── Node[](数组 + 链表 + 红黑树,和 HashMap 类似)
├── 索引 0 → 链表 / 红黑树
├── 索引 1 → 链表 / 红黑树
└── ...
  • 锁机制:CAS + synchronized(只锁住桶的头节点,并发度更高)
  • put() 流程
    1. 如果桶为空,用 CAS 插入(无锁)
    2. 如果桶不为空,用 synchronized 锁住桶的头节点
    3. 插入成功后,如果链表长度 > 8,转为红黑树

为什么 JDK 8 放弃分段锁?

  • 分段锁的段数量固定(并发度固定为 16)
  • CAS + synchronized 的并发度更高(只锁住桶的头节点)

面试加分回答

“在实际项目中,关于 ConcurrentHashMap 的 最佳实践

  1. JDK 7 用分段锁:默认 16 个段,并发度 16
  2. JDK 8 用 CAS + synchronized:只锁住桶的头节点,并发度更高
  3. 初始化时指定容量new ConcurrentHashMap<>(1024)(避免频繁扩容)
  4. 为什么 JDK 8 放弃分段锁?:分段锁的并发度固定(16),CAS + synchronized 并发度更高”

Q25: HashSet 的底层原理?

一句话总结:HashSet 底层使用 HashMap(元素作为 HashMap 的 key,value 是一个固定的 PRESENT 对象)。

深度解析

HashSet 的底层实现

1
2
3
4
5
6
7
8
9
10
11
12
public class HashSet<E> {
private transient HashMap<E, Object> map; // 底层使用 HashMap
private static final Object PRESENT = new Object(); // 固定的 value

public boolean add(E e) {
return map.put(e, PRESENT) == null; // 元素作为 key,value 是 PRESENT
}

public boolean remove(Object o) {
return map.remove(o) == PRESENT; // 删除 key
}
}

HashSet 的特点

  • 元素不重复:因为 HashMap 的 key 不重复
  • 允许 null:因为 HashMap 允许 null 键
  • 无序:因为 HashMap 无序
  • 非线程安全:和 HashMap 一样

LinkedHashSet

  • 继承 HashSet
  • 底层使用 LinkedHashMap(维护插入顺序)

TreeSet

  • 底层使用 TreeMap(红黑树,按元素大小排序)
  • 元素必须实现 Comparable 接口,或在构造时传入 Comparator

面试加分回答

“在实际项目中,关于 Set 的选择:

  1. 需要去重:用 HashSet(底层 HashMap,效率高)
  2. 需要维护插入顺序:用 LinkedHashSet(底层 LinkedHashMap)
  3. 需要排序:用 TreeSet(底层 TreeMap,红黑树)
  4. 大坑提醒:自定义对象作为 HashSet 的元素,必须重写 equals()hashCode()(否则去重失效)”

Q26: HashMap 为什么线程不安全?

一句话总结:HashMap 在并发场景下可能出现数据覆盖、死循环(JDK 7)、数据丢失等问题,因为 put() 操作不是原子操作。

深度解析

JDK 7 的死循环问题(扩容时):

1
2
3
4
5
线程 A 和线程 B 同时扩容:
1. 线程 A 暂停在转移链表时(头插法)
2. 线程 B 完成扩容,链表顺序反转
3. 线程 A 恢复执行,继续转移链表
4. 因为头插法,链表可能成环 → 死循环(CPU 100%

JDK 8 的数据覆盖问题

1
2
3
4
// 线程 A 和线程 B 同时 put:
1. 线程 A 计算桶位置 = 5,暂停
2. 线程 B 计算桶位置 = 5,插入成功
3. 线程 A 恢复执行,直接覆盖线程 B 的 value

并发场景下的具体问题

问题 JDK 7 JDK 8
死循环 ✅(头插法扩容) ❌(尾插法)
数据覆盖
数据丢失
size 不准确

面试加分回答

“在实际项目中,关于 HashMap 线程不安全的问题:

  1. JDK 7 有死循环问题:因为头插法扩容,多线程可能让链表成环(CPU 100%)
  2. JDK 8 修复了死循环问题:改用尾插法,但仍有数据覆盖问题
  3. 多线程环境用 ConcurrentHashMap:不要用 HashMap(线程不安全)
  4. 需要用 Collections.synchronizedMap():如果只是偶尔并发,可以用这个(性能比 ConcurrentHashMap 差)”

Q27: Queue 和 Deque 的区别?

一句话总结:Queue 是单向队列(FIFO,队尾插入,队头取出);Deque 是双端队列(队头和队尾都可以插入和取出)。

深度解析

Queue 接口的方法

1
2
3
4
5
6
7
8
9
10
interface Queue<E> {
boolean add(E e); // 插入成功返回 true,失败抛 IllegalStateException
boolean offer(E e); // 插入成功返回 true,失败返回 false

E remove(); // 取出并删除队头,队列为空抛 NoSuchElementException
E poll(); // 取出并删除队头,队列为空返回 null

E element(); // 只取出队头(不删除),队列为空抛 NoSuchElementException
E peek(); // 只取出队头(不删除),队列为空返回 null
}

Deque 接口的方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
interface Deque<E> {
// 队头操作
void addFirst(E e);
boolean offerFirst(E e);
E getFirst();
E peekFirst();
E removeFirst();
E pollFirst();

// 队尾操作
void addLast(E e);
boolean offerLast(E e);
E getLast();
E peekLast();
E removeLast();
E pollLast();
}

常见实现类

实现类 底层结构 是否线程安全 特点
LinkedList 双向链表 可以作为 Queue 或 Deque
ArrayDeque 动态数组 效率比 LinkedList 高(数组随机访问)
PriorityQueue 优先堆 元素按优先级排序(不是 FIFO)
LinkedBlockingDeque 双向链表 阻塞双端队列

面试加分回答

“在实际项目中,关于 Queue 和 Deque 的选择:

  1. 需要 FIFO 队列:用 LinkedListArrayDeque
  2. 需要双端队列:用 ArrayDeque(效率比 LinkedList 高)
  3. 需要优先级队列:用 PriorityQueue(元素按优先级排序)
  4. 需要阻塞队列:用 LinkedBlockingQueueArrayBlockingQueue(多线程环境)”

Q28: Collections 工具类的常用方法?

一句话总结:Collections 是集合工具类,提供排序、反转、二分查找、同步化、不可变集合等静态方法。

深度解析

常用方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// 1. 排序
List<Integer> list = new ArrayList<>(Arrays.asList(3, 1, 2));
Collections.sort(list); // [1, 2, 3]
Collections.sort(list, Comparator.reverseOrder()); // 降序

// 2. 反转
Collections.reverse(list); // [3, 2, 1]

// 3. 二分查找(必须先排序)
Collections.sort(list);
int index = Collections.binarySearch(list, 2); // 1

// 4. 最大值、最小值
Integer max = Collections.max(list);
Integer min = Collections.min(list);

// 5. 填充
Collections.fill(list, 0); // [0, 0, 0]

// 6. 同步化(将线程不安全的集合转成线程安全的)
List<Integer> syncList = Collections.synchronizedList(list);
Map<String, Integer> syncMap = Collections.synchronizedMap(new HashMap<>());

// 7. 不可变集合
List<Integer> unmodifiableList = Collections.unmodifiableList(list); // 只读

Collections.synchronizedList() 的底层

  • synchronized 修饰所有方法(锁住整个 list)
  • 效率比 CopyOnWriteArrayList 低(读也需要获取锁)

面试加分回答

“在实际项目中,关于 Collections 工具类的 最佳实践

  1. 需要线程安全的 List:用 CopyOnWriteArrayList(读多写少场景)或 Collections.synchronizedList()(写多读少场景)
  2. 需要不可变集合:用 Collections.unmodifiableList() 或 Guava 的 ImmutableList
  3. 排序用 Collections.sort() 或 List.sort():JDK 8+ 使用 TimSort(归并 + 插入,稳定且高效)
  4. 大坑提醒Collections.synchronizedList() 的迭代需要手动加锁(否则可能 ConcurrentModificationException)”

Q29: Comparable 和 Comparator 的区别?

一句话总结:Comparable 是自然排序(类实现 compareTo() 方法,只能有一种排序规则);Comparator 是定制排序(传入 compare() 方法,可以有多种排序规则)。

深度解析

Comparable 接口

1
2
3
4
5
6
7
8
9
10
11
12
13
class Person implements Comparable<Person> {
private String name;
private int age;

@Override
public int compareTo(Person o) {
return this.age - o.age; // 按年龄升序(自然排序)
}
}

// 使用
List<Person> list = new ArrayList<>();
Collections.sort(list); // 按年龄升序

Comparator 接口

1
2
3
4
5
6
7
8
9
10
11
12
13
// 按年龄升序
Comparator<Person> byAge = (p1, p2) -> p1.getAge() - p2.getAge();
Collections.sort(list, byAge);

// 按姓名降序
Comparator<Person> byName = (p1, p2) -> p2.getName().compareTo(p1.getName());
Collections.sort(list, byName);

// 链式比较(先按年龄,再按姓名)
Comparator<Person> comparator = Comparator
.comparing(Person::getAge)
.thenComparing(Person::getName);
Collections.sort(list, comparator);

主要区别

对比项 Comparable Comparator
java.lang java.util
方法 compareTo() compare()
排序规则数量 1 种(自然排序) 多种(定制排序)
是否需要修改类

面试加分回答

“在实际项目中,关于 Comparable 和 Comparator 的选择:

  1. 需要自然排序:让类实现 Comparable(如 StringInteger 都实现了 Comparable)
  2. 需要多种排序规则:用 Comparator(不需要修改类)
  3. JDK 8+ 用 LambdaComparator.comparing(Person::getAge)(代码更简洁)
  4. 链式比较Comparator.comparing().thenComparing()(先按一个字段,再按另一个字段)”

Q30: fail-fast 机制是什么?

一句话总结:fail-fast 是快速失败机制(遍历集合时,如果集合被修改,立即抛出 ConcurrentModificationException)。

深度解析

fail-fast 的实现原理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// ArrayList 的迭代器
private class Itr implements Iterator<E> {
int expectedModCount = modCount; // 迭代器创建时记录 modCount

public E next() {
checkForComodification(); // 检查 modCount 是否被修改
...
}

final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException(); // 快速失败
}
}

触发 fail-fast 的场景

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
List<String> list = new ArrayList<>(Arrays.asList("a", "b", "c"));

// 场景 1:遍历时直接修改集合(会 fail-fast)
for (String s : list) {
if ("b".equals(s)) {
list.remove(s); // ConcurrentModificationException
}
}

// 场景 2:用迭代器修改集合(不会 fail-fast)
Iterator<String> it = list.iterator();
while (it.hasNext()) {
if ("b".equals(it.next())) {
it.remove(); // 正确!用迭代器删除
}
}

// 场景 3:JDK 8+ 用 removeIf()(不会 fail-fast)
list.removeIf("b"::equals);

fail-safe 机制(不失败):

  • CopyOnWriteArrayListConcurrentHashMap 等并发集合使用 fail-safe 机制
  • 遍历的是集合的副本(修改原集合不会影响遍历)

面试加分回答

“在实际项目中,关于 fail-fast 机制的 最佳实践

  1. 遍历时不要直接修改集合:用迭代器的 remove() 方法
  2. JDK 8+ 用 removeIf()list.removeIf("b"::equals)(代码更简洁)
  3. 多线程环境用并发集合CopyOnWriteArrayListConcurrentHashMap(fail-safe,不会抛异常)
  4. 大坑提醒for-each 底层是迭代器,遍历时直接修改集合会 fail-fast”


Q31: Checked 异常和 Unchecked 异常的区别?

一句话总结:Checked 异常必须显式捕获或声明抛出(Exception 子类,除 RuntimeException);Unchecked 异常不需要(RuntimeException 及其子类、Error)。

深度解析

异常体系结构

1
2
3
4
5
6
7
8
9
10
11
Throwable
├── Error(错误,不需要捕获,如 OutOfMemoryError)
└── Exception(异常,需要捕获或声明抛出)
├── RuntimeException(运行时异常,Unchecked)
│ ├── NullPointerException
│ ├── IndexOutOfBoundsException
│ └── ...
└── 其他 Exception(编译时异常,Checked)
├── IOException
├── SQLException
└── ...

Checked 异常(必须处理):

1
2
3
4
5
6
7
8
9
10
11
// 方式 1:try-catch 捕获
try {
FileInputStream fis = new FileInputStream("file.txt");
} catch (IOException e) {
e.printStackTrace();
}

// 方式 2:throws 声明抛出
public void readFile() throws IOException {
FileInputStream fis = new FileInputStream("file.txt");
}

Unchecked 异常(可以不处理):

1
2
3
4
5
// 不需要 try-catch,也不需要 throws 声明
public void method() {
String s = null;
System.out.println(s.length()); // NullPointerException(Unchecked)
}

使用场景

  • Checked 异常:调用者必须处理(如 IOExceptionSQLException
  • Unchecked 异常:程序 bug(如 NullPointerExceptionIndexOutOfBoundsException

面试加分回答

“在实际项目中,关于 Checked 和 Unchecked 异常的选择:

  1. 可恢复的错误用 Checked 异常:如 IOException(文件不存在,用户可以修复)
  2. 程序 bug 用 Unchecked 异常:如 IllegalArgumentException(参数错误,程序 bug)
  3. JDK 8+ 用 try-with-resources:自动释放资源(不需要手写 finally)
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
       try (FileInputStream fis = new FileInputStream("file.txt")) {
    // 使用 fis
    } catch (IOException e) {
    // 异常处理
    }
    ```"

    ---

    ### Q32: try-catch-finally 的执行顺序?

    **一句话总结**:`try` → `catch`(如果有异常)→ `finally`(无论是否异常都会执行)→ `return`(如果有的话)。

    #### 深度解析

    **正常执行(无异常)**:
    ```java
    try {
    System.out.println("try");
    } catch (Exception e) {
    System.out.println("catch");
    } finally {
    System.out.println("finally");
    }
    return;
    // 输出:try → finally

异常执行(有异常)

1
2
3
4
5
6
7
8
9
try {
System.out.println("try");
int i = 1 / 0; // ArithmeticException
} catch (Exception e) {
System.out.println("catch");
} finally {
System.out.println("finally");
}
// 输出:try → catch → finally

return 的执行时机(重要!面试高频):

1
2
3
4
5
6
7
8
9
10
11
12
public static int test() {
try {
System.out.println("try");
return 1; // 先执行 finally,再 return
} catch (Exception e) {
System.out.println("catch");
return 2;
} finally {
System.out.println("finally");
}
}
// 调用 test(),输出:try → finally → return 1

finally 不执行的情况

  1. System.exit() 退出 JVM
  2. 程序所在线程死亡
  3. 关闭 CPU(物理关机)

面试加分回答

“在实际项目中,关于 try-catch-finally 的 最佳实践

  1. 释放资源放在 finally 中:如关闭文件流、关闭数据库连接
  2. JDK 7+ 使用 try-with-resources:自动释放资源(不需要手写 finally)
  3. 不要在 finally 中使用 return:会覆盖 try 中的 return 值
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
       public static int test() {
    try {
    return 1;
    } finally {
    return 2; // 会覆盖 try 中的 return 1
    }
    }
    // 调用 test(),返回 2(finally 的 return 覆盖了 try 的 return)
    ```"

    ---

    ### Q33: throw 和 throws 的区别?

    **一句话总结**:`throw` 是**抛出异常**(在方法体内使用);`throws` 是**声明异常**(在方法签名上使用,表示该方法可能抛出的异常)。

    #### 深度解析

    **`throw` 的使用**:
    ```java
    public void method() {
    if (age < 0) {
    throw new IllegalArgumentException("年龄不能为负数"); // 抛出异常
    }
    }

throws 的使用

1
2
3
4
// 声明异常(该方法可能抛出 IOException,调用者必须处理)
public void readFile() throws IOException {
FileInputStream fis = new FileInputStream("file.txt");
}

同时使用 throwthrows

1
2
3
4
5
6
public void method() throws IOException {
if (fileNotExists) {
throw new IOException("文件不存在"); // throw 抛出异常
}
// 其他方法可能抛出 IOException(如 FileInputStream 构造器)
}

对比

对比项 throw throws
使用位置 方法体内 方法签名上
作用 抛出异常 声明异常(告诉调用者需要处理)
后面跟的内容 异常对象(new XXXException() 异常类(如 IOExceptionSQLException

面试加分回答

“在实际项目中,关于 throw 和 throws 的 最佳实践

  1. 自定义异常用 throw 抛出:如参数校验不通过时,抛出 IllegalArgumentException
  2. 受检异常用 throws 声明:如 IOExceptionSQLException(调用者必须处理)
  3. 运行时异常不需要 throws 声明:如 NullPointerExceptionIllegalArgumentException(Unchecked 异常)”

Q34: 自定义异常应该如何设计?

一句话总结:自定义异常应该继承 Exception(Checked 异常)或继承 RuntimeException**(Unchecked 异常),并提供多个构造器(无参、字符串、异常链)。

深度解析

自定义 Checked 异常

1
2
3
4
5
6
// 继承 Exception(Checked 异常,调用者必须处理)
public class BusinessException extends Exception {
public BusinessException() { super(); }
public BusinessException(String message) { super(message); }
public BusinessException(String message, Throwable cause) { super(message, cause); }
}

自定义 Unchecked 异常

1
2
3
4
5
6
// 继承 RuntimeException(Unchecked 异常,调用者可以不处理)
public class BusinessRuntimeException extends RuntimeException {
public BusinessRuntimeException() { super(); }
public BusinessRuntimeException(String message) { super(message); }
public BusinessRuntimeException(String message, Throwable cause) { super(message, cause); }
}

异常链(Exception Chaining)

1
2
3
4
5
6
try {
FileInputStream fis = new FileInputStream("file.txt");
} catch (IOException e) {
// 将 IOException 包装成 BusinessException(异常链)
throw new BusinessException("文件读取失败", e); // 保留原始异常信息
}

使用场景

  • 可恢复的错误:自定义 Checked 异常(如 BusinessException
  • 程序 bug:自定义 Unchecked 异常(如 IllegalArgumentException

面试加分回答

“在实际项目中,关于自定义异常的设计:

  1. 可恢复的错误用 Checked 异常:如 BusinessException(调用者必须处理)
  2. 程序 bug 用 Unchecked 异常:如 IllegalArgumentException(调用者可以不处理)
  3. 提供异常链构造器public BusinessException(String message, Throwable cause)(保留原始异常信息)
  4. 不要用异常控制业务流程:异常是用来处理错误情况的,不要用异常控制正常业务流程(如用异常做 if-else)”

Q35: 常见的 RuntimeException 有哪些?

一句话总结:常见的 RuntimeExceptionNullPointerExceptionIndexOutOfBoundsExceptionClassCastExceptionArithmeticExceptionIllegalArgumentException

深度解析

常见的 RuntimeException

异常类 触发场景 示例
NullPointerException 调用 null 对象的方法或字段 String s = null; s.length();
IndexOutOfBoundsException 数组或字符串索引越界 int[] arr = new int[10]; arr[10] = 1;
ClassCastException 类型转换失败 Object o = "abc"; Integer i = (Integer) o;
ArithmeticException 算术错误(如除以 0) int i = 1 / 0;
IllegalArgumentException 参数非法 new Thread(null);(参数不能为 null)
ConcurrentModificationException 遍历集合时修改集合 for (String s : list) { list.add("d"); }

如何避免 NullPointerException

1
2
3
4
5
6
7
8
9
10
11
// 1. 使用 Objects.requireNonNull()
public void method(String s) {
this.s = Objects.requireNonNull(s, "s 不能为 null");
}

// 2. 使用 Optional(JDK 8+)
Optional<String> opt = Optional.ofNullable(s);
opt.ifPresent(System.out::println);

// 3. 使用 @NotNull 和 @Nullable 注解(IDE 会检查)
public void method(@NotNull String s) { ... }

面试加分回答

“在实际项目中,关于 RuntimeException 的 最佳实践

  1. 避免 NullPointerException:使用 Objects.requireNonNull()Optional
  2. 遍历集合时不要修改集合:用迭代器的 remove() 方法,或用 JDK 8+ 的 removeIf()
  3. 参数校验用 IllegalArgumentException:如参数不能为 null、参数超出范围”

Q36: 什么是反射(Reflection)?

一句话总结:反射是在运行时动态获取类信息、调用方法、修改字段的机制(不需要提前知道类名)。

深度解析

反射的核心类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 1. 获取 Class 对象(三种方式)
Class<?> clazz1 = Person.class; // 方式 1:类名.class
Class<?> clazz2 = new Person().getClass(); // 方式 2:对象.getClass()
Class<?> clazz3 = Class.forName("com.example.Person"); // 方式 3:Class.forName()(最常用)

// 2. 获取构造器
Constructor<?> constructor = clazz.getConstructor(String.class, int.class);
Person person = (Person) constructor.newInstance("张三", 18);

// 3. 获取字段
Field field = clazz.getDeclaredField("name");
field.setAccessible(true); // 设置可访问(private 字段也可以访问)
field.set(person, "李四");

// 4. 获取方法
Method method = clazz.getDeclaredMethod("setName", String.class);
method.setAccessible(true); // 设置可访问(private 方法也可以调用)
method.invoke(person, "王五");

反射的使用场景

  1. 框架开发:Spring 的 IOC 容器(通过反射创建 Bean)
  2. 配置驱动:根据配置文件动态加载类(如 Class.forName("com.mysql.cj.jdbc.Driver"))
  3. 注解处理:运行时获取注解信息(如 Spring 的 @Autowired

面试加分回答

“在实际项目中,反射的 实际运用

  1. Spring 的 IOC 容器:通过反射创建 Bean(Class.forName() → 构造器.newInstance())
  2. JDBC 驱动加载Class.forName("com.mysql.cj.jdbc.Driver")(动态加载驱动类)
  3. JSON 反序列化:Jackson、Fastjson 通过反射将 JSON 字符串转换成 Java 对象
  4. 反射的性能问题:反射调用比直接调用慢(JVM 无法内联优化),可以用 setAccessible(true) 提高性能(跳过访问检查)”

Q37: 反射的优缺点?

一句话总结:反射的优点是灵活(运行时动态操作类);缺点是性能差(比直接调用慢)、安全性低(可以访问 private 成员)、代码复杂

深度解析

反射的优点

  1. 灵活:运行时动态操作类(不需要提前知道类名)
  2. 可扩展:通过配置文件动态加载类(不用修改代码)
  3. 框架基础:Spring、Hibernate 等框架都依赖反射

反射的缺点

  1. 性能差:反射调用比直接调用慢(JVM 无法内联优化)
    • 解决方法:用 setAccessible(true) 跳过访问检查(提高性能)
  2. 安全性低:可以访问 private 成员(破坏封装性)
    • 解决方法:使用 SecurityManager(限制反射操作)
  3. 代码复杂:反射代码比直接调用复杂(可读性差)

性能对比(大致数据):

调用方式 耗时(纳秒/次)
直接调用 ~ 10 ns
反射调用(不缓存) ~ 1000 ns(慢 100 倍)
反射调用(缓存 Method) ~ 100 ns(慢 10 倍)

面试加分回答

“在实际项目中,关于反射的 最佳实践

  1. 不要滥用反射:如果可以用直接调用,就不要使用反射(性能差)
  2. 缓存反射对象MethodFieldConstructor 都缓存起来(避免重复获取)
  3. 使用 setAccessible(true):提高反射性能(跳过访问检查)
  4. 框架才用反射:业务代码不要用反射(可读性差,性能差)”

Q38: 什么是注解(Annotation)?

一句话总结:注解是元数据(描述数据的数据),不直接影响代码执行,但可以被编译器、框架、工具读取并处理。

深度解析

常见的内置注解

1
2
3
4
5
6
7
8
9
10
11
// 1. @Override:检查是否重写了父类方法(编译器检查)
@Override
public void method() { ... }

// 2. @Deprecated:标记已过时的方法(编译器警告)
@Deprecated
public void oldMethod() { ... }

// 3. @SuppressWarnings:抑制编译器警告
@SuppressWarnings("unchecked")
public void method() { ... }

自定义注解

1
2
3
4
5
6
7
8
9
10
11
// 定义注解(@Target 指定使用位置,@Retention 指定保留范围)
@Target(ElementType.METHOD) // 只能用在方法上
@Retention(RetentionPolicy.RUNTIME) // 运行时保留(可以通过反射读取)
public @interface MyAnnotation {
String value() default ""; // 注解参数(可以有默认值)
int count() default 0;
}

// 使用注解
@MyAnnotation(value = "test", count = 10)
public void method() { ... }

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

元注解 作用
@Target 指定注解的使用位置(如 ElementType.METHODElementType.FIELD
@Retention 指定注解的保留范围(如 RetentionPolicy.RUNTIMERetentionPolicy.CLASS
@Documented 注解会被 javadoc 提取
@Inherited 子类会继承父类的注解

面试加分回答

“在实际项目中,关于注解的 实际运用

  1. Spring 的 @Autowired:自动注入 Bean(通过反射读取注解,然后注入)
  2. Spring MVC 的 @RequestMapping:映射 URL(通过反射读取注解,然后调用对应方法)
  3. Junit 的 @Test:标记测试方法(通过反射读取注解,然后执行测试方法)
  4. 自定义注解 + AOP:实现日志、权限控制(如 @Log@RequiresPermissions)”

Q39: 什么是泛型(Generics)?

一句话总结:泛型是参数化类型(将类型作为参数传递),可以在编译时检查类型安全,避免强制类型转换。

深度解析

泛型的优点

  1. 类型安全:编译时检查类型(避免运行时 ClassCastException
  2. 代码复用:同一个类/方法可以支持多种类型
  3. 可读性高:不需要强制类型转换

泛型类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 定义泛型类
public class Box<T> {
private T item;

public void setItem(T item) { this.item = item; }
public T getItem() { return item; }
}

// 使用泛型类
Box<String> box1 = new Box<>();
box1.setItem("abc"); // 只能放 String
String s = box1.getItem(); // 不需要强制类型转换

Box<Integer> box2 = new Box<>();
box2.setItem(100); // 只能放 Integer
Integer i = box2.getItem(); // 不需要强制类型转换

泛型方法

1
2
3
4
5
6
7
8
// 定义泛型方法
public <T> T getFirst(List<T> list) {
return list.get(0);
}

// 使用泛型方法
String s = getFirst(Arrays.asList("a", "b")); // T = String
Integer i = getFirst(Arrays.asList(1, 2)); // T = Integer

泛型接口

1
2
3
4
5
6
7
8
9
10
// 定义泛型接口
public interface MyList<T> {
void add(T item);
T get(int index);
}

// 实现泛型接口(指定具体类型)
public class MyArrayList implements MyList<String> {
// 只能处理 String 类型
}

面试加分回答

“在实际项目中,关于泛型的 最佳实践

  1. 集合框架都使用泛型:如 List<String>Map<String, Integer>(类型安全,避免强制类型转换)
  2. 不要用 raw type:如 List(raw type,没有泛型信息),应该用 List<Object>
  3. 泛型擦除:泛型信息只存在于编译时,运行时会被擦除(如 List<String>List<Integer> 的 Class 对象相同,都是 List.class)”

Q40: 泛型擦除(Type Erasure)是什么?

一句话总结:泛型擦除是编译器在编译时去掉泛型信息(将泛型类型替换成上界或 Object),运行时无法获取泛型的具体类型。

深度解析

泛型擦除的过程

1
2
3
4
5
6
7
8
9
// 源代码
List<String> list = new ArrayList<>();
list.add("abc");
String s = list.get(0);

// 编译后(泛型擦除)
List list = new ArrayList(); // 泛型信息被擦除
list.add("abc");
String s = (String) list.get(0); // 强制类型转换(编译器自动插入)

泛型擦除的证据

1
2
3
4
5
List<String> list1 = new ArrayList<>();
List<Integer> list2 = new ArrayList<>();

System.out.println(list1.getClass() == list2.getClass()); // true
// List<String> 和 List<Integer> 的 Class 对象相同(都是 List.class)

泛型擦除的补偿(不能用 instanceof 检查泛型类型):

1
2
3
4
5
// 错误:不能用 instanceof 检查泛型类型
if (list instanceof List<String>) { ... } // 编译错误!

// 正确:只能检查原始类型
if (list instanceof List) { ... }

泛型擦除的上界

1
2
3
4
5
6
7
// 没有指定上界,默认上界是 Object
public class Box<T> { ... } // 等价于 Box<T extends Object>

// 指定上界
public class Box<T extends Number> { ... } // T 必须是 Number 的子类
Box<Integer> box = new Box<>(); // OK
Box<String> box2 = new Box<>(); // 编译错误!String 不是 Number 的子类

面试加分回答

“在实际项目中,关于泛型擦除的 实际影响

  1. 不能用 instanceof 检查泛型类型:如 list instanceof List<String> 编译错误
  2. 不能创建泛型数组:如 new List<String>[10] 编译错误(因为泛型擦除后,List<String>[] 会变成 List[],类型不安全)
  3. 泛型信息只存在于编译时:运行时无法获取泛型的具体类型(可以用反射 + 通配符获取,如 TypeReference)”


Q41: 泛型通配符 ? 的作用?

一句话总结? 是无界通配符(表示任意类型);? extends T 是上界通配符(T 或 T 的子类);? super T 是下界通配符(T 或 T 的父类)。

深度解析

三种通配符

通配符 名称 说明 使用场景
<?> 无界通配符 任意类型 只读取,不写入
<? extends T> 上界通配符 T 或 T 的子类 生产者(只读取)
<? super T> 下界通配符 T 或 T 的父类 消费者(只写入)

<? extends T>(上界通配符)

1
2
3
4
// 可以读取,不能写入(除了 null)
List<? extends Number> list = new ArrayList<Integer>();
Number n = list.get(0); // OK,读取
list.add(100); // 编译错误!不能写入(除了 null)

<? super T>(下界通配符)

1
2
3
4
5
// 可以写入 T 或 T 的子类,读取只能是 Object
List<? super Integer> list = new ArrayList<Number>();
list.add(100); // OK,写入
list.add(200); // OK,写入
Object o = list.get(0); // OK,读取(但只能是 Object)

PECS 原则(Producer Extends, Consumer Super):

  • 生产者(只读取):用 ? extends T
  • 消费者(只写入):用 ? super T

面试加分回答

“在实际项目中,关于泛型通配符的 最佳实践

  1. 生产者用 extends:如 List<? extends Number>(只读取,不写入)
  2. 消费者用 super:如 List<? super Integer>(只写入,不读取)
  3. PECS 原则:Producer Extends, Consumer Super(生产者用 extends,消费者用 super)
  4. 不要用通配符作为返回类型:会让调用者很困惑(List<?> 作为返回类型)”

Q42: 为什么不能创建泛型数组?

一句话总结:因为泛型擦除,创建泛型数组会导致类型不安全(可以在数组中放入错误类型的元素)。

深度解析

为什么不能创建泛型数组?

1
2
3
4
5
6
// 编译错误!不能创建泛型数组
List<String>[] array = new List<String>[10]; // 编译错误

// 原因:泛型擦除后,List<String>[] 变成 List[],类型不安全
Object[] objArray = array; // 泛型擦除后,List<String>[] 变成 List[]
objArray[0] = new ArrayList<Integer>(); // 运行时才会发现类型错误(类型不安全)

为什么普通数组是类型安全的?

1
2
3
String[] strArray = new String[10];
Object[] objArray = strArray; // OK
objArray[0] = 100; // ArrayStoreException(编译时知道数组类型,类型安全)

解决方案

1
2
3
4
5
// 方案 1:使用 List(推荐)
List<List<String>> list = new ArrayList<>();

// 方案 2:使用通配符(不推荐)
List<?>[] array = new List<?>[10];

面试加分回答

“在实际项目中,关于泛型数组的 最佳实践

  1. 不要用泛型数组:用 List<List<String>> 代替 List<String>[]
  2. 泛型擦除导致类型不安全:创建泛型数组后,可以在数组中放入错误类型的元素(编译时不知道数组类型)
  3. 数组是协变的,泛型是不可变的String[]Object[] 的子类(协变),但 List<String> 不是 List<Object> 的子类(不可变)”

Q43: BIO、NIO、AIO 的区别?

一句话总结:BIO 是同步阻塞(一个连接一个线程);NIO 是同步非阻塞(一个线程管理多个连接);AIO 是异步非阻塞(操作完成后回调)。

深度解析

三种 IO 模型的对比

对比项 BIO NIO AIO
全称 Blocking I/O Non-blocking I/O Asynchronous I/O
阻塞性 同步阻塞 同步非阻塞 异步非阻塞
线程模型 一个连接一个线程 一个线程管理多个连接(多路复用) 操作完成后回调(不需要线程等待)
适用场景 连接数少且固定 连接数多且连接时间短(如聊天服务器) 连接数多且连接时间长(如文件读写)
编程难度 简单 复杂 复杂

BIO 的工作流程

1
客户端连接 → 服务端创建线程 → 线程阻塞等待数据 → 数据到达 → 处理 → 返回结果

NIO 的三大核心组件

  1. Channel(通道):数据读写的通道(如 SocketChannelServerSocketChannel
  2. Buffer(缓冲区):数据读写的容器(如 ByteBuffer
  3. Selector(选择器):多路复用器(一个线程管理多个 Channel)

AIO 的工作流程

1
客户端连接 → 服务端发起异步操作 → 操作完成后回调 → 处理 → 返回结果

面试加分回答

“在实际项目中,我们对 BIO/NIO/AIO 的选择:

  1. 连接数少且固定:用 BIO(如内部管理系统)
  2. 连接数多且连接时间短:用 NIO(如聊天服务器、网关)
  3. 连接数多且连接时间长:用 AIO(如文件读写、视频流传输)
  4. Netty 基于 NIO:Netty 是对 JDK NIO 的封装(性能更好,编程更简单)”

Q44: 什么是序列化和反序列化?

一句话总结:序列化是把对象转换成字节序列(方便存储或传输);反序列化是把字节序列转换成对象(恢复对象状态)。

深度解析

序列化的使用场景

  1. 网络传输:对象需要在网络中传输(如 RMI、Dubbo)
  2. 本地存储:对象需要持久化到磁盘(如 Session 持久化)
  3. 分布式缓存:对象需要存储到 Redis(需要序列化)

实现序列化

1
2
3
4
5
6
7
8
import java.io.Serializable;

public class Person implements Serializable {
private static final long serialVersionUID = 1L; // 序列化版本号(重要!)
private String name;
private int age;
private transient String password; // transient 修饰的字段不会序列化
}

序列化版本号 serialVersionUID 的作用

  • 如果序列化时 serialVersionUID 和反序列化时不同,会抛 InvalidClassException
  • 如果不显式声明,JVM 会根据类结构自动生成(类结构变化后,自动生成的 serialVersionUID 会变化,导致反序列化失败)

序列化的注意事项

  1. 显式声明 serialVersionUID:避免类结构变化后反序列化失败
  2. transient 修饰的字段不会序列化:如密码、敏感信息
  3. 静态字段不会序列化:静态字段属于类,不属于对象
  4. 父类需要实现 Serializable:否则父类字段不会序列化

面试加分回答

“在实际项目中,关于序列化的 最佳实践

  1. 显式声明 serialVersionUID:避免类结构变化后反序列化失败
  2. 敏感信息用 transient 修饰:如密码、Token(不会序列化)
  3. 选择高效的序列化框架:如 Protobuf、Kryo(比 JDK 自带的序列化效率高 10 倍以上)
  4. 注意循环引用:如果对象之间有循环引用,JDK 自带的序列化会栈溢出(需要用 JSON 或 Protobuf)”

Q45: transient 关键字的作用?

一句话总结transient 修饰的字段不会序列化(在序列化的过程中,该字段会被忽略)。

深度解析

transient 的使用场景

  1. 敏感信息:如密码、Token(不能序列化到磁盘或网络)
  2. 临时缓存:如本地缓存(不需要持久化)
  3. 大对象:如文件内容(序列化后体积太大,影响性能)

示例代码

1
2
3
4
5
6
public class Person implements Serializable {
private static final long serialVersionUID = 1L;
private String name; // 会序列化
private transient String password; // 不会序列化(敏感信息)
private transient Cache cache; // 不会序列化(临时缓存)
}

transient 的注意事项

  1. transient 只能修饰字段:不能修饰方法、类
  2. 反序列化后 transient 字段是默认值:如 null0false
  3. 静态字段不会序列化:不需要用 transient 修饰

自定义序列化(如果需要控制序列化过程):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class Person implements Serializable {
private transient String password; // 不想直接序列化

// 自定义序列化
private void writeObject(ObjectOutputStream out) throws IOException {
out.defaultWriteObject(); // 默认序列化
out.writeObject(encrypt(password)); // 加密后再序列化
}

// 自定义反序列化
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
in.defaultReadObject(); // 默认反序列化
this.password = decrypt((String) in.readObject()); // 解密
}
}

面试加分回答

“在实际项目中,关于 transient最佳实践

  1. 敏感信息用 transient 修饰:如密码、Token(不会序列化)
  2. 需要加密的字段:用 transient 修饰,然后在 writeObject() 中加密后再序列化
  3. 静态字段不需要 transient:静态字段属于类,不属于对象(不会序列化)
  4. 反序列化后 transient 字段是默认值:如 null0false(需要在 readObject() 中恢复)”

Q46: BigDecimal 为什么可以精确计算?

一句话总结BigDecimal 使用整数 + 小数位来表示小数(不会出现二进制无法精确表示的问题),适合金钱计算。

深度解析

为什么 float/double 精度丢失?

1
System.out.println(0.1 + 0.2);  // 0.30000000000000004(精度丢失)
  • 因为 float/double 是二进制表示,0.1 在二进制中是无限循环小数(无法精确表示)

BigDecimal 的精度原理

1
2
3
BigDecimal a = new BigDecimal("0.1");  // 用字符串构造(精确)
BigDecimal b = new BigDecimal("0.2");
System.out.println(a.add(b)); // 0.3(精确)

BigDecimal 的构造方式

1
2
3
4
5
6
7
8
// 方式 1:用字符串构造(推荐,精确)
BigDecimal a = new BigDecimal("0.1");

// 方式 2:用 double 构造(不推荐,精度丢失)
BigDecimal b = new BigDecimal(0.1); // 0.1 在二进制中是无限循环小数

// 方式 3:用 BigDecimal.valueOf(double)(推荐,精确)
BigDecimal c = BigDecimal.valueOf(0.1); // 内部转换成字符串

BigDecimal 的常用方法

1
2
3
4
5
6
7
8
BigDecimal a = new BigDecimal("0.1");
BigDecimal b = new BigDecimal("0.2");

a.add(b); // 加法
a.subtract(b); // 减法
a.multiply(b); // 乘法
a.divide(b, 2, RoundingMode.HALF_UP); // 除法(指定小数位数和舍入模式)
a.compareTo(b); // 比较(返回 -1、0、1)

面试加分回答

“在实际项目中,关于 BigDecimal最佳实践

  1. 金钱计算用 BigDecimal:不要用 float/double(精度丢失)
  2. 用字符串构造 BigDecimalnew BigDecimal("0.1")(不要用 new BigDecimal(0.1)
  3. 除法要指定舍入模式a.divide(b, 2, RoundingMode.HALF_UP)(否则除不尽会抛异常)
  4. 比较用 compareTo():不要用 equals()equals() 会比较小数位数,如 0.1 和 0.10 会返回 false)”

Q47: Java 中的 IO 流体系?

一句话总结:Java 的 IO 流分为字节流InputStream/OutputStream)和字符流Reader/Writer),字节流处理二进制数据,字符流处理文本数据。

深度解析

IO 流的四大抽象类

类型 输入流 输出流
字节流 InputStream OutputStream
字符流 Reader Writer

常见的 IO 流实现类

类别 字节流 字符流
文件流 FileInputStreamFileOutputStream FileReaderFileWriter
缓冲流 BufferedInputStreamBufferedOutputStream BufferedReaderBufferedWriter
对象流 ObjectInputStreamObjectOutputStream -
转换流 - InputStreamReaderOutputStreamWriter

字节流 vs 字符流

  • 字节流:处理二进制数据(如图片、视频、音频)
  • 字符流:处理文本数据(如 txt、java、xml)

缓冲流的作用

  • 减少 IO 次数(每次读写一个缓冲区,而不是一个字节)
  • 提高 IO 效率(如 BufferedInputStream 默认缓冲区 8KB)

面试加分回答

“在实际项目中,关于 IO 流的 最佳实践

  1. 处理二进制数据用字节流:如图片、视频、音频(不要用字符流,会乱码)
  2. 处理文本数据用字符流:如 txt、java、xml(字符流会自动处理编码)
  3. 用缓冲流包装:如 BufferedInputStream(减少 IO 次数,提高性能)
  4. 记得关闭流:用 try-with-resources(JDK 7+),不需要手写 finally

Q48: 字节流和字符流的区别?

一句话总结:字节流处理二进制数据(如图片、视频);字符流处理文本数据(如 txt、java),会自动处理编码。

深度解析

字节流(InputStream/OutputStream

1
2
3
4
5
6
7
8
9
// 字节流:处理二进制数据(如图片、视频)
try (InputStream in = new FileInputStream("input.jpg");
OutputStream out = new FileOutputStream("output.jpg")) {
byte[] buffer = new byte[1024];
int len;
while ((len = in.read(buffer)) != -1) {
out.write(buffer, 0, len);
}
}

字符流(Reader/Writer

1
2
3
4
5
6
7
8
9
// 字符流:处理文本数据(如 txt、java)
try (Reader reader = new FileReader("input.txt");
Writer writer = new FileWriter("output.txt")) {
char[] buffer = new char[1024];
int len;
while ((len = reader.read(buffer)) != -1) {
writer.write(buffer, 0, len);
}
}

转换流(InputStreamReader/OutputStreamWriter

1
2
3
4
5
6
7
8
// 将字节流转换成字符流(可以指定编码)
try (BufferedReader reader = new BufferedReader(
new InputStreamReader(new FileInputStream("input.txt"), StandardCharsets.UTF_8))) {
String line;
while ((line = reader.readLine()) != null) {
System.out.println(line);
}
}

面试加分回答

“在实际项目中,关于字节流和字符流的 最佳实践

  1. 处理二进制数据用字节流:如图片、视频、音频(不要用字符流,会乱码)
  2. 处理文本数据用字符流:如 txt、java、xml(字符流会自动处理编码)
  3. 用转换流指定编码:如 new InputStreamReader(in, StandardCharsets.UTF_8)(避免乱码)
  4. 用缓冲流包装:如 BufferedReaderBufferedInputStream(减少 IO 次数,提高性能)”

Q49: try-with-resources 是什么?

一句话总结try-with-resources 是 JDK 7 引入的语法糖,自动关闭资源(实现了 AutoCloseable 接口的对象),不需要手写 finally

深度解析

传统方式(JDK 6 及之前)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
InputStream in = null;
try {
in = new FileInputStream("file.txt");
// 使用 in
} catch (IOException e) {
e.printStackTrace();
} finally {
if (in != null) {
try {
in.close(); // 手动关闭资源
} catch (IOException e) {
e.printStackTrace();
}
}
}

try-with-resources 方式(JDK 7+)

1
2
3
4
5
try (InputStream in = new FileInputStream("file.txt")) {
// 使用 in
} catch (IOException e) {
e.printStackTrace();
} // 不需要手写 finally,in 会自动关闭

原理

  • 实现了 AutoCloseable 接口的对象,可以用 try-with-resources
  • 编译器会自动生成 finally 块,调用 close() 方法

多个资源

1
2
3
4
5
6
try (InputStream in = new FileInputStream("input.txt");
OutputStream out = new FileOutputStream("output.txt")) {
// 使用 in 和 out
} catch (IOException e) {
e.printStackTrace();
} // in 和 out 都会自动关闭(先关闭 out,再关闭 in)

面试加分回答

“在实际项目中,关于 try-with-resources最佳实践

  1. JDK 7+ 用 try-with-resources:不需要手写 finally(自动关闭资源)
  2. 多个资源用分号分隔:如 try (in; out) { ... }
  3. 实现 AutoCloseable 接口:自定义资源类可以实现 AutoCloseable 接口(用 try-with-resources 自动关闭)
  4. close() 方法不要抛异常:如果 close() 抛异常,会覆盖 try 块中的异常(用 try-with-resources 可以抑制 close() 的异常)”

Q50: Java 基础高频实战题总结

一句话总结:Java 基础的核心是集合框架、面向对象、异常处理、反射、注解、泛型、IO 流,这些是面试最高频的题目。

深度解析

Java 基础高频题分类

分类 高频题 重要程度
集合框架 HashMap、ConcurrentHashMap、ArrayList vs LinkedList ⭐⭐⭐⭐⭐
面向对象 重载 vs 重写、抽象类 vs 接口、多态 ⭐⭐⭐⭐⭐
异常处理 Checked vs Unchecked、try-catch-finally ⭐⭐⭐⭐
反射 Class.forName()、反射调用方法 ⭐⭐⭐
注解 内置注解、自定义注解、元注解 ⭐⭐⭐
泛型 泛型擦除、通配符、PECS 原则 ⭐⭐⭐⭐
IO 流 BIO vs NIO vs AIO、字节流 vs 字符流 ⭐⭐⭐

面试高频追问

  • Q1: HashMapHashtable 的区别?
    • A: HashMap 线程不安全、允许 null 键值;Hashtable 线程安全、不允许 null 键值(已过时,不推荐使用)。
  • Q2: ArrayListLinkedList 的区别?
    • A: ArrayList 基于动态数组(随机访问快,增删慢);LinkedList 基于双向链表(随机访问慢,增删快)。
  • Q3: 反射的优缺点?
    • A: 优点是灵活(运行时动态操作类);缺点是性能差(比直接调用慢)、安全性低(可以访问 private 成员)。

面试加分回答

“在实际项目中,Java 基础的 实际应用

  1. 集合框架HashMap 用于缓存、ArrayList 用于列表、ConcurrentHashMap 用于并发缓存
  2. 面向对象:抽象类用于模板方法模式、接口用于策略模式
  3. 异常处理:自定义业务异常(继承 ExceptionRuntimeException
  4. 反射:Spring 的 IOC 容器(通过反射创建 Bean)、Jackson(通过反射将 JSON 转换成 Java 对象)
  5. 泛型:集合框架都使用泛型(类型安全,避免强制类型转换)
  6. IO 流:文件上传用字节流、读取配置文件用字符流”


🎉 总结

本文详细讲解了 Java 基础 50 道高频面试题,从基础概念到高级特性,再到实战技巧,帮你系统掌握 Java 基础面试要点。

核心要点回顾

  1. 基础概念:JDK/JRE/JVM 的关系、编译型 vs 解释型语言
  2. 数据类型:8 种基础数据类型、Integer 缓存机制(-128~127)
  3. String 相关:String 不可变、String vs StringBuffer vs StringBuilder
  4. 集合框架:ArrayList vs LinkedList、HashMap vs ConcurrentHashMap
  5. 面向对象:重载 vs 重写、抽象类 vs 接口、封装/继承/多态
  6. 异常:Checked vs Unchecked、try-catch-finally 执行顺序
  7. 反射:Class.forName()、反射调用方法、反射的优缺点
  8. 注解:内置注解、自定义注解、元注解
  9. 泛型:泛型擦除、通配符(? extends T? super T)、PECS 原则
  10. IO 流:BIO vs NIO vs AIO、字节流 vs 字符流

下一步学习建议

  • 如果你对集合框架还不熟悉,先回头把 HashMap、ConcurrentHashMap 搞清楚
  • 如果你对面向对象感兴趣,可以深入研究设计模式(单例、工厂、策略)
  • 如果你准备面试,重点看 Q1-Q20(基础概念 + 集合框架)

📚 扩展学习资源

官方文档

书籍推荐

  • 📚 《Effective Java》(Joshua Bloch)— Java 圣经,必读
  • 📚 《Java 核心技术》(Core Java)— 基础全面
  • 📚 《深入理解 Java 虚拟机》— JVM 原理

博客推荐


🎉 恭喜你!学完本文后,你已经掌握了 Java 基础的 入门知识

下一步建议:

  1. 实战练习:自己写代码验证本文的示例(如 HashMap put 流程、反射调用方法)
  2. 深入研究:选一个方向(如集合框架、JVM 原理)深入研究
  3. 关注社区:订阅 Java 官方博客、Spring 官方博客,学习最新特性
  4. 准备面试:刷 LeetCode Java 题 + 本文的 50 道面试题

Java 基础面试八股文(50题)
https://whyalwaysme.lol/2026/06/15/2026-06-15-java-basic-interview-deep/
作者
Cassiur
发布于
2026年6月15日
许可协议