Java基础(三):进阶机制与专题
Java基础(三):进阶机制与专题
导语:本篇聚焦 Java 进阶机制与专题考点。反射与注解、异常体系与使用最佳实践、序列化与深浅拷贝、内部类、泛型,共 22 题。
一、反射与注解
1. 什么是反射?它有什么用途?
答: 反射是 JVM 在运行时动态获取类的结构信息(属性、方法、构造器、注解)并操作对象的能力。用途:
- 框架核心:Spring(IOC/DI 靠反射创建并注入 Bean)、MyBatis、JUnit、JDBC 驱动加载(
Class.forName)等; - 动态代理、注解处理器;
- 运行时序列化/反序列化、通用工具(如 Apache Commons BeanUtils)。
典型场景细化:① Spring IoC 启动时扫描
@Component/@Service等注解,用反射实例化 Bean 并通过@Autowired/构造器注入依赖;② AOP/动态代理:InvocationHandler.invoke中通过反射Method.invoke调用目标方法;③ ORM(MyBatis/Hibernate)通过反射读取实体字段、把查询结果映射并设值;④@Value等注解由反射读取内容并赋值给字段。
2. 获取 Class 对象有哪几种方式?
答: 主要有三种(外加基本类型包装类的 TYPE):
// 1. 类名.class(编译期已知,最安全高效)
Class<?> c1 = Foo.class;
// 2. 对象.getClass()
Class<?> c2 = foo.getClass();
// 3. Class.forName(全限定名)(最常用,可处理未知类)
Class<?> c3 = Class.forName("com.xxx.Foo");
// 4. 基本类型包装类.TYPE
Class<?> c4 = Integer.TYPE; // 等价于 int.class3. 反射创建对象有哪两种方式?反射效率如何?
答:
clazz.newInstance()(Java 9 已废弃,推荐用clazz.getDeclaredConstructor().newInstance());- 通过
Constructor对象的newInstance(...)传参构造。
效率:反射低于 new(需查找类、做安全检查、无法被 JIT 充分内联优化)。优化手段:setAccessible(true) 关闭访问检查、缓存 Class/Method、使用 ReflectASM 等字节码生成方案。结论:避免在热点代码路径滥用反射,但框架级、低频使用完全可接受。
4. Java 反射的核心 API 有哪些类?
答: 位于 java.lang.reflect:
Class:反射入口,描述类本身;Field:成员变量,可读取/设置值;Method:方法,可调用;Constructor:构造器;Modifier:解析修饰符;Array:动态操作数组。
5. 反射的优缺点是什么?
答:
- 优点:极高灵活性,支持运行时动态行为,是众多框架基石。
- 缺点:① 性能低于直接调用;② 破坏封装性(可访问
private);③ 绕过编译期类型检查,易出运行时错误;④ 在安全受限环境受限。
6. Java 的 4 个标准元注解是什么?
答:
@Target:注解可作用的目标(类、方法、字段、参数等);@Retention:保留期——SOURCE(源码,如@Override)、CLASS(字节码,默认)、RUNTIME(运行时,反射可读,最常用);@Documented:是否纳入 Javadoc;@Inherited:子类是否继承父类的注解。
@Retention(RetentionPolicy.RUNTIME)是自定义注解配合反射/AOP 生效的前提。
7. 自定义注解的典型应用场景与实现?
答: 常用于权限校验、日志、统一异常处理、接口加解密、重试等,结合 Spring AOP 实现。实现步骤:
- 定义注解(指定
@Target、@Retention(RUNTIME)); - 在目标方法/类上标注;
- 编写 AOP 切面(
@Around等),通过反射判断方法是否带该注解并执行横切逻辑。
二、异常
8. Java 异常的体系结构是怎样的?Error 与 Exception 的区别?
答: 顶层是 Throwable,下分两支:
Error:JVM 层面严重错误(如OutOfMemoryError、StackOverflowError),不应也不应捕获,程序通常无法恢复。Exception:程序可处理的异常,分两类:- 受检异常(checked):
Exception除去RuntimeException及其子类,编译器强制处理(try-catch或throws),如IOException、SQLException。 - 非受检异常(unchecked):
RuntimeException及其子类(如NullPointerException、IllegalArgumentException),编译器不强制处理。
- 受检异常(checked):
9. 运行时异常与受检异常的区别?何时用哪种?
答: 区分标准是编译器是否强制处理。运行时异常表示程序逻辑错误(多因代码 bug),应通过修复代码避免,不必强制捕获;受检异常表示外部不可控因素(IO、网络),编译器强制处理。经验:无特殊要求优先用 RuntimeException,仅在调用方必须处理时才用受检异常。
10. throw 与 throws 的区别?
答:
- 位置:
throw在方法内部,后跟异常对象;throws在方法签名处,后跟异常类型(可多个)。 - 作用:
throw主动抛出一个具体异常(制造异常);throws声明该方法可能抛出的异常,提醒调用者处理。 - 数量:
throw一次抛一个;throws可声明多个。
11. try-catch-finally 中,finally 一定执行吗?finally 中的 return 会怎样?
答: 正常情况下 finally 总会执行(即便 try/catch 有 return)。但有两种例外:① JVM 退出(System.exit());② 所在线程被杀死。
关键坑:若 finally 中也有 return 或抛异常,它会覆盖 try/catch 中的 return 值与异常。因此不要在 finally 中写 return 或抛异常。
int test() {
try { return 1; }
finally { return 2; } // 最终返回 2,try 的 return 1 被丢弃
}12. 什么是 try-with-resources?有什么好处?
答: Java 7 引入的语法糖,自动关闭实现了 AutoCloseable 的资源(流、连接等),无需手写 finally:
try (BufferedReader br = new BufferedReader(new FileReader("a.txt"))) {
System.out.println(br.readLine());
} catch (IOException e) { e.printStackTrace(); }好处:代码简洁、避免资源泄漏、异常抑制机制更合理(被关闭时的异常作为 suppressed 附加,不会掩盖主异常)。
13. 如何自定义异常?生产环境为何常用自定义异常?
答: 继承 Exception(受检)或 RuntimeException(非受检),通常提供无参和带 message 两个构造器:
public class BizException extends RuntimeException {
public BizException(String msg) { super(msg); }
}生产中自定义异常可表达业务语义(如余额不足、无权限),且便于在统一异常处理器中按类型做差异化处理与国际化。
三、异常使用注意事项
14. 使用异常时有哪些注意事项 / 最佳实践?
答: 关键注意事项:
- 不要把异常定义为静态变量:静态异常对象会被多处复用,导致异常栈信息错乱;每次抛出应
new一个新异常; - 异常信息要有意义:便于定位问题,避免空泛;
- 抛出更具体的异常:如字符串转数字失败应抛
NumberFormatException而非其父类的IllegalArgumentException,便于调用方精准处理; - 避免重复记录日志:在
catch中已记录足够信息后,再次向外抛异常时不要再重复记录相同日志,否则日志膨胀、掩盖真实原因; - 不要用异常做正常流程控制,也不建议吞掉异常(空的
catch)。
四、序列化与拷贝
15. Java 序列化是什么?如何实现一个类可序列化?
答: 序列化是把对象转为字节序列以便持久化或网络传输;反序列化是还原。实现:让类实现 java.io.Serializable 标记接口,用 ObjectOutputStream/ObjectInputStream 读写。
public class User implements Serializable {
private static final long serialVersionUID = 1L;
private String name;
// transient 字段不参与序列化
private transient String temp;
}16. transient 和 static 变量在序列化时如何处理?serialVersionUID 有什么用?
答:
transient:修饰的字段不会被序列化(反序列化后为默认值)。static:静态变量属于类而非对象,不随对象序列化;反序列化后读取的是当前 JVM 中该类的静态值。serialVersionUID:序列化版本号。反序列化时 JVM 比对字节流中的 UID 与本地类的 UID,不一致抛InvalidClassException。显式声明可保证类结构微调(如加字段)后仍兼容旧数据,推荐总是显式定义。
17. 浅拷贝与深拷贝的区别?如何实现深拷贝?
答:
- 浅拷贝:只复制对象本身及基本类型字段,引用类型字段仍指向原对象的同一引用(修改会互相影响)。
- 深拷贝:引用类型字段也递归复制出独立对象,两者完全隔离。
实现深拷贝的常用方式:① 逐层重写 clone() 并手动 new 引用对象(需实现 Cloneable);② 通过序列化(对象写出再读回)得到全新副本;③ 使用拷贝构造器/工具(如 Apache Commons SerializationUtils.clone、MapStruct)。
18. Cloneable 接口与 clone() 方法有什么注意点?
答: Cloneable 是标记接口,Object.clone() 是 protected 的浅拷贝。正确做法:类实现 Cloneable,重写 clone() 为 public 并调用 super.clone()。默认 clone() 是浅拷贝,引用字段需自行处理才能得到深拷贝。因设计缺陷(破坏构造器安全、需强转),现代代码更推荐用拷贝构造器或序列化替代 clone()。
五、序列化协议
19. 常见的序列化协议有哪些?为什么不太推荐 JDK 自带的序列化?
答: 序列化协议大致分两类:
- 二进制协议(性能好、体积小):Hessian、Kryo、Protobuf、ProtoStuff 等;
- 文本协议(可读性好、性能差):JSON、XML 等。
不推荐 JDK 原生(ObjectOutputStream)序列化的原因:
- 不支持跨语言:字节流格式为 Java 私有,其他语言服务无法反序列化;
- 性能差、体积大:相比 Protobuf 等,序列化后字节数组更大,传输/存储成本高;
- 存在安全漏洞:若反序列化的输入可被用户控制,攻击者可构造恶意数据触发反序列化远程代码执行(RCE)。
因此生产上多用 Hessian/Kryo/Protobuf 等替代,或改用 JSON(跨语言、易调试,但性能一般)。
六、内部类
20. Java 内部类有哪几种?各自特点?
答: 四类:
- 成员内部类:定义在类内部、方法外,可访问外部类所有成员(含 private),需先有外部类实例才能创建。
- 静态内部类:
static修饰,不持有外部类引用,只能访问外部类静态成员;创建方式new Outer.Inner()。 - 局部内部类:定义在方法/代码块内,作用域仅限该方法,可访问方法的
final或 effectively final 局部变量。 - 匿名内部类:无名字,必须继承一个类或实现一个接口,且不能定义静态成员,常用于回调/事件监听。
21. 局部内部类和匿名内部类访问局部变量,为什么变量必须是 final(或 effectively final)?
答: 局部变量存于栈帧,方法结束后即销毁;而内部类对象可能存活更久(被返回或异步使用)。若允许内部类修改该局部变量,会出现"变量已销毁却还能改"的矛盾。JVM 的做法是把该变量拷贝一份进内部类,为保证内外两份副本一致,要求变量不可变(final 或 Java 8 的"事实上 final"——未用 final 修饰但从未被重新赋值)。
七、泛型
22. 什么是泛型?有什么作用?泛型有哪几种使用方式?
答: 泛型(Generics,JDK 5 引入)指在定义类、接口、方法时使用类型参数(占位符,如 T/E/K/V),使用时再指定具体类型。
作用:
- 编译期类型检查:如
List<String>只能放String,传错类型直接编译报错; - 免除强制类型转换:原生
List取元素是Object,需手动强转,泛型由编译器自动转换; - 提高代码复用与可读性:一套逻辑适配多种类型。
使用方式(三种):
- 泛型类:
class Box<T> { private T value; } - 泛型接口:
interface Comparable<T> { int compareTo(T o); } - 泛型方法:
public <E> void printArray(E[] arr) { ... }
注意:静态方法无法使用类上声明的泛型(静态方法加载早于类实例化),静态泛型方法必须自己声明类型参数(如
public static <E> void f(E e))。另外,泛型在运行时会被类型擦除,仅作编译期约束。
