跳到正文
Java 入门到面试指南
返回

04-异常-IO-反射-注解

阶段:入门篇 | 建议时长:4 天 | 前置:02-面向对象与核心类.md

学习目标

  • 能默画异常体系图(Throwable → Error/Exception → Checked/Unchecked)并正确选择受检/非受检异常。
  • 会用 try-with-resources 管理资源,能解释 finally 的执行时机与吞异常的危害。
  • 会用字节缓冲流拷贝文件并用 System.nanoTime 对比性能。
  • 掌握获取 Class 的三种方式与反射读写字段、调用方法,能写一个 JDK 动态代理 demo。
  • 能定义注解并用反射编写一个「模拟 @Test」的注解执行器。

核心知识点

异常体系图

面试官想听:先画层级,再答「受检异常编译器强制处理、运行时异常交给程序员」这条主线。

Throwable
 ├─ Error(严重错误,程序无法处理): OutOfMemoryError / StackOverflowError
 └─ Exception
     ├─ Checked 受检异常(编译期强制 try-catch 或 throws)
     │    IOException / SQLException / ClassNotFoundException
     └─ RuntimeException(Unchecked 非受检)
          NullPointerException / ArrayIndexOutOfBoundsException
          ClassCastException / ArithmeticException / IllegalArgumentException
  • Error 与 RuntimeException 及其子类无需强制捕获;其他 Exception 必须显式处理。
  • 编译时异常 vs 运行时异常:受检异常调用方必须面对;运行时异常通常是程序 bug,应修代码而不是到处 catch。

try-catch-finally 与 try-with-resources

try (FileInputStream in = new FileInputStream("a.txt")) {   // 自动 close,无需 finally
    // 业务
} catch (FileNotFoundException e) {          // 从小到大捕获,子类在前
    log.error("文件不存在", e);               // 必须携带原始异常
} finally {
    // 一定执行(除 System.exit/进程被杀),用于释放非 AutoCloseable 资源
}
  • 多类型 catch:catch (IOException | SQLException e);finally 中 return 会吞掉 try 中的返回值和异常——禁止在 finally 写 return。
  • try-with-resources(Java 7+):资源类实现 AutoCloseable,编译器自动生成 close 调用,异常抑制机制保留原始异常(getSuppressed()),优先于手写 finally 关闭。

自定义异常与不要吞异常

public class BizException extends RuntimeException {      // 业务异常继承 RuntimeException
    private final int code;
    public BizException(int code, String message) {
        super(message);
        this.code = code;
    }
    public int getCode() { return code; }
}
  • 业务代码统一用非受检自定义异常(带错误码),在全局层(如 Spring 的 @ControllerAdvice)兜底转换成响应。
  • 吞异常反模式:catch (Exception e) {} 或只 e.printStackTrace()——排障时线索全丢;正确做法是记录日志(带堆栈)后向上抛出或转换为业务异常。
  • 不要用异常做流程控制;捕获范围尽量精确。

IO:File 与流分类

  • File 表示路径:exists()、createNewFile()、mkdirs()、delete()、listFiles();Java 11+ 推荐 Path/Files。
  • 四个抽象基类(按「输入/输出 × 字节/字符」):InputStream/OutputStream/Reader/Writer。
  • 字节流适合一切二进制(图片、视频);字符流适合文本(自动处理编码);缓冲流包装原始流减少系统调用:
try (BufferedInputStream bis = new BufferedInputStream(new FileInputStream(src), 8192);
     BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(dest))) {
    byte[] buf = new byte[8192];
    int len;
    while ((len = bis.read(buf)) != -1) bos.write(buf, 0, len);
}
  • 编码:new InputStreamReader(in, StandardCharsets.UTF_8) 显式指定,避免平台默认编码差异。

序列化:Serializable 与 serialVersionUID

面试官想听:知道 serialVersionUID 的作用(版本兼容校验)和不写的后果,以及 transient 关键字。

  • 实现标记接口 Serializable 后,可用 ObjectOutputStream.writeObject 持久化/传输对象,ObjectInputStream.readObject 反序列化。
  • serialVersionUID:类的版本号;反序列化时若流中的版本号与当前类不一致抛 InvalidClassException。不显式声明时 JVM 自动生成,类一改动就变化,导致旧数据无法反序列化——必须显式写 private static final long serialVersionUID = 1L;。
  • transient 修饰的字段不参与序列化(如密码);static 字段属于类也不序列化。
  • 静态成员变量不参与序列化(属于类不属于对象);子类实现序列化时父类字段需父类也可序列化或有无参构造。

