本文纲要
- 异常体系结构和异常分类
- 虚拟机默认处理异常方式
- throws 声明异常
- 声明异常注意事项
- throw 抛出异常
- try … catch 自己处理异常
- try … catch 常见问题
- Throwable 成员方法
- 异常小练习
- 自定义异常
异常体系结构和异常分类
当代码出现问题时,Java 程序会以异常的形式体现。例如:
- 数组索引越界:
ArrayIndexOutOfBoundsException - 空指针:
NullPointerException - 日期解析错误:
ParseException
在 Java 中,异常并非语法错误(如关键字拼写错误),而是一种特殊的对象体系。所有的异常类都继承自 Throwable,其下分为两大分支:
Error:严重问题,程序无法处理(如内存溢出),不需要我们关心。Exception:程序本身可以处理的问题,是开发者关注的重点。Exception 又分为:- 运行时异常:
RuntimeException及其子类,如NullPointerException、ArrayIndexOutOfBoundsException,在编译期不强制处理。 - 编译时异常:除
RuntimeException之外的其他异常,如ParseException,必须在编译期处理,否则代码无法通过编译。
- 运行时异常:
演示不同类型的异常:
public class ExceptionDemo1 {
public static void main(String[] args) throws ParseException {
// 1. 运行时异常:数组索引越界
// int[] arr = {1, 2, 3, 4, 5};
// System.out.println(arr[10]); // ArrayIndexOutOfBoundsException
// 2. 运行时异常:空指针
// String s = null;
// System.out.println(s.equals("嘿嘿")); // NullPointerException
// 3. 编译时异常:日期解析异常
SimpleDateFormat sdf = new SimpleDateFormat("yyyy年MM月dd日");
sdf.parse("2048-1月1日"); // ParseException
}
}
虚拟机默认处理异常方式
程序运行过程中,当某行代码出现异常时,JVM 会做以下事情:
- 创建对应的异常对象。例如
new ArrayIndexOutOfBoundsException()。 - 检查是否有自己编写的异常处理代码。
- 如果没有,将异常交给本方法的调用者,最终交给虚拟机。
- JVM 默认处理异常会做两件事:
- 将异常的类名、原因、位置等信息以红色字体输出在控制台。
- 停止程序,异常位置之后的代码不再执行。
public class ExceptionDemo2 {
public static void main(String[] args) {
int[] arr = {1, 2, 3, 4, 5};
System.out.println(arr[10]); // 这里抛出 ArrayIndexOutOfBoundsException
// 这行代码不会执行
System.out.println("嘿嘿嘿,我最帅");
}
}
throws 声明异常
throws 关键字用于在方法声明处标识可能出现的异常,本身不处理异常,而是告知调用者该方法可能产生的异常,由调用者决定如何处理。
格式:
访问修饰符 返回值类型 方法名(参数列表) throws 异常类名 {
// 方法体
}
public class ExceptionDemo6 {
public static void main(String[] args) throws ParseException {
method1(); // 调用者没有处理,最终交给虚拟机
method2(); // 同样交给虚拟机
}
// 声明运行时异常,throws 可以省略不写
private static void method1() /throws NullPointerException/ {
int[] arr = null;
for (int i = 0; i < arr.length; i++) { // 这里发生空指针异常
System.out.println(arr[i]);
}
}
// 声明编译时异常,必须显式写出
private static void method2() throws ParseException {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy年MM月dd日");
sdf.parse("2048-10月10日");
}
}
声明异常注意事项
编译时异常:因为编译阶段就会检查,所以必须在方法签名上显式声明,否则代码会报错。
运行时异常:在编译阶段不检查,throws 声明可以省略,即使不写,程序也能通过编译。
// 运行时异常可省略 throws NullPointerException
private static void method1() {
// ...
}
// 编译时异常必须保留 throws ParseException
private static void method2() throws ParseException {
// ...
}
throw 抛出异常
throw 用于在方法体内部手动抛出异常对象。一旦执行到throw,其后的代码将不再执行,异常会被交给调用者处理。
格式:
throw new 异常类名("异常信息");
使用场景:
当方法参数非法,继续运行没有意义时,应当主动抛出异常,让方法提前结束,并告知调用者问题所在。
1 ) 演进:从输出提示到抛出异常
最初的写法(不推荐):非法数据时只打印一句提示,调用者无法感知是否真正成功。
private static void printArr(int[] arr) {
if (arr == null) {
System.out.println("参数不能为null"); // 调用者并不知道是否成功
} else {
for (int i = 0; i < arr.length; i++) {
System.out.println(arr[i]);
}
}
}
改进:用 throw 抛出异常,明确告知调用者
public class ExceptionDemo8 {
public static void main(String[] args) {
int[] arr = null;
printArr(arr); // 这里会接收到异常,目前尚未处理,最终交给 JVM
}
private static void printArr(int[] arr) {
if (arr == null) {
throw new NullPointerException(); // 手动创建并抛出异常对象
} else {
for (int i = 0; i < arr.length; i++) {
System.out.println(arr[i]);
}
}
}
}
2 ) 对比 throw 和 throws:
| 关键字 | 位置 | 后面跟 | 作用 |
|---|---|---|---|
| throws | 方法声明处 | 异常类名 | 声明异常,告知调用者可能发生 |
| throw | 方法体内部 | 异常对象 | 手动抛出异常,结束方法 |
try … catch 自己处理异常
若希望异常发生后程序仍能继续运行,就需要自己捕获并处理异常。
格式:
try {
// 可能出现异常的代码
} catch (异常类名 变量名) {
// 出现异常后的处理代码
}
示例:
public class ExceptionDemo9 {
public static void main(String[] args) {
int[] arr = null;
try {
printArr(arr);
} catch (NullPointerException e) {
System.out.println("参数不能为null");
}
System.out.println("嘿嘿嘿,我最帅!!!"); // 能够正常执行
}
private static void printArr(int[] arr) {
if (arr == null) {
throw new NullPointerException();
} else {
for (int i = 0; i < arr.length; i++) {
System.out.println(arr[i]);
}
}
}
}
好处: 即使出现异常,程序不会终止,后面的代码仍可继续执行。
try … catch 常见问题
1 ) try 中没有遇到问题
会顺序执行 try 块内所有代码。
不会进入 catch 块。
执行完 try-catch 整体后,继续执行下面的代码。
2 ) try 中遇到问题
出现异常的代码之后的 try 块内代码将不再执行。
跳转到匹配的 catch 块中处理。
catch 执行完毕后,继续执行 try-catch 之后的代码。
3 ) 异常没有被捕获
如果 catch 中声明的异常类型与发生的异常不匹配,则相当于没有处理。
最终会交回给 JVM 默认处理:打印红色错误信息并终止程序。
4 ) 同时出现多个异常
可以写多个 catch 进行分别处理。
注意:如果多个异常存在子父类关系,父类异常的 catch 必须写在下方,否则子类的 catch 永远无法执行。
public class ExceptionDemo10 {
public static void main(String[] args) {
// 正确写法:子类异常在前,父类在后
try {
Scanner sc = new Scanner(System.in);
System.out.println("请输入你的年龄");
String line = sc.nextLine();
int age = Integer.parseInt(line); // 可能产生 NumberFormatException
System.out.println(age);
System.out.println(2 / 0); // 可能产生 ArithmeticException
} catch (NumberFormatException e) {
System.out.println("格式化异常出现了");
} catch (ArithmeticException e) {
System.out.println("数学运算异常出现了");
}
System.out.println("测试456");
// 不推荐直接捕获 Exception 而没有针对性处理
/*
try {
// ...
} catch (Exception e) {
// 所有异常都用同一种方式处理,无法区分
}
*/
}
}
Throwable 成员方法
所有异常类均继承自 Throwable,因此都可以调用以下三个方法:
| 方法 | 说明 |
|---|---|
getMessage() | 返回异常的详细消息字符串 |
toString() | 返回异常的简短描述(类名 + 消息) |
printStackTrace() | 将异常的完整堆栈跟踪以红色字体输出到控制台 |
public class ExceptionDemo11 {
public static void main(String[] args) {
try {
int[] arr = {1, 2, 3, 4, 5};
System.out.println(arr[10]); // 产生 ArrayIndexOutOfBoundsException
} catch (ArrayIndexOutOfBoundsException e) {
// 方法一:获取消息
// String message = e.getMessage();
// System.out.println(message);
// 方法二:输出简短描述
// String s = e.toString();
// System.out.println(s);
// 方法三:打印红色错误堆栈,但程序继续执行
e.printStackTrace();
}
System.out.println("嘿嘿嘿"); // 仍然会执行
}
}
值得注意的是:printStackTrace() 虽然输出红色错误信息,但并不会终止程序,因此和 JVM 默认处理有本质区别。
异常小练习
需求: 键盘录入学生姓名和年龄,年龄必须介于 18~25 周岁,若不满足则要求重新录入,直到正确为止。
分析:
- 创建学生类
Student,在setAge()中加入数据校验,若不合法则抛出异常。 - 测试类中使用
try-catch捕获可能出现的两种异常:数字格式异常、年龄超范围异常。 - 使用
while(true)循环实现重复录入,直到数据合法后break跳出。
public class Student {
private String name;
private int age;
// 构造器、getter、setter、toString 省略...
public void setAge(int age) {
if (age >= 18 && age <= 25) {
this.age = age;
} else {
// 抛出自定义异常,让异常信息更见名知意
throw new AgeOutOfBoundsException("年龄超出了范围");
}
}
}
public class ExceptionDemo12 {
public static void main(String[] args) {
Student s = new Student();
Scanner sc = new Scanner(System.in);
System.out.println("请输入姓名");
String name = sc.nextLine();
s.setName(name);
while (true) {
System.out.println("请输入年龄");
String ageStr = sc.nextLine();
try {
int age = Integer.parseInt(ageStr);
s.setAge(age);
break; // 赋值成功,结束循环
} catch (NumberFormatException e) {
System.out.println("请输入一个整数");
} catch (AgeOutOfBoundsException e) {
System.out.println(e.toString());
System.out.println("请输入一个符合范围的年龄");
}
}
System.out.println(s);
}
}
在这个练习中:
- 校验数据 时使用
throw抛出异常,阻止不合法的赋值。 - 主调方法 中用
try-catch自行处理,让程序可以重复输入,而不是直接终止。
自定义异常
当 Java 内置的异常类不能清晰表达业务含义时,就需要自定义异常。
自定义异常步骤:
- 定义类,类名要见名知意(如
AgeOutOfBoundsException)。 - 继承
RuntimeException(运行时)或Exception(编译时),多数情况下继承RuntimeException。 - 提供空参构造和带消息的构造方法。
项目结构示例:
src/
└── com/wb/exce/
├── AgeOutOfBoundsException.java // 自定义异常
├── Student.java // 学生实体类
└── ExceptionDemo12.java // 测试入口
// 自定义运行时期异常
public class AgeOutOfBoundsException extends RuntimeException {
public AgeOutOfBoundsException() {
}
public AgeOutOfBoundsException(String message) {
super(message);
}
}
在 Student 的 setAge 方法中使用:
public void setAge(int age) {
if (age >= 18 && age <= 25) {
this.age = age;
} else {
throw new AgeOutOfBoundsException("年龄超出了范围");
}
}
当调用者捕获到 AgeOutOfBoundsException 时,就能精确知道是年龄超出范围,而不再是一个模糊的 RuntimeException,达到了异常信息见名知意的目的。
总结
以上便是 Java 异常处理机制的基础入门,从异常体系、JVM 默认处理,到两种处理方式(throws/throw 与 try-catch),再到自定义异常的编写,构成了一个完整的知识闭环
动手实践这些示例,能够帮助快速掌握异常处理的核心理念
1741

被折叠的 条评论
为什么被折叠?



