mobile wallpaper 1mobile wallpaper 2mobile wallpaper 3mobile wallpaper 4mobile wallpaper 5mobile wallpaper 6
7324 字
37 分钟
【面试八股】(一)
2026-03-16
统计加载中...

Exception 和 Error 有什么区别?#

ExceptionError 都继承自 Throwable,都表示程序运行时出现的问题,但它们的含义和处理方式不同。

1. Exception#

Exception 表示程序运行过程中出现的可处理问题
这类问题通常可以通过代码捕获并处理,比如:

  • NullPointerException
  • IOException
  • SQLException

特点:

  • 一般由程序或外部环境引起
  • 程序员通常可以预防或处理
  • 可以用 try-catch 捕获

2. Error#

Error 表示 JVM 或系统层面的严重错误
这类问题通常说明程序已经处于不正常状态,业务代码一般无法恢复,比如:

  • OutOfMemoryError
  • StackOverflowError

特点:

  • 通常是系统级问题
  • 一般不建议程序主动捕获处理
  • 出现后程序可能无法继续正常运行

3. 核心区别#

  • Exception:程序运行中的可处理异常
  • Error:系统级、严重且通常不可恢复的错误

4. 面试一句话回答#

Exception 是程序运行过程中可预期、可处理的问题;Error 是 JVM 或系统层面的严重错误,通常不可恢复。

Checked Exception 和 Unchecked Exception 有什么区别?#

Checked ExceptionUnchecked Exception 的核心区别在于:编译器是否强制处理

1. Checked Exception(受检异常)#

Checked Exception 是在编译阶段就会检查的异常。
如果方法可能抛出这类异常,必须:

  • try-catch 捕获
  • 或者用 throws 显式声明抛出

否则代码无法通过编译。

常见例子:

  • IOException
  • SQLException
  • ClassNotFoundException

特点:

  • 编译器强制处理
  • 通常表示外部环境或业务上可预期的问题
  • 调用方需要明确感知并处理

2. Unchecked Exception(非受检异常)#

Unchecked Exception 是指编译器不强制处理的异常。
它通常指 RuntimeException 及其子类,比如:

  • NullPointerException
  • ArrayIndexOutOfBoundsException
  • IllegalArgumentException

特点:

  • 编译器不要求必须处理
  • 通常表示程序逻辑错误或编码问题
  • 可以不写 try-catch,在运行时才暴露出来

3. 继承关系区别#

从继承结构看:

  • Checked Exception:继承 Exception,但不继承 RuntimeException
  • Unchecked Exception:继承 RuntimeException

4. 核心区别总结#

  • Checked Exception:编译器要求处理
  • Unchecked Exception:编译器不强制处理

5. 面试一句话回答#

Checked Exception 是编译器强制处理的异常,通常表示可预期的外部问题;Unchecked Exception 是运行时异常,编译器不强制处理,通常表示程序逻辑错误。

如何使用 try-with-resources 代替 try-catch-finally?#

try-with-resources 是 Java 7 引入的一种语法,用于自动关闭资源,可以代替传统 try-catch-finally 中手动释放资源的写法,使代码更简洁、更安全。

1. 传统写法#

传统方式通常需要在 finally 中手动关闭资源:

