Java 基础核心复习笔记 (Day 04) —— 异常、反射与动态代理深度解析

1. 异常体系 (Exception Hierarchy)

Java 的异常机制不仅是为了报错,更是为了让程序在出错时能“软着陆”。

1.1 核心图解:Throwable 家族

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
          [ Throwable ]
/ \
[ Error ] [ Exception ]
| |
OutOfMemoryError (Checked Exception)
StackOverflowError |
NoClassDefFoundError IOException
ClassNotFoundException
SQLException
|
[ RuntimeException ] (Unchecked)
|
NullPointerException
IndexOutOfBoundsException
IllegalArgumentException

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 异常使用最佳实践 (避坑指南) (新增)

  1. ❌ 不要把异常定义为静态变量:

    • 异常包含栈信息(Stack Trace),静态变量是单例的,会导致异常栈信息错乱,不知道是哪次调用抛出的。每次都要 new Exception()
  2. ❌ 抛出的异常信息一定要有意义:

    • 不要抛 new RuntimeException("Error"),而要抛 new RuntimeException("用户ID不能为空")
  3. ✅ 抛出具体的异常:

    • 字符串转数字失败,应该抛 NumberFormatException,而不是笼统的 IllegalArgumentException,方便上层精准捕获。
  4. ✅ 避免重复记录日志:

    • 错误示范:

      1
      2
      3
      4
      try { ... } catch (Exception e) {
      logger.error("出错啦", e); // 记录了一次
      throw e; // 又抛出去了,上层捕获后可能又记录一次
      }
    • 这会导致日志文件膨胀,且干扰排查。要么记录并吞掉,要么直接抛出不记录。

2. 泛型与语法糖 (Generics & Syntax Sugar)

2.1 什么是泛型?有什么用?(新增)

泛型 (Generics) 本质是 参数化类型

  • 作用:
    1. 类型安全: 编译时检查类型,避免 ClassCastException
    2. 消除强转: 代码里不用写 (String) list.get(0) 了。
    3. 代码复用: 一套代码,可以处理 String,也可以处理 Integer。

2.1.1 泛型的三种使用方式

  1. 泛型类: public class Box<T> { private T t; }
  2. 泛型接口: public interface Generator<T> { public T next(); }
  3. 泛型方法: 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 常见语法糖举例

  1. For-each 循环:

    • 源代码:

      1
      2
      String[] strs = {"Java", "Guide"};
      for (String s : strs) { System.out.println(s); }
    • 解糖后 (底层原理): 如果是数组,底层转为普通的 for(int i=0...)。如果是集合,底层转为 迭代器 (Iterator) 遍历。

  2. Try-With-Resources (JDK 7+):

    • 作用: 自动关闭流资源,代替繁琐的 try-catch-finally

    • 源代码:

      1
      2
      3
      try (FileInputStream fis = new FileInputStream("a.txt")) {
      fis.read();
      } catch (IOException e) { ... }
    • 解糖后: 编译器自动生成 finally 块,并在里面调用 fis.close(),同时处理了极其复杂的异常抑制逻辑(避免关闭流时的异常覆盖了业务异常)。

3. 反射与动态代理深度解析 (核心重难点)

3.1 什么是反射?

反射 (Reflection) 是 Java 的灵魂。它允许程序在运行时

  1. 获取任意类的内部信息(属性、方法、构造器)。
  2. 动态创建对象、调用方法。

🛠️ 图解:反射机制

1
2
[ 正向操作 ] :  类名 -> new -> 对象 -> 调方法
[ 反射操作 ] : 对象 -> getClass() -> 类信息 -> 调方法
  • 优点: 灵活,是框架(Spring, MyBatis)的基础。
  • 缺点: 慢(涉及动态解析),不安全(可以无视 private 修饰符)。

3.2 静态代理 vs 动态代理

这也是面试必问。

  • 静态代理:
    • 原理: 代理类是我们手写.java 文件。
    • 缺点: 一个目标类对应一个代理类,代码冗余,扩展麻烦。
  • 动态代理:
    • 原理: 代理类是程序运行时自动生成的字节码。
    • 优点: 一个代理处理器可以服务于无数个目标类(解耦)。

🛠️ 图解:动态代理 vs 静态代理

1
2
3
4
5
6
静态代理:  [UserProxy] ---> [UserService]
[OrderProxy] ---> [OrderService] (手写死累)

动态代理: [通用 Handler] ---> [UserService]
---> [OrderService]
---> [ProductService] (以一当十)

3.3 JDK 动态代理 vs CGLIB (源码级深度解析)

这里我们用最通俗的语言,结合逐行注释,彻底搞懂这两者的代码实现。

3.3.1 JDK 动态代理 (基于接口)

核心: 必须有接口。代理类和目标类是兄弟关系(都实现了同一个接口)。

