Java 基础面试八股文(50题)
Java 基础面试八股文(50 题)
📚 面试高频:本文覆盖 Java 基础核心知识点,从基础概念到高级特性,帮你系统掌握 Java 基础面试要点。
🎯 面试加分:每个问题都包含深度解析和面试加分回答,让你在面试中脱颖而出。
⚡ 快速掌握:所有问题都附有通俗易懂的解释和实战经验分享。
📖 学习指南
🎯 学习目标:通过本文,你将系统掌握 Java 基础的核心概念、底层原理和实战技巧,能够自信地应对任何 Java 基础相关的面试问题。
适合人群
- 🔰 初学者:想系统学习 Java 基础的开发者
- 🚀 有经验者:想深入理解 Java 原理的高级开发者
- 💼 面试准备者:想刷 Java 基础面试题的求职者
学习建议
- 先理解概念,再深入原理:先搞懂”是什么”,再搞懂”为什么”
- 结合实战场景:不要死记硬背,要理解实际应用场景
- 动手实践:在自己电脑上编写本文的示例代码
- 反复复习:面试前一周,每天复习 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 基础问题,不要只背概念,要结合实际项目经验回答。
回答思路
- 先说一句话总结(让面试官快速理解)
- 再深入解析(底层原理、源码分析)
- 最后面试加分回答(实际项目经验、踩过的坑)
举例说明
问题:String、StringBuffer、StringBuilder 的区别?
一句话总结:
- String 不可变(线程安全,适合字符串不常修改的场景)
- StringBuffer 可变、线程安全(适合多线程环境)
- StringBuilder 可变、线程不安全(适合单线程环境,效率最高)
面试加分回答:
“在实际项目中,字符串拼接用 StringBuilder(单线程效率高),多线程环境用 StringBuffer。不要直接用 String 拼接(会创建大量对象,内存浪费)。”
Q1: JDK、JRE、JVM 的区别?
一句话总结:JDK = JRE + 开发工具;JRE = JVM + 核心类库;JVM 是 Java 跨平台的基石。
深度解析
三者的关系:
1 | |
| 组件 | 全称 | 作用 | 面向人群 |
|---|---|---|---|
| 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-jre比jdk镜像小 100MB+
面试加分回答
“在实际项目中,我们对 JDK/JRE/JVM 的理解:
- 开发环境:安装 JDK(需要 javac 编译器)
- 生产环境:只需要 JRE(不需要编译器,减小镜像体积)
- Docker 镜像优化:使用
eclipse-temurin:17-jre基础镜像,比 JDK 镜像小 100MB+- JVM 调优:生产环境需要根据服务器内存调整 JVM 参数(
-Xms、-Xmx)”
Q2: Java 是编译型语言还是解释型语言?
一句话总结:Java 是半编译半解释型语言(先编译成字节码,再由 JVM 解释执行,热点代码由 JIT 编译成本地机器码)。
深度解析
编译型语言 vs 解释型语言:
| 类型 | 代表语言 | 优点 | 缺点 |
|---|---|---|---|
| 编译型 | C、C++ | 执行速度快(直接编译成机器码) | 跨平台需要重新编译 |
| 解释型 | Python、JavaScript | 跨平台(解释器直接执行源码) | 执行速度慢 |
Java 的编译 + 解释过程:
1 | |
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 |
注意事项:
boolean的大小:JVM 规范没有精确定义,通常 1 字节(Oracle JVM 实现)- 整型字面量默认是
int:long l = 10000000000L;需要加L后缀 - 浮点数字面量默认是
double:float f = 3.14f;需要加f后缀 char可以存储中文:因为 Java 使用 Unicode 编码(2 字节)
实际项目经验:
- 数据库 ID 用
Long(避免超过int上限) - 金钱计算用
BigDecimal(不要用float/double,精度丢失) - 布尔值用
boolean(不要用int的 0/1 表示)
面试加分回答
“在实际项目中,我们对基础数据类型的选择:
- 数据库 ID:用
Long(int上限 21 亿,不够用)- 金钱计算:用
BigDecimal(不要用float/double,会有精度丢失问题)- 布尔值:用
boolean(不要用int的 0/1 表示,降低代码可读性)- 字符存储:用
char或String(不要用byte,中文会乱码)”
Q4: 基础数据类型和包装类型的区别?
一句话总结:基础数据类型是值,包装类型是对象;基础数据类型存在栈中,包装类型存在堆中。
深度解析
主要区别:
| 对比项 | 基础数据类型 | 包装类型 |
|---|---|---|
| 存储位置 | 栈(局部变量) | 堆(对象) |
是否有 null 值 |
否(有默认值) | 是(可以为 null) |
| 是否包含方法 | 否 | 是(如 Integer.parseInt()) |
| 比较方式 | == 比较值 |
== 比较地址,equals() 比较值 |
| 效率 | 高(直接操作栈) | 低(需要堆内存分配、垃圾回收) |
自动装箱(Boxing)和自动拆箱(Unboxing):
1 | |
Integer 缓存机制(重要!面试高频):
1 | |
实际项目经验:
- 数据库查询的
NULL值需要用包装类型接收(如Integer、Long) - 不要用
==比较包装类型,要用equals() - 优先使用基础数据类型(效率高),除非需要表示
null值
面试加分回答
“在实际项目中,我们对基础数据类型和包装类型的选择:
- POJO 类的属性:用包装类型(如
Integer age,可以表示null)- 局部变量:用基础数据类型(效率高)
- 集合中的元素:只能用包装类型(集合不能存储基础数据类型)
Integer缓存:-128 ~ 127的整数会被缓存,比较时用equals()而不是==“
Q5: 什么是自动装箱和自动拆箱?
一句话总结:自动装箱是编译器自动将基础数据类型转换成包装类型(int → Integer);自动拆箱是编译器自动将包装类型转换成基础数据类型(Integer → int)。
深度解析
自动装箱(Autoboxing):
1 | |
自动拆箱(Unboxing):
1 | |
自动装箱/拆箱的使用场景:
1 | |
NullPointerException 陷阱(重要!):
1 | |
实际项目经验:
- 避免
Integer和int混用(容易NullPointerException) - 数据库查询返回
null时,自动拆箱会抛异常
面试加分回答
“在实际项目中,自动装箱/拆箱带来的
NullPointerException陷阱需要注意:
1
2Integer 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
30Integer 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 | |
重写 equals() 的规则(重要!):
- 自反性:
x.equals(x)必须返回true - 对称性:
x.equals(y)和y.equals(x)结果必须相同 - 传递性:如果
x.equals(y)和y.equals(z)都为true,则x.equals(z)必须为true - 一致性:多次调用
x.equals(y)结果必须相同 - 非空性:
x.equals(null)必须返回false
实际项目经验:
- 比较字符串内容用
equals(),不要用== - 比较包装类型内容用
equals(),不要用== - 重写
equals()必须重写hashCode()(否则 HashMap、HashSet 会出问题)
面试加分回答
“在实际项目中,关于
==和equals()的 最佳实践:
- 比较字符串内容:用
equals()(不要用==)- 比较包装类型内容:用
equals()(不要用==)- 重写
equals()必须重写hashCode():
- 如果两个对象
equals()返回true,则hashCode()必须相同- 如果
hashCode()相同,equals()不一定返回true(哈希冲突)- 不重写
hashCode()会导致 HashMap、HashSet 无法正常工作”
Q7: 为什么重写 equals() 必须重写 hashCode()?
一句话总结:因为 HashMap、HashSet 先比较 hashCode(),再比较 equals();如果只重写 equals() 不重写 hashCode(),会导致两个相等的对象被当作不相等的(存入 HashMap 时产生重复键)。
深度解析
hashCode() 的契约(Contract):
- 如果两个对象
equals()返回true,则hashCode()必须相同 - 如果
hashCode()相同,equals()不一定返回true(哈希冲突) - 多次调用同一个对象的
hashCode()必须返回相同的值(前提:对象未被修改)**
HashMap 的 put() 流程(重要!):
1 | |
只重写 equals() 不重写 hashCode() 的后果:
1 | |
正确的重写方式:
1 | |
面试加分回答
“在实际项目中,关于
equals()和hashCode()的 最佳实践:
- 使用 IDE 自动生成:IntelliJ IDEA 可以自动生成
equals()和hashCode()(Alt + Insert)- 使用相同的字段:
equals()和hashCode()使用相同的字段计算(否则会违反契约)- 使用
Objects.hash()计算 hashCode():避免手动计算哈希值(容易出错)- 不可变对象:如果对象是不可变的(所有字段是
final),可以缓存hashCode()值(提高性能)”
Q8: final 关键字的作用?
一句话总结:final 修饰变量表示常量(不可修改),修饰方法表示不可重写,修饰类表示不可继承。
深度解析
final 修饰变量:
1 | |
final 修饰方法:
1 | |
final 修饰类:
1 | |
final 的好处:
- 安全性:防止子类修改父类的关键方法(如
String不能被继承,防止绕过字符串常量池) - 性能优化:JVM 可以对
final方法进行内联优化(减少方法调用开销) - 线程安全:
final字段在构造函数中初始化后,其他线程可以看到(无需volatile)
实际项目经验:
- 工具类(如
Math、Arrays)应该声明为final(防止被继承) - 模板方法模式中的模板方法应该声明为
final(防止子类破坏模板流程)
面试加分回答
“在实际项目中,我们对
final的使用:
- 工具类声明为
final:如Math、Arrays(防止被继承)- 模板方法声明为
final:模板方法模式中的模板方法(防止子类破坏模板流程)final提高性能:JVM 可以对final方法进行内联优化(减少方法调用开销)String是final类:防止绕过字符串常量池(保证字符串常量池的安全性)”
Q9: final、finally、finalize 的区别?
一句话总结:final 修饰符(常量、不可重写、不可继承);finally 异常处理的 finally 块(无论是否抛异常都会执行);finalize() 是 Object 类的方法(垃圾回收前调用,已废弃)。
深度解析
final(修饰符):
- 修饰变量:常量(不可修改)
- 修饰方法:不可重写
- 修饰类:不可继承
finally(异常处理):
1 | |
finally 的执行时机(重要!面试高频):
1 | |
finalize()(方法,已废弃):
Object类的方法,垃圾回收前调用- 已废弃(JDK 9+):不确定何时调用,性能差,推荐使用
Cleaner或PhantomReference
finally 不执行的情况(重要!):
System.exit()退出 JVM- 程序所在线程死亡
- 关闭 CPU(物理关机)
面试加分回答
“在实际项目中,关于
finally的 最佳实践:
- 释放资源:
finally块中释放资源(如关闭文件流、关闭数据库连接)- JDK 7+ 使用 try-with-resources:自动释放资源(不需要手写
finally)
1
2
3
4
5
6try (FileInputStream fis = new FileInputStream("file.txt")) {
// 使用 fis
} catch (IOException e) {
// 异常处理
}
// 不需要 finally,fis 会自动关闭finalize()已废弃:不要重写finalize()方法(不确定何时调用,性能差)”
Q10: static 关键字的作用?
一句话总结:static 修饰的字段或方法属于类,而不是属于对象;静态字段在类加载时初始化,静态方法可以直接通过类名调用。
深度解析
static 修饰字段:
1 | |
static 修饰方法:
- 静态方法可以直接通过类名调用(不需要创建对象)
- 静态方法中不能使用
this(因为this代表当前对象,而静态方法属于类) - 静态方法中只能访问静态字段和静态方法
static 修饰代码块:
1 | |
static 的使用场景:
- 工具类的方法:如
Math.random()、Arrays.sort() - 常量:
public static final int MAX_SIZE = 100; - 单例模式:私有构造函数 + 静态方法返回实例
- 静态工厂方法:如
Integer.valueOf()、LocalDate.now()
面试加分回答
“在实际项目中,关于
static的 最佳实践:
- 工具类的方法声明为
static:如Math、Arrays(不需要创建对象,直接通过类名调用)- 常量声明为
static final:public static final String APP_NAME = "MyApp";- 单例模式使用
static:私有构造函数 + 静态方法返回实例- 静态代码块初始化静态字段:类加载时执行(只执行一次)”
Q11: 为什么 String 是不可变的?
一句话总结:String 不可变是因为内部使用 final char[](JDK 8 及之前)或 final byte[](JDK 9+)存储字符串,且没有提供修改内容的方法。
深度解析
String 不可变的底层实现(JDK 8):
1 | |
String 不可变的好处:
- 字符串常量池:相同的字符串字面量指向同一个对象(节省内存)
- 线程安全:不可变对象天生线程安全(不需要同步)
- 哈希码缓存:
hashCode()的结果可以缓存(提高 HashMap 性能) - 安全性:防止被篡改(如数据库用户名、密码)
字符串常量池(String Pool):
1 | |
JDK 9+ 的改进:
- JDK 8:
char value[](每个字符 2 字节) - JDK 9+:
byte value[](根据编码选择 1 或 2 字节,节省空间)
面试加分回答
“在实际项目中,String 不可变的好处:
- 字符串常量池:相同的字符串字面量指向同一个对象(节省内存)
- 线程安全:不可变对象天生线程安全(不需要同步)
- 哈希码缓存:
hashCode()的结果可以缓存(提高 HashMap 性能)- JDK 9+ 的改进:
char[]改成byte[](节省空间,根据编码选择 1 或 2 字节)”
Q12: String、StringBuffer、StringBuilder 的区别?
一句话总结:String 不可变(线程安全);StringBuffer 可变、线程安全(方法加 synchronized);StringBuilder 可变、线程不安全(效率高)。
深度解析
| 对比项 | String | StringBuffer | StringBuilder |
|---|---|---|---|
| 可变性 | 不可变 | 可变 | 可变 |
| 线程安全 | 是(不可变天生安全) | 是(方法加 synchronized) |
否 |
| 效率 | 低(每次修改创建新对象) | 中(同步开销) | 高(无同步开销) |
| 适用场景 | 字符串不常修改 | 多线程环境 | 单线程环境 |
使用场景:
1 | |
toString() 方法中的 StringBuilder:
1 | |
面试加分回答
“在实际项目中,我们对三者的选择:
- 字符串不常修改:用
String(如常量、配置项)- 单线程环境字符串拼接:用
StringBuilder(效率高)- 多线程环境字符串拼接:用
StringBuffer(线程安全)- 不要直接用 String 拼接:在循环中使用
String拼接会创建大量对象(内存浪费)”
Q13: String s = new String(“abc”) 创建了几个对象?
一句话总结:1 个或 2 个对象(如果字符串常量池中已有 “abc”,则创建 1 个;如果没有,则创建 2 个)。
深度解析
创建过程分析:
1 | |
示例代码:
1 | |
面试高频追问:
- Q:
String.intern()方法的作用?- A: 将字符串对象放入字符串常量池(如果常量池中已有,则返回常量池中的引用;如果没有,则放入常量池并返回引用)
面试加分回答
“在实际项目中,关于
new String("abc")的 最佳实践:
- 不要使用
new String("abc"):优先使用字面量String s = "abc"(利用字符串常量池,节省内存)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 | |
wait() 和 notify() 的使用:
1 | |
面试加分回答
“在实际项目中,关于 Object 类方法的 最佳实践:
- 重写
equals()必须重写hashCode():否则 HashMap、HashSet 会出问题- 重写
toString():便于日志输出和调试clone()不推荐使用:推荐使用拷贝构造函数或拷贝工厂方法
1
2
3
4class Person {
private String name;
public Person(Person other) { this.name = other.name; } // 拷贝构造函数
}finalize()已废弃:不推荐使用(不确定何时调用,性能差)”
Q15: Java 的 4 种访问修饰符?
一句话总结:private(当前类)→ default(同包)→ protected(同包 + 子类)→ public(任意地方)。
深度解析
4 种访问修饰符的访问范围:
| 修饰符 | 当前类 | 同包 | 子类 | 任意地方 |
|---|---|---|---|---|
private |
✅ | ❌ | ❌ | ❌ |
default(默认) |
✅ | ✅ | ❌ | ❌ |
protected |
✅ | ✅ | ✅ | ❌ |
public |
✅ | ✅ | ✅ | ✅ |
使用建议:
- 字段尽量用
private:封装性(外部不能直接访问) - 提供
getter/setter:控制字段的读写权限 - 方法尽量用
public:对外提供的 API - 工具类构造器用
private:防止被实例化(如Math、Arrays)
实际项目经验:
- 实体类(POJO)的字段用
private,提供getter/setter - 工具类的构造器用
private(如Math、Arrays) - 单例模式的构造器用
private(防止被实例化)
面试加分回答
“在实际项目中,关于访问修饰符的 最佳实践:
- 字段尽量用
private:封装性(外部不能直接访问)- 提供
getter/setter:控制字段的读写权限- 工具类构造器用
private:防止被实例化(如Math、Arrays)- 单例模式的构造器用
private:防止被实例化”
Q16: 接口和抽象类的区别?
一句话总结:抽象类只能单继承,可以有非抽象方法和字段;接口可以多实现,JDK 8+ 可以有 default 方法和静态方法,JDK 9+ 可以有 private 方法。
深度解析
| 对比项 | 抽象类 | 接口 |
|---|---|---|
| 继承/实现 | 单继承(extends) |
多实现(implements) |
| 字段 | 可以有任意字段 | 只能是 public static final 常量 |
| 方法 | 可以有抽象方法和非抽象方法 | JDK 8+ 可以有 default 方法和静态方法 |
| 构造器 | 有构造器(但不能实例化) | 没有构造器 |
| 适用场景 | “is-a” 关系(如 Animal → Dog) |
“can-do” 关系(如 Runnable、Serializable) |
JDK 8+ 接口的改进:
1 | |
实际项目经验:
- 抽象类:用于”模板方法模式”(定义算法骨架,子类实现具体步骤)
- 接口:用于定义规范(如
Service层接口、DAO层接口)
面试加分回答
“在实际项目中,我们对抽象类和接口的选择:
- “is-a” 关系用抽象类:如
Animal→Dog(狗是动物)- “can-do” 关系用接口:如
Runnable(可以运行)、Serializable(可以序列化)- 需要定义规范用接口:如
Service层接口、DAO层接口- 需要提供默认实现用
default方法:JDK 8+ 接口可以有default方法(有方法体,子类可以不重写)”
Q17: 重载(Overload)和重写(Override)的区别?
一句话总结:重载是同一个类中方法名相同、参数不同(编译时多态);重写是子类中方法签名相同、方法体不同(运行时多态)。
深度解析
重载(Overload):
- 发生在同一个类中
- 方法名相同,参数列表不同(参数类型、参数个数、参数顺序)
- 返回类型可以不同
- 访问修饰符可以不同
- 编译时多态(编译器根据方法签名确定调用哪个方法)
1 | |
重写(Override):
- 发生在父子类之间
- 方法签名相同(方法名、参数列表、返回类型)
- 访问修饰符不能比父类更严格(如父类是
public,子类不能是protected) - 异常不能比父类更广泛(如父类抛出
IOException,子类不能抛出Exception) - 运行时多态(JVM 根据对象实际类型调用对应方法)
1 | |
@Override 注解的作用:
- 编译器检查是否真的重写了父类方法(防止拼写错误)
- 提高代码可读性(明确这是重写方法)
面试加分回答
“在实际项目中,关于重载和重写的 最佳实践:
- 重写方法加
@Override注解:编译器检查是否真的重写了父类方法(防止拼写错误)- 重载的用途:构造函数重载(提供多个构造函数)、方法重载(提供多个参数列表)
- 重写的用途:模板方法模式(父类定义算法骨架,子类实现具体步骤)”
Q18: 构造器(Constructor)是否可以被重写?
一句话总结:构造器不能被重写(因为构造器名称必须与类名相同,子类和父类类名不同),但可以被重载。
深度解析
为什么构造器不能被重写?
- 重写要求方法签名相同(方法名、参数列表、返回类型)
- 构造器名称必须与类名相同
- 子类和父类的类名不同 → 构造器名称不同 → 不满足重写条件
构造器可以被重载:
1 | |
子类构造器默认调用父类无参构造器:
1 | |
如果父类没有无参构造器,子类必须显式调用 super(参数):
1 | |
面试加分回答
“在实际项目中,关于构造器的 最佳实践:
- 提供无参构造器:防止子类构造器编译错误(如果父类没有无参构造器,子类必须显式调用
super(参数))- 构造器中不要调用可被重写的方法:因为子类对象初始化时,父类构造器先执行,此时子类重写的方法会被调用(容易导致空指针异常)
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
31class 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 | |
this() 和 super() 不能同时出现:
- 因为
this()和super()都必须在构造器的第一行 - 如果同时出现,编译器不知道哪一个是第一行
面试加分回答
“在实际项目中,关于
this和super的 最佳实践:
this()和super()不能同时出现:因为都必须在构造器的第一行- 使用
this区分字段和参数:当字段和参数同名时,用this.field区分- 重写方法中使用
super调用父类方法:如toString()中调用super.toString()“
Q20: 面向对象的三大特性?
一句话总结:封装(隐藏实现细节,提供公共访问方式);继承(子类复用父类代码,单继承);多态(父类引用指向子类对象,运行时动态绑定)。
深度解析
1. 封装(Encapsulation):
- 核心思想:隐藏实现细节,提供公共访问方式
- 实现方式:字段用
private修饰,提供public的getter/setter - 好处:提高安全性(外部不能直接访问字段)、提高代码复用性
1 | |
2. 继承(Inheritance):
- 核心思想:子类复用父类代码
- 实现方式:
class Child extends Parent - 限制:Java 只支持单继承(一个类只能有一个直接父类)
- 好处:代码复用、方法重写(运行时多态)
1 | |
3. 多态(Polymorphism):
- 核心思想:父类引用指向子类对象,运行时动态绑定
- 前提条件:继承 + 重写 + 父类引用指向子类对象
- 好处:提高代码扩展性(新增子类不需要修改父类代码)
1 | |
面试加分回答
“在实际项目中,面向对象三大特性的 实际运用:
- 封装:实体类(POJO)的字段用
private,提供getter/setter- 继承:
BaseController(基础控制器)、BaseService(基础服务层)- 多态:策略模式(不同的策略实现同一个接口,运行时动态选择策略)”
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 | |
LinkedList 的双向链表结构:
1 | |
使用场景:
- ArrayList:需要频繁随机访问(如
get(i)),增删不多 - LinkedList:需要频繁在头部/中部增删,随机访问不多
面试加分回答
“在实际项目中,我们对 ArrayList 和 LinkedList 的选择:
- 需要随机访问:用 ArrayList(如根据索引获取元素)
- 需要频繁在头部增删:用 LinkedList(如实现队列、栈)
- 需要频繁在尾部增删:两者都可以(ArrayList 更高效,均摊 O(1))
- 大坑提醒:ArrayList 扩容会复制数组(耗时),初始化时尽量指定容量(
new ArrayList<>(1000))”
Q22: HashMap 的底层原理(JDK 7 vs JDK 8)?
一句话总结:JDK 7 使用数组 + 链表(头插法,多线程扩容可能死循环);JDK 8 使用数组 + 链表 + 红黑树(尾插法,链表长度 > 8 且数组长度 ≥ 64 时转红黑树)。
深度解析
JDK 7 的 HashMap:
1 | |
- 扩容死循环问题:多线程同时扩容,头插法可能导致链表成环(CPU 100%)
JDK 8 的 HashMap:
1 | |
- 负载因子 0.75:平衡空间和时间成本
- 扩容阈值 = 容量 × 0.75(如容量 16,阈值 12)
- 链表 → 红黑树条件:链表长度 > 8 且 数组长度 ≥ 64
- 红黑树 → 链表条件:红黑树节点数 < 6
put() 流程(JDK 8):
1 | |
面试加分回答
“在实际项目中,关于 HashMap 的 最佳实践:
- 初始化时指定容量:
new HashMap<>(1024)(避免频繁扩容)- 不要用可变对象作为 key:如
new HashMap<List, String>()(List 内容变了,hashCode() 也变,找不到 value)- 多线程环境用 ConcurrentHashMap:HashMap 线程不安全
- 为什么负载因子是 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 的选择:
- 单线程环境:用 HashMap
- 多线程环境:用 ConcurrentHashMap(不要用 Hashtable,已过时)
- 需要线程安全的 Map:用 ConcurrentHashMap 或
Collections.synchronizedMap()- Hashtable 已过时:不推荐使用(效率低,已有更好的替代方案)”
Q24: ConcurrentHashMap 的底层原理(JDK 7 vs JDK 8)?
一句话总结:JDK 7 使用分段锁(Segment);JDK 8 使用 CAS + synchronized(锁住桶的头节点,并发度更高)。
深度解析
JDK 7 的 ConcurrentHashMap:
1 | |
- 并发度:默认 16(可以同时有 16 个线程操作不同的段)
- 缺点:段数量固定,扩容时需要重新计算哈希(耗时)
JDK 8 的 ConcurrentHashMap:
1 | |
- 锁机制:CAS + synchronized(只锁住桶的头节点,并发度更高)
put()流程:- 如果桶为空,用 CAS 插入(无锁)
- 如果桶不为空,用
synchronized锁住桶的头节点 - 插入成功后,如果链表长度 > 8,转为红黑树
为什么 JDK 8 放弃分段锁?
- 分段锁的段数量固定(并发度固定为 16)
- CAS + synchronized 的并发度更高(只锁住桶的头节点)
面试加分回答
“在实际项目中,关于 ConcurrentHashMap 的 最佳实践:
- JDK 7 用分段锁:默认 16 个段,并发度 16
- JDK 8 用 CAS + synchronized:只锁住桶的头节点,并发度更高
- 初始化时指定容量:
new ConcurrentHashMap<>(1024)(避免频繁扩容)- 为什么 JDK 8 放弃分段锁?:分段锁的并发度固定(16),CAS + synchronized 并发度更高”
Q25: HashSet 的底层原理?
一句话总结:HashSet 底层使用 HashMap(元素作为 HashMap 的 key,value 是一个固定的 PRESENT 对象)。
深度解析
HashSet 的底层实现:
1 | |
HashSet 的特点:
- 元素不重复:因为 HashMap 的 key 不重复
- 允许 null:因为 HashMap 允许 null 键
- 无序:因为 HashMap 无序
- 非线程安全:和 HashMap 一样
LinkedHashSet:
- 继承 HashSet
- 底层使用
LinkedHashMap(维护插入顺序)
TreeSet:
- 底层使用
TreeMap(红黑树,按元素大小排序) - 元素必须实现
Comparable接口,或在构造时传入Comparator
面试加分回答
“在实际项目中,关于 Set 的选择:
- 需要去重:用 HashSet(底层 HashMap,效率高)
- 需要维护插入顺序:用 LinkedHashSet(底层 LinkedHashMap)
- 需要排序:用 TreeSet(底层 TreeMap,红黑树)
- 大坑提醒:自定义对象作为 HashSet 的元素,必须重写
equals()和hashCode()(否则去重失效)”
Q26: HashMap 为什么线程不安全?
一句话总结:HashMap 在并发场景下可能出现数据覆盖、死循环(JDK 7)、数据丢失等问题,因为 put() 操作不是原子操作。
深度解析
JDK 7 的死循环问题(扩容时):
1 | |
JDK 8 的数据覆盖问题:
1 | |
并发场景下的具体问题:
| 问题 | JDK 7 | JDK 8 |
|---|---|---|
| 死循环 | ✅(头插法扩容) | ❌(尾插法) |
| 数据覆盖 | ✅ | ✅ |
| 数据丢失 | ✅ | ✅ |
| size 不准确 | ✅ | ✅ |
面试加分回答
“在实际项目中,关于 HashMap 线程不安全的问题:
- JDK 7 有死循环问题:因为头插法扩容,多线程可能让链表成环(CPU 100%)
- JDK 8 修复了死循环问题:改用尾插法,但仍有数据覆盖问题
- 多线程环境用 ConcurrentHashMap:不要用 HashMap(线程不安全)
- 需要用 Collections.synchronizedMap():如果只是偶尔并发,可以用这个(性能比 ConcurrentHashMap 差)”
Q27: Queue 和 Deque 的区别?
一句话总结:Queue 是单向队列(FIFO,队尾插入,队头取出);Deque 是双端队列(队头和队尾都可以插入和取出)。
深度解析
Queue 接口的方法:
1 | |
Deque 接口的方法:
1 | |
常见实现类:
| 实现类 | 底层结构 | 是否线程安全 | 特点 |
|---|---|---|---|
LinkedList |
双向链表 | 否 | 可以作为 Queue 或 Deque |
ArrayDeque |
动态数组 | 否 | 效率比 LinkedList 高(数组随机访问) |
PriorityQueue |
优先堆 | 否 | 元素按优先级排序(不是 FIFO) |
LinkedBlockingDeque |
双向链表 | 是 | 阻塞双端队列 |
面试加分回答
“在实际项目中,关于 Queue 和 Deque 的选择:
- 需要 FIFO 队列:用
LinkedList或ArrayDeque- 需要双端队列:用
ArrayDeque(效率比 LinkedList 高)- 需要优先级队列:用
PriorityQueue(元素按优先级排序)- 需要阻塞队列:用
LinkedBlockingQueue或ArrayBlockingQueue(多线程环境)”
Q28: Collections 工具类的常用方法?
一句话总结:Collections 是集合工具类,提供排序、反转、二分查找、同步化、不可变集合等静态方法。
深度解析
常用方法:
1 | |
Collections.synchronizedList() 的底层:
- 用
synchronized修饰所有方法(锁住整个 list) - 效率比
CopyOnWriteArrayList低(读也需要获取锁)
面试加分回答
“在实际项目中,关于 Collections 工具类的 最佳实践:
- 需要线程安全的 List:用
CopyOnWriteArrayList(读多写少场景)或Collections.synchronizedList()(写多读少场景)- 需要不可变集合:用
Collections.unmodifiableList()或 Guava 的ImmutableList- 排序用 Collections.sort() 或 List.sort():JDK 8+ 使用 TimSort(归并 + 插入,稳定且高效)
- 大坑提醒:
Collections.synchronizedList()的迭代需要手动加锁(否则可能ConcurrentModificationException)”
Q29: Comparable 和 Comparator 的区别?
一句话总结:Comparable 是自然排序(类实现 compareTo() 方法,只能有一种排序规则);Comparator 是定制排序(传入 compare() 方法,可以有多种排序规则)。
深度解析
Comparable 接口:
1 | |
Comparator 接口:
1 | |
主要区别:
| 对比项 | Comparable | Comparator |
|---|---|---|
| 包 | java.lang |
java.util |
| 方法 | compareTo() |
compare() |
| 排序规则数量 | 1 种(自然排序) | 多种(定制排序) |
| 是否需要修改类 | 是 | 否 |
面试加分回答
“在实际项目中,关于 Comparable 和 Comparator 的选择:
- 需要自然排序:让类实现 Comparable(如
String、Integer都实现了 Comparable)- 需要多种排序规则:用 Comparator(不需要修改类)
- JDK 8+ 用 Lambda:
Comparator.comparing(Person::getAge)(代码更简洁)- 链式比较:
Comparator.comparing().thenComparing()(先按一个字段,再按另一个字段)”
Q30: fail-fast 机制是什么?
一句话总结:fail-fast 是快速失败机制(遍历集合时,如果集合被修改,立即抛出 ConcurrentModificationException)。
深度解析
fail-fast 的实现原理:
1 | |
触发 fail-fast 的场景:
1 | |
fail-safe 机制(不失败):
CopyOnWriteArrayList、ConcurrentHashMap等并发集合使用 fail-safe 机制- 遍历的是集合的副本(修改原集合不会影响遍历)
面试加分回答
“在实际项目中,关于 fail-fast 机制的 最佳实践:
- 遍历时不要直接修改集合:用迭代器的
remove()方法- JDK 8+ 用 removeIf():
list.removeIf("b"::equals)(代码更简洁)- 多线程环境用并发集合:
CopyOnWriteArrayList、ConcurrentHashMap(fail-safe,不会抛异常)- 大坑提醒:
for-each底层是迭代器,遍历时直接修改集合会 fail-fast”
Q31: Checked 异常和 Unchecked 异常的区别?
一句话总结:Checked 异常必须显式捕获或声明抛出(Exception 子类,除 RuntimeException);Unchecked 异常不需要(RuntimeException 及其子类、Error)。
深度解析
异常体系结构:
1 | |
Checked 异常(必须处理):
1 | |
Unchecked 异常(可以不处理):
1 | |
使用场景:
- Checked 异常:调用者必须处理(如
IOException、SQLException) - Unchecked 异常:程序 bug(如
NullPointerException、IndexOutOfBoundsException)
面试加分回答
“在实际项目中,关于 Checked 和 Unchecked 异常的选择:
- 可恢复的错误用 Checked 异常:如
IOException(文件不存在,用户可以修复)- 程序 bug 用 Unchecked 异常:如
IllegalArgumentException(参数错误,程序 bug)- 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
26try (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 | |
return 的执行时机(重要!面试高频):
1 | |
finally 不执行的情况:
System.exit()退出 JVM- 程序所在线程死亡
- 关闭 CPU(物理关机)
面试加分回答
“在实际项目中,关于 try-catch-finally 的 最佳实践:
- 释放资源放在 finally 中:如关闭文件流、关闭数据库连接
- JDK 7+ 使用 try-with-resources:自动释放资源(不需要手写 finally)
- 不要在 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
25public 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 | |
同时使用 throw 和 throws:
1 | |
对比:
| 对比项 | throw |
throws |
|---|---|---|
| 使用位置 | 方法体内 | 方法签名上 |
| 作用 | 抛出异常 | 声明异常(告诉调用者需要处理) |
| 后面跟的内容 | 异常对象(new XXXException()) |
异常类(如 IOException、SQLException) |
面试加分回答
“在实际项目中,关于 throw 和 throws 的 最佳实践:
- 自定义异常用
throw抛出:如参数校验不通过时,抛出IllegalArgumentException- 受检异常用
throws声明:如IOException、SQLException(调用者必须处理)- 运行时异常不需要
throws声明:如NullPointerException、IllegalArgumentException(Unchecked 异常)”
Q34: 自定义异常应该如何设计?
一句话总结:自定义异常应该继承 Exception(Checked 异常)或继承 RuntimeException**(Unchecked 异常),并提供多个构造器(无参、字符串、异常链)。
深度解析
自定义 Checked 异常:
1 | |
自定义 Unchecked 异常:
1 | |
异常链(Exception Chaining):
1 | |
使用场景:
- 可恢复的错误:自定义 Checked 异常(如
BusinessException) - 程序 bug:自定义 Unchecked 异常(如
IllegalArgumentException)
面试加分回答
“在实际项目中,关于自定义异常的设计:
- 可恢复的错误用 Checked 异常:如
BusinessException(调用者必须处理)- 程序 bug 用 Unchecked 异常:如
IllegalArgumentException(调用者可以不处理)- 提供异常链构造器:
public BusinessException(String message, Throwable cause)(保留原始异常信息)- 不要用异常控制业务流程:异常是用来处理错误情况的,不要用异常控制正常业务流程(如用异常做 if-else)”
Q35: 常见的 RuntimeException 有哪些?
一句话总结:常见的 RuntimeException 有 NullPointerException、IndexOutOfBoundsException、ClassCastException、ArithmeticException、IllegalArgumentException。
深度解析
常见的 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 | |
面试加分回答
“在实际项目中,关于 RuntimeException 的 最佳实践:
- 避免
NullPointerException:使用Objects.requireNonNull()或Optional- 遍历集合时不要修改集合:用迭代器的
remove()方法,或用 JDK 8+ 的removeIf()- 参数校验用
IllegalArgumentException:如参数不能为 null、参数超出范围”
Q36: 什么是反射(Reflection)?
一句话总结:反射是在运行时动态获取类信息、调用方法、修改字段的机制(不需要提前知道类名)。
深度解析
反射的核心类:
1 | |
反射的使用场景:
- 框架开发:Spring 的 IOC 容器(通过反射创建 Bean)
- 配置驱动:根据配置文件动态加载类(如
Class.forName("com.mysql.cj.jdbc.Driver")) - 注解处理:运行时获取注解信息(如 Spring 的
@Autowired)
面试加分回答
“在实际项目中,反射的 实际运用:
- Spring 的 IOC 容器:通过反射创建 Bean(
Class.forName()→ 构造器.newInstance())- JDBC 驱动加载:
Class.forName("com.mysql.cj.jdbc.Driver")(动态加载驱动类)- JSON 反序列化:Jackson、Fastjson 通过反射将 JSON 字符串转换成 Java 对象
- 反射的性能问题:反射调用比直接调用慢(JVM 无法内联优化),可以用
setAccessible(true)提高性能(跳过访问检查)”
Q37: 反射的优缺点?
一句话总结:反射的优点是灵活(运行时动态操作类);缺点是性能差(比直接调用慢)、安全性低(可以访问 private 成员)、代码复杂。
深度解析
反射的优点:
- 灵活:运行时动态操作类(不需要提前知道类名)
- 可扩展:通过配置文件动态加载类(不用修改代码)
- 框架基础:Spring、Hibernate 等框架都依赖反射
反射的缺点:
- 性能差:反射调用比直接调用慢(JVM 无法内联优化)
- 解决方法:用
setAccessible(true)跳过访问检查(提高性能)
- 解决方法:用
- 安全性低:可以访问 private 成员(破坏封装性)
- 解决方法:使用
SecurityManager(限制反射操作)
- 解决方法:使用
- 代码复杂:反射代码比直接调用复杂(可读性差)
性能对比(大致数据):
| 调用方式 | 耗时(纳秒/次) |
|---|---|
| 直接调用 | ~ 10 ns |
| 反射调用(不缓存) | ~ 1000 ns(慢 100 倍) |
| 反射调用(缓存 Method) | ~ 100 ns(慢 10 倍) |
面试加分回答
“在实际项目中,关于反射的 最佳实践:
- 不要滥用反射:如果可以用直接调用,就不要使用反射(性能差)
- 缓存反射对象:
Method、Field、Constructor都缓存起来(避免重复获取)- 使用
setAccessible(true):提高反射性能(跳过访问检查)- 框架才用反射:业务代码不要用反射(可读性差,性能差)”
Q38: 什么是注解(Annotation)?
一句话总结:注解是元数据(描述数据的数据),不直接影响代码执行,但可以被编译器、框架、工具读取并处理。
深度解析
常见的内置注解:
1 | |
自定义注解:
1 | |
元注解(修饰注解的注解):
| 元注解 | 作用 |
|---|---|
@Target |
指定注解的使用位置(如 ElementType.METHOD、ElementType.FIELD) |
@Retention |
指定注解的保留范围(如 RetentionPolicy.RUNTIME、RetentionPolicy.CLASS) |
@Documented |
注解会被 javadoc 提取 |
@Inherited |
子类会继承父类的注解 |
面试加分回答
“在实际项目中,关于注解的 实际运用:
- Spring 的
@Autowired:自动注入 Bean(通过反射读取注解,然后注入)- Spring MVC 的
@RequestMapping:映射 URL(通过反射读取注解,然后调用对应方法)- Junit 的
@Test:标记测试方法(通过反射读取注解,然后执行测试方法)- 自定义注解 + AOP:实现日志、权限控制(如
@Log、@RequiresPermissions)”
Q39: 什么是泛型(Generics)?
一句话总结:泛型是参数化类型(将类型作为参数传递),可以在编译时检查类型安全,避免强制类型转换。
深度解析
泛型的优点:
- 类型安全:编译时检查类型(避免运行时
ClassCastException) - 代码复用:同一个类/方法可以支持多种类型
- 可读性高:不需要强制类型转换
泛型类:
1 | |
泛型方法:
1 | |
泛型接口:
1 | |
面试加分回答
“在实际项目中,关于泛型的 最佳实践:
- 集合框架都使用泛型:如
List<String>、Map<String, Integer>(类型安全,避免强制类型转换)- 不要用 raw type:如
List(raw type,没有泛型信息),应该用List<Object>- 泛型擦除:泛型信息只存在于编译时,运行时会被擦除(如
List<String>和List<Integer>的 Class 对象相同,都是List.class)”
Q40: 泛型擦除(Type Erasure)是什么?
一句话总结:泛型擦除是编译器在编译时去掉泛型信息(将泛型类型替换成上界或 Object),运行时无法获取泛型的具体类型。
深度解析
泛型擦除的过程:
1 | |
泛型擦除的证据:
1 | |
泛型擦除的补偿(不能用 instanceof 检查泛型类型):
1 | |
泛型擦除的上界:
1 | |
面试加分回答
“在实际项目中,关于泛型擦除的 实际影响:
- 不能用
instanceof检查泛型类型:如list instanceof List<String>编译错误- 不能创建泛型数组:如
new List<String>[10]编译错误(因为泛型擦除后,List<String>[]会变成List[],类型不安全)- 泛型信息只存在于编译时:运行时无法获取泛型的具体类型(可以用反射 + 通配符获取,如
TypeReference)”
Q41: 泛型通配符 ? 的作用?
一句话总结:? 是无界通配符(表示任意类型);? extends T 是上界通配符(T 或 T 的子类);? super T 是下界通配符(T 或 T 的父类)。
深度解析
三种通配符:
| 通配符 | 名称 | 说明 | 使用场景 |
|---|---|---|---|
<?> |
无界通配符 | 任意类型 | 只读取,不写入 |
<? extends T> |
上界通配符 | T 或 T 的子类 | 生产者(只读取) |
<? super T> |
下界通配符 | T 或 T 的父类 | 消费者(只写入) |
<? extends T>(上界通配符):
1 | |
<? super T>(下界通配符):
1 | |
PECS 原则(Producer Extends, Consumer Super):
- 生产者(只读取):用
? extends T - 消费者(只写入):用
? super T
面试加分回答
“在实际项目中,关于泛型通配符的 最佳实践:
- 生产者用
extends:如List<? extends Number>(只读取,不写入)- 消费者用
super:如List<? super Integer>(只写入,不读取)- PECS 原则:Producer Extends, Consumer Super(生产者用 extends,消费者用 super)
- 不要用通配符作为返回类型:会让调用者很困惑(
List<?>作为返回类型)”
Q42: 为什么不能创建泛型数组?
一句话总结:因为泛型擦除,创建泛型数组会导致类型不安全(可以在数组中放入错误类型的元素)。
深度解析
为什么不能创建泛型数组?
1 | |
为什么普通数组是类型安全的?
1 | |
解决方案:
1 | |
面试加分回答
“在实际项目中,关于泛型数组的 最佳实践:
- 不要用泛型数组:用
List<List<String>>代替List<String>[]- 泛型擦除导致类型不安全:创建泛型数组后,可以在数组中放入错误类型的元素(编译时不知道数组类型)
- 数组是协变的,泛型是不可变的:
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 的三大核心组件:
- Channel(通道):数据读写的通道(如
SocketChannel、ServerSocketChannel) - Buffer(缓冲区):数据读写的容器(如
ByteBuffer) - Selector(选择器):多路复用器(一个线程管理多个 Channel)
AIO 的工作流程:
1 | |
面试加分回答
“在实际项目中,我们对 BIO/NIO/AIO 的选择:
- 连接数少且固定:用 BIO(如内部管理系统)
- 连接数多且连接时间短:用 NIO(如聊天服务器、网关)
- 连接数多且连接时间长:用 AIO(如文件读写、视频流传输)
- Netty 基于 NIO:Netty 是对 JDK NIO 的封装(性能更好,编程更简单)”
Q44: 什么是序列化和反序列化?
一句话总结:序列化是把对象转换成字节序列(方便存储或传输);反序列化是把字节序列转换成对象(恢复对象状态)。
深度解析
序列化的使用场景:
- 网络传输:对象需要在网络中传输(如 RMI、Dubbo)
- 本地存储:对象需要持久化到磁盘(如 Session 持久化)
- 分布式缓存:对象需要存储到 Redis(需要序列化)
实现序列化:
1 | |
序列化版本号 serialVersionUID 的作用:
- 如果序列化时
serialVersionUID和反序列化时不同,会抛InvalidClassException - 如果不显式声明,JVM 会根据类结构自动生成(类结构变化后,自动生成的
serialVersionUID会变化,导致反序列化失败)
序列化的注意事项:
- 显式声明
serialVersionUID:避免类结构变化后反序列化失败 transient修饰的字段不会序列化:如密码、敏感信息- 静态字段不会序列化:静态字段属于类,不属于对象
- 父类需要实现
Serializable:否则父类字段不会序列化
面试加分回答
“在实际项目中,关于序列化的 最佳实践:
- 显式声明
serialVersionUID:避免类结构变化后反序列化失败- 敏感信息用
transient修饰:如密码、Token(不会序列化)- 选择高效的序列化框架:如 Protobuf、Kryo(比 JDK 自带的序列化效率高 10 倍以上)
- 注意循环引用:如果对象之间有循环引用,JDK 自带的序列化会栈溢出(需要用 JSON 或 Protobuf)”
Q45: transient 关键字的作用?
一句话总结:transient 修饰的字段不会序列化(在序列化的过程中,该字段会被忽略)。
深度解析
transient 的使用场景:
- 敏感信息:如密码、Token(不能序列化到磁盘或网络)
- 临时缓存:如本地缓存(不需要持久化)
- 大对象:如文件内容(序列化后体积太大,影响性能)
示例代码:
1 | |
transient 的注意事项:
transient只能修饰字段:不能修饰方法、类- 反序列化后
transient字段是默认值:如null、0、false - 静态字段不会序列化:不需要用
transient修饰
自定义序列化(如果需要控制序列化过程):
1 | |
面试加分回答
“在实际项目中,关于
transient的 最佳实践:
- 敏感信息用
transient修饰:如密码、Token(不会序列化)- 需要加密的字段:用
transient修饰,然后在writeObject()中加密后再序列化- 静态字段不需要
transient:静态字段属于类,不属于对象(不会序列化)- 反序列化后
transient字段是默认值:如null、0、false(需要在readObject()中恢复)”
Q46: BigDecimal 为什么可以精确计算?
一句话总结:BigDecimal 使用整数 + 小数位来表示小数(不会出现二进制无法精确表示的问题),适合金钱计算。
深度解析
为什么 float/double 精度丢失?
1 | |
- 因为
float/double是二进制表示,0.1 在二进制中是无限循环小数(无法精确表示)
BigDecimal 的精度原理:
1 | |
BigDecimal 的构造方式:
1 | |
BigDecimal 的常用方法:
1 | |
面试加分回答
“在实际项目中,关于
BigDecimal的 最佳实践:
- 金钱计算用
BigDecimal:不要用float/double(精度丢失)- 用字符串构造
BigDecimal:new BigDecimal("0.1")(不要用new BigDecimal(0.1))- 除法要指定舍入模式:
a.divide(b, 2, RoundingMode.HALF_UP)(否则除不尽会抛异常)- 比较用
compareTo():不要用equals()(equals()会比较小数位数,如 0.1 和 0.10 会返回 false)”
Q47: Java 中的 IO 流体系?
一句话总结:Java 的 IO 流分为字节流(InputStream/OutputStream)和字符流(Reader/Writer),字节流处理二进制数据,字符流处理文本数据。
深度解析
IO 流的四大抽象类:
| 类型 | 输入流 | 输出流 |
|---|---|---|
| 字节流 | InputStream |
OutputStream |
| 字符流 | Reader |
Writer |
常见的 IO 流实现类:
| 类别 | 字节流 | 字符流 |
|---|---|---|
| 文件流 | FileInputStream、FileOutputStream |
FileReader、FileWriter |
| 缓冲流 | BufferedInputStream、BufferedOutputStream |
BufferedReader、BufferedWriter |
| 对象流 | ObjectInputStream、ObjectOutputStream |
- |
| 转换流 | - | InputStreamReader、OutputStreamWriter |
字节流 vs 字符流:
- 字节流:处理二进制数据(如图片、视频、音频)
- 字符流:处理文本数据(如 txt、java、xml)
缓冲流的作用:
- 减少 IO 次数(每次读写一个缓冲区,而不是一个字节)
- 提高 IO 效率(如
BufferedInputStream默认缓冲区 8KB)
面试加分回答
“在实际项目中,关于 IO 流的 最佳实践:
- 处理二进制数据用字节流:如图片、视频、音频(不要用字符流,会乱码)
- 处理文本数据用字符流:如 txt、java、xml(字符流会自动处理编码)
- 用缓冲流包装:如
BufferedInputStream(减少 IO 次数,提高性能)- 记得关闭流:用 try-with-resources(JDK 7+),不需要手写
finally“
Q48: 字节流和字符流的区别?
一句话总结:字节流处理二进制数据(如图片、视频);字符流处理文本数据(如 txt、java),会自动处理编码。
深度解析
字节流(InputStream/OutputStream):
1 | |
字符流(Reader/Writer):
1 | |
转换流(InputStreamReader/OutputStreamWriter):
1 | |
面试加分回答
“在实际项目中,关于字节流和字符流的 最佳实践:
- 处理二进制数据用字节流:如图片、视频、音频(不要用字符流,会乱码)
- 处理文本数据用字符流:如 txt、java、xml(字符流会自动处理编码)
- 用转换流指定编码:如
new InputStreamReader(in, StandardCharsets.UTF_8)(避免乱码)- 用缓冲流包装:如
BufferedReader、BufferedInputStream(减少 IO 次数,提高性能)”
Q49: try-with-resources 是什么?
一句话总结:try-with-resources 是 JDK 7 引入的语法糖,自动关闭资源(实现了 AutoCloseable 接口的对象),不需要手写 finally。
深度解析
传统方式(JDK 6 及之前):
1 | |
try-with-resources 方式(JDK 7+):
1 | |
原理:
- 实现了
AutoCloseable接口的对象,可以用try-with-resources - 编译器会自动生成
finally块,调用close()方法
多个资源:
1 | |
面试加分回答
“在实际项目中,关于
try-with-resources的 最佳实践:
- JDK 7+ 用
try-with-resources:不需要手写finally(自动关闭资源)- 多个资源用分号分隔:如
try (in; out) { ... }- 实现
AutoCloseable接口:自定义资源类可以实现AutoCloseable接口(用try-with-resources自动关闭)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:
HashMap和Hashtable的区别?- A:
HashMap线程不安全、允许 null 键值;Hashtable线程安全、不允许 null 键值(已过时,不推荐使用)。
- A:
- Q2:
ArrayList和LinkedList的区别?- A:
ArrayList基于动态数组(随机访问快,增删慢);LinkedList基于双向链表(随机访问慢,增删快)。
- A:
- Q3: 反射的优缺点?
- A: 优点是灵活(运行时动态操作类);缺点是性能差(比直接调用慢)、安全性低(可以访问 private 成员)。
面试加分回答
“在实际项目中,Java 基础的 实际应用:
- 集合框架:
HashMap用于缓存、ArrayList用于列表、ConcurrentHashMap用于并发缓存- 面向对象:抽象类用于模板方法模式、接口用于策略模式
- 异常处理:自定义业务异常(继承
Exception或RuntimeException)- 反射:Spring 的 IOC 容器(通过反射创建 Bean)、Jackson(通过反射将 JSON 转换成 Java 对象)
- 泛型:集合框架都使用泛型(类型安全,避免强制类型转换)
- IO 流:文件上传用字节流、读取配置文件用字符流”
🎉 总结
本文详细讲解了 Java 基础 50 道高频面试题,从基础概念到高级特性,再到实战技巧,帮你系统掌握 Java 基础面试要点。
核心要点回顾:
- 基础概念:JDK/JRE/JVM 的关系、编译型 vs 解释型语言
- 数据类型:8 种基础数据类型、
Integer缓存机制(-128~127) - String 相关:String 不可变、String vs StringBuffer vs StringBuilder
- 集合框架:ArrayList vs LinkedList、HashMap vs ConcurrentHashMap
- 面向对象:重载 vs 重写、抽象类 vs 接口、封装/继承/多态
- 异常:Checked vs Unchecked、
try-catch-finally执行顺序 - 反射:Class.forName()、反射调用方法、反射的优缺点
- 注解:内置注解、自定义注解、元注解
- 泛型:泛型擦除、通配符(
? extends T、? super T)、PECS 原则 - IO 流:BIO vs NIO vs AIO、字节流 vs 字符流
下一步学习建议:
- 如果你对集合框架还不熟悉,先回头把 HashMap、ConcurrentHashMap 搞清楚
- 如果你对面向对象感兴趣,可以深入研究设计模式(单例、工厂、策略)
- 如果你准备面试,重点看 Q1-Q20(基础概念 + 集合框架)
📚 扩展学习资源
官方文档
书籍推荐
- 📚 《Effective Java》(Joshua Bloch)— Java 圣经,必读
- 📚 《Java 核心技术》(Core Java)— 基础全面
- 📚 《深入理解 Java 虚拟机》— JVM 原理
博客推荐
- 📝 JavaGuide — Java 面试题汇总
- 📝 Effective Java 中文版 — 实战练习
🎉 恭喜你!学完本文后,你已经掌握了 Java 基础的 入门知识。
下一步建议:
- 实战练习:自己写代码验证本文的示例(如 HashMap put 流程、反射调用方法)
- 深入研究:选一个方向(如集合框架、JVM 原理)深入研究
- 关注社区:订阅 Java 官方博客、Spring 官方博客,学习最新特性
- 准备面试:刷 LeetCode Java 题 + 本文的 50 道面试题