BufferedReader br = null;
try {
br = new BufferedReader(new FileReader("test.txt"));
String line = br.readLine();
System.out.println(line);
} catch (IOException e) {
e.printStackTrace();
} finally {
if (br != null) {
try {
br.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}

这种写法的问题是:

  • 代码比较冗长
  • close() 也可能抛异常,需要再套一层 try-catch
  • 容易因为疏忽导致资源泄漏

2. try-with-resources 写法#

使用 try-with-resources 后,可以直接把资源写在 try() 中:

try (BufferedReader br = new BufferedReader(new FileReader("test.txt"))) {
String line = br.readLine();
System.out.println(line);
} catch (IOException e) {
e.printStackTrace();
}

执行结束后,系统会自动调用资源的 close() 方法,不需要再在 finally 中手动关闭。

3. 使用前提#

try-with-resources 只能用于实现了 AutoCloseableCloseable 接口的资源,比如:

  • InputStream
  • OutputStream
  • Reader
  • Writer
  • Connection
  • PreparedStatement
  • ResultSet

4. 多个资源的写法#

多个资源可以同时写在 try() 中,按顺序声明,关闭时按相反顺序自动关闭:

try (
FileInputStream in = new FileInputStream("a.txt");
FileOutputStream out = new FileOutputStream("b.txt")
) {
byte[] buffer = new byte[1024];
int len;
while ((len = in.read(buffer)) != -1) {
out.write(buffer, 0, len);
}
} catch (IOException e) {
e.printStackTrace();
}

5. 优点#

相比传统 try-catch-finallytry-with-resources 的优点是:

  • 自动关闭资源,避免资源泄漏
  • 代码更简洁、可读性更好
  • 不需要在 finally 中手动写关闭逻辑
  • 异常处理更规范

6. 面试一句话回答#

try-with-resources 是 Java 7 提供的自动资源管理语法,可以把实现了 AutoCloseableCloseable 接口的资源声明在 try() 中,执行结束后自动调用 close(),从而替代传统 try-catch-finally 中手动关闭资源的写法。

异常使用有哪些需要注意的地方?#

异常使用时需要注意的核心是:不要滥用、要分层处理、要保留关键信息、要便于排查

1. 不要用异常控制正常业务流程#

异常表示的是非正常情况,不要把它当成普通的 if-else 来用。
如果本来可以通过条件判断解决,就不要依赖抛异常控制流程,否则会影响代码可读性,也会带来额外性能开销。

2. 尽量抛出具体异常,不要直接抛 Exception#

不要动不动就写:

throw new Exception("出错了");

应该尽量使用更明确的异常类型,比如:

  • 参数错误:IllegalArgumentException
  • 状态错误:IllegalStateException
  • 空指针问题:NullPointerException
  • 下标越界:IndexOutOfBoundsException

这样更利于定位问题,也方便上层分类处理。

3. 不要只 catch 不处理#

以下写法都不推荐:

catch (Exception e) {}

或者:

catch (Exception e) {
e.printStackTrace();
}

因为这类写法要么直接吞掉异常,要么没有真正处理问题。
正确做法通常是:

  • 记录日志
  • 转换异常
  • 进行补偿处理
  • 或继续向上抛出

4. 封装异常时要保留原始异常#

包装异常时一定要带上原始异常 cause,例如:

throw new RuntimeException("保存订单失败", e);

如果不保留原始异常,最后只能看到表面现象,排查不到根本原因。

5. 合理选择 Checked Exception 和 Unchecked Exception#

  • Checked Exception:适合那些调用方需要明确处理的异常
  • Unchecked Exception:适合参数错误、状态错误、编程缺陷等问题

在实际 Java 后端开发中,更多时候会优先使用 RuntimeException 体系,但也不能一概而论,要结合场景。

6. 不要过度捕获大而全的异常#

不要一上来就写:

catch (Exception e)

因为这样容易把真正的异常类型掩盖掉。
更推荐优先捕获具体异常,只有在统一兜底时才捕获较大的异常范围。

7. 资源要及时释放#

涉及文件流、数据库连接、网络连接时,要注意资源关闭。
优先使用 try-with-resources,不要只依赖手动 finally 关闭,否则容易造成资源泄漏。

8. 异常信息要清晰,便于排查#

抛异常时,信息不要只写“失败了”“出错了”,最好带上关键上下文,比如:

  • 订单号
  • 用户 ID
  • 请求参数摘要
  • 关键业务状态

但同时要注意,不要把密码、密钥、身份证号等敏感信息直接打到异常或日志里

9. 分层处理中要职责清晰#

一般来说:

  • DAO 层 / 持久层:保留底层异常信息
  • Service 层:做业务语义转换
  • Controller 层:统一返回友好错误信息

不要把底层技术异常直接暴露给前端,否则既不安全,也不利于统一管理。

10. 日志不要重复打印#

同一个异常如果在每一层都 log.error(),最后日志会重复很多遍,反而影响排查。
通常原则是:谁真正处理异常,谁记录关键日志;如果只是向上抛,通常不需要层层都打。

11. 自定义异常不要滥用#

自定义异常要有明确业务语义,比如:

  • OrderNotFoundException
  • InventoryNotEnoughException

不要为了“看起来规范”就给每个小场景都定义一个异常,否则会让异常体系过于臃肿。

12. 面试一句话回答#

异常使用时要注意:不要滥用异常控制流程,尽量抛出具体异常,合理捕获并分层处理,封装时保留原始异常,保证资源及时释放,同时让日志和异常信息具备可排查性。

什么是泛型?有什么作用?#

1. 什么是泛型#

泛型(Generic)是 Java 中的一种类型参数化机制,可以在定义类、接口、方法时,先不指定具体类型,而是在使用时再确定具体类型。

例如:

List<String> list = new ArrayList<>();
List<Integer> nums = new ArrayList<>();

这里的 StringInteger 就是传入的具体类型参数。

也就是说,泛型本质上就是:把类型当作参数传进去

2. 泛型的作用#

(1)提高代码复用性#

如果没有泛型,很多类和方法只能写死某一种类型,或者统一写成 Object
有了泛型之后,同一套代码可以适配多种类型,不需要重复写很多类似逻辑。

例如:

public class Box<T> {
private T value;
public void set(T value) {
this.value = value;
}
public T get() {
return value;
}
}

这里 Box<T> 就可以存放 StringIntegerUser 等任意类型。

(2)保证类型安全#

泛型可以在编译期检查类型,避免把错误类型的数据放进去。

例如:

List<String> list = new ArrayList<>();
list.add("abc");
list.add(123); // 编译报错

如果不用泛型,使用 List 原始类型,就可能在运行时才出问题。

(3)减少强制类型转换#

没有泛型时,从集合中取出元素通常需要强转:

List list = new ArrayList();
list.add("hello");
String s = (String) list.get(0);

使用泛型后:

List<String> list = new ArrayList<>();
list.add("hello");
String s = list.get(0);

这样代码更简洁,也更安全。

(4)让代码语义更清晰#

看到:

List<String>

就知道这个集合里放的是字符串;看到:

Map<String, Object>

就知道 key 是字符串,value 是对象。
可读性会更好。

3. 泛型常见使用位置#

泛型通常可以用在三个地方:

(1)泛型类#

例如:

class Box<T> { }

(2)泛型接口#

例如:

interface Comparable<T> { }

(3)泛型方法#

例如:

public <T> T getFirst(T[] arr) {
return arr[0];
}

4. 泛型的本质#

Java 泛型在编译后会发生类型擦除
也就是说,泛型主要是在编译阶段起作用,编译后字节码中很多泛型信息会被擦除,运行时通常拿不到具体泛型类型。

例如:

List<String>
List<Integer>

在运行时其实都是 List

5. 面试一句话回答#

泛型就是把类型参数化,在定义类、接口、方法时先不指定具体类型,等到使用时再确定。它的主要作用是提高代码复用性、保证类型安全、减少强制类型转换,并让代码语义更清晰。

什么是反射?#

1. 什么是反射#

反射(Reflection)是 Java 提供的一种机制,允许程序在运行时动态获取类的信息,并操作类或对象

通过反射,可以在运行时做到这些事情:

  • 获取类的名称、父类、接口、包信息
  • 获取类中的构造方法、成员变量、成员方法
  • 创建对象
  • 调用方法
  • 访问和修改属性
  • 甚至可以操作 private 成员

也就是说,反射让程序具备了“运行时自我分析和动态操作”的能力。

2. 反射能做什么#

常见能力包括:

(1)获取类信息#

例如获取类名、方法、字段、构造器等。

(2)动态创建对象#

不直接 new,而是在运行时通过类信息创建对象。

(3)动态调用方法#

运行时根据方法名找到对应方法并执行。

(4)访问和修改属性#

包括访问普通属性,甚至可以通过暴力反射访问私有属性。

3. 反射相关核心类#

Java 反射主要依赖 java.lang.reflect 包,常用核心类有:

  • Class:表示类的字节码对象
  • Constructor:表示构造方法
  • Method:表示成员方法
  • Field:表示成员变量

其中最核心的是 Class,因为反射通常都是从拿到 Class 对象开始的。

4. 获取 Class 对象的方式#

常见有三种:

(1)通过类名.class#

Class<?> clazz = User.class;

(2)通过对象.getClass()#

User user = new User();
Class<?> clazz = user.getClass();

(3)通过 Class.forName()#

Class<?> clazz = Class.forName("com.demo.User");

其中 Class.forName() 最能体现反射的动态性,因为类名可以在运行时传入。

5. 反射的优点#

  • 提高灵活性和扩展性
  • 可以在运行时动态加载和操作类
  • 非常适合做框架、组件、通用工具开发

很多框架底层都大量使用反射,比如:

  • Spring
  • MyBatis
  • Hibernate
  • JDK 动态代理

6. 反射的缺点#

  • 性能比直接调用低
  • 会破坏封装性,可以访问私有成员
  • 代码相对复杂,可读性较差
  • 过度使用会增加维护成本

7. 反射的典型应用场景#

  • 框架中根据配置文件动态加载类
  • 依赖注入(IOC)
  • 动态代理
  • ORM 框架字段映射
  • 注解解析

8. 面试一句话回答#

反射是 Java 提供的一种运行时机制,允许程序在运行时动态获取类的信息,并创建对象、调用方法、访问属性。它让程序具备更强的灵活性,很多框架底层都依赖反射实现动态扩展。

如何实现动态代理?#

动态代理是指:在程序运行时动态生成代理对象,而不是在编译期就写好代理类
它的核心作用是:在不修改目标对象代码的前提下,对目标方法进行增强,比如加日志、权限校验、事务控制等。

Java 中常见的动态代理实现方式有两种:

  • JDK 动态代理
  • CGLIB 动态代理

1. JDK 动态代理#

(1)适用场景#

JDK 动态代理要求目标对象必须实现接口
它是基于 InvocationHandlerProxy 来实现的。

(2)实现步骤#

第一步:定义接口#

public interface UserService {
void save();
}

第二步:定义目标类#

public class UserServiceImpl implements UserService {
@Override
public void save() {
System.out.println("保存用户");
}
}

第三步:定义 InvocationHandler#

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
public class MyInvocationHandler implements InvocationHandler {
private Object target;
public MyInvocationHandler(Object target) {
this.target = target;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("方法执行前");
Object result = method.invoke(target, args);
System.out.println("方法执行后");
return result;
}
}

第四步:生成代理对象#

import java.lang.reflect.Proxy;
public class Test {
public static void main(String[] args) {
UserService target = new UserServiceImpl();
UserService proxy = (UserService) Proxy.newProxyInstance(
target.getClass().getClassLoader(),
target.getClass().getInterfaces(),
new MyInvocationHandler(target)
);
proxy.save();
}
}

(3)执行结果#

方法执行前
保存用户
方法执行后

(4)原理#

JDK 动态代理会在运行时生成一个代理类,这个代理类实现了目标对象的接口
当调用代理对象的方法时,最终会统一进入 InvocationHandlerinvoke() 方法,在这里可以做增强逻辑,再调用目标方法。


2. CGLIB 动态代理#

(1)适用场景#

如果目标类没有实现接口,就不能用 JDK 动态代理,这时通常使用 CGLIB。
CGLIB 是通过继承目标类并重写方法来实现代理的。

(2)实现思路#

CGLIB 核心是 MethodInterceptorEnhancer

示例:

import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;
import java.lang.reflect.Method;
public class UserService {
public void save() {
System.out.println("保存用户");
}
}
public class Test {
public static void main(String[] args) {
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(UserService.class);
enhancer.setCallback(new MethodInterceptor() {
@Override
public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
System.out.println("方法执行前");
Object result = proxy.invokeSuper(obj, args);
System.out.println("方法执行后");
return result;
}
});
UserService proxy = (UserService) enhancer.create();
proxy.save();
}
}

(3)原理#

CGLIB 在运行时生成目标类的子类,并重写父类方法,在重写的方法中加入增强逻辑。

(4)限制#

由于是继承实现,所以:

  • 不能代理 final
  • 不能代理 final 方法

3. JDK 动态代理和 CGLIB 的区别#

JDK 动态代理#

  • 目标类必须实现接口
  • 基于接口生成代理对象
  • 使用 JDK 自带 API 实现

CGLIB 动态代理#

  • 目标类不需要实现接口
  • 基于继承生成子类代理
  • 不能代理 final 类和 final 方法

4. Spring 中的动态代理#

Spring AOP 底层就使用了动态代理:

  • 如果目标类实现了接口,默认优先使用 JDK 动态代理
  • 如果目标类没有实现接口,使用 CGLIB 动态代理
  • 当然也可以强制指定使用 CGLIB

5. 面试一句话回答#

动态代理是在运行时动态生成代理对象,从而在不修改目标类代码的情况下对方法进行增强。实现方式主要有两种:JDK 动态代理和 CGLIB 动态代理。JDK 动态代理基于接口和 InvocationHandler 实现,要求目标类必须实现接口;CGLIB 基于继承和字节码生成实现,不要求目标类实现接口,但不能代理 final 类和 final 方法。

何为注解?#

1. 什么是注解#

注解(Annotation)是 Java 提供的一种元数据机制,用于给类、方法、属性、参数等程序元素添加额外信息。

这些信息本身不会直接影响程序业务逻辑,但可以被编译器、JVM 或框架读取并利用,从而实现一些特殊功能。

可以简单理解为:

注解就是给代码打标记。


2. 注解的作用#

注解本质上不是业务代码,而是给程序或框架“看的说明信息”。
它常见的作用有:

  • 生成文档
  • 编译期检查
  • 运行时动态处理
  • 替代繁琐的 XML 配置
  • 配合反射实现框架功能

例如:

  • @Override:告诉编译器这是重写方法
  • @Deprecated:表示方法或类已过时
  • @SuppressWarnings:抑制警告
  • Spring 里的 @Component@Autowired
  • MyBatis 里的 @Select

3. 注解的常见分类#

(1)JDK 内置注解#

Java 自带的一些基础注解,例如:

  • @Override
  • @Deprecated
  • @SuppressWarnings

(2)元注解#

元注解是“用来修饰注解的注解”,常见有:

  • @Target:指定注解能作用在哪些位置
  • @Retention:指定注解保留到哪个阶段
  • @Documented:是否生成到文档中
  • @Inherited:子类是否可以继承父类注解

(3)自定义注解#

开发者也可以自己定义注解,用于项目中的特定场景,比如:

  • 操作日志注解
  • 权限校验注解
  • 接口幂等注解
  • 参数校验注解

4. 注解和注释的区别#

很多人容易把注解和注释混淆,它们完全不同:

注释#

  • 写给开发人员看的
  • 编译后一般不会保留
  • 主要用于解释代码含义

注解#

  • 写给编译器、JVM、框架看的
  • 可以参与编译和运行逻辑
  • 可被程序读取并处理

所以:

注释是说明文字,注解是程序可识别的元数据。


5. 注解为什么重要#

在现代 Java 开发中,注解非常常见,因为很多框架都大量依赖注解来简化开发。

例如:

  • Spring 用注解实现 IOC、AOP、依赖注入
  • JUnit 用注解标记测试方法
  • MyBatis 用注解写 SQL
  • Lombok 用注解自动生成代码

也就是说,注解极大提升了代码的简洁性和开发效率。


6. 注解的本质#

从本质上说,注解是一种特殊接口,默认继承 java.lang.annotation.Annotation
程序可以在编译期或运行期读取注解中的信息,并据此执行对应逻辑。

所以注解本身不直接“干活”,真正起作用的是:

  • 编译器
  • 反射机制
  • 框架对注解的解析和处理

7. 面试一句话回答#

注解是 Java 提供的一种元数据机制,用于给类、方法、属性等程序元素添加额外信息,供编译器、JVM 或框架在编译期、类加载期或运行期读取和处理,本质上可以理解为给代码打标记。

讲讲 SPI#

1. 什么是 SPI#

SPI 全称是 Service Provider Interface,即服务提供者接口
它是 Java 提供的一种扩展机制,允许框架或系统在运行时发现并加载某个接口的实现类,从而实现解耦和可插拔

可以简单理解为:

API 是“我定义好功能给别人调用”,SPI 是“我定义好接口,让别人来实现,我运行时再加载这些实现”。


2. SPI 的核心思想#

SPI 的核心是:

  • 定义一个接口
  • 不在代码里写死具体实现类
  • 由不同的服务提供者去实现这个接口
  • 系统在运行时自动加载这些实现类

这样做的好处是:
框架只依赖接口,不依赖具体实现,扩展能力很强。


3. SPI 的实现原理#

Java SPI 的标准实现依赖两个东西:

(1)接口定义#

先定义一个服务接口,例如:

public interface MyService {
void execute();
}

(2)实现类#

不同厂商或模块提供具体实现:

public class MyServiceImpl implements MyService {
@Override
public void execute() {
System.out.println("执行实现类逻辑");
}
}

(3)配置文件#

META-INF/services/ 目录下创建一个文件,文件名必须是接口的全限定类名

META-INF/services/com.demo.MyService

文件内容写该接口实现类的全限定类名:

com.demo.MyServiceImpl

(4)通过 ServiceLoader 加载#

Java 提供了 ServiceLoader 来动态加载实现类:

ServiceLoader<MyService> loader = ServiceLoader.load(MyService.class);
for (MyService service : loader) {
service.execute();
}

运行时,ServiceLoader 会去扫描 classpath 下所有 META-INF/services/接口全限定名 文件,把里面配置的实现类加载进来。


4. SPI 的执行流程#

SPI 的整体流程可以概括为:

  1. 框架定义接口
  2. 第三方实现这个接口
  3. META-INF/services/ 下配置实现类
  4. 框架运行时通过 ServiceLoader 扫描配置文件
  5. 加载并实例化对应实现类
  6. 使用实现类完成扩展功能

5. SPI 的典型应用场景#

(1)JDBC 驱动加载#

这是 SPI 最经典的例子。

以前使用 JDBC 时常写:

Class.forName("com.mysql.cj.jdbc.Driver");

后来 JDBC 4.0 以后,很多驱动都基于 SPI 自动注册。
只要驱动 jar 包里正确配置了 META-INF/services/java.sql.Driver,JVM 就能自动发现并加载驱动类。

(2)日志框架#

一些日志门面或插件扩展机制也会用到 SPI。

(3)Dubbo#

Dubbo 对 SPI 做了增强,扩展性非常强,是面试中常提到的例子。

(4)Spring Boot 自动装配的某些思想#

虽然 Spring Boot 不完全是标准 SPI,但“通过配置发现扩展实现”的思路和 SPI 很接近。


6. SPI 的优点#

(1)解耦#

调用方只依赖接口,不依赖具体实现。

(2)可扩展#

新增实现类时,不需要修改原有框架代码,只需要新增实现和配置即可。

(3)可插拔#

不同实现可以像插件一样动态接入。


7. SPI 的缺点#

(1)无法按需精准加载#

标准 Java SPI 一般会遍历并实例化所有实现,可能造成资源浪费。

(2)配置方式依赖约定#

必须严格按照 META-INF/services/接口全限定名 的格式来写,出错不容易排查。

(3)不够灵活#

标准 SPI 只支持按接口查找实现,不支持按名称、条件、优先级等复杂场景。


8. SPI 和 API 的区别#

API#

  • 接口由平台或框架提供
  • 调用方来使用这些接口
  • 重点是“调用”

SPI#

  • 接口由平台或框架定义
  • 实现由第三方提供
  • 平台运行时发现并加载实现
  • 重点是“扩展”

一句话理解:

API 是给调用方用的,SPI 是给扩展方实现的。


9. 为什么框架喜欢用 SPI#

因为框架本身往往需要预留扩展点。
如果把实现类写死,后面就很难扩展;而 SPI 可以让第三方在不改框架源码的情况下接入自己的实现。

这对中间件、驱动、插件系统、微服务框架都非常重要。


10. 面试一句话回答#

SPI 是一种服务发现和扩展机制,框架先定义接口,由第三方提供实现,并通过 META-INF/services 配置文件让系统在运行时自动发现和加载实现类。它的核心价值是解耦、可扩展和可插拔,JDBC 驱动加载就是经典应用场景。

序列化和反序列化#

1. 什么是序列化#

序列化(Serialization)是指:把 Java 对象转换成可以存储或传输的字节序列的过程

通俗理解就是:

把内存中的对象,变成可以写到文件、网络、缓存里的数据。

例如:

  • 把对象写入文件
  • 通过网络传输对象
  • 将对象保存到 Redis、MQ、磁盘中

2. 什么是反序列化#

反序列化(Deserialization)是指:把字节序列重新恢复成 Java 对象的过程

也就是说:

  • 序列化:对象 → 字节流
  • 反序列化:字节流 → 对象

它们通常是成对出现的。


3. 为什么需要序列化#

因为 Java 对象本身只存在于 JVM 内存中,不能直接跨进程、跨网络、持久化存储。
如果想让对象“离开内存”,就需要先把它转换成某种可传输、可保存的格式。

常见使用场景有:

(1)对象持久化#

把对象保存到文件、数据库、磁盘中。

(2)网络传输#

RPC 调用、Socket 通信时,需要把对象转成字节流发送给对方。

(3)缓存#

把对象写入 Redis 等缓存系统时,通常也需要序列化。

(4)分布式场景#

多个服务之间传输对象数据时,也离不开序列化和反序列化。


4. Java 中如何实现序列化#

Java 原生序列化要求对象实现 Serializable 接口。

例如:

import java.io.Serializable;
public class User implements Serializable {
private String name;
private int age;
}

然后就可以通过 ObjectOutputStream 进行序列化,通过 ObjectInputStream 进行反序列化。


5. 示例#

(1)序列化#

ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("user.obj"));
oos.writeObject(new User("Tom", 18));
oos.close();

