阶段:入门篇 | 建议时长: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 |