[Java]异常
在程序运行时,如果遇到问题(比如除以零、文件找不到等),程序会发生异常。异常就像是程序的“错误提醒”,当程序运行中出错时,它会停止,给出一个错误信息。我们可以通过异常处理来控制这些错误,避免程序崩溃。异常处理机制基于“try-catch-finally”语法。
1.异常的分类
-
编译时异常(Checked Exception):这种异常是编译器检查到的,程序必须处理这种异常。比如,文件找不到时,Java会要求你去处理这个问题。必须捕获或声明处理。
- 例子:
IOException
(文件操作时出现错误)。
- 例子:
-
运行时异常(Unchecked Exception):这种异常发生在程序运行时,通常是一些小错误,程序员可以选择是否处理它。比如,除以零就是一个运行时异常。
RuntimeException
是 Java 中常见的运行时异常,它是所有 运行时异常(Unchecked Exceptions)类的父类。RuntimeException
及其子类的异常通常是在程序运行时抛出的,而不像 Checked Exception(如IOException
、SQLException
等)那样需要显式地在方法签名中声明或捕获。不需要显式捕获,但可以捕获。下面第五点有关于RuntimeException的详细介绍。
- 例子:
ArithmeticException
(除以零),NullPointerException
(空指针错误)。
- 例子:
2.异常处理
Java提供了try-catch-finally
语法来处理异常。
2.1 try-catch:捕获异常并处理
- try块中包含可能会抛出异常的代码。
- catch块用于捕获特定类型的异常。
try { int result = 10 / 0; // 可能会抛出 ArithmeticException } catch (ArithmeticException e) { System.out.println("除以零错误:" + e.getMessage()); }
2.2 finally:
无论是否发生异常,finally
中的代码总是会执行。通常用于清理资源,比如关闭文件流、数据库连接等。
try {
// 可能发生异常的代码
} catch (Exception e) {
// 异常处理代码
} finally {
// 清理资源的代码
}
示例:
import java.io.FileReader;
import java.io.IOException;
public class FinallyExample {
public static void main(String[] args) {
FileReader file = null;
try {
// 尝试打开文件并读取
file = new FileReader("test.txt"); // 假设文件存在
int data = file.read();
System.out.println((char) data);
} catch (IOException e) {
System.out.println("文件读取失败:" + e.getMessage());
} finally {
// 确保文件流总是关闭,即使发生了异常
try {
if (file != null) {
file.close();
System.out.println("文件已关闭。");
}
} catch (IOException e) {
System.out.println("关闭文件时出错:" + e.getMessage());
}
}
}
}
/*
输出:
文件已关闭。
*/
解释:
try
块中我们尝试打开一个文件并读取其中的内容。- 如果文件读取过程中没有出现异常,
finally
块会确保文件流被关闭。 finally
块中的代码无论是否发生异常都会执行,这对于资源管理(如关闭文件流、数据库连接等)非常重要。
3.抛出异常
可以通过throw
关键字手动抛出一个异常。通过throws
声明方法可能抛出的异常。
3.1 throw:用来抛出一个异常对象。
throw new ArithmeticException("除以零异常");
3.2 throws:用来声明方法可能抛出的异常。
public void myMethod() throws IOException {
// 可能抛出IOException的方法
}
综合使用示例:
public class ThrowFinallyExample {
public static void main(String[] args) {
try {
processFile("test.txt");
} catch (IOException e) {
System.out.println("捕获到异常: " + e.getMessage());
}
}
// 处理文件的方法,可能会抛出 IOException
public static void processFile(String fileName) throws IOException {
try {
System.out.println("开始处理文件:" + fileName);
// 假设处理过程中发生了异常
if (fileName == null) {
throw new IOException("文件名不能为空!");
}
System.out.println("文件处理完成。");
} catch (IOException e) {
System.out.println("处理文件时发生异常:" + e.getMessage());
throw e; // 将异常抛到外层
} finally {
// 这里模拟资源关闭,如果发生了错误,就抛出异常
System.out.println("资源清理中...");
if (fileName == null) {
throw new IOException("资源清理失败:文件名为空!");
}
System.out.println("资源清理完成。");
}
}
}
/*
输出:
开始处理文件:test.txt
资源清理中...
资源清理完成。
*/
解释:
- 在
processFile
方法中,首先模拟文件处理过程中发生的IOException
。 catch
捕获并处理该异常后,将其重新抛出,传递到方法外层。- 在
finally
块中,无论是否发生异常,都执行资源清理的代码。如果清理过程中发生了问题,我们会抛出一个新的异常。
3.3 总结:
throw
:用于显式抛出异常。你可以在代码中主动抛出异常,以便在某些条件不满足时提前中止执行,提示错误。finally
:用于保证无论是否发生异常,某些代码都会执行,通常用于清理工作,如关闭文件流、数据库连接等资源。
throw
抛出的异常需要在try-catch
中进行捕获,或者通过throws
声明抛出。finally
块中的代码始终会执行,即使try
块或catch
块抛出异常。
4.常见的异常类
4.1 Throwable:是Java异常体系的根类,所有异常类的父类。
4.1.1 Error:
Error
主要指系统级错误,通常不应该尝试捕获这些错误。因为一旦出现了 Error
类型的异常,程序通常会无法恢复,例如 OutOfMemoryError
或StackOverflowError
。在实际开发中,遇到这类错误时通常是代码本身或者运行环境出现了问题,需要从根本上修复,而不是捕获异常后继续执行(即一般不需要捕获,而需要你手动把代码改正确来)。
4.1.2 Exception:
Exception
是程序中的常见错误,我们可以通过 try-catch
语句进行捕获和处理。常见的异常包括 IOException
、ArithmeticException
、NullPointerException
等。我们需要根据具体异常选择合适的处理方式。
4.2 常见的异常类:
4.2.1 IOException:
输入输出操作异常,例如文件读取时文件不存在或者无法读取就会抛出IOException错误。
import java.io.*;
public class IOExceptionExample {
public static void main(String[] args) {
try {
// 打开一个不存在的文件,会抛出 IOException
FileReader file = new FileReader("nonexistentfile.txt");
BufferedReader reader = new BufferedReader(file);
reader.read();
reader.close();
} catch (IOException e) {
// 捕获并处理 IOException
System.out.println("文件操作异常:" + e.getMessage());
}
}
}
解释:代码尝试打开一个不存在的文件,导致抛出 IOException
。在 catch
块中,我们捕获该异常并打印错误信息,而不是让程序崩溃。
4.2.2 NullPointerException:
当你试图访问或操作一个 null
对象时,JVM 无法执行相关操作,因此抛出 NullPointerException
。这种异常通常会发生在以下几种情况中:
- 试图调用
null
引用的实例方法。 - 试图访问
null
引用的字段。 - 试图获取
null
引用的数组长度。 - 试图将
null
引用传递给需要非null
参数的方法。
public class NullPointerExceptionExample {
public static void main(String[] args) {
try {
String str = null;
System.out.println(str.length()); // str 为 null,会抛出 NullPointerException
} catch (NullPointerException e) {
// 捕获并处理空指针异常
System.out.println("发生了空指针异常: " + e.getMessage());
}
}
}
解释:str
为 null
,调用 str.length()
会抛出 NullPointerException
。我们通过 try-catch
捕获该异常,避免程序崩溃。
4.2.3 ArithmeticException:
算术运算异常,例如当你进行除法操作时,如果除数为零,程序会抛出 ArithmeticException
。
public class ArithmeticExceptionExample {
public static void main(String[] args) {
try {
int result = 10 / 0; // 除以零,抛出 ArithmeticException
} catch (ArithmeticException e) {
// 捕获并处理除零错误
System.out.println("发生了算术异常: " + e.getMessage());
}
}
}
解释:除以零会抛出 ArithmeticException
,我们捕获异常并输出错误信息,而不是让程序崩溃。
4.2.4 ArrayIndexOutOfBoundsException:
当你尝试访问一个数组时,使用了无效的索引(即越界索引)时,会抛出ArrayIndexOutOfBoundsException异常。
public class ArrayIndexOutOfBoundsExceptionExample {
public static void main(String[] args) {
int[] arr = new int[3]; // 创建一个长度为 3 的数组
try {
// 尝试访问不存在的索引,数组长度为 3,最大索引为 2
arr[5] = 10; // 这会抛出 ArrayIndexOutOfBoundsException
} catch (ArrayIndexOutOfBoundsException e) {
System.out.println("发生了数组下标越界异常: " + e.getMessage());
}
}
}
解释:数组 arr
的长度为 3,它的有效索引是 0, 1, 2
。但是我们尝试访问索引 5
,这是越界的,导致 ArrayIndexOutOfBoundsException
异常。通过 try-catch
语句捕获异常,我们避免了程序崩溃,并输出了错误信息。
4.2.5 ClassNotFoundException:
ClassNotFoundException
是 Exception
类的子类,通常发生在 动态加载类 时,如果找不到指定的类,会抛出此异常。常见于使用反射、Class.forName()
或类加载器时。
发生的原因:
- 你试图通过
Class.forName()
、ClassLoader.loadClass()
等方法动态加载一个类,但该类在 classpath 中无法找到。 - 这通常发生在类路径配置错误,或者尝试加载一个未编译或缺失的类时。
public class ClassNotFoundExceptionExample { public static void main(String[] args) { try { // 使用 Class.forName 加载不存在的类 Class.forName("com.example.NonExistentClass"); } catch (ClassNotFoundException e) { System.out.println("发生了类未找到异常: " + e.getMessage()); } } }
解释:这里我们使用
Class.forName("com.example.NonExistentClass")
来加载一个不存在的类,这会抛出ClassNotFoundException
异常。通过try-catch
捕获异常并输出错误信息,避免了程序崩溃。
解决方法:
- 确保类路径配置正确,类文件已经编译并位于 classpath 下。
- 在动态加载类之前,可以使用
ClassLoader
的getResource()
或getResourceAsStream()
等方法检查类是否存在。ClassLoader classLoader = getClass().getClassLoader(); if (classLoader.getResource("com/example/NonExistentClass.class") != null) { // 类存在,可以加载 Class.forName("com.example.NonExistentClass"); } else { System.out.println("类文件不存在!"); }
反射中使用 Class.forName()
通常情况下,我们会用反射动态加载类,尤其是在类名只有在运行时才能确定时。
public class ReflectionExample { public static void main(String[] args) { try { // 动态加载一个类 Class<?> clazz = Class.forName("java.util.ArrayList"); System.out.println("加载成功: " + clazz.getName()); } catch (ClassNotFoundException e) { System.out.println("类未找到: " + e.getMessage()); } } }
解释:
- 这段代码成功加载了
java.util.ArrayList
类并打印出类的名称。 - 如果
Class.forName()
中提供的类名无法找到,程序会抛出ClassNotFoundException
。
4.2.6 FileNotFoundException
(文件未找到异常):
FileNotFoundException
是一种输入输出异常,通常发生在你尝试访问一个不存在的文件时。- 比如,尝试打开一个根本没有的文件,就会抛出这个异常。
File file = new File("nonexistentfile.txt"); FileReader fr = new FileReader(file); // 如果文件不存在,会抛出 FileNotFoundException
如何避免?
-
在访问文件时,先检查文件是否存在:
File file = new File("nonexistentfile.txt"); if (file.exists()) { FileReader fr = new FileReader(file); } else { System.out.println("文件不存在!"); }
4.2.7 SQLException
(SQL 异常): SQLException
是数据库操作中常见的异常,通常发生在数据库查询失败时。- 比如,执行错误的 SQL 查询语句时,会抛出这个异常。
Connection conn = DriverManager.getConnection("jdbc:mysql://localhost/test", "user", "password"); Statement stmt = conn.createStatement(); stmt.executeUpdate("INVALID SQL QUERY"); // 会抛出 SQLException
如何避免?
- 在执行 SQL 操作时,确保 SQL 语句的正确性,并正确处理异常。
try { stmt.executeUpdate("SELECT * FROM users"); // 正确的 SQL 查询 } catch (SQLException e) { System.out.println("SQL 执行错误:" + e.getMessage()); }
5.自定义异常
5.1 解释:
可以根据需要定义自己的异常类,通常自定义异常类需要继承Exception
或RuntimeException
。
class MyException extends Exception {
public MyException(String message) {
super(message);
}
}
public class Test {
public static void test() throws MyException {
throw new MyException("自定义异常");
}
public static void main(String[] args) {
try {
test();
} catch (MyException e) {
System.out.println("捕获到自定义异常:" + e.getMessage());
}
}
}
5.2 关于RuntimeException:
5.2.1 RuntimeException
的基本介绍
- 继承关系:
RuntimeException
继承自Exception
类,并且是 未检查异常(Unchecked Exception)的基类。 - 运行时异常:运行时异常通常表示程序的逻辑错误或不合适的状态,开发者不需要强制捕获这些异常。
- 不需要显式声明:与受检查异常不同,
RuntimeException
不要求你在方法中使用throws
声明它,且也不强制在代码中进行try-catch
捕获。
5.2.2 常见的 RuntimeException
子类
NullPointerException
:访问空对象引用时抛出的异常。ArithmeticException
:发生算术运算错误时抛出的异常,例如除以零。ArrayIndexOutOfBoundsException
:访问数组时,索引越界时抛出的异常。ClassCastException
:进行不合法的类型转换时抛出的异常。IllegalArgumentException
:当方法接收到不合法的参数时抛出的异常。IllegalStateException
:方法被调用时,当前对象状态不合法时抛出的异常。
5.2.3 RuntimeException
的特点
- 不需要捕获:
RuntimeException
是未检查异常,所以开发者不必在代码中显式捕获它,也不必在方法签名中声明它。 - 通常是程序错误:运行时异常通常是因为代码中的逻辑错误或者数据错误,比如访问空指针、数组越界等。修复这些异常一般需要修改代码逻辑。
5.2.4 如何使用 RuntimeException
虽然大多数情况下,RuntimeException
及其子类是由 JVM 自动抛出的,但你也可以在自己的代码中显式抛出 RuntimeException
或其子类,来指示某种错误。
抛出 RuntimeException
示例:
public class RuntimeExceptionExample {
public static void main(String[] args) {
try {
checkAge(15); // 传入一个非法的年龄值,抛出异常
} catch (RuntimeException e) {
System.out.println("捕获到异常: " + e.getMessage());
}
}
// 自定义方法,检查年龄是否合法
public static void checkAge(int age) {
if (age < 18) {
throw new IllegalArgumentException("年龄不能小于 18!");
}
System.out.println("年龄合格:" + age);
}
}
/*
捕获到异常: 年龄不能小于 18!
*/
解释:
checkAge
方法中,如果传入的年龄小于 18,则主动抛出一个IllegalArgumentException
异常,表示年龄不合法。RuntimeException
的子类IllegalArgumentException
被抛出,并在catch
块中捕获和处理。
5.2.5 为什么使用 RuntimeException
RuntimeException
和其他未检查异常(Unchecked Exception)通常用于:
- 表示代码逻辑中的错误,而不是外部条件导致的异常。
- 表示不容易预见或者不容易恢复的错误,开发者可以通过修改代码来避免这种错误发生。
- 用于捕捉错误输入、错误参数、程序不符合逻辑的状态等情况。
5.2.6 RuntimeException
与其他异常的区别
特性 | RuntimeException | IOException 、SQLException 等 |
---|---|---|
检查异常/非检查异常 | 非检查异常(Unchecked Exception) | 检查异常(Checked Exception) |
是否强制捕获 | 不强制捕获或声明 | 强制捕获或声明 |
抛出原因 | 程序逻辑错误、非法操作 | 外部因素、资源不可用等 |
常见场景 | 空指针、除零、数组越界等 | 文件读写失败、数据库操作失败等 |
5.2.7 何时使用 RuntimeException
你可以在以下情况使用 RuntimeException
:
- 不合法的参数:当方法的参数不符合预期时(如负数、空值等),你可以抛出一个
IllegalArgumentException
。 - 非法的状态:当对象的状态不适合调用某个方法时,可以抛出一个
IllegalStateException
。 - 算术错误:如除以零时,抛出
ArithmeticException
。 - 不合理的类型转换:如进行不合法的类型强制转换,抛出
ClassCastException
。
6.异常链
异常链(Exception Chaining)是指在捕获异常时,将原始异常作为另一个异常的原因(cause
)抛出。Java 提供了一种机制,允许我们在抛出新的异常时,把原本抛出的异常附加到新的异常中。这样做可以帮助我们保留原始异常的详细信息,方便后续调试和问题定位。
6.1 异常链的作用
异常链的主要作用是帮助我们追踪问题的根源。当我们捕获到异常后,可以将其作为另一个异常的原因抛出,这样就能保留原始异常的信息,便于定位问题的源头。例如,捕获一个 SQLException
,并将其作为 IOException
的原因重新抛出,方便上层调用者了解到底是哪里出的问题。
6.2 异常链的基本使用
Java 提供了 Throwable
类的构造方法,允许我们在抛出异常时指定一个原始的异常对象:
public Throwable(String message, Throwable cause)
public Throwable(String message, Throwable cause)
是 Java 中 Throwable
类的一个构造方法,它用于创建一个带有 错误消息 和 原始异常 的异常对象。我们通常使用它来创建新的异常,同时保存引起该异常的原始原因。
6.2.1 构造方法的参数说明
String message
:这个参数是一个 错误消息,用于描述当前异常的具体情况。通常是一个简短的字符串,用来说明异常的原因或上下文。例如:"File not found"
。Throwable cause
:这个参数是另一个异常对象,表示 导致当前异常的原始异常。它通常是一个已经存在的异常对象,我们把它传递到新的异常中来形成 异常链。
6.2.2 构造函数的作用
- 当我们在程序中捕获到一个异常,并且想要抛出一个新的异常时,可以通过这个构造函数将 原始异常(
cause
)传递给新异常。 - 这样做的目的是保留 原始异常 的信息,帮助开发者追踪错误的根源。它是一种 异常链 技术,可以让我们知道一个异常是如何引发其他异常的。
6.2.3 为什么使用这个构造函数?
使用这个构造函数的好处是:
- 我们可以 抛出新的异常,并且 保留原始异常的上下文信息,从而有助于追踪问题的根源。
- 通过
getCause()
方法,后续的异常处理者能够获取到原始异常并进行处理。
6.2.4 例子:如何使用 Throwable(String message, Throwable cause)
假设我们在处理一个文件操作时发生了异常,我们可以通过异常链来传递原始的 IOException
异常,使得上层调用者能够获取到详细的错误信息。
代码示例:
public class ExceptionChainingDemo {
public static void main(String[] args) {
try {
processFile();
} catch (Exception e) {
e.printStackTrace();
}
}
public static void processFile() throws Exception {
try {
openFile();
} catch (Exception e) {
// 将原始异常 e 包装到一个新的异常中并抛出
throw new Exception("Failed to process the file", e);
}
}
public static void openFile() throws Exception {
// 模拟抛出文件操作异常
throw new java.io.IOException("File not found");
}
}
输出:
java.lang.Exception: Failed to process the file
at ExceptionChainingDemo.processFile(ExceptionChainingDemo.java:9)
at ExceptionChainingDemo.main(ExceptionChainingDemo.java:4)
Caused by: java.io.IOException: File not found
at ExceptionChainingDemo.openFile(ExceptionChainingDemo.java:15)
at ExceptionChainingDemo.processFile(ExceptionChainingDemo.java:7)
... 1 more
6.2.5 解释
- 在上面的代码中:
openFile()
方法抛出了一个IOException
异常(模拟文件未找到的情况)。- 在
processFile()
方法中,我们捕获了这个IOException
异常,并且通过new Exception("Failed to process the file", e)
创建了一个新的Exception
异常。 - 这里的
"Failed to process the file"
是新的异常的描述信息,而e
(即原始的IOException
异常)被传递作为 原始异常(即cause
)。
- 当我们打印异常信息时,
printStackTrace()
显示了当前异常的信息,并且通过Caused by
显示了引发当前异常的原始异常。
6.2.6 使用 Exception
和 RuntimeException
创建异常链
Exception
类和 RuntimeException
类都提供了带有 cause
参数的构造函数,因此你可以在抛出这两种异常时都使用异常链。
示例:创建 RuntimeException
异常链
public class RuntimeExceptionChaining {
public static void main(String[] args) {
try {
method1();
} catch (RuntimeException e) {
// 捕获并输出异常信息,显示异常链
System.out.println("Caught exception: " + e);
Throwable cause = e.getCause();
if (cause != null) {
System.out.println("Cause: " + cause);
}
}
}
public static void method1() {
try {
method2();
} catch (RuntimeException e) {
// 捕获 method2 中的异常,并将其作为 method1 的原因
throw new RuntimeException("Error occurred in method1", e);
}
}
public static void method2() {
// 模拟抛出一个 RuntimeException
throw new RuntimeException("An error occurred in method2");
}
}
输出:
Caught exception: java.lang.RuntimeException: Error occurred in method1
Cause: java.lang.RuntimeException: An error occurred in method2
6.2.7 为什么要使用异常链?
-
保留原始异常信息:当我们在捕获到一个异常后,可以将它附加到一个新的异常中,这样上层代码就可以通过
getCause()
方法查看到原始的异常信息,帮助定位问题。 -
帮助追踪错误的根源:异常链可以让我们清楚地看到异常是如何传播的,特别是在复杂的系统中,错误可能从底层层级一直传递到上层应用,使用异常链可以更容易追踪整个错误过程。
-
提高代码可读性:通过异常链,能够避免在捕获异常后丢失原始的错误信息,增强代码的可读性和可维护性。
6.2.8 getCause()
和 printStackTrace()
getCause()
:可以用来获取原始异常(如果存在的话)。printStackTrace()
:会打印当前异常以及异常链中的所有异常。
6.2.8.1 getCause()
方法
getCause()
方法用于获取当前异常的原始异常(即引发当前异常的异常)。它是 Throwable
类的一部分,所有异常类(包括 Exception
和 Error
)都继承自 Throwable
,因此都可以使用该方法。
1. 功能:
getCause()
返回的是一个Throwable
对象,它代表的是导致当前异常发生的原始异常(如果存在的话)。- 如果当前异常是直接抛出的,没有原始异常,则返回
null
。
2. 示例:
public class GetCauseExample {
public static void main(String[] args) {
try {
method1();
} catch (Exception e) {
// 获取并打印原始异常
System.out.println("Caught exception: " + e.getMessage());
if (e.getCause() != null) {
System.out.println("Cause: " + e.getCause());
}
}
}
public static void method1() throws Exception {
try {
method2();
} catch (Exception e) {
// 捕获异常并将其作为原因抛出
throw new Exception("Error in method1", e);
}
}
public static void method2() throws Exception {
// 模拟抛出一个异常
throw new Exception("Error in method2");
}
}
输出:
Caught exception: Error in method1
Cause: java.lang.Exception: Error in method2
3. 解释:
method2()
抛出了一个异常:"Error in method2"。method1()
捕获了该异常,并将它作为原因(cause
)抛出了一个新的异常:"Error in method1"。- 在
main()
方法中,我们捕获了这个新的异常,并通过getCause()
方法获取到原始的异常信息。
6.2.8.2 printStackTrace()
方法
printStackTrace()
是 Throwable
类的一个方法,它用于打印异常的堆栈跟踪信息。堆栈跟踪信息通常包含以下内容:
- 异常的类型和消息。
- 异常发生时的方法调用栈(即方法的调用路径)。
- 异常发生的具体位置(行号和类名)。
1. 功能:
printStackTrace()
方法会将异常的堆栈信息输出到控制台,帮助开发人员了解异常发生的详细上下文。
2. 示例:
public class PrintStackTraceExample {
public static void main(String[] args) {
try {
method1();
} catch (Exception e) {
// 打印异常的堆栈信息
e.printStackTrace();
}
}
public static void method1() throws Exception {
try {
method2();
} catch (Exception e) {
// 捕获异常并将其作为原因抛出
throw new Exception("Error in method1", e);
}
}
public static void method2() throws Exception {
// 模拟抛出一个异常
throw new Exception("Error in method2");
}
}
输出:
java.lang.Exception: Error in method1
at PrintStackTraceExample.method1(PrintStackTraceExample.java:10)
at PrintStackTraceExample.main(PrintStackTraceExample.java:5)
Caused by: java.lang.Exception: Error in method2
at PrintStackTraceExample.method2(PrintStackTraceExample.java:16)
at PrintStackTraceExample.method1(PrintStackTraceExample.java:8)
... 1 more
3. 解释:
- 在
method2()
中,我们抛出了一个异常"Error in method2"
,然后在method1()
中捕获该异常并将其作为原因抛出了新的异常"Error in method1"
。 - 当在
main()
中捕获到异常并调用printStackTrace()
时,异常信息不仅显示当前异常,还显示了由原始异常引起的Caused by
部分。这样,我们可以清晰地看到异常链,追踪错误的根本原因。
7.异常的最佳实践
- 捕获特定异常:尽量捕获具体的异常,而不是捕获
Exception
。 - 不要忽略异常:避免捕获异常后什么都不做,这会隐藏程序中的问题。
- 及时释放资源:在
finally
中关闭文件流、数据库连接等资源,确保资源能够正确释放。 - 使用自定义异常:在合适的情况下,定义并抛出自定义异常,提供更加具体的错误信息。
8.异常的传递
在Java中,异常可以在方法内部被捕获并处理,也可以向上传递。异常的传递是通过方法声明中的throws
来实现的。如果方法中抛出了异常且该异常没有被处理,Java虚拟机会将其传递给调用该方法的地方。
8.1 异常传播:
8.1.1 解释:
如果一个方法抛出一个异常,而该方法的调用者没有处理(即没有捕获或声明throws
),这个异常将会被继续抛出,直到它被某个方法捕获或最终未被捕获而导致程序终止。
public void methodA() throws Exception {
methodB(); // methodB 可能抛出异常
}
public void methodB() throws Exception {
throw new Exception("Something went wrong");
}
异常的多层次处理: 在多层方法调用中,如果外层方法没有处理异常,内层方法抛出的异常就会一直向上传递。
try {
methodA(); // methodA 中会抛出异常
} catch (Exception e) {
System.out.println("异常被捕获:" + e.getMessage());
}
8.1.2 具体代码举例:
public class ExceptionHandlingExample {
// methodA 抛出 Exception
public void methodA() throws Exception {
System.out.println("In methodA");
methodB(); // 调用 methodB,methodB 可能抛出异常
}
// methodB 抛出一个异常
public void methodB() throws Exception {
System.out.println("In methodB");
// 模拟抛出异常
throw new Exception("Something went wrong in methodB");
}
public static void main(String[] args) {
ExceptionHandlingExample example = new ExceptionHandlingExample();
try {
example.methodA(); // 调用 methodA,methodA 中会调用 methodB,methodB 抛出异常
} catch (Exception e) {
// 捕获异常并处理
System.out.println("异常被捕获: " + e.getMessage()); // 打印异常消息
e.printStackTrace(); // 打印异常的堆栈信息
}
}
}
8.1.3 代码说明:
: methodA
methodA
声明throws Exception
,意味着它会抛出Exception
类型的异常。- 在
methodA
中,我们调用了methodB()
,而methodB
可能会抛出一个异常。
methodB
:
methodB
也声明了 throws Exception
,表示该方法可能会抛出异常。
在 methodB
中,我们模拟抛出了一个 Exception
,并传递了错误消息 "Something went wrong in methodB"
。
main
方法:
- 在
main
方法中,我们创建了ExceptionHandlingExample
的实例,并调用methodA()
。 methodA()
调用methodB()
,而methodB()
会抛出一个异常,因此methodA()
也会抛出异常。- 在
try
块中,我们捕获了methodA()
抛出的异常,使用catch
语句块处理异常。 - 我们使用
e.getMessage()
打印异常的消息,并使用e.printStackTrace()
打印异常的堆栈跟踪信息。
输出结果:
In methodA
In methodB
异常被捕获: Something went wrong in methodB
java.lang.Exception: Something went wrong in methodB
at ExceptionHandlingExample.methodB(ExceptionHandlingExample.java:17)
at ExceptionHandlingExample.methodA(ExceptionHandlingExample.java:9)
at ExceptionHandlingExample.main(ExceptionHandlingExample.java:27)
解释:
- 程序首先进入
methodA
,然后调用methodB
。 methodB
抛出了一个异常"Something went wrong in methodB"
,并且异常被methodA
捕获。methodA
继续抛出该异常,最终在main
方法中的try-catch
块中捕获到这个异常。- 异常的消息
"Something went wrong in methodB"
被打印出来,且e.printStackTrace()
打印了详细的堆栈信息,显示异常是如何从methodB
传递到methodA
的。
9.捕获多个异常
9.1 解释:
Java 7 引入了多重异常捕获(Multi-catch),允许在一个 catch
块中捕获多个异常,这样可以减少重复的代码,并提高代码的可读性。你只需要在 catch
块中的异常类型之间使用 |
(管道符)进行分隔。
9.2 代码格式:
try {
// 可能发生异常的代码
} catch (IOException | SQLException e) { // 捕获多种异常
System.out.println("发生异常:" + e.getMessage());
}
9.3 要求:
- 多个异常类必须有共同的父类,通常是
Exception
或其子类(例如:IOException
和SQLException
都是Exception
的子类)。 - 在
catch
块中,捕获的异常类的对象(这里是e
)会变成Throwable
的父类,因此你不能再对e
进行多态特有的方法调用。
9.4 具体例子:
假设我们有两个异常:IOException
(输入输出异常)和 SQLException
(SQL 异常)。我们会模拟一个程序,在操作文件和数据库时分别抛出这两个异常,并使用 Java 7 的多重异常捕获来处理。
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.sql.SQLException;
public class MultiCatchExample {
// 模拟读取文件
public static void readFile() throws IOException {
// 模拟文件未找到异常
throw new FileNotFoundException("文件未找到");
}
// 模拟数据库操作
public static void connectToDatabase() throws SQLException {
// 模拟SQL异常
throw new SQLException("数据库连接失败");
}
public static void main(String[] args) {
try {
readFile(); // 可能抛出 IOException
connectToDatabase(); // 可能抛出 SQLException
} catch (IOException | SQLException e) { // 捕获多种异常
System.out.println("发生异常:" + e.getMessage());
}
}
}
9.4.1 代码解释:
-
readFile()
方法:- 该方法模拟读取文件的操作,可能会抛出
IOException
类型的异常。为了演示,我们使用FileNotFoundException
(它是IOException
的子类)来模拟文件未找到的异常。
- 该方法模拟读取文件的操作,可能会抛出
-
connectToDatabase()
方法:- 该方法模拟连接数据库的操作,可能会抛出
SQLException
类型的异常。我们直接抛出一个SQLException
。
- 该方法模拟连接数据库的操作,可能会抛出
-
main()
方法:- 我们在
try
块中依次调用readFile()
和connectToDatabase()
方法,这两个方法都有可能抛出异常。 - 在
catch
块中,我们使用|
操作符捕获了IOException
和SQLException
,并通过e.getMessage()
打印了异常消息。
- 我们在
9.4.2 输出结果:
发生异常:文件未找到
9.5 总结:
- 多重异常捕获:Java 7 引入了多重异常捕获,允许你在同一个
catch
块中捕获多个异常。你只需使用|
分隔异常类,如IOException | SQLException
。 - 减少重复代码:这种方式让你避免了为每个异常写一个
catch
块的冗余代码,从而使代码更简洁、可读性更强。 - 共同父类:多个异常类必须有共同的父类,通常是
Exception
或其子类,否则不能进行多重异常捕获。
10.异常的性能
异常处理会影响程序的性能,尤其是在频繁抛出异常的情况下。为了优化性能,应该避免在正常的程序流程中使用异常。例如,不应该使用异常来控制程序流程,尤其是在循环或频繁执行的代码块中。
-
异常的成本: 抛出异常是一个相对昂贵的操作,因为它需要创建异常对象并进行堆栈跟踪。因此,最好在必要时才抛出异常。
-
异常的优化:
- 避免过多的
try-catch
块,尤其是在循环中。 - 捕获异常的块应尽量简短,不要做复杂的逻辑处理。
- 避免过多的
11.异常的嵌套与多线程中的异常处理
-
嵌套异常: 异常可能会嵌套。例如,一个方法抛出的异常被另一个方法捕获并进一步抛出,这样形成了嵌套异常链。可以通过
getCause()
方法获取引起当前异常的根本原因。try { throw new IOException("File not found"); } catch (IOException e) { throw new RuntimeException("Failed to read file", e); // 将 IOException 作为 RuntimeException 的根本原因 }
使用
e.getCause()
可以获取到原始的异常对象,从而追踪到真正的错误源。 -
多线程中的异常处理: 在多线程编程中,异常处理稍显复杂。每个线程都有自己的执行栈,因此在每个线程中都可能发生异常。Java提供了
Thread.UncaughtExceptionHandler
接口来处理未捕获的线程异常。Thread thread = new Thread(() -> { // 可能抛出异常的代码 }); thread.setUncaughtExceptionHandler((t, e) -> { System.out.println("线程 " + t.getName() + " 抛出了异常:" + e.getMessage()); }); thread.start();
通过设置未捕获异常处理器,我们可以对线程中的异常进行集中处理,而不会让整个应用崩溃。
12.Java 8 引入的异常流处理
在Java 8中,引入了流式API(Stream API),这使得在进行流操作时,异常处理变得更加重要。流中的方法(如map()
、filter()
等)通常要求无异常的输入,但是你可能会遇到需要在流中处理异常的场景。
一种常见的方式是通过try-catch
包装流中的异常:
List<String> data = Arrays.asList("1", "2", "abc", "4");
List<Integer> result = data.stream()
.map(str -> {
try {
return Integer.parseInt(str);
} catch (NumberFormatException e) {
return null; // 处理异常,返回null
}
})
.filter(Objects::nonNull)
.collect(Collectors.toList());
System.out.println(result); // 输出:[1, 2, 4]
在这种情况下,我们使用了map()
来处理每个元素可能发生的NumberFormatException
异常,并返回null
值,最后通过filter()
去掉null
值。
13.资源管理与自动关闭(Java 7引入的AutoCloseable)
从Java 7开始,引入了自动资源管理(ARM),即try-with-resources
语句,专门用于处理需要关闭的资源(如文件、数据库连接等)。资源实现了AutoCloseable
接口,保证无论是否发生异常,都会自动关闭资源。
try (FileReader fr = new FileReader("file.txt")) {
// 读取文件
} catch (IOException e) {
e.printStackTrace();
} // FileReader 会在此自动关闭,无论是否发生异常
这种方式能够确保即使在异常发生时,资源也能正确地被释放,避免了资源泄漏问题。