(2)反序列化#

ObjectInputStream ois = new ObjectInputStream(new FileInputStream("user.obj"));
User user = (User) ois.readObject();
ois.close();

6. Serializable 接口的特点#

Serializable 是一个标记接口,本身没有任何方法。
它的作用只是告诉 JVM:这个类的对象允许被序列化

如果一个类没有实现 Serializable,在序列化时会抛出:

NotSerializableException

7. serialVersionUID 是什么#

serialVersionUID 是序列化版本号,用来标识类的版本一致性。

例如:

private static final long serialVersionUID = 1L;

作用:

  • 序列化时会把这个版本号一起写入
  • 反序列化时会校验本地类和字节流中的版本号是否一致
  • 如果不一致,可能抛出 InvalidClassException

为什么建议手动定义? 因为如果不手动写,Java 会根据类结构自动生成。
一旦类发生改动,自动生成的值可能变化,导致以前序列化的数据无法反序列化。


8. transient 关键字的作用#

如果某个字段不想参与序列化,可以使用 transient 修饰。

例如:

private transient String password;

这样在序列化时,password 字段不会被写入字节流。

常见场景:

  • 密码
  • 临时计算字段
  • 不需要持久化的敏感信息

9. 静态变量会被序列化吗#

不会。

因为 static 变量属于类,不属于对象。
序列化保存的是对象状态,而不是类状态。