💻 源码解析:

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
32
33
34
35
36
37
38
39
// 1. 定义一个调用处理器 (InvocationHandler)
// 这就是“中介”的核心逻辑,所有的代理操作都在这里发生
public class DebugInvocationHandler implements InvocationHandler {
// 目标对象 (被代理的真实对象),比如原本的 UserServiceImpl
private final Object target;

// 构造方法,注入目标对象
public DebugInvocationHandler(Object target) {
this.target = target;
}

// invoke 方法:当你调用代理对象的方法时,JVM 会自动回调这个方法
// proxy: 代理对象本身 (一般不用)
// method: 正在被调用的方法 (如 login)
// args: 方法的参数
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// --- 增强逻辑 (前置) ---
System.out.println("前置通知:准备调用 " + method.getName());

// 【关键代码】反射调用目标对象的方法
// 相当于 target.method(args)
Object result = method.invoke(target, args);

// --- 增强逻辑 (后置) ---
System.out.println("后置通知:调用结束,结果是 " + result);

return result; // 返回方法的执行结果
}
}

// 2. 获取代理对象
// Proxy.newProxyInstance 是生成代理对象的工厂方法
UserService proxy = (UserService) Proxy.newProxyInstance(
UserService.class.getClassLoader(), // 类加载器:用于加载动态生成的代理类
new Class<?>[]{UserService.class}, // 接口数组:告诉 JDK 我要代理哪些接口
new DebugInvocationHandler(new UserServiceImpl()) // 处理器:把目标对象传进去
);
proxy.login(); // 调用代理对象的方法,会自动跳转到 invoke()

3.3.2 CGLIB 动态代理 (基于继承)

核心: 不需要接口。代理类是目标类的子类(认干爹模式)。

💻 源码解析:

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
32
// 1. 定义一个方法拦截器 (MethodInterceptor)
// 类似于 JDK 的 InvocationHandler
public class DebugMethodInterceptor implements MethodInterceptor {

// intercept 方法:拦截所有目标类方法的调用
// obj: 代理对象 (CGLIB 生成的子类实例)
// method: 目标方法
// args: 参数
// proxy: 方法代理对象 (MethodProxy),用于调用父类方法,比反射快
@Override
public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
// --- 前置增强 ---
System.out.println("CGLIB 前置:事务开启");

// 【关键代码】调用父类(目标类)的方法
// invokeSuper: 调用父类的方法。千万别调成 invoke,否则会死循环!
// 它的底层不是反射,而是通过 FastClass 索引机制直接调用,效率更高
Object result = proxy.invokeSuper(obj, args);

// --- 后置增强 ---
System.out.println("CGLIB 后置:事务提交");

return result;
}
}

// 2. 生成代理对象
Enhancer enhancer = new Enhancer(); // 创建增强器 (CGLIB 的核心类)
enhancer.setSuperclass(UserServiceImpl.class); // 设置父类 (目标类)
enhancer.setCallback(new DebugMethodInterceptor()); // 设置拦截器
UserServiceImpl proxy = (UserServiceImpl) enhancer.create(); // 创建代理对象
proxy.login();

3.3.3 终极对比 (背诵表)

特性 JDK 动态代理 CGLIB 动态代理
底层原理 反射 + 接口实现 ASM 字节码 + 子类继承
代理要求 目标类必须实现接口 目标类不能是 final (无法继承)
效率 创建快,运行稍慢 (反射) 创建慢,运行快 (FastClass 索引)
Spring 选择 Bean 有接口时默认用 JDK Bean 无接口时强制用 CGLIB

4. 序列化与 I/O (Serialization & I/O)

4.1 序列化与反序列化

  • 序列化: 对象 -> 字节序列 (存盘/传输)。
  • 反序列化: 字节序列 -> 对象。

4.1.1 关键问题

  1. transient 关键字:
    • 修饰的字段不会被序列化。常用于密码、敏感信息。
    • 注意:static 变量也不参与序列化(因为它属于类,不属于对象)。
  2. 为什么不推荐 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
2
3
4
5
6
7
API (正向):
[ 我定义接口 ] ---> [ 我写实现类 ] ---> [ 你调用 ]
(例如:你自己写的 Service 类)

SPI (反向/插件化):
[ 我定义接口 ] <--- [ 你写实现类 ] <--- [ 我加载你的实现 ]
(例如:JDBC 驱动、Spring Boot Starter)
  • API: 也就是我们平时写的代码,具体实现由接口定义方控制。
  • SPI: 控制反转。标准制定者(如 Java 官方)只定义接口(如 java.sql.Driver),厂商(MySQL, Oracle)去写实现。

5.2 实际应用场景

  1. JDBC: Java 定义 Driver 接口,MySQL 导入 jar 包后,Java 自动扫描并加载 MySQL 的驱动。
  2. 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:

  1. Spring AOP: 可以在不修改业务代码的情况下,添加日志、权限校验。如果类实现了接口用 JDK 代理,否则用 CGLIB。
  2. MyBatis: Mapper 接口没有实现类,为什么能注入?因为 MyBatis 用 JDK 动态代理 生成了一个代理对象,拦截了接口方法,转而去执行 SQL 语句。
  3. RPC (Dubbo): 调用远程服务像调用本地方法一样,其实是调用了一个动态代理对象,它在底层封装了网络通信。

Q3: 为什么 I/O 流要分为字节流和字符流?只用字节流不行吗?

A:

  • 理论上字节流可以处理所有文件。但在处理 文本 时,字节流非常痛苦。
  • 比如 UTF-8 编码,一个汉字占 3 个字节。如果用字节流读,可能读了一半(1个或2个字节)就转成字符,会导致乱码
  • 字符流 智能处理了编码问题,它是“字节流 + 编码表”的封装,专门用于高效、正确地处理文本。