Java 异常处理详解
一、异常处理的重要性
-
错误分类:程序运行中可能出现用户输入错误(如
NumberFormatException)、资源缺失(如FileNotFoundException)、系统级错误(如内存耗尽、网络中断)等。 -
处理方式对比:
- 错误码(如 C 语言) :通过返回整数表示错误,需手动枚举和判断,代码繁琐。
- Java 异常机制:通过
class封装错误类型,可在任意位置抛出,上层统一捕获,分离错误处理逻辑。
二、Java 异常继承体系
Object
├─ Throwable(所有异常/错误的根)
│ ├─ Error(严重错误,程序无法处理,无需捕获)
│ │ 例:`OutOfMemoryError`(内存耗尽)、`StackOverflowError`(栈溢出)、`NoClassDefFoundError`(类缺失)
│ └─ Exception(可处理的运行时错误)
│ ├─ RuntimeException(非受检异常,编译器不强制捕获)
│ │ 例:`NullPointerException`(空指针)、`IndexOutOfBoundsException`(索引越界)、`IllegalArgumentException`(非法参数)
│ └─ 非RuntimeException(受检异常,编译器强制要求捕获或声明抛出)
│ 例:`IOException`(IO错误)、`UnsupportedEncodingException`(编码不支持)、`SQLException`(数据库错误)
三、受检异常(Checked Exception)与非受检异常(Unchecked Exception)
| 特性 | 受检异常(Exception 非 Runtime 子类) | 非受检异常(RuntimeException 及其子类 / Error) |
|---|---|---|
| 编译器检查 | 强制要求try...catch捕获或throws声明 | 不强制,可自由选择是否处理 |
| 处理责任 | 调用方必须处理 | 通常由程序员修复代码逻辑错误,而非运行时处理 |
| 常见场景 | 外部资源操作(文件、网络、数据库) | 程序逻辑错误(空指针、越界、非法参数等) |
四、异常处理核心语法:try...catch与throws
-
try...catch捕获异常:-
将可能抛出异常的代码放入
try块,通过多个catch块捕获不同类型的异常(子类异常需放在父类之前)。 -
示例:
try { String data = readFile("data.txt"); // 可能抛出FileNotFoundException } catch (FileNotFoundException e) { System.out.println("文件未找到:" + e.getMessage()); e.printStackTrace(); // 打印异常栈轨迹 } catch (IOException e) { // 处理更宽泛的异常 System.out.println("IO错误:" + e); }
-
-
throws声明异常:-
方法定义时用
throws声明可能抛出的受检异常,将处理责任交给调用方。 -
示例:
static byte[] toGBK(String s) throws UnsupportedEncodingException { return s.getBytes("GBK"); // 声明抛出受检异常,调用方必须处理 }
-
-
注意事项:
- 受检异常必须处理:未捕获或声明受检异常会导致编译错误。
- 避免空捕获:捕获异常后至少记录日志(如
printStackTrace()),避免静默忽略错误。 main方法作为最后屏障:可声明throws Exception直接终止程序,或在main中统一捕获所有异常。
捕获异常
一、核心概念
在 Java 中,try...catch机制用于捕获可能抛出的异常。将可能引发异常的代码置于try块中,通过catch块捕获对应的Exception及其子类。
二、多 catch 语句
-
执行逻辑:JVM 按顺序匹配
catch块,匹配成功后执行对应代码块,不再继续匹配。 -
顺序要求:子类异常必须写在父类异常前面,否则子类异常永远无法被捕获。
- 错误示例:
catch(IOException e)后接catch(UnsupportedEncodingException e)(子类在后,无法捕获)。 - 正确示例:先子类
UnsupportedEncodingException,再父类IOException。
- 错误示例:
三、finally 语句
-
作用:无论是否发生异常,
finally块中的代码都会执行,用于资源清理等必须执行的逻辑。 -
特点
- 非必需,可省略。
- 始终最后执行:正常执行完
try后执行;发生异常时,执行完匹配的catch后执行。 - 可单独使用
try...finally(无需catch,但需方法声明抛出异常)。
四、捕获多种异常
-
场景:处理逻辑相同但无继承关系的异常(如
IOException和NumberFormatException)。 -
语法:使用
|合并多个异常类型,简化代码。catch(IOException | NumberFormatException e) { // 统一处理逻辑 }
抛出异常
一、异常的传播
- 传播机制:当方法抛出异常且未被捕获时,异常会向上层调用链传播,直至被
try...catch捕获。 - 调试工具:通过
printStackTrace()打印异常调用栈,显示异常发生的方法调用层次及源码行号,便于定位问题。示例中NumberFormatException的栈信息清晰展示了从main()到Integer.parseInt()的调用路径。
二、抛出异常的核心要点
-
基本步骤
- 创建
Exception实例(如new NullPointerException())。 - 使用
throw语句抛出,可合并为一行代码(throw new NullPointerException();)。
- 创建
-
异常转换与原始信息保留
- 捕获异常后若需抛出新异常,应在新异常构造器中传入原始异常(如
throw new IllegalArgumentException(e)),通过Caused by保留完整栈信息,避免丢失 “第一案发现场”。 - 最佳实践:始终保留原始异常,确保调用链信息完整。
- 捕获异常后若需抛出新异常,应在新异常构造器中传入原始异常(如
-
finally语句的执行规则- 即使在
try或catch中抛出异常,finally仍会执行,JVM 会先执行finally再抛出异常。
- 即使在
三、异常屏蔽与处理
- 屏蔽现象:若
finally中抛出新异常,catch中准备抛出的异常会被屏蔽(称为Suppressed Exception)。 - 保留被屏蔽异常:通过
Throwable.addSuppressed(origin)将原始异常添加到新异常中,可通过getSuppressed()获取。 - 最佳实践:绝大多数情况下,避免在
finally中抛出异常,减少调试复杂度。
四、异常信息的重要性
- 提问或调试时,必须提供完整的异常栈信息(包括
Caused by和Suppressed部分),否则难以定位问题根源。
五、核心总结
| 关键点 | 说明 |
|---|---|
| 异常传播 | 异常沿调用链向上抛出,printStackTrace()打印栈信息辅助定位。 |
| 抛出异常 | throw语句创建并抛出异常,转换异常时传入原始异常以保留完整栈(Caused by)。 |
finally规则 | 无论是否抛出异常,finally都会执行;避免在finally中抛异常,防止屏蔽原始异常。 |
| 异常屏蔽 | 通过addSuppressed()保存被屏蔽异常,通常无需处理,但需知晓机制。 |
| 调试建议 | 贴出完整异常栈(含Caused by),是定位问题的关键线索。 |
自定义异常
一、Java 标准库异常体系
-
核心结构
- 顶层异常为
Exception,分为RuntimeException(运行时异常)和受检异常(如IOException)。 - 常见运行时异常:
NullPointerException、IllegalArgumentException、IndexOutOfBoundsException等。 - 常见受检异常:
IOException、SQLException、ParseException等。
- 顶层异常为
二、自定义异常的设计原则
-
优先复用标准异常
- 当参数检查不合法时,应抛出
IllegalArgumentException及其子类(如NumberFormatException)。 - 避免重复造轮子,标准异常已覆盖大多数通用场景。
- 当参数检查不合法时,应抛出
-
定义根异常(BaseException)
- 继承选择:推荐从
RuntimeException派生,作为业务异常的根类,避免强制try-catch。 - 作用:统一业务异常体系,便于集中处理和区分系统级异常(如 JDK 原生异常)。
- 继承选择:推荐从
-
派生业务异常
- 从
BaseException派生出具体业务异常类,如UserNotFoundException(用户未找到)、LoginFailedException(登录失败)。 - 示例代码:
public class BaseException extends RuntimeException { // 构造方法需覆盖父类所有重载形式 public BaseException() { super(); } public BaseException(String message) { super(message); } public BaseException(String message, Throwable cause) { super(message, cause); } public BaseException(Throwable cause) { super(cause); } } public class UserNotFoundException extends BaseException { /* 同上构造方法 */ } public class LoginFailedException extends BaseException { /* 同上构造方法 */ } - 从
三、构造方法的实现要求
- 完整覆盖父类构造:需包含无参、单字符串参数、字符串 + Throwable、单 Throwable 参数四种构造方法,确保异常创建的灵活性。
- IDE 辅助:通过 IDE 自动生成构造方法,避免手动编写错误。
四、最佳实践
-
异常分类清晰
- 根异常
BaseException作为业务异常顶层,便于全局捕获(如catch (BaseException e)统一处理业务逻辑异常)。
- 根异常
-
减少受检异常
- 继承
RuntimeException的非受检异常,无需在方法签名中声明throws,简化代码结构。
- 继承
-
提供有意义的错误信息
- 构造异常时传入具体消息(如
new UserNotFoundException("用户ID 123未找到")),便于调试定位。
- 构造异常时传入具体消息(如
Java断言
一、核心概念
- 断言定义:一种调试工具,用于在开发和测试阶段验证程序状态,通过
assert关键字实现。 - 作用:确保代码逻辑符合预期,捕获潜在错误,辅助调试。
二、语法与用法
-
基本语法
java
assert 条件表达式; // 断言条件为 true,否则抛出 AssertionError assert 条件表达式 : 断言消息; // 失败时附带自定义消息- 例:
assert x >= 0 : "x 必须非负";
- 例:
-
执行逻辑
- 条件为
false时,抛出AssertionError(Error 的子类),终止程序。 - 断言消息可选,用于增强错误信息可读性。
- 条件为
三、核心特点
-
适用场景
- 仅限开发 / 测试阶段:用于检查 “不应该发生” 的错误(如参数合法性的极端假设)。
- 不处理可恢复错误:对可预期的程序错误(如
null参数),应显式抛出异常(如IllegalArgumentException)并由上层处理,而非断言。
-
默认行为
- JVM 默认关闭断言,
assert语句会被忽略,需手动启用。
- JVM 默认关闭断言,
四、启用断言的方式
-
全局启用
java -enableassertions 或 -ea 类名- 例:
java -ea Main
- 例:
-
局部启用
- 指定类:
-ea:包名.类名(如-ea:com.example.Main)。 - 指定包(含子包) :
-ea:包名...(如-ea:com.example...)。
- 指定类:
反射
反射就是Reflection,Java的反射是指程序在运行期可以拿到一个对象的所有信息。所以,反射是为了解决在运行期,对某个实例一无所知的情况下,如何调用其方法。
Class 类
一、Class 类本质
-
定义:JVM 为每个加载的
class(含interface)创建的唯一实例,存储类的完整元信息(类名、包名、父类、接口、字段、方法等)。 -
特性
- 由 JVM 动态加载,首次使用时加载到内存。
Class类构造方法为private,仅 JVM 可创建实例,用户无法直接实例化。- 每个
class/interface对应唯一Class实例,可通过==比较是否为同一类型。
二、获取 Class 实例的三种方式
| 方法 | 示例 | 说明 |
|---|---|---|
类字面量(.class) | Class<String> cls1 = String.class; | 直接通过类的静态变量class获取,编译期确定类型 |
实例的getClass() | String str = "hello"; <br> Class<?> cls2 = str.getClass(); | 通过对象实例调用,返回其运行时类型的Class实例 |
Class.forName("全类名") | Class<?> cls3 = Class.forName("java.lang.String"); | 运行期根据全类名动态加载,可能抛出ClassNotFoundException |
- 关键点:三种方式获取的
Class实例相同,可通过==验证(如cls1 == cls2 == cls3为true)。
三、Class 实例核心功能
1. 类型信息查询
| 方法 | 作用 | 示例 |
|---|---|---|
getName() | 返回完整类名(含包名) | String[].class.getName() → [Ljava.lang.String;(数组类名特殊格式) |
getSimpleName() | 返回简单类名(无包名) | String.class.getSimpleName() → String |
getPackage() | 获取包信息 | String.class.getPackage().getName() → java.lang |
isInterface() | 判断是否为接口 | Runnable.class.isInterface() → true |
isEnum() | 判断是否为枚举类 | Month.class.isEnum() → true |
isArray() | 判断是否为数组类型 | String[].class.isArray() → true |
isPrimitive() | 判断是否为基本类型 | int.class.isPrimitive() → true |
2. 创建类实例
-
方法:
newInstance() -
示例
Class<String> cls = String.class; String obj = cls.newInstance(); // 等价于 `new String()`(调用无参构造方法) -
限制:仅能调用
public无参构造方法,不支持有参或非public构造方法。
3. 与instanceof的区别
| 特性 | instanceof | Class实例比较(==) |
|---|---|---|
| 核心功能 | 判断对象是否为某类型或其子类型(支持多态) | 精确判断是否为同一类型(不考虑继承关系) |
| 示例 | Integer num instanceof Number → true(多态匹配父类) | num.getClass() == Number.class → false(Integer是Number子类,类型不同) |
四、动态加载机制
-
JVM 特性:按需加载,首次使用时加载类到内存(如首次调用类的方法、创建实例或访问静态成员时)。
-
应用场景:运行期根据条件加载不同实现类(如日志框架优先级选择)
// 示例:优先使用Log4j,不存在则用JDK日志 boolean isLog4jPresent = false; try { Class.forName("org.apache.log4j.Logger"); isLog4jPresent = true; // 加载Log4j类,存在则使用 } catch (ClassNotFoundException e) { // 未找到Log4j,使用JDK日志 }
五、特殊类型的 Class 实例
-
基本类型:JVM 预定义
Class实例,如int.class、boolean.class、void.class。 -
数组类型
- 引用类型数组:类名为
[L全类名;(如String[].class→[Ljava.lang.String;)。 - 基本类型数组:类名为
[基本类型缩写(如int[]→[I,boolean[]→[Z)。
- 引用类型数组:类名为
Java 反射之访问字段
一、核心知识点:通过反射获取字段信息
-
获取
Field实例的方法getField(name):获取某个public 字段(包括父类)。getDeclaredField(name):获取当前类的任意字段(不包括父类,可获取 private/protected/default 字段)。getFields():获取所有public 字段(包括父类)。getDeclaredFields():获取当前类的所有字段(不包括父类)。
-
Field对象包含的字段信息getName():返回字段名称(如"name")。getType():返回字段类型(Class实例,如String.class,数组类型用[B表示byte[])。getModifiers():返回字段修饰符(int值),可通过Modifier类判断(如Modifier.isPrivate(m))。
二、核心操作:读写字段值
-
获取字段值(
Field.get(Object instance))- 示例:
Object value = field.get(p),获取实例p的字段值。 - 访问限制:若字段为非 public(如 private),需先调用
field.setAccessible(true)突破访问限制(可能受SecurityManager限制,如无法修改 Java 核心类字段)。
- 示例:
-
设置字段值(
Field.set(Object instance, Object value))- 示例:
field.set(p, "新值"),修改实例p的字段值。 - 注意:同样需通过
setAccessible(true)访问非 public 字段,支持修改基本类型和引用类型字段。
- 示例:
三、代码示例与注意事项
-
示例代码逻辑
- 获取字段:通过
Class实例(如Student.class或p.getClass())调用字段获取方法。 - 突破封装:反射可访问私有字段,但会破坏类的封装性,仅用于工具 / 框架等非常规场景。
- 获取字段:通过
-
安全限制
setAccessible(true)可能被SecurityManager阻止(如 Java 核心包java.*/javax.*的类),确保 JVM 安全。
四、小结
-
反射访问字段的核心步骤
- 通过
Class获取Field实例(区分是否包含父类、是否 public)。 - 通过
Field获取字段元信息(名称、类型、修饰符)。 - 通过
get()/set()读写字段值,非 public 字段需setAccessible(true)。
- 通过
-
适用场景与风险
- 用途:底层框架、工具开发(未知对象结构时动态操作字段)。
- 风险:破坏封装性,代码繁琐,受安全策略限制,需谨慎使用。
调用方法
二、核心内容总结
1. 获取 Method 对象的方法
通过Class实例获取Method对象,支持不同范围和权限的方法获取:
getMethod(String name, Class... parameterTypes):获取类及其父类的public 方法(需指定参数类型)。getDeclaredMethod(String name, Class... parameterTypes):获取当前类的任意方法(包括 private,不包括父类)。getMethods():获取类及其父类的所有 public 方法(返回Method[])。getDeclaredMethods():获取当前类的所有方法(包括 private,返回Method[])。
2. Method 对象的信息获取
通过Method实例可获取方法的元数据:
getName():返回方法名称(如"getScore")。getReturnType():返回返回值类型(Class实例,如String.class)。getParameterTypes():返回参数类型数组(Class[],如{String.class, int.class})。getModifiers():返回修饰符(int类型,通过位运算解析 public/private 等)。
3. 调用方法的方式
-
实例方法调用:使用
Object invoke(Object instance, Object... args),instance为方法所属对象,args为参数。
示例:Method m = String.class.getMethod("substring", int.class); String result = (String) m.invoke("Hello", 2); // 调用"ello" -
静态方法调用:
invoke的第一个参数为null(无需实例)。
示例:Method m = Integer.class.getMethod("parseInt", String.class); Integer num = (Integer) m.invoke(null, "123"); // 调用静态方法 -
非 public 方法调用:需先通过
setAccessible(true)破除访问限制(可能受SecurityManager限制)。
示例:Method privateMethod = MyClass.class.getDeclaredMethod("privateMethod"); privateMethod.setAccessible(true); privateMethod.invoke(instance);
4. 多态特性
反射调用方法时遵循多态原则:调用实际对象类型的覆写方法(即使通过父类Class获取方法)。
示例:
子类Student覆写父类Person的hello()方法,通过Person.class.getMethod("hello")获取方法后,调用Student实例仍执行子类实现。
三、关键代码示例
-
获取不同权限的方法:
Class<Student> clazz = Student.class; clazz.getMethod("publicMethod"); // 公共方法(含父类) clazz.getDeclaredMethod("privateMethod"); // 当前类私有方法 -
处理方法重载:通过参数类型区分同名方法,如获取
substring(int start)和substring(int start, int end)需指定不同参数类型。 -
异常处理:调用
invoke可能抛出IllegalAccessException(权限问题)、InvocationTargetException(方法内部异常)等,需显式处理或声明抛出。
四、小结
- 反射调用方法的核心步骤:获取
Class→获取Method→调用invoke(处理权限和参数)。 - 方法访问范围:
getMethod用于公共方法(含继承),getDeclaredMethod用于当前类任意方法(需破除权限)。 - 多态支持:反射调用严格遵循 Java 多态规则,实际调用对象的覆写方法。
- 应用场景:动态调用未知方法、框架底层扩展(如 Spring 依赖注入)、单元测试工具等。
调用构造方法(Java 反射机制)
一、实例创建的常规方式与反射基础
-
常规实例创建
使用new操作符直接创建实例:Person p = new Person(); -
反射创建实例(无参构造)
通过Class.newInstance()方法调用 public 无参数构造方法:Person p = Person.class.newInstance();局限:仅支持无参且 public 的构造方法,无法处理有参或非 public 构造方法。
二、Constructor 对象:反射调用任意构造方法
Constructor 类封装了构造方法的详细信息,可用于创建实例,支持 有参、非 public 构造方法。
-
获取
Constructor对象的方法getConstructor(Class<?>... parameterTypes):获取 public 构造方法(指定参数类型)。getDeclaredConstructor(Class<?>... parameterTypes):获取 任意访问权限 的构造方法(包括非 public)。getConstructors():获取所有 public 构造方法(返回数组)。getDeclaredConstructors():获取所有构造方法(包括非 public,返回数组)。
注意:构造方法属于当前类,与父类无关,无多态特性。
-
调用构造方法创建实例
使用newInstance(Object... args)传入参数创建实例:// 调用 Integer(int) 构造方法 Constructor<Integer> cons1 = Integer.class.getConstructor(int.class); Integer n1 = cons1.newInstance(123); // 结果:123 // 调用 Integer(String) 构造方法 Constructor<Integer> cons2 = Integer.class.getConstructor(String.class); Integer n2 = cons2.newInstance("456"); // 结果:456 -
访问非 public 构造方法
需通过setAccessible(true)强制设置访问权限(可能受安全策略限制):Constructor<MyClass> cons = MyClass.class.getDeclaredConstructor(); cons.setAccessible(true); // 允许访问非 public 构造方法 MyClass obj = cons.newInstance();
三、核心要点总结
-
Constructor的作用
封装构造方法信息,支持调用 任意访问权限、任意参数的构造方法,弥补Class.newInstance()的局限。 -
关键方法对比
方法名 功能描述 访问权限 参数类型匹配 getConstructor获取 public 构造方法 public 精确匹配参数 getDeclaredConstructor获取任意构造方法(包括非 public) 任意 精确匹配参数 getConstructors获取所有 public 构造方法(数组) public 全部 getDeclaredConstructors获取所有构造方法(包括非 public) 任意 全部 -
注意事项
- 非 public 构造方法需通过
setAccessible(true)解锁访问,可能抛出安全异常。 - 构造方法调用结果始终返回实例,无返回值(与普通方法不同)。
- 非 public 构造方法需通过