Files 工具类(Java 7+ NIO.2)

  • 一行流式 API 替代手工流:Files.readAllLines(Path)、Files.writeString、Files.copy(src, dest, REPLACE_EXISTING)、Files.walk 遍历目录树。
  • 大文件仍应使用缓冲流分块读取,readAllBytes 会把整个文件读入内存。

反射

  • 运行时动态获取类信息并操作(构造、字段、方法),是 Spring/Swagger/序列化框架的基石。
// 三种获取 Class 的方式(同一份 Class 对象)
Class<?> c1 = Class.forName("com.demo.User");   // 全限定名,最常用
Class<?> c2 = User.class;                        // 编译期已知类
Class<?> c3 = user.getClass();                   // 已有实例

Object u = c1.getDeclaredConstructor().newInstance();
Field field = c1.getDeclaredField("name");
field.setAccessible(true);                       // 突破 private(关闭安全检查)
field.set(u, "Tom");
Method m = c1.getMethod("sayHello", String.class);
m.invoke(u, "hi");
  • 优点:解耦、支持「配置驱动」的通用框架;缺点:性能低于直接调用、绕过编译期检查、破坏封装。
  • 无法访问被安全管理器限制的成员(setAccessible 在模块化下可能受限)。

JDK 动态代理

面试官想听:说出「运行时生成接口实现类、方法调用转发给 InvocationHandler」,以及它只能代理接口这个限制。

interface UserService { void save(String name); }

public class LogHandler implements InvocationHandler {
    private final Object target;                       // 被代理对象
    public LogHandler(Object target) { this.target = target; }
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("before " + method.getName());   // 增强逻辑
        Object result = method.invoke(target, args);        // 反射调用原方法
        System.out.println("after");
        return result;
    }
}
UserService proxy = (UserService) Proxy.newProxyInstance(
        UserService.class.getClassLoader(), new Class[]{UserService.class},
        new LogHandler(new UserServiceImpl()));
proxy.save("Tom");     // before -> 原逻辑 -> after
  • 这是 AOP 的底层原理:把「日志、事务」等横切逻辑与业务解耦。

注解与元注解

面试官想听:注解本身不做任何事,「注解 + 反射读取」才构成机制;能现场写一个自定义注解。

@Retention(RetentionPolicy.RUNTIME)     // 保留到运行期(反射可读)
@Target(ElementType.METHOD)             // 只能标注在方法上
public @interface MyTest {              // 模拟 JUnit 的 @Test
    int times() default 1;              // 带默认值的属性
}
  • 元注解:@Retention(SOURCE/CLASS/RUNTIME 三档保留期)、@Target(可用位置)、@Documented、@Inherited、@Repeatable。
  • 解析:Method#getAnnotation(MyTest.class) 返回 null 表示未标注;反射调用标注的方法即实现「注解驱动」。
for (Method m : cls.getDeclaredMethods()) {
    MyTest t = m.getAnnotation(MyTest.class);
    if (t != null) for (int i = 0; i < t.times(); i++) m.invoke(instance);
}

动手实践

任务 1:自定义业务异常体系

  • 要求:定义 BizException(含 code)与子类 OrderNotFoundException;写 findOrder(int id) 模拟 id<0 抛运行时异常、查不到抛业务异常;main 中分层 catch 并打印错误码与消息。
  • 验收标准:无 catch (Exception e) {} 空捕获;日志输出包含完整堆栈;能口头说出「为什么业务异常选 RuntimeException 而不是 IOException 那类受检异常」。

任务 2:缓冲流拷贝性能对比

  • 要求:生成一个 ≥ 10MB 的文件;分别用「单字节 read/write」「8KB 缓冲数组」「BufferedInputStream/OutputStream」三种方式拷贝,System.nanoTime() 计时。
  • 验收标准:三组耗时打印成对比表格(预期缓冲数组与缓冲流快一个数量级以上);全部使用 try-with-resources;能解释缓冲快的原因(减少系统调用次数)。

