Java 基础核心复习笔记 (Day 04) —— 异常、反射与动态代理深度解析
Java 基础核心复习笔记 (Day 04) —— 异常、反射与动态代理深度解析
1. 异常体系 (Exception Hierarchy)
Java 的异常机制不仅是为了报错,更是为了让程序在出错时能“软着陆”。
1.1 核心图解:Throwable 家族
1 | [ Throwable ] |
1.2 关键区别辨析
1.2.1 Error vs Exception
- Error: 系统级错误,通常是 JVM 出了问题(如内存溢出
OutOfMemoryError、栈溢出StackOverflowError)。程序无法处理,也不建议捕获(catch 也没救,赶紧重启吧)。 - Exception: 程序级错误,程序可以捕获并恢复。
1.2.2 Checked vs Unchecked (Runtime) Exception
| 特性 | Checked Exception (受检异常) | Unchecked Exception (非受检异常) |
|---|---|---|
| 定义 | 继承自 Exception 但不继承 RuntimeException |
继承自 RuntimeException |
| 编译器行为 | 强制处理。必须 try-catch 或 throws,否则编译报错(红线)。 | 不强制。编译期不检查,运行时才炸。 |
| 语义 | 表示“可预期的”外部错误(如文件不存在、网络中断)。 | 表示“代码逻辑错误”(如空指针、数组越界、除数为0)。 |
| 事务回滚 | Spring 默认不回滚 Checked 异常。 | Spring 默认回滚所有 Runtime 异常。 |
1.2.3 ClassNotFoundException vs NoClassDefFoundError (必问)
- ClassNotFoundException (Exception):
- 场景: 找不到类。通常发生在 反射动态加载 时(如
Class.forName("xxx")),类路径下没有这个类。 - 心态: “我想找个朋友,但查无此人。”
- 场景: 找不到类。通常发生在 反射动态加载 时(如
- NoClassDefFoundError (Error):
- 场景: 类定义找不到。通常发生在 编译时类存在,但运行时类文件消失了(或者 Jar 包版本不对)。
- 心态: “我记得明明有这个朋友的(编译通过了),怎么突然人间蒸发了?”
1.3 Throwable 常用方法 (新增)
当我们在日志中打印异常时,这几个方法最常用:
- String getMessage():
- 返回异常发生时的详细信息(简短描述)。例如
File not found。
- 返回异常发生时的详细信息(简短描述)。例如
- String toString():
- 返回异常发生时的简要描述。格式通常是:
异常类名: getMessage()。
- 返回异常发生时的简要描述。格式通常是:
- String getLocalizedMessage():
- 返回本地化信息。如果没有被子类覆盖,默认等于
getMessage()。
- 返回本地化信息。如果没有被子类覆盖,默认等于
- void printStackTrace():
- 最常用。在控制台打印 Throwable 对象封装的异常信息,包括异常类型、错误信息以及调用栈轨迹(哪一行代码报错了)。
1.4 异常使用最佳实践 (避坑指南) (新增)
❌ 不要把异常定义为静态变量:
- 异常包含栈信息(Stack Trace),静态变量是单例的,会导致异常栈信息错乱,不知道是哪次调用抛出的。每次都要
new Exception()。
- 异常包含栈信息(Stack Trace),静态变量是单例的,会导致异常栈信息错乱,不知道是哪次调用抛出的。每次都要
❌ 抛出的异常信息一定要有意义:
- 不要抛
new RuntimeException("Error"),而要抛new RuntimeException("用户ID不能为空")。
- 不要抛
✅ 抛出具体的异常:
- 字符串转数字失败,应该抛
NumberFormatException,而不是笼统的IllegalArgumentException,方便上层精准捕获。
- 字符串转数字失败,应该抛
✅ 避免重复记录日志:
错误示范:
1
2
3
4try { ... } catch (Exception e) {
logger.error("出错啦", e); // 记录了一次
throw e; // 又抛出去了,上层捕获后可能又记录一次
}这会导致日志文件膨胀,且干扰排查。要么记录并吞掉,要么直接抛出不记录。
2. 泛型与语法糖 (Generics & Syntax Sugar)
2.1 什么是泛型?有什么用?(新增)
泛型 (Generics) 本质是 参数化类型。
- 作用:
- 类型安全: 编译时检查类型,避免
ClassCastException。 - 消除强转: 代码里不用写
(String) list.get(0)了。 - 代码复用: 一套代码,可以处理 String,也可以处理 Integer。
- 类型安全: 编译时检查类型,避免
2.1.1 泛型的三种使用方式
- 泛型类:
public class Box<T> { private T t; } - 泛型接口:
public interface Generator<T> { public T next(); } - 泛型方法:
public <E> void printArray(E[] inputArray) { ... }
2.2 泛型类型擦除 (Type Erasure)
Java 的泛型是 伪泛型。
- 编译前:
List<String>和List<Integer>是不同的。 - 编译后: 泛型信息被擦除,它们都变成了原生
List(底层存 Object)。 - 验证:
list1.getClass() == list2.getClass()结果为true。
2.3 什么是语法糖?(新增)
语法糖 (Syntactic Sugar): 编程语言为了方便程序员而设计的特殊语法。
- 特点: 对功能没影响,但代码更简洁。
- 原理: JVM 其实不认识语法糖,编译器 (javac) 会在编译阶段把它们“解糖”成基础语法。
2.3.1 常见语法糖举例
For-each 循环:
源代码:
1
2String[] strs = {"Java", "Guide"};
for (String s : strs) { System.out.println(s); }解糖后 (底层原理): 如果是数组,底层转为普通的
for(int i=0...)。如果是集合,底层转为 迭代器 (Iterator) 遍历。
Try-With-Resources (JDK 7+):
作用: 自动关闭流资源,代替繁琐的
try-catch-finally。源代码:
1
2
3try (FileInputStream fis = new FileInputStream("a.txt")) {
fis.read();
} catch (IOException e) { ... }解糖后: 编译器自动生成
finally块,并在里面调用fis.close(),同时处理了极其复杂的异常抑制逻辑(避免关闭流时的异常覆盖了业务异常)。
3. 反射与动态代理深度解析 (核心重难点)
3.1 什么是反射?
反射 (Reflection) 是 Java 的灵魂。它允许程序在运行时:
- 获取任意类的内部信息(属性、方法、构造器)。
- 动态创建对象、调用方法。
🛠️ 图解:反射机制
1 | [ 正向操作 ] : 类名 -> new -> 对象 -> 调方法 |
- 优点: 灵活,是框架(Spring, MyBatis)的基础。
- 缺点: 慢(涉及动态解析),不安全(可以无视 private 修饰符)。
3.2 静态代理 vs 动态代理
这也是面试必问。
- 静态代理:
- 原理: 代理类是我们手写的
.java文件。 - 缺点: 一个目标类对应一个代理类,代码冗余,扩展麻烦。
- 原理: 代理类是我们手写的
- 动态代理:
- 原理: 代理类是程序运行时自动生成的字节码。
- 优点: 一个代理处理器可以服务于无数个目标类(解耦)。
🛠️ 图解:动态代理 vs 静态代理
1 | 静态代理: [UserProxy] ---> [UserService] |
3.3 JDK 动态代理 vs CGLIB (源码级深度解析)
这里我们用最通俗的语言,结合逐行注释,彻底搞懂这两者的代码实现。
3.3.1 JDK 动态代理 (基于接口)
核心: 必须有接口。代理类和目标类是兄弟关系(都实现了同一个接口)。
💻 源码解析:
1 | // 1. 定义一个调用处理器 (InvocationHandler) |
3.3.2 CGLIB 动态代理 (基于继承)
核心: 不需要接口。代理类是目标类的子类(认干爹模式)。
💻 源码解析:
1 | // 1. 定义一个方法拦截器 (MethodInterceptor) |
3.3.3 终极对比 (背诵表)
| 特性 | JDK 动态代理 | CGLIB 动态代理 |
|---|---|---|
| 底层原理 | 反射 + 接口实现 | ASM 字节码 + 子类继承 |
| 代理要求 | 目标类必须实现接口 | 目标类不能是 final (无法继承) |
| 效率 | 创建快,运行稍慢 (反射) | 创建慢,运行快 (FastClass 索引) |
| Spring 选择 | Bean 有接口时默认用 JDK | Bean 无接口时强制用 CGLIB |
4. 序列化与 I/O (Serialization & I/O)
4.1 序列化与反序列化
- 序列化: 对象 -> 字节序列 (存盘/传输)。
- 反序列化: 字节序列 -> 对象。
4.1.1 关键问题
- transient 关键字:
- 修饰的字段不会被序列化。常用于密码、敏感信息。
- 注意:
static变量也不参与序列化(因为它属于类,不属于对象)。
- 为什么不推荐 JDK 原生序列化?
- 不支持跨语言: Python 读不懂 Java 的序列化流。
- 性能差: 序列化后流太大,占带宽。
- 不安全: 容易导致反序列化漏洞攻击。
- 替代: JSON (Jackson), Protobuf。
4.2 I/O 流:字节流 vs 字符流
- 字节流 (Byte Stream):
InputStream,OutputStream。- 处理单元:字节 (8bit)。
- 场景:万能。图片、视频、MP3、文本都能读。
- 字符流 (Char Stream):
Reader,Writer。- 处理单元:字符 (2 byte)。
- 场景:纯文本。
- 为什么要有它? 因为不同编码(UTF-8, GBK)中汉字占用的字节数不同,字节流容易乱码。字符流内置了缓冲区和编码转换,读文本更方便。
5. SPI 机制 (Service Provider Interface)
5.1 什么是 SPI?和 API 有啥区别?
🛠️ 图解:SPI vs API
1 | API (正向): |
- API: 也就是我们平时写的代码,具体实现由接口定义方控制。
- SPI: 控制反转。标准制定者(如 Java 官方)只定义接口(如
java.sql.Driver),厂商(MySQL, Oracle)去写实现。
5.2 实际应用场景
- JDBC: Java 定义
Driver接口,MySQL 导入 jar 包后,Java 自动扫描并加载 MySQL 的驱动。 - Spring Boot: 自动配置的核心。
spring.factories文件里配置了大量的配置类,Spring 启动时通过 SPI 机制自动扫描并加载它们,实现了“开箱即用”。
🔥 大厂面试通关检查 (Section Final)
Q1: 在 try-catch-finally 中,如果 try 中 return 了,finally 里的代码还会执行吗?
A: 会执行。
- 只要 try 块开始执行,finally 块一定会执行。
- 陷阱: 如果
finally中也有return,它会覆盖 try 中的返回值!这是非常危险的代码,绝对禁止在 finally 中写 return。
Q2: 介绍一下动态代理在框架中的实际应用?
A:
- Spring AOP: 可以在不修改业务代码的情况下,添加日志、权限校验。如果类实现了接口用 JDK 代理,否则用 CGLIB。
- MyBatis: Mapper 接口没有实现类,为什么能注入?因为 MyBatis 用 JDK 动态代理 生成了一个代理对象,拦截了接口方法,转而去执行 SQL 语句。
- RPC (Dubbo): 调用远程服务像调用本地方法一样,其实是调用了一个动态代理对象,它在底层封装了网络通信。
Q3: 为什么 I/O 流要分为字节流和字符流?只用字节流不行吗?
A:
- 理论上字节流可以处理所有文件。但在处理 文本 时,字节流非常痛苦。
- 比如 UTF-8 编码,一个汉字占 3 个字节。如果用字节流读,可能读了一半(1个或2个字节)就转成字符,会导致乱码。
- 字符流 智能处理了编码问题,它是“字节流 + 编码表”的封装,专门用于高效、正确地处理文本。