10. Java 原生序列化的缺点#

(1)性能较差#

Java 原生序列化生成的字节流通常比较大,序列化和反序列化速度也不够快。

(2)可读性差#

序列化后的内容是二进制,不方便阅读和调试。

(3)跨语言支持差#

Java 原生序列化主要适用于 Java 体系内部,不适合跨语言系统之间传输。

(4)安全性问题#

反序列化如果处理不当,可能存在安全风险,甚至可能被利用进行攻击。


11. 常见替代方案#

在实际开发中,很多项目不会直接使用 Java 原生序列化,而会选择更通用的方案,比如:

  • JSON:如 Jackson、Gson
  • XML
  • Hessian
  • Kryo
  • Protobuf

其中:

  • JSON:可读性好,跨语言方便
  • Protobuf:体积小、性能高,适合 RPC 和高性能传输
  • Kryo:性能高,但更偏 Java 内部使用

12. 序列化和深拷贝的关系#

序列化和反序列化还能用于实现深拷贝
因为对象序列化成字节流后,再反序列化回来,相当于创建了一个全新的对象副本。

不过这种方式性能一般,不是最优方案。


13. 面试中的常见追问#

(1)哪些对象可以被序列化?#

实现了 Serializable 接口的对象才可以被序列化。

(2)父类没实现 Serializable 可以吗?#

可以,但父类必须有无参构造器,因为反序列化时父类部分需要通过构造器初始化。

(3)子类实现 Serializable,父类的字段会被序列化吗?#

如果父类没有实现 Serializable,父类字段不会走序列化机制,而是在反序列化时通过父类无参构造器初始化。

(4)transient 修饰的字段一定无法恢复吗?#

默认不会被序列化,所以反序列化后一般是默认值。
但如果自定义序列化逻辑,仍然可以手动处理。


14. 面试一句话回答#

序列化就是把对象转换成可存储、可传输的字节序列,反序列化就是把字节序列恢复成对象。它主要用于对象持久化、网络传输、缓存和分布式通信。Java 中通常通过实现 Serializable 接口来支持序列化,并可通过 serialVersionUID 控制版本一致性,transient 控制字段不参与序列化。

【面试八股】(一)
http://hgqwd.icu/posts/bagu1/
作者
天线宝宝死于谋杀
发布于
2026-03-16
许可协议
CC BY-NC-SA 4.0

部分信息可能已经过时

封面
Sample Song
Sample Artist
封面
Sample Song
Sample Artist
0:00 / 0:00