任务 3:自定义注解执行器(模拟 @Test)

  • 要求:定义 @MyTest(times);写一个类包含 3 个方法,其中 2 个标注注解;编写 runTests(Class<?>) 用反射扫描并执行标注方法,执行结果打印「方法名 通过/异常」。
  • 验收标准:新增/删除注解方法时执行器代码零改动;一个方法抛异常不影响其他方法执行;能说出 @Retention 必须是 RUNTIME 的原因。

任务 4(可选):JDK 动态代理增强

  • 验收标准:基于任务 3 的类生成代理,为所有 public 方法自动加耗时统计;打印结果包含方法名与纳秒耗时;能说明「为什么 Proxy 要求传 Class[] 接口」。

高频面试题

Q:Error 和 Exception 的区别?⭐

  • 都继承 Throwable;Error 是 JVM 层面的严重错误(OutOfMemoryError、StackOverflowError),程序不应也不必捕获。
  • Exception 是程序可以处理的异常情况。
  • Exception 再分受检(编译器强制处理,如 IOException)与非受检 RuntimeException(如 NPE,通常靠改进代码避免)。

Q:受检异常和非受检异常怎么选?

  • 调用方「可以恢复/必须处理」的外部故障用受检异常(IO、SQL),编译器兜底。
  • 程序 bug 或业务错误用 RuntimeException,避免到处 throws 污染方法签名。
  • 业务系统常见做法:统一 RuntimeException 子类 + 全局异常处理器。

Q:final、finally、finalize 的区别?⭐

  • final:修饰符——类不可继承、方法不可重写、变量值/指向不可变。
  • finally:try 块的收尾代码,保证资源释放(现在更多用 try-with-resources)。
  • finalize:Object 的方法,GC 回收前的回调,执行时机不确定且已被 Java 9 标记 @Deprecated,资源释放不要依赖它。

Q:反射是什么?优缺点和应用场景?⭐

  • 反射让程序在运行时获取类的结构(构造器/字段/方法)并动态调用。
  • 优点:解耦、通用化,支持配置驱动和插件化;缺点:有性能开销、绕过编译期检查、破坏封装。
  • 应用:Spring 的 IoC 依配置反射创建 Bean、MyBatis 结果映射、Jackson 序列化、各类注解处理。

Q:JDK 动态代理和 CGLIB 的区别?⭐

  • JDK 动态代理:JDK 内置,基于接口——运行时生成接口实现类,转发到 InvocationHandler;目标类必须实现接口。
  • CGLIB:第三方库,基于继承——运行时生成目标类的子类覆写方法(final/private 方法无法代理)。
  • Spring AOP:目标有接口时默认用 JDK 代理(Spring Boot 2.x 起默认统一 CGLIB,可通过配置切换)。

Q:serialVersionUID 有什么用?不写会怎样?

  • 反序列化时校验发送方与接收方的类版本是否兼容;不一致抛 InvalidClassException。
  • 不写时 JVM 根据类结构自动计算,任何改动(加字段/改方法)都会导致旧数据无法反序列化。
  • 显式声明固定值可让类演进时旧序列化数据仍可读。

Q:try-with-resources 相比 try-finally 好在哪?

  • 自动生成 close,代码更简洁,不会忘记关闭。
  • 多个资源逆序关闭;异常抑制机制通过 getSuppressed 保留 close 抛出的次要异常,而手写 finally 可能覆盖主异常。

推荐资料

资料说明链接
廖雪峰 Java 异常处理异常主线教程,含自定义异常https://liaoxuefeng.com/books/java/exception/index.html
廖雪峰 Java IO流、序列化与 Files 用法https://liaoxuefeng.com/books/java/io/index.html
廖雪峰 Java 反射Class 对象与访问字段方法https://liaoxuefeng.com/books/java/reflection/index.html
廖雪峰 Java 注解元注解与注解处理原理https://liaoxuefeng.com/books/java/annotation/index.html
JavaGuide IO 基础流分类与序列化面试要点https://javaguide.cn/java/io/io-basis.html