
在Java中,异常(Exception)是指在程序运行过程中,由于错误或意外事件而自动生成的特殊对象。每当程序遇到无法正常处理的情况(比如除以零、数组越界、文件未找到等), Java运行时系统就会创建一个异常对象,并“抛出”这个对象。这个异常对象中包含了错误的类型、发生的位置以及相关的上下文信息。
如果程序没有对异常进行适当的检测和处理,异常会沿着方法调用栈逐层向上传递,最终由Java的默认异常处理机制接管,导致程序异常终止并输出错误信息。 为了避免这种情况,保证程序的健壮性和用户体验,开发者需要在代码中主动编写异常检测和处理逻辑。通过使用try-catch语句块,可以捕获并处理异常,从而防止程序崩溃,并根据实际情况给出友好的提示或采取补救措施。
在Java应用程序运行期间,可能会发生许多错误条件,这些条件会导致程序停止执行。到目前为止,你可能已经多次经历过这种情况。例如,看下面的程序,它试图读取超出数组边界的内容:
|public class BadArray { public static void main(String[] args) { // 创建一个包含3个元素的数组 int[] numbers = { 1, 2, 3 }; // 尝试读取超出数组边界的元素 for (int i = 0; i <= 3; i++) System.out.println(numbers[i]); } }
运行这个程序会产生以下输出:
|1 2 3 Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException at BadArray.main(BadArray.java:15)
这个程序中的numbers数组只有三个元素,下标为0到2。当程序尝试读取numbers[3]处的元素时,程序崩溃并显示类似于程序输出末尾的错误消息。这个消息表明发生了异常,并给出了一些关于它的信息。
异常是在内存中作为错误或意外事件的结果而生成的对象。当异常被生成时,我们说它被"抛出"了。除非应用程序检测到异常并处理它,否则它会导致应用程序停止。
为了检测异常已被抛出并防止它停止你的应用程序,Java允许你创建异常处理器。异常处理器是当异常被抛出时优雅地响应异常的代码段。 拦截和响应异常的过程称为异常处理。如果你的代码在异常被抛出时没有处理它,默认异常处理器会处理它,如上面的例子所示。默认异常处理器打印错误消息并崩溃程序。
如前所述,异常是对象。异常对象是从Java API中的类创建的。API有一个广泛的异常类层次结构。
所有类都从Throwable类继承。就在Throwable类下面是Error和Exception类。从Error继承的类用于在发生关键错误时抛出的异常,例如Java虚拟机中的内部错误或内存不足。你的应用程序不应该尝试处理这些错误,因为它们是严重情况的结果。
你将处理的所有异常都是从Exception继承的类的实例。IOException和RuntimeException是这些类的例子。这些类也作为超类。IOException作为与输入和输出操作相关的异常的超类。RuntimeException作为由编程错误导致的异常的超类,例如越界数组下标。
要处理异常,你使用try语句。让我们看看try语句的几种变体,从以下一般格式开始:
|try { // try块语句... } catch (ExceptionType parameterName) { // catch块语句... }
首先出现关键字try。接下来,在必需的大括号内出现代码块。这个代码块称为try块。try块是一个或多个被执行的语句,可能会抛出异常。你可以将try块中的代码视为"受保护的",因为如果try块抛出异常,应用程序不会停止。
在try块之后,出现catch子句。catch子句以关键字catch开始,后跟代码(ExceptionType parameterName)。 这是一个参数变量声明,其中ExceptionType是异常类的名称,parameterName是变量名。如果try块中的代码抛出ExceptionType类的异常,那么参数变量将引用异常对象。此外,紧跟在catch子句后面的代码被执行。紧跟在catch子句后面的代码称为catch块。同样,大括号是必需的。
让我们看一个使用try语句的代码例子。以下try块中的语句尝试打开文件MyFile.txt。如果文件不存在,Scanner对象会抛出FileNotFoundException类的异常。这段代码设计为在抛出该异常时处理它:
|try { File file = new File("MyFile.txt"); Scanner inputFile = new Scanner(file); } catch (FileNotFoundException e) { System.out.println("文件未找到。"); }
让我们仔细看看。首先,执行try块中的代码。如果这段代码抛出异常,Java虚拟机搜索可以处理该异常的catch子句。为了使catch子句能够处理异常,其参数必须是与该异常类型兼容的类型。这里是这段代码的catch子句:
|catch (FileNotFoundException e)
这个catch子句声明了一个名为e的引用变量作为其参数。e变量可以引用FileNotFoundException类的对象。 所以,这个catch子句可以处理FileNotFoundException类的异常。如果try块中的代码抛出FileNotFoundException类的异常,e变量将引用异常对象,catch块中的代码将执行。在这种情况下,将打印消息"文件未找到。"。catch块执行后,程序将继续执行整个try/catch结构后面的代码。
每个异常对象都有一个名为getMessage的方法,可用于检索异常的默认错误消息。这与异常未被处理且应用程序停止时显示的消息相同。
|public class ExceptionMessage { public static void main(String[] args) { String fileName; // 保存文件名 // 从用户获取文件名 fileName = JOptionPane.showInputDialog("输入文件名:"); // 尝试打开文件 try { File file = new File(fileName); Scanner inputFile = new Scanner(file);
到目前为止,我们所学习的示例程序只演示了如何捕获和处理单一类型的异常,也就是try块中只可能抛出一种特定的异常类型。 然而,在实际开发中,try块内的代码往往会涉及多种可能出错的操作,因此可能会抛出多种不同类型的异常。 例如,文件操作时既可能因为文件不存在而抛出FileNotFoundException,也可能因为读取到格式不正确的数据而抛出InputMismatchException。
在这种情况下,Java允许你在一个try语句后面编写多个catch子句。每个catch子句都指定一种可以被捕获和处理的异常类型。 当try块中的代码抛出异常时,Java虚拟机会依次检查每个catch子句,找到第一个能够匹配该异常类型的catch子句,并执行其中的代码。 这样,你就可以针对不同类型的异常,分别编写不同的处理逻辑,从而使程序更加健壮和灵活。
需要注意的是,每个catch子句的参数类型应该与try块中可能抛出的异常类型相对应。如果try块中可能抛出多种异常,就需要为每种异常类型分别编写catch子句。 catch子句的顺序也很重要,应该将子类异常放在父类异常之前,否则父类异常会屏蔽子类异常的catch子句,导致子类异常无法被单独处理。
例如,下面的程序读取名为SalesData.txt的文件的内容。文件中的每一行包含一个月的销售金额,文件有几行。以下是文件的内容:
|24987.62 26978.97 32589.45 31978.47 22781.76 29871.44
这个程序从文件中读取每个数字并将其添加到累加器变量中。try块包含可以抛出不同类型异常的代码。 例如,Scanner类的构造函数如果找不到文件会抛出FileNotFoundException,Scanner类的nextDouble方法如果从文件中读取非数字值会抛出InputMismatchException。为了处理这些异常,try语句有两个catch子句:
|public class SalesReport { public static void main(String[] args) { String filename = "SalesData.txt"; // 文件名 int months = 0; // 月份计数器 double oneMonth; // 一个月的销售 double totalSales = 0.0; // 总销售 double averageSales; // 平均销售 try {
当try块中的代码抛出异常时,JVM开始搜索try语句中能够处理它的catch子句。它从上到下搜索catch子句,并将程序控制权传递给第一个参数与异常兼容的catch子句。
上面的程序展示了如何通过在try语句中编写多个catch子句,分别捕获并处理不同类型的异常。例如,当文件未找到时会捕获FileNotFoundException异常,而当文件中存在非数字数据时会捕获InputMismatchException异常。 每当try块中的代码抛出异常时,JVM会依次检查每个catch子句,找到第一个能够处理该异常类型的catch块,并将程序控制权转交给它。
然而,这个程序虽然能够识别并报告不同的错误类型,但它并没有真正实现“从错误中恢复”。 也就是说,无论是文件不存在,还是文件中包含无效的非数字数据,只要发生异常,程序都会显示错误信息后直接终止运行(调用System.exit(0))。 用户没有机会修正错误或继续操作,程序也不会尝试跳过错误数据或请求用户输入新的文件名。因此,这个程序的异常处理仅限于错误提示,而没有实现更进一步的容错和恢复机制。
下面的程序是有效异常处理的更好例子。它尝试从尽可能多的异常中恢复:
|public class SalesReport2 { public static void main(String[] args) { String filename = "SalesData.txt"; // 文件名 int months = 0; // 月份计数器 double oneMonth; // 一个月的销售 double totalSales = 0.0; // 总销售 double averageSales; // 平均销售 // 通过调用openFile方法尝试打开文件
让我们看看这个程序如何从FileNotFoundException中恢复。openFile方法接受文件名作为参数。 该方法创建一个File对象(将文件名传递给构造函数)和一个Scanner对象。如果Scanner类构造函数抛出FileNotFoundException,该方法返回null。 否则,它返回对Scanner对象的引用。在main方法中,使用循环来询问用户不同的文件名,以防openFile方法返回null。
现在让我们看看程序如何从意外遇到文件中的非数字项中恢复。调用Scanner类nextDouble方法的语句被包装在捕获InputMismatchException的try语句中。 如果nextDouble方法抛出此异常,catch块显示消息,指示遇到了非数字项,无效记录将被跳过。然后使用nextLine方法从文件中读取无效数据。 因为months++语句在try块中,当异常发生时它不会被执行,所以月份数仍然是正确的。循环继续处理文件中的下一行。
在Java中,try语句除了可以包含一个或多个catch子句来处理异常外,还可以有一个可选的finally子句。finally子句必须位于所有catch子句之后。 finally子句的主要作用是无论try块中是否发生异常,其内部的代码都会被执行,通常用于释放资源(如关闭文件、释放数据库连接等),确保程序的清理工作能够顺利完成。
带有finally子句的try语句的一般格式如下:
|try { // try块语句... } catch (ExceptionType ParameterName) { // catch块语句... } finally { // finally块语句... }
finally块是一个或多个语句,它们在try块执行后以及如果抛出异常则在任何catch块执行后总是被执行。finally块中的语句无论是否发生异常都会执行。
例如,以下代码打开一个double文件并读取其内容。外部try语句打开文件并有一个捕获FileNotFoundException的catch子句。 内部try语句从文件读取值并有一个捕获InputMismatchException的catch子句。finally块无论是否发生InputMismatchException都会关闭文件:
|try { // 打开文件 File file = new File(filename); Scanner inputFile = new Scanner(file); try { // 读取并显示文件内容 while (inputFile.hasNext()) { System.out.println(inputFile.nextDouble()); } } catch (InputMismatchException e) { System.out.println("找到无效数据。"); }
通常,一个方法会调用另一个方法,而另一个方法又会调用另一个方法。例如,方法A调用方法B,方法B调用方法C。调用栈是当前正在执行的所有方法的内部列表。
当由在多层方法调用下执行的方法抛出异常时,有时了解哪些方法负责调用该方法是有帮助的。栈跟踪是调用栈中所有方法的列表。它指示异常发生时正在执行的方法以及为执行该方法而调用的所有方法。
例如,看下面的程序。它有三个方法:main、myMethod和produceError。main方法调用myMethod,myMethod调用produceError。produceError方法通过向String类的charAt方法传递无效位置号来导致异常。异常不被程序处理,而是由默认异常处理器处理:
|public class StackTrace { public static void main(String[] args) { System.out.println("调用myMethod..."); myMethod(); System.out.println("方法main完成。"); } public static void myMethod() { System.out.println("调用produceError..."); produceError();
运行这个程序会产生以下输出:
|调用myMethod... 调用produceError... Exception in thread "main" java.lang.StringIndexOutOfBoundsException: String index out of range: 3 at java.lang.String.charAt(Unknown Source) at StackTrace.produceError(StackTrace.java:35) at StackTrace.myMethod(StackTrace.java:22) at StackTrace.main(StackTrace.java:11)
当异常发生时,错误消息显示栈跟踪,列出为产生异常而按顺序调用的方法。栈跟踪中列出的第一个方法charAt是负责异常的方法。 下一个方法produceError是调用charAt的方法。下一个方法myMethod是调用produceError的方法。最后一个方法main是调用myMethod的方法。栈跟踪显示了异常被抛出时调用的方法链。
注意:所有异常对象都有一个printStackTrace方法,从Throwable类继承,可用于打印栈跟踪。
在Java 7之前的版本中,每个catch子句只能处理一种类型的异常。然而,从Java 7开始,catch子句可以处理多种类型的异常。这可以减少需要捕获多个异常但为每个异常执行相同操作的try语句中的大量重复代码。
例如,假设我们在程序中有以下try语句:
|try { // try块语句... } catch(NumberFormatException ex) { respondToError(); } catch(IOException ex) { respondToError(); }
这个try语句有两个catch子句:一个处理NumberFormatException,另一个处理IOException。注意两个catch块做同样的事情:它们调用名为respondToError的方法。 因为两个catch块执行相同的操作,catch子句可以合并成一个处理两种类型异常的单个catch子句,如下所示:
|try { // try块语句... } catch(NumberFormatException | IOException ex) { respondToError(); }
注意在catch子句中,异常类型用|符号分隔,这与逻辑OR操作符使用的符号相同。你可以将其理解为该子句将捕获NumberFormatException或IOException。
以下代码显示了一个处理三种类型异常的catch子句:
|try { // try块语句... } catch(NumberFormatException | IOException | InputMismatchException ex) { respondToError(); }
在这段代码中,catch子句将处理NumberFormatException或IOException或InputMismatchException。
使用单个catch子句捕获多种类型异常的能力称为多捕获,是在Java 7中引入的。
在Java中,除了系统自动检测到错误时会抛出异常外,程序员也可以在代码中主动抛出异常。这通常用于在检测到某些不符合业务逻辑或非法的情况时,立即中断当前流程,并将异常信息传递给调用者进行处理。要手动抛出异常,可以使用throw语句。throw语句的基本语法如下:
|throw new ExceptionType(MessageString);
throw语句导致异常对象被创建和抛出。在这个一般格式中,ExceptionType是异常类名,MessageString是传递给异常对象构造函数的可选String参数。MessageString参数包含可以从异常对象的getMessage方法检索的自定义错误消息。如果你不向构造函数传递消息,异常将有一个null消息。以下是throw语句的例子:
|throw new Exception("燃料不足");
这个语句创建Exception类的对象并将字符串"燃料不足"传递给对象的构造函数。然后对象被抛出,这导致异常处理过程开始。
注意:不要混淆throw语句和throws子句。throw语句导致异常被抛出。throws子句通知编译器方法抛出一个或多个异常。
想一下有一个Die类。该类模拟游戏骰子,其构造函数接受指定骰子面数的参数。假设我们想要确保面数不小于最小值(毕竟,有一个单面骰子或零面骰子是没有意义的)。
实现这一点的一种方法是让构造函数在传递无效参数时抛出异常:
|public class Die { private final int MIN_SIDES = 4; private int sides; // 面数 private int value; // 骰子的值 /** * 构造函数执行骰子的初始投掷 * @param numSides 这个骰子的面数 */ public Die(int numSides) { // 验证面数 if (numSides < MIN_SIDES) {
第9行声明了一个名为MIN_SIDES的final字段,初始化为值4。这是类将接受的面数的最小值。在构造函数中,第22行我们测试numSides参数的值,以确定它是否小于MIN_SIDES。 如果是,第24到26行抛出IllegalArgumentException。消息"骰子必须至少有4面"被传递给异常对象的构造函数。当我们捕获这个异常时,我们可以通过调用对象的getMessage方法来检索消息。
为了满足特定类或应用程序的需求,你可以通过扩展Exception类或其子类之一来创建自己的异常类。
让我们看一个使用程序员定义异常的例子。回忆第6章的BankAccount类。这个类保存银行账户的数据。
有许多错误可能导致BankAccount对象错误地执行其职责。以下是一些具体例子:
我们可以创建代表这些错误条件中每一个的自己异常。然后我们可以重写类,使其在发生这些错误中的任何一个时抛出我们的自定义异常之一。让我们从为负起始余额创建异常类开始:
|/** * 当向构造函数传递负起始余额时,BankAccount类抛出NegativeStartingBalance异常 */ public class NegativeStartingBalance extends Exception { /** * 这个构造函数使用通用错误消息 */ public NegativeStartingBalance() { super("错误:负起始余额"); } /** * 这个构造函数在错误消息中指定坏的起始余额 * @param amount 坏的起始余额 */ public NegativeStartingBalance(double amount) {
注意这个类扩展了Exception类。它有两个构造函数。无参构造函数将字符串"错误:负起始余额"传递给超类构造函数。这是可以从对象的getMessage方法检索的错误消息。 第二个构造函数接受起始余额作为double参数。这个金额用于向超类构造函数传递包含起始余额金额的更详细的错误消息。
以下代码显示了重写的BankAccount构造函数之一,当传递负值作为起始余额时抛出NegativeStartingBalance异常:
|public BankAccount(double startBalance) throws NegativeStartingBalance { if (startBalance < 0) throw new NegativeStartingBalance(startBalance); balance = startBalance; }
注意NegativeStartingBalance扩展了Exception类。这意味着它是一个受检查的异常类。因此,构造函数头必须有一个列出异常类型的throws子句。
8. 基本异常处理练习
创建一个程序,使用try-catch语句处理除零异常(ArithmeticException)。
|import java.util.Scanner; public class BasicExceptionDemo { public static void main(String[] args) { Scanner scanner = new Scanner(System.in); try { System.out.print("请输入被除数: "); int dividend = scanner.nextInt(); System.out.print
9. 多异常处理练习
创建一个程序,处理数组越界异常和数字格式异常。
|import java.util.Scanner; public class MultipleExceptionDemo { public static void main(String[] args) { Scanner scanner = new Scanner(System.in); int[] numbers = {10, 20, 30, 40, 50}; try { System.out.print
10. finally子句练习
创建一个程序,演示finally子句的执行,无论是否发生异常。
|public class FinallyDemo { public static void main(String[] args) { System.out.println("=== 测试1: 正常执行 ==="); testMethod(10, 2); System.out.println("\n=== 测试2: 发生异常 ==="); testMethod(10, 0); }
测试示例1(正常情况):
|请输入被除数: 10 请输入除数: 2 结果: 5 程序执行完毕
测试示例2(除零异常):
|请输入被除数: 10 请输入除数: 0 错误: 除数不能为零! 异常信息: / by zero 程序执行完毕
说明:
try块包含可能抛出异常的代码catch块捕获并处理特定类型的异常catch块,按顺序匹配异常类型finally块无论是否发生异常都会执行e.getMessage()获取异常信息测试示例1(正常情况):
|请输入数组索引(0-4): 2 索引 2 的值是: 30 程序执行完毕
测试示例2(数字格式异常):
|请输入数组索引(0-4): abc 错误: 输入的不是有效数字! 程序执行完毕
测试示例3(数组越界异常):
|请输入数组索引(0-4): 10 错误: 数组索引超出范围! 有效索引范围: 0 到 4 程序执行完毕
说明:
catch块处理不同类型的异常ArrayIndexOutOfBoundsException是数组越界异常NumberFormatException是数字格式异常输出结果:
|=== 测试1: 正常执行 === try块开始执行 计算结果: 5 try块执行完毕 finally块: 这里总是会执行 方法执行完毕 === 测试2: 发生异常 === try块开始执行 catch块: 捕获到除零异常 catch块执行完毕 finally块: 这里总是会执行 方法执行完毕
说明:
finally块无论是否发生异常都会执行try或catch中有return语句,finally块也会在return之前执行finally块通常用于清理资源,如关闭文件、释放连接等finally块是可选的,但建议在需要清理资源时使用