1. try-catch
1.1. 作用
- try 块:包含可能会抛出异常的代码。当 Java 虚拟机执行 try 块中的代码时,如果遇到异常,它会立即停止执行 try 块中剩余的代码,并开始查找匹配的 catch 块来处理异常。
- catch 块:用于捕获并处理 try 块中抛出的异常。每个 catch 块都声明了它能够处理的异常类型。当 try 块抛出异常时,Java 虚拟机会按顺序检查每个 catch 块,找到匹配异常类型的 catch 块后,就会执行该 catch 块中的代码。如果没有找到匹配的 catch 块,异常将被传递给上层调用栈,直到找到合适的处理程序或程序终止。
1.2. 示例
java
public class TryCatchExample {
public static void main(String[] args) {
try {
int result = 10 / 0; // 这行代码会抛出ArithmeticException异常
System.out.println("结果是: " + result);
} catch (ArithmeticException e) {
System.out.println("捕获到算术异常: " + e.getMessage());
}
System.out.println("程序继续执行...");
}
}
输出内容:
text
异常了/ by zero
程序继续执行
说明:在这个例子中,try 块中的 10 / 0 会抛出 ArithmeticException 异常。一旦异常抛出,try 块中后续的代码(System.out.println("结果是: " + result);
)将不会执行。程序会跳转到 catch 块,打印出捕获到的异常信息,然后继续执行 catch 块之后的代码(System.out.println("程序继续执行...");
)。
2. 多个catch块
可以在一个 try 块后接多个 catch 块,分别捕获不同类型的异常。
2.1. 示例
java
public static void multipleCatch() {
try {
FileReader reader = new FileReader("nonexistentfile.txt");
int data = reader.read();
int result = 10 / 0;
} catch (FileNotFoundException e) {
System.out.println("文件未找到异常: " + e.getMessage());
} catch (ArithmeticException e) {
System.out.println("算术异常: " + e.getMessage());
} catch (IOException e) {
System.out.println("I/O 异常: " + e.getMessage());
}
}
2.2. 异常捕获顺序
在这个示例中,异常捕获的顺序很重要。
- FileNotFoundException 是 IOException 的子类,所以它的 catch 块必须放在 IOException 的 catch 块之前。如果顺序颠倒,FileNotFoundException 会被 IOException 的 catch 块捕获,导致无法针对文件未找到的情况进行特定处理。
- ArithmeticException 与 IOException 没有继承关系,其 catch 块的位置在 IOException 之前或之后都可以。
所以,异常捕获的原则是先捕获子类,后捕获父类。
2.3. 应用场景
- 输入验证与处理:在处理用户输入时,可能会遇到多种类型的输入错误。例如,用户输入的字符串无法解析为数字(NumberFormatException),或者输入的内容不符合特定的业务规则(IllegalArgumentException)。通过多个 catch 块,可以分别处理这些不同类型的输入异常,向用户提供更准确的错误提示。
- 资源操作:当涉及到文件、数据库连接等资源操作时,可能会因为资源不存在(FileNotFoundException 或 SQLException 中与连接相关的异常)、资源损坏(IOException)、操作不合法(如数据库事务中的约束违反 SQLIntegrityConstraintViolationException)等原因抛出不同的异常。使用多个 catch 块可以针对不同的资源操作异常进行相应的处理,比如记录日志、尝试重新连接资源等。
3. 异常向上传播
如果当前方法中没有合适的catch块来捕获异常,异常会被抛给调用该方法的上一层方法。这个过程会持续进行,就像异常在方法调用栈中 “冒泡” 一样,直到找到能够处理该异常的catch块。例如:
3.1. 异常沿着调用栈向上传播
java
public class ExceptionPropagation {
public static void method1() {
method2();
}
public static void method2() {
int result = 10 / 0; // 抛出ArithmeticException异常
}
public static void main(String[] args) {
try {
method1();
} catch (ArithmeticException e) {
System.out.println("在main方法中捕获到算术异常: " + e.getMessage());
}
}
}
解释:在上述代码中,method2 抛出 ArithmeticException 异常,由于 method2 中没有 catch 块,异常被传递到 method1。method1 同样没有处理该异常的 catch 块,所以异常继续向上传播到 main 方法。在 main 方法中,try - catch 块捕获到了这个异常并进行处理。
3.2. 最终由JVM处理
如果异常一直传播到主线程(对于 Java 应用程序通常是main方法),且主线程也没有捕获该异常,那么 Java 虚拟机(JVM)会采取默认的处理方式。JVM 会在控制台打印异常的堆栈跟踪信息,显示异常发生的位置和调用路径,然后终止当前线程。如果该线程是程序的主线程,整个程序将会终止。
java
package com.one.pojian.codes;
public class Exception01 {
public static void main(String[] args) {
int a = 1 / 0;
System.out.println("a = " + a);
}
}
运行上述代码,控制台会输出类似以下的堆栈跟踪信息:
text
Exception in thread "main" java.lang.ArithmeticException: / by zero
at com.one.pojian.codes.Exception01.main(Exception01.java:9)
由于没有捕获该异常,JVM 打印了异常信息并终止了程序。
3.3. 异常处理最佳实践建议
- 适当捕获异常:在编写代码时,应尽量在合适的位置捕获并处理异常,避免异常无限制地向上传播到 JVM。这样可以使程序更加健壮,防止意外终止,并能提供更好的用户体验。例如,在进行文件操作、数据库连接等可能抛出异常的操作时,要及时捕获并处理相关异常,如IOException、SQLException等。
- 使用日志记录:在捕获异常时,除了进行相应的处理,还应使用日志记录工具(如java.util.logging、SLF4J等)记录异常信息,以便于调试和排查问题。这样即使异常没有导致程序崩溃,开发人员也能通过日志了解程序运行过程中出现的问题。
- 避免捕获宽泛的异常:虽然可以使用catch (Exception e) 捕获所有类型的异常,但应尽量避免这样做,除非有充分的理由。因为捕获宽泛的异常可能会掩盖真正的问题,使得调试变得困难。应该针对具体的异常类型进行捕获和处理,以确保能够准确应对不同的错误情况。