目录
[模块一:异常体系(项目架构核心 + 面试高频场景题)](#模块一:异常体系(项目架构核心 + 面试高频场景题))
[1. 异常体系整体架构与分类](#1. 异常体系整体架构与分类)
[2. try-catch-finally 执行规则(含 return 场景)](#2. try-catch-finally 执行规则(含 return 场景))
[3. try-with-resources 语法与底层原理](#3. try-with-resources 语法与底层原理)
[4. 自定义异常与全局异常处理最佳实践](#4. 自定义异常与全局异常处理最佳实践)
[模块二:IO 流体系(文件操作核心)](#模块二:IO 流体系(文件操作核心))
[1. IO 流整体分类与核心区别](#1. IO 流整体分类与核心区别)
[2. 核心处理流:缓冲流、转换流、序列化流](#2. 核心处理流:缓冲流、转换流、序列化流)
[3. Serializable 接口与 serialVersionUID](#3. Serializable 接口与 serialVersionUID)
[模块三:NIO 核心(高性能 IO + 面试进阶考点)](#模块三:NIO 核心(高性能 IO + 面试进阶考点))
[1. NIO 三大核心组件](#1. NIO 三大核心组件)
[2. BIO/NIO/AIO 核心对比与适用场景](#2. BIO/NIO/AIO 核心对比与适用场景)
[3. 零拷贝的概念与应用场景](#3. 零拷贝的概念与应用场景)
[异常体系、IO 与 NIO 面试模拟题(实习面试专属,附标准答案)](#异常体系、IO 与 NIO 面试模拟题(实习面试专属,附标准答案))
[一、基础必考题(8 道,中小厂实习 100% 覆盖)](#一、基础必考题(8 道,中小厂实习 100% 覆盖))
[1. Error 和 Exception 的核心区别是什么?受检异常和非受检异常的区别是什么?](#1. Error 和 Exception 的核心区别是什么?受检异常和非受检异常的区别是什么?)
[2. finally 代码块是不是一定会执行?什么情况下不会执行?](#2. finally 代码块是不是一定会执行?什么情况下不会执行?)
[3. try-with-resources 的好处是什么?底层实现原理是什么?](#3. try-with-resources 的好处是什么?底层实现原理是什么?)
[4. 字节流和字符流的核心区别是什么?各自的适用场景是什么?](#4. 字节流和字符流的核心区别是什么?各自的适用场景是什么?)
[5. 缓冲流(BufferedXXX)为什么能大幅提升 IO 性能?](#5. 缓冲流(BufferedXXX)为什么能大幅提升 IO 性能?)
[6. serialVersionUID 的作用是什么?不手动指定会有什么问题?](#6. serialVersionUID 的作用是什么?不手动指定会有什么问题?)
[7. BIO、NIO、AIO 的核心区别和适用场景是什么?](#7. BIO、NIO、AIO 的核心区别和适用场景是什么?)
[8. 零拷贝的原理是什么?有什么核心优势?](#8. 零拷贝的原理是什么?有什么核心优势?)
[二、场景坑点题(6 道,大厂实习高频业务题 / 代码题)](#二、场景坑点题(6 道,大厂实习高频业务题 / 代码题))
[1. 以下代码的输出结果是什么?为什么?](#1. 以下代码的输出结果是什么?为什么?)
[2. 你的校园二手平台要实现商品图片上传功能,应该用字节流还是字符流?为什么?用什么优化手段?](#2. 你的校园二手平台要实现商品图片上传功能,应该用字节流还是字符流?为什么?用什么优化手段?)
[3. 你的校园二手平台怎么设计异常体系?全局异常处理器怎么实现?](#3. 你的校园二手平台怎么设计异常体系?全局异常处理器怎么实现?)
[4. 你的校园二手平台把商品对象存入 Redis 缓存,修改了 Goods 类加了一个字段后,反序列化失败,是什么原因?怎么解决?](#4. 你的校园二手平台把商品对象存入 Redis 缓存,修改了 Goods 类加了一个字段后,反序列化失败,是什么原因?怎么解决?)
[5. 以下代码读取商品的 TXT 描述文件,输出中文乱码,是什么原因?怎么解决?](#5. 以下代码读取商品的 TXT 描述文件,输出中文乱码,是什么原因?怎么解决?)
[6. 你的校园二手平台要实现商品大视频的下载功能,怎么优化下载性能?](#6. 你的校园二手平台要实现商品大视频的下载功能,怎么优化下载性能?)
[三、进阶原理题(4 道,中大厂实习拔高题)](#三、进阶原理题(4 道,中大厂实习拔高题))
[1. 为什么现代 Java 框架(Spring、MyBatis)都把受检异常转为运行时异常?受检异常有什么设计弊端?](#1. 为什么现代 Java 框架(Spring、MyBatis)都把受检异常转为运行时异常?受检异常有什么设计弊端?)
[2. NIO 的多路复用原理是什么?为什么单线程就能管理成千上万的连接?](#2. NIO 的多路复用原理是什么?为什么单线程就能管理成千上万的连接?)
[3. 为什么 NIO 的性能远高于 BIO?](#3. 为什么 NIO 的性能远高于 BIO?)
[4. Java 序列化是浅拷贝还是深拷贝?怎么实现深拷贝?](#4. Java 序列化是浅拷贝还是深拷贝?怎么实现深拷贝?)
模块一:异常体系(项目架构核心 + 面试高频场景题)
1. 异常体系整体架构与分类
核心定义
Java 异常的顶层父类是Throwable,分为两大分支:
(1)核心层级划分
| 顶层类 | 分类 | 定义与特点 | 代表示例 |
|---|---|---|---|
Throwable |
Error |
程序无法处理的严重错误,JVM 层面的问题,开发中无需捕获 | OutOfMemoryError(OOM)、StackOverflowError、NoClassDefFoundError |
Exception |
程序可以捕获处理的异常,分为两类: | ||
① 非受检异常(运行时异常RuntimeException) |
编译期不强制捕获,运行时抛出,绝大多数是代码逻辑问题 | NullPointerException、IndexOutOfBoundsException、IllegalArgumentException |
|
| ② 受检异常(Checked Exception) | 编译期强制捕获 / 抛出,否则编译不通过 | IOException、SQLException、InterruptedException |
(2)核心设计原则
异常是用来处理程序运行时的意外情况,不是用来控制业务流程的;业务异常统一继承运行时异常,避免强制捕获的代码冗余。
个人理解
Error 是致命错误,程序无法恢复,只能提前预防;受检异常是 Java 的历史设计,强制开发者处理可能的异常,但会造成大量的 try-catch 冗余,现代框架(Spring)都统一转为运行时异常处理;业务开发中 99% 的场景都是自定义运行时异常 + 全局统一捕获。
项目实际使用场景
结合校园二手平台开发实践:
- JVM 错误预防:项目启动时配置 JVM 堆内存参数,避免商品图片上传、大订单导出时出现 OOM;递归处理商品分类树时控制递归深度,避免栈溢出;
- 运行时异常处理:空指针、数组越界等代码异常,由全局异常处理器统一捕获,返回统一的错误码和提示;
- 受检异常转换 :文件操作的
IOException、数据库的SQLException,在 Service 层捕获后转为自定义业务异常,抛给全局异常处理器,避免代码里大量 try-catch。
面试考点标注
✅ 必问:Error 和 Exception 的核心区别;
✅ 必问:受检异常和非受检异常的区别,受检异常的设计弊端;
✅ 场景题:常见的运行时异常有哪些,怎么避免空指针异常。
【
不用 try-catch 也能编译,运行时才报错,面试高频:
-
**NullPointerException(空指针 NPE)**调用 null 对象的方法 / 属性
-
**IndexOutOfBoundsException(下标越界)**数组 / 集合访问了不存在的下标
-
**IllegalArgumentException(非法参数)**传了错误参数
-
**IllegalStateException(状态异常)**方法调用时机不对
-
**ClassCastException(类型转换异常)**强制类型转换失败
-
**ArithmeticException(算术异常)**除 0 等
-
**NoSuchElementException(没有元素)**迭代器、集合取空元素
-
**UnsupportedOperationException(不支持操作)**调用了不支持的方法
空指针异常是最常见的运行时异常,避免它的核心:调用前判空、常量放前面、返回空集合不返回 null、善用 Optional 和工具类。
】
2. try-catch-finally 执行规则(含 return 场景)
核心定义
核心执行规则:
finally代码块一定会执行 ,唯一不执行的情况:调用System.exit(0)终止 JVM、JVM 崩溃、线程死亡;- 执行顺序:try → 异常则 catch → finally;无异常则 try → finally;
- 带 return 的核心规则 :
- try/catch 中的 return 会先保存返回值的快照,再执行 finally,最后返回快照值;
- finally 中修改基本类型的返回值无效,修改引用类型的内容有效;
- finally 中写 return 会覆盖 try/catch 的返回值,开发中绝对禁止。
个人理解
finally 的设计初衷是释放资源(关流、释放锁),不是用来处理业务逻辑;开发中不要在 finally 里写业务代码,更不要写 return,否则会出现逻辑混乱。
项目实际使用场景
- 旧代码资源释放:早期 JDK7 之前的文件流操作,在 finally 里手动关闭流,避免流泄漏;
- 锁释放:分布式锁、本地锁的释放,必须放在 finally 里,保证异常情况下也能释放锁,避免死锁;
- 避坑实践:绝对不在 finally 里写 return,之前踩过坑,finally 的 return 覆盖了业务异常,导致异常无法抛出。
面试考点标注
✅ 必问:finally 是不是一定会执行?什么情况下不执行?
✅ 高频场景题:给出带 return 的 try-catch-finally 代码,问输出结果;
✅ 坑点:finally 中修改返回值的生效场景。
3. try-with-resources 语法与底层原理
核心定义
Java7 引入的语法糖,用来自动释放资源,核心规则:
- 所有实现了
AutoCloseable接口的资源(流、连接、锁等),都可以放在 try 后的括号里; - 代码执行完后,自动调用资源的
close()方法,无需手动写 finally 关流; - 底层原理:编译期自动生成 finally 块,按资源声明的逆序调用 close () 方法,比手动写 finally 更安全,不会漏关流。
个人理解
try-with-resources 解决了手动 finally 关流的两大问题:一是嵌套流的代码冗余(地狱式缩进),二是手动关流容易漏关、顺序错误导致资源泄漏,是 IO 操作的最佳实践。
项目实际使用场景
- 商品图片上传下载:文件输入输出流、缓冲流全部用 try-with-resources 管理,自动关流,代码简洁,不会出现流泄漏导致的文件占用问题;
- 数据库连接、Redis 连接:连接资源用 try-with-resources 管理,自动释放连接,避免连接池耗尽;
- 订单 Excel 导出:POI 的工作簿、输出流,用 try-with-resources 管理,避免大文件导出时的内存泄漏。
面试考点标注
✅ 必问:try-with-resources 的好处,底层实现原理;
✅ 对比:和手动 finally 关流的区别,为什么更安全;
✅ 细节:资源关闭的顺序是声明的逆序,避免依赖问题。
4. 自定义异常与全局异常处理最佳实践
核心定义
(1)自定义异常设计规范
- 业务异常统一继承
RuntimeException,避免受检异常的强制捕获冗余; - 区分不同业务场景的异常:
OrderException、UserException、PayException、FileException等; - 异常中携带错误码、错误信息,方便全局处理。
(2)全局异常处理
基于 Spring 的@RestControllerAdvice + @ExceptionHandler实现,统一捕获所有异常,返回统一格式的响应,避免每个 Controller 单独处理异常。
个人理解
自定义异常是业务分层的核心,把业务错误和系统错误区分开,全局异常处理统一收口,代码更整洁,异常处理更规范,是企业级项目的标准架构。
项目实际使用场景
- 自定义业务异常 :
- 订单模块:
OrderException,抛出「库存不足」「订单状态异常」「无权操作订单」等业务错误; - 用户模块:
UserException,抛出「用户名已存在」「密码错误」「用户未登录」等错误; - 文件模块:
FileException,抛出「文件过大」「格式不支持」「上传失败」等错误;
- 订单模块:
- 全局异常处理器 :
- 统一捕获自定义业务异常,返回对应错误码和提示;
- 捕获参数校验异常、空指针等系统异常,返回通用错误提示,打印错误日志;
- 异常信息统一上报日志平台,方便排查问题。
面试考点标注
✅ 场景题:项目中怎么设计异常体系,自定义异常的好处;
✅ 必问:Spring 全局异常处理的实现方式;
✅ 设计题:怎么区分业务异常和系统异常,怎么处理异常日志。
模块二:IO 流体系(文件操作核心)
1. IO 流整体分类与核心区别
核心定义
IO 流是 Java 中数据传输的通道,按不同维度分类:
(1)按数据类型划分(核心区别)
| 分类 | 核心操作 | 适用场景 | 顶层抽象类 | 实现类示例 |
|---|---|---|---|---|
| 字节流 | 操作二进制字节数据,无编码问题 | 图片、视频、音频、Excel、压缩包等所有非文本文件 | InputStream/OutputStream |
FileInputStream、FileOutputStream |
| 字符流 | 操作文本字符数据,自带编码处理 | 纯文本文件、配置文件、字符串等文本内容 | Reader/Writer |
FileReader、FileWriter |
(2)按功能划分
- 节点流:直接操作数据源的流(FileInputStream、FileReader);
- 处理流(包装流):包装节点流,增加额外功能(缓冲流、转换流、序列化流)。
个人理解
字节流是万能流,可以操作所有类型的文件;字符流是专门处理文本的,解决了字节流读取中文乱码的问题;开发中优先用处理流(缓冲流),性能比节点流高几十倍。
项目实际使用场景
- 商品图片 / 视频上传下载:用字节流操作,避免编码问题导致的文件损坏;
- 订单 Excel 导出、商品批量导入:用字节流操作 Excel 文件;
- 配置文件读取、接口响应文本处理:用字符流操作,避免中文乱码;
- 性能优化 :所有 IO 操作都加缓冲流,减少磁盘 IO 次数,比如商品大图片上传用
BufferedInputStream/BufferedOutputStream,性能提升 10 倍以上。
面试考点标注
✅ 必问:字节流和字符流的核心区别,各自的适用场景;
✅ 坑点:字节流读取中文会乱码,为什么?怎么解决?
✅ 细节:字符流的底层还是字节流,只是做了编码转换。
2. 核心处理流:缓冲流、转换流、序列化流
核心定义
(1)缓冲流(BufferedXXX)
- 原理:内置 8KB 的缓冲区,先把数据写到缓冲区,缓冲区满了再一次性写到磁盘 / 读取到内存,减少磁盘 IO 次数,大幅提升性能;
- 实现类:
BufferedInputStream、BufferedOutputStream、BufferedReader、BufferedWriter。
(2)转换流(InputStreamReader/OutputStreamWriter)
- 作用:字节流和字符流的桥梁,可以指定编码(UTF-8、GBK),解决不同编码的文本乱码问题;
- 场景:读取 GBK 编码的老系统文件,转换为 UTF-8。
(3)序列化流(ObjectInputStream/ObjectOutputStream)
- 作用:把 Java 对象转为字节序列(序列化),或者把字节序列转回 Java 对象(反序列化),用于对象的持久化、网络传输。
个人理解
缓冲流是 IO 性能优化的必选项,几乎所有 IO 操作都要加缓冲流;转换流是处理编码乱码的终极方案;序列化是对象持久化、分布式缓存的基础。
项目实际使用场景
- 缓冲流:商品图片上传、大文件导出,全部用缓冲流,避免频繁磁盘 IO,提升上传下载速度;
- 转换流:处理用户上传的 GBK 编码的商品批量导入 CSV 文件,转为 UTF-8,避免中文乱码;
- 序列化流:把用户对象、商品对象序列化后存入 Redis,实现分布式缓存;微服务之间的对象传输,底层也是序列化。
面试考点标注
✅ 必问:缓冲流为什么能提升 IO 性能?
✅ 场景题:怎么解决 IO 读取中文乱码问题?
✅ 细节:缓冲流的 flush () 方法,手动把缓冲区数据刷到磁盘。
3. Serializable 接口与 serialVersionUID
核心定义
- Serializable 接口:是一个标记接口,没有任何方法,作用是告诉 JVM 这个类可以被序列化;
- serialVersionUID :序列化的版本号,是序列化和反序列化的唯一标识:
- 序列化时会把 serialVersionUID 写入字节序列;
- 反序列化时会对比字节序列的 serialVersionUID 和类的 serialVersionUID,不一致会抛出
InvalidClassException; - 不手动指定的话,JVM 会根据类的属性、方法自动生成,修改类的结构会导致版本号变化,反序列化失败。
个人理解
serialVersionUID 是序列化的兼容性保障,手动指定版本号可以控制类修改后的反序列化兼容性;不指定的话,类加个字段就会导致反序列化失败,是开发中的高频坑。
项目实际使用场景
- Redis 缓存对象 :所有存入 Redis 的实体类(Goods、User、Order)都实现
Serializable接口,手动指定serialVersionUID,避免类修改后 Redis 缓存反序列化失败; - Session 序列化:用户 Session 对象序列化,实现分布式 Session 共享;
- 避坑实践 :所有需要序列化的类,都手动加
private static final long serialVersionUID = 1L;,绝对不依赖 JVM 自动生成。
面试考点标注
✅ 必问:serialVersionUID 的作用是什么?不指定会有什么问题?
✅ 坑点:哪些字段不会被序列化?(static 静态字段、transient 修饰的字段)
✅ 细节:序列化是浅拷贝,对象的引用属性也需要实现 Serializable 接口。
模块三:NIO 核心(高性能 IO + 面试进阶考点)
1. NIO 三大核心组件
核心定义
NIO(New IO/Non-blocking IO)是 Java1.4 引入的非阻塞 IO,面向缓冲区,三大核心组件:
- Buffer(缓冲区) :数据的容器,底层是数组,所有数据都通过缓冲区读写,核心实现:
ByteBuffer、CharBuffer等;核心模式:读模式、写模式,通过flip()切换。 - Channel(通道) :双向的数据传输通道,可以同时读写,对应 IO 的流,核心实现:
FileChannel(文件通道)、SocketChannel(网络通道)。 - Selector(多路复用器):一个线程可以管理多个 Channel,监听多个 Channel 的事件(连接、读、写),实现单线程管理大量连接,是 NIO 高并发的核心。
个人理解
传统 IO 是面向流,单向,阻塞;NIO 是面向缓冲区,双向,非阻塞;核心优势是用少量线程管理大量连接,适合高并发网络场景,是 Tomcat、Netty 等容器 / 框架的底层核心。
项目实际使用场景
- 文件操作 :用 NIO 的
Files工具类实现文件的快速拷贝、删除、读取,比传统 IO 性能更高; - 大文件传输 :商品大图片、视频的下载,用
FileChannel的transferTo()方法实现零拷贝,大幅提升传输速度; - 底层框架:项目用的 Spring Boot 内嵌 Tomcat,底层就是 NIO 实现,支持高并发请求。
面试考点标注
✅ 必问:NIO 和传统 IO(BIO)的核心区别;
✅ 原理:NIO 三大组件的作用;
✅ 细节:Buffer 的 flip ()、clear () 方法的作用。
2. BIO/NIO/AIO 核心对比与适用场景
核心定义
| 类型 | 全称 | 核心特点 | 线程模型 | 性能 | 适用场景 |
|---|---|---|---|---|---|
| BIO | 阻塞 IO | 每个连接一个线程,读写阻塞 | 一个连接对应一个线程 | 低,连接多了线程数爆炸 | 连接数少、固定的场景,传统老项目 |
| NIO | 非阻塞 IO | 单线程管理多个连接,多路复用 | 一个线程管理 N 个连接 | 高,线程开销小 | 连接数多、高并发场景(互联网项目、网关、RPC) |
| AIO | 异步 IO | 完全非阻塞,操作系统完成 IO 后通知应用 | 无阻塞,事件回调 | 最高 | 连接数多、长连接场景,实际应用较少 |
个人理解
BIO 是同步阻塞,简单但性能差;NIO 是同步非阻塞,是目前的主流,Tomcat、Netty 都是基于 NIO;AIO 是异步非阻塞,理论上性能最高,但实际开发中用的很少,Windows 下实现成熟,Linux 下底层还是用 NIO 模拟。
项目实际使用场景
- 项目容器:Spring Boot Tomcat 用 NIO 模式,支持高并发接口请求;
- 文件操作:本地文件操作多用 NIO 的工具类,性能更高;
- 避坑实践:BIO 只适合小工具、测试代码,生产环境高并发场景绝对不用 BIO。
面试考点标注
✅ 必问:BIO、NIO、AIO 的核心区别、各自的适用场景;
✅ 原理:NIO 的多路复用是什么?
✅ 扩展:Netty 是基于 NIO 封装的,解决了 NIO 的原生缺陷。
3. 零拷贝的概念与应用场景
核心定义
传统 IO 的文件传输,需要经过 4 次拷贝:磁盘→内核缓冲区→用户缓冲区→Socket 缓冲区→网卡,还有 2 次用户态和内核态的切换,性能差;零拷贝是减少数据在用户态和内核态的拷贝次数 ,Java 中通过FileChannel.transferTo()实现,底层是 Linux 的sendfile系统调用,数据直接从内核缓冲区拷贝到 Socket 缓冲区,不需要经过用户态,只有 2 次拷贝,性能提升数倍。
个人理解
零拷贝是大文件传输、静态资源服务器的核心优化,本质是减少 CPU 的拷贝开销,解放 CPU,提升吞吐量,是大厂面试的高频考点。
项目实际使用场景
- 商品大文件下载:商品视频、高清图片的下载接口,用零拷贝实现,下载速度提升 3-5 倍,服务器 CPU 占用大幅降低;
- 静态资源服务:项目的图片、CSS、JS 等静态资源,用零拷贝传输,提升静态资源访问性能。
面试考点标注
✅ 必问:零拷贝的原理是什么?有什么优势?
✅ 场景题:大文件下载怎么优化?
✅ 细节:Java 中零拷贝的实现方式。
当日验收清单
- 不看资料口述:异常的完整分类、BIO 和 NIO 的核心差异、try-with-resources 的优势;
- 结合项目口述:你的项目中自定义了哪些业务异常,全局异常处理器怎么设计的,文件上传用了什么流;
- 避坑确认:serialVersionUID 不指定的坑、finally 中写 return 的坑、字节流读取中文乱码的问题。
异常体系、IO 与 NIO 面试模拟题(实习面试专属,附标准答案)
一、基础必考题(8 道,中小厂实习 100% 覆盖)
1. Error 和 Exception 的核心区别是什么?受检异常和非受检异常的区别是什么?
【标准答案】
(1)Error 和 Exception 的区别
| 对比维度 | Error | Exception |
|---|---|---|
| 层级 | 是 Throwable 的子类 | 是 Throwable 的子类 |
| 定义 | JVM 层面的严重错误,程序无法处理和恢复 | 程序运行时的意外情况,可以捕获和处理 |
| 示例 | OutOfMemoryError、StackOverflowError | NullPointerException、IOException、SQLException |
| 处理方式 | 无需捕获,只能提前预防和优化 | 可以通过 try-catch 捕获处理 |
(2)受检异常和非受检异常(运行时异常)的区别
| 对比维度 | 受检异常(Checked Exception) | 非受检异常(RuntimeException) |
|---|---|---|
| 编译期检查 | 编译期强制捕获 / 抛出,否则编译不通过 | 编译期不强制检查,运行时抛出 |
| 继承关系 | 继承 Exception,不继承 RuntimeException | 继承 RuntimeException |
| 设计意义 | 强制开发者处理可预期的外部异常(IO、数据库) | 代码逻辑错误,开发者应该提前避免 |
| 示例 | IOException、SQLException、InterruptedException | NullPointerException、IndexOutOfBoundsException |
【考点对应】模块一:异常体系分类
2. finally 代码块是不是一定会执行?什么情况下不会执行?
【标准答案】finally 代码块绝大多数情况一定会执行,只有 3 种极端情况不会执行:
- 调用
System.exit(0)/System.exit(1)主动终止 JVM; - JVM 崩溃、进程被强制杀死(比如 kill -9);
- 执行 finally 的线程死亡(比如线程被 interrupt 终止)。
补充:return、抛出异常都不会阻止 finally 执行。
【考点对应】模块一:try-catch-finally 执行规则
3. try-with-resources 的好处是什么?底层实现原理是什么?
【标准答案】
核心好处
- 自动释放资源:无需手动写 finally 关流,避免资源泄漏(流、连接未关闭);
- 代码简洁:避免多层 finally 嵌套的地狱式缩进;
- 更安全:编译期自动生成关闭逻辑,不会出现漏关、顺序错误的问题。
底层原理
是 Java7 引入的语法糖,编译期自动做两件事:
- 给所有实现了
AutoCloseable接口的资源,自动生成 finally 代码块; - 按资源声明的逆序 调用
close()方法,保证依赖的资源先关闭。
【考点对应】模块一:try-with-resources 语法
4. 字节流和字符流的核心区别是什么?各自的适用场景是什么?
【标准答案】
| 对比维度 | 字节流 | 字符流 |
|---|---|---|
| 操作单元 | 二进制字节(1 字节) | Unicode 字符(2 字节) |
| 编码处理 | 无编码,不会自动转换 | 自带编码转换,处理文本自动适配编码 |
| 乱码问题 | 读取中文会乱码 | 读取纯文本无乱码 |
| 性能 | 更高,无编码转换开销 | 略低,有编码转换开销 |
| 适用场景 | 所有二进制文件:图片、视频、音频、Excel、压缩包、可执行文件 | 纯文本文件:TXT、配置文件、代码、字符串 |
【考点对应】模块二:IO 流核心分类
5. 缓冲流(BufferedXXX)为什么能大幅提升 IO 性能?
【标准答案】核心原理是减少磁盘 IO 次数:
- 缓冲流内置了默认 8KB 的缓冲区,读写数据时先写到缓冲区,缓冲区满了再一次性和磁盘交互;
- 传统节点流每次读写都要和磁盘交互,磁盘 IO 的速度比内存慢几个数量级;
- 缓冲流把大量小的磁盘 IO,合并成少量大的磁盘 IO,性能可以提升 10-100 倍。
【考点对应】模块二:缓冲流核心原理
6. serialVersionUID 的作用是什么?不手动指定会有什么问题?
【标准答案】
作用
serialVersionUID 是 Java 序列化的版本唯一标识:
- 序列化时,会把 serialVersionUID 写入字节序列中;
- 反序列化时,JVM 会对比字节序列中的 serialVersionUID 和本地类的 serialVersionUID,不一致会抛出
InvalidClassException,反序列化失败。
不手动指定的问题
不手动指定时,JVM 会根据类的属性、方法、结构自动生成版本号:
- 只要修改类的结构(加字段、改方法、删属性),版本号就会变化;
- 导致之前序列化的对象(比如 Redis 缓存、本地文件)反序列化失败,是生产环境高频坑。
最佳实践:所有需要序列化的类,手动指定
private static final long serialVersionUID = 1L;。
【考点对应】模块二:序列化核心机制
7. BIO、NIO、AIO 的核心区别和适用场景是什么?
【标准答案】
| 类型 | 全称 | 核心特点 | 线程模型 | 性能 | 适用场景 |
|---|---|---|---|---|---|
| BIO | 同步阻塞 IO | 每个连接占用一个线程,读写全程阻塞 | 1 连接 = 1 线程 | 低,连接多了线程数爆炸,OOM 风险 | 连接数少、固定的简单场景、测试代码 |
| NIO | 同步非阻塞 IO | 单线程通过多路复用器管理大量连接,读写非阻塞 | 1 线程 = N 个连接 | 高,线程开销极小 | 高并发、连接数多的互联网场景(Tomcat、Netty、网关、RPC) |
| AIO | 异步非阻塞 IO | 完全非阻塞,IO 操作由操作系统完成,完成后回调通知 | 无阻塞,事件驱动 | 理论最高 | 长连接、高并发场景,实际生产应用极少 |
【考点对应】模块三:NIO 核心对比
8. 零拷贝的原理是什么?有什么核心优势?
【标准答案】
传统 IO 的问题
传统文件传输需要 4 次数据拷贝 + 2 次用户态内核态切换:磁盘 → 内核缓冲区 → 用户缓冲区 → Socket 缓冲区 → 网卡,大量 CPU 时间浪费在拷贝上。
零拷贝原理
零拷贝是消除用户态和内核态之间的数据拷贝 ,Java 中通过FileChannel.transferTo()实现,底层是 Linux 的sendfile系统调用:数据直接从内核缓冲区拷贝到 Socket 缓冲区,不需要经过用户缓冲区,只有 2 次拷贝,全程在内核态完成,无用户态切换。
核心优势
- 大幅提升大文件传输性能,速度提升 3-5 倍;
- 降低 CPU 占用,解放 CPU 去处理其他业务;
- 减少内存占用,不需要在用户态开辟缓冲区。
【考点对应】模块三:零拷贝核心原理
二、场景坑点题(6 道,大厂实习高频业务题 / 代码题)
1. 以下代码的输出结果是什么?为什么?
java
public class Test {
public static void main(String[] args) {
System.out.println(test());
}
public static int test() {
int a = 10;
try {
a = 20;
return a;
} catch (Exception e) {
a = 30;
return a;
} finally {
a = 40;
}
}
}
【标准答案】输出结果:20原因:
- try 中的 return 执行时,会先把 a 的当前值(20)保存到返回值快照中;
- 再执行 finally 代码块,修改 a 的值为 40,但修改的是局部变量 a,不会影响已经保存的返回快照;
- 最后返回快照中的 20。
补充:如果 a 是引用类型,finally 中修改对象的内容会生效,因为快照保存的是对象的地址,指向同一个堆对象。
【考点对应】模块一:带 return 的 try-catch-finally 执行规则
2. 你的校园二手平台要实现商品图片上传功能,应该用字节流还是字符流?为什么?用什么优化手段?
【标准答案】
- 必须用字节流:原因:图片是二进制文件,字符流是处理文本的,用字符流读取二进制文件会破坏文件结构,导致图片损坏;字符流的编码转换会篡改字节内容,无法还原。
- 优化手段 :
- 加缓冲流
BufferedInputStream/BufferedOutputStream,减少磁盘 IO 次数,提升上传速度; - 用 try-with-resources 管理流,自动关闭,避免流泄漏;
- 大图片用 NIO 零拷贝优化,提升传输性能。
- 加缓冲流
【考点对应】模块二:IO 流选型、性能优化
3. 你的校园二手平台怎么设计异常体系?全局异常处理器怎么实现?
【标准答案】
异常体系设计
- 基础异常类 :自定义
BusinessException继承RuntimeException,携带错误码、错误信息,所有业务异常的父类; - 业务细分异常 :按模块拆分
OrderException、UserException、PayException、FileException,继承BusinessException,区分不同业务场景的错误; - 避免受检异常:所有业务异常都是运行时异常,避免强制捕获的代码冗余。
全局异常处理器实现
基于 Spring Boot 的@RestControllerAdvice + @ExceptionHandler实现:
- 捕获自定义
BusinessException,返回对应错误码和提示信息; - 捕获参数校验异常、空指针等系统异常,返回通用友好提示,打印错误日志;
- 捕获 Throwable 兜底,避免未捕获异常直接返回给前端,保证接口响应格式统一。
【考点对应】模块一:自定义异常与全局异常处理
4. 你的校园二手平台把商品对象存入 Redis 缓存,修改了 Goods 类加了一个字段后,反序列化失败,是什么原因?怎么解决?
【标准答案】
原因
Goods 类没有手动指定serialVersionUID,JVM 自动根据类结构生成版本号;加字段后类结构变化,版本号改变,Redis 中旧的序列化对象的版本号和新类的版本号不一致,反序列化抛出InvalidClassException。
解决方案
- 给所有需要序列化的实体类手动指定固定的
serialVersionUID,比如private static final long serialVersionUID = 1L;,类修改后版本号不变,兼容旧的序列化对象; - 清空 Redis 中旧的缓存数据,重新写入新的序列化对象;
- 扩展字段加
transient修饰,不参与序列化,避免版本号变化。
【考点对应】模块二:serialVersionUID 坑点
5. 以下代码读取商品的 TXT 描述文件,输出中文乱码,是什么原因?怎么解决?
java
FileInputStream fis = new FileInputStream("goods.txt");
byte[] bytes = new byte[1024];
int len;
while ((len = fis.read(bytes)) != -1) {
System.out.println(new String(bytes, 0, len));
}
fis.close();
【标准答案】
乱码原因
用字节流读取中文文本,中文在 UTF-8 中占 3 个字节,字节流每次读取固定长度的字节,会把一个中文的字节拆分,导致编码解析错误,出现乱码。
解决方案
- 方案 1:用字符流
BufferedReader读取,指定 UTF-8 编码,自动处理中文编码; - 方案 2:用转换流
InputStreamReader把字节流转为字符流,指定编码,解决不同编码文件的乱码问题; - 方案 3:一次性读取文件的所有字节,再转字符串,避免字节拆分。
【考点对应】模块二:IO 中文乱码问题
6. 你的校园二手平台要实现商品大视频的下载功能,怎么优化下载性能?
【标准答案】核心优化手段是零拷贝,具体实现:
- 用 NIO 的
FileChannel.transferTo()方法实现零拷贝,避免用户态和内核态的拷贝开销,下载速度提升 3-5 倍,CPU 占用大幅降低; - 开启浏览器缓存,设置静态资源的缓存头,避免重复下载;
- 大文件分片下载,支持断点续传,提升用户体验;
- 用 CDN 加速静态资源,降低服务器压力。
【考点对应】模块三:零拷贝应用场景、大文件优化
三、进阶原理题(4 道,中大厂实习拔高题)
1. 为什么现代 Java 框架(Spring、MyBatis)都把受检异常转为运行时异常?受检异常有什么设计弊端?
【标准答案】受检异常的核心设计弊端:
- 代码冗余:强制开发者捕获 / 抛出,每个方法都要写大量 try-catch,代码可读性差,嵌套严重;
- 破坏封装:上层调用者需要知道底层抛出的受检异常,底层修改异常会影响所有上层调用,违反开闭原则;
- 无实际意义:绝大多数受检异常(IOException、SQLException)上层调用者无法处理,只能打印日志抛给前端,强制捕获没有意义;
- Lambda/Stream 不兼容:函数式编程中无法抛出受检异常,需要额外处理,代码繁琐。
现代框架统一转为运行时异常,由全局异常处理器统一处理,代码更简洁,解耦性更强,符合企业级开发的最佳实践。
【考点对应】模块一:受检异常设计弊端
2. NIO 的多路复用原理是什么?为什么单线程就能管理成千上万的连接?
【标准答案】NIO 多路复用的核心是Selector 多路复用器 ,底层依赖操作系统的select/poll/epoll系统调用:
- 所有 Channel 都注册到 Selector 上,Selector 监听 Channel 的读写事件;
- 没有事件时,Selector 线程阻塞,不占用 CPU;
- 有事件时,操作系统通知 Selector,Selector 线程只需要处理有事件的 Channel,不需要轮询所有连接;
- 全程只有一个线程处理所有连接的事件,线程开销极小,因此可以管理成千上万的连接。
对比 BIO:每个连接一个线程,连接多了线程数爆炸,NIO 用单线程就可以处理大量连接,这就是高并发的核心。
【考点对应】模块三:NIO 多路复用原理
3. 为什么 NIO 的性能远高于 BIO?
【标准答案】核心差异在三个维度:
- 线程模型:BIO 是 1 连接 = 1 线程,连接多了线程上下文切换开销极大,容易 OOM;NIO 是 1 线程管理 N 个连接,线程开销几乎可以忽略;
- 阻塞模式:BIO 读写全程阻塞,线程只能等待 IO 完成,无法做其他事;NIO 非阻塞,没有事件时线程可以处理其他任务,CPU 利用率极高;
- 数据处理:BIO 面向流,每次读写一个字节 / 字符,频繁 IO;NIO 面向缓冲区,批量读写数据,IO 次数少,性能更高。
【考点对应】模块三:NIO 性能优势原理
4. Java 序列化是浅拷贝还是深拷贝?怎么实现深拷贝?
【标准答案】
- 默认序列化是浅拷贝:序列化对象时,只会拷贝对象本身和对象中的基本类型属性;如果对象中有引用类型属性,只会拷贝引用的地址,不会拷贝引用指向的对象,两个对象会共享同一个引用对象。
- 实现深拷贝的方式 :
- 方式 1:所有引用属性也实现 Serializable 接口,递归序列化;
- 方式 2:重写 writeObject 和 readObject 方法,手动序列化引用属性;
- 方式 3:用 JSON 序列化(Jackson、Gson)把对象转 JSON 再转回对象,实现深拷贝;
- 方式 4:用 Apache Commons Lang 的 SerializationUtils 工具类实现深拷贝。
【考点对应】模块二:序列化深浅拷贝问题
实习面试答题加分技巧(绑定你的校园二手平台项目)
- 所有异常题都绑定项目架构 :
- 问异常体系:主动说「我做校园二手平台的时候,按模块拆分了订单、用户、支付等自定义业务异常,全部继承运行时异常,用 Spring 的全局异常处理器统一捕获,代码里没有冗余的 try-catch」;
- 所有 IO 题都绑定项目实践 :
- 问 IO 优化:主动说「我做商品图片上传的时候,用缓冲流 + try-with-resources,大文件下载用零拷贝优化,之前踩过字节流读取中文乱码、serialVersionUID 不指定导致 Redis 反序列化失败的坑」;
- 答题逻辑固定为「结论→原理→我的项目踩坑 / 实践」,比单纯背知识点的候选人认可度高很多。