一、异常的介绍
-
在Java中,将程序执行过程中发生的不正常行为称为异常。
-
Java中,描述异常是根据类来进行描述的。例如:
-
算术异常:
System.out.println(3/0); //执行结果 //Exception in thread "main" java.lang.ArithmeticException: / by zero //at Test.main(Test.java:10)
-
数组越界异常:
int[] arr = {1,2,3}; System.out.println(arr[10]); //执行结果 //Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: Index 10 out of bounds for length 3 //at Test.main(Test.java:11)
-
空指针异常:
int[] arr = null; System.out.println(arr.length); //执行结果 //Exception in thread "main" java.lang.NullPointerException: Cannot read the array length because "arr" is null //at Test.main(Test.java:11)
可知,Java中不同类型的异常,都有与其对应的类来进行描述。
-
-
异常的体系结构
由图可知:
- Throwable:是异常体系的顶层类,其派生出两个重要的子类,Error和Exception
- Error:指的是Java虚拟机无法解决的严重问题,比如:JVM的内部错误、资源耗尽等,典型代表:StackOverflowError和OutOfMemoryError,一旦发生回力乏术。
- Exception:异常产生后程序员可以通过代码进行处理,使程序继续执行。
-
异常的分类
-
编译时异常(Checked Exception):在程序编译期间发生的异常,也称为受检查异常。
class Person { private String name; @Override protected Object clone() { return (Person)super.clone(); } } //编译时报错 //Java: 未报告的异常错误java.lang.CloneNotSupportedException; 必须对其进行捕获或声明以便抛出
-
运行时异常(Unchecked Exception):程序执行期间发生的异常,也称为非受检查异常。RunTimeException以及其子类对应的异常,都称为运行时异常。比如:NullPointerException、ArrayIndexOutOfBoundsException、ArithmeticException。
注意:编译时出现的语法性错误,不能称之为异常。
-
二、异常的处理
-
防御式编程
主要的方式:
-
LBYL(Look Before You Leap):事前防御型
// 防御性检查:确保用户名和密码不为null且不为空 if (username == null || username.trim().isEmpty()) { throw new IllegalArgumentException("Username cannot be null or empty"); } if (password == null || password.isEmpty()) { throw new IllegalArgumentException("Password cannot be null or empty"); }
缺陷:正常流程和错误处理流程代码混在一起,代码整体显得比较混乱。
-
EAFP(It's Easier to Ask Forgiveness than Permission):事后认错型
try { int[] numbers = {1,2,3}; System.out.println(numbers[10]); } catch (ArrayIndexOutOfBoundsException e) { System.out.println("捕获到数组越界异常:" + e.getMessage()); } //执行结果 //捕获到数组越界异常:Index 10 out of bounds for length 3
-
-
异常的抛出
使用throw关键字抛出异常:
public static void main(String[] args) { int[] arr = {1,2,3}; get(arr, 10); } public static int get(int[] arr, int index) { if (null == arr) { throw new NullPointerException("传递的数组为null"); } if (index < 0 || index >= arr.length) { throw new ArrayIndexOutOfBoundsException("传递的数组下标越界"); } return arr[index]; } //执行结果 //Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: 传递的数组下标越界
注意:
- throw必须写在方法体内;
- 抛出的对象必须是Exception或Exception的子类对象
- 如果抛出的是RunTimeException或者RunTimeException的子类,则可以不用处理,直接交给JVM来处理
- 如果抛出的是编译时异常,用户必须处理,否则无法通过编译
- 异常一旦抛出,其后的代码就不会执行
-
异常的捕获
主要有两种方式:异常声明throws 以及 try-catch捕获处理。
-
异常声明throws
public class ThrowsDemo { public static void riskyBusiness() throws Exception { throw new Exception("丢掉错误"); } public static void main(String[] args) { try { riskyBusiness(); } catch (Exception e) { System.out.println("主方法收拾烂摊子: " + e.getMessage()); } } }
-
try-catch捕获并处理
try{ // 将可能出现异常的代码放在这里 }
catch(要捕获的异常类型 e){
// 如果try中的代码抛出异常了,此处catch捕获时异常类型与try中抛出的异常类型一致时,或者是try中抛出异常的基类 时,就会被捕获到
// 对异常就可以正常处理,处理完成后,跳出try-catch结构,继续执行后序代码 }
[catch(异常类型 e){ // 对异常进行处理 }
finally{
// 此处代码一定会被执行到
}]
// 后序代码
// 当异常被捕获到时,异常就被处理了,这里的后序代码一定会执行
// 如果捕获了,由于捕获时类型不对,那就没有捕获到,这里的代码就不会被执行
示例:
public static void main(String[] args) { try { System.out.println(10/0); } catch (ArithmeticException e) { System.out.println("出错了:" + e.getMessage()); //这里可以进行错误处理,比如记录日志、恢复状态等 } finally { System.out.println("不管如何,这块总会执行"); } System.out.println("程序继续执行..."); }
注意:
- try块内抛出异常位置之后的代码将不会被执行
- 如果抛出异常类型与catch时异常类型不匹配,异常不会被成功捕获
- try中可能会抛出多个不同的异常对象,则必须用多个catch来捕获
多个异常的处理方式:
int[] arr = {1, 2, 3}; try { System.out.println("before"); // arr = null; System.out.println(arr[100]); System.out.println("after"); } catch (ArrayIndexOutOfBoundsException e) { System.out.println("这是个数组下标越界异常"); e.printStackTrace(); } catch (NullPointerException e) { System.out.println("这是个空指针异常"); e.printStackTrace(); } System.out.println("after try catch");
如果多个异常的处理方式是完全相同,也可以写成:
catch (ArrayIndexOutOfBoundsException | NullPointerException e) { ... }
如果异常之间具有父子关系,一定是子类异常在前catch,父类异常在后catch,否则语法错误。
int[] arr = {1, 2, 3}; try { System.out.println("before"); arr = null; System.out.println(arr[100]); System.out.println("after"); } catch (Exception e) { // Exception可以捕获到所有异常 e.printStackTrace(); } catch (NullPointerException e){ // 永远都捕获执行到 e.printStackTrace(); } System.out.println("after try catch");
-
-
Finally
finally块中的代码无论是否发生异常,都会被执行。通常用于资源清理的工作。
try { // 可能会发生异常的代码 } catch (异常类型 e) { // 对捕获到的异常进行处理 } finally { // 此处的语句无论是否发生异常,都会被执行到 }
注意:finally中的return会覆盖try或catch块中的return,但这是一种不好的编程习惯,应该避免。
-
异常的执行流程
- 程序先执行try中的代码
- 如果try中的代码出现异常,就会结束try中的代码,看和catch中的异常类型是否匹配
- 如果找到匹配的异常类型,就会执行catch中的代码
- 如果没有找到匹配的异常类型,就会将异常向上传递到上层调用者
- 无论是否找到匹配的异常类型,finally中的代码都会被执行到(在该方法结束之前执行)
- 如果上层调用者也没有处理的了异常,就继续向上传递
- 一直到main方法也没有合适的代码处理异常,就会交给JVM来进行处理,此时程序就会异常终止
三、自定义异常类
-
创建自定义异常类
public class InvalidAgeException extends Exception { public InvalidAgeException(String message) { super(message); } }
-
使用自定义异常
public Person(String name, int age) throws InvalidAgeException { this.name = name; if (age < 0 || age > 150) { throw new InvalidAgeException("年龄必须在0到150之间"); } this.age = age; }
-
处理自定义异常
try { Person person1 = new Person("Alice", 25); System.out.println("Person 1 created successfully."); Person person2 = new Person("Bob", -5); } catch (InvalidAgeException e) { System.out.println("Error: " + e.getMessage()); }
完整示例:
// 自定义异常类
public class InvalidAgeException extends Exception {
public InvalidAgeException(String message) {
super(message);
}
}
// 使用自定义异常的类
public class Person {
private String name;
private int age;
public Person(String name, int age) throws InvalidAgeException {
this.name = name;
if (age < 0 || age > 150) {
throw new InvalidAgeException("年龄必须在0到150之间");
}
this.age = age;
}
public static void main(String[] args) {
try {
Person person1 = new Person("Alice", 25);
System.out.println("Person 1 created successfully.");
Person person2 = new Person("Bob", -5);
} catch (InvalidAgeException e) {
System.out.println("Error: " + e.getMessage());
}
}
}
四、异常的深入探讨
-
Checked异常和Unchecked异常的区别及使用场景
-
Checked异常:
- 继承自Exception类,但不包括RuntimeException及其子类
- 编译器强制要求处理这类异常
- 通常用于可预见的、可恢复的错误情况
- 例如:IOException, SQLException
- 使用场景: 当你希望调用者必须意识到并处理可能的异常情况时 对于重要的业务逻辑,需要确保异常得到适当处理 在API设计中,用于强制客户端处理某些错误情况
-
Unchecked异常:
- 继承自RuntimeException
- 编译器不强制要求处理
- 通常用于程序错误或不可恢复的状态
- 例如:NullPointerException, IllegalArgumentException
- 使用场景: 对于程序逻辑错误,通常不应该被捕获,而应该修复代码. 当异常是由于编程错误导致,而非外部因素. 在不希望强制调用者处理异常的情况下
-
-
选择使用检查型异常还是非检查型异常的最佳实践:
- 如果异常是可恢复的,并且你希望强制调用者处理它,使用checked异常
- 如果异常表示编程错误,使用unchecked异常
- 如果不确定调用者是否能够恢复from这个异常,倾向于使用unchecked异常
- 对于公共API,使用checked异常可以清晰地表达方法的契约
- 在应用程序内部,更多地使用unchecked异常以减少代码中的异常处理逻辑
- 避免过度使用checked异常,因为它们会导致代码变得冗长且难以维护
- 考虑创建自定义异常来更好地描述特定的错误情况
-
异常处理对程序性能的影响:
异常处理本身会带来一定的性能开销,主要体现在以下几个方面:
- 创建异常对象:创建异常对象比普通对象更昂贵,因为它涉及收集堆栈跟踪信息
- 捕获异常:try-catch块会略微降低代码的执行速度,即使异常没有被抛出
- 堆栈跟踪:当异常被抛出时,生成堆栈跟踪信息是一个相对昂贵的操作
然而,在大多数情况下,异常处理的性能影响是可以接受的,尤其是与异常处理带来的程序健壮性相比。
-
性能优化建议:
- 避免使用异常进行正常的程序流程控制
- 在可能的情况下,先进行条件检查,而不是依赖于捕获异常。
- 在捕获异常时,尽可能精确地指定异常类型,避免使用笼统的Exception。
- 在频繁执行的代码中,特别注意异常处理的使用。
- 考虑使用日志框架而不是直接打印堆栈跟踪,可以更灵活地控制日志级别和输出。