《Java 程序设计》第 12 章 - 异常处理
大家好!今天我们来学习《Java 程序设计》中的第 12 章 —— 异常处理。在编程过程中,错误和异常是不可避免的。一个健壮的程序必须能够妥善处理各种异常情况。本章将详细介绍 Java 中的异常处理机制,帮助大家编写出更稳定、更可靠的 Java 程序。
思维导图
12.1 异常与异常类
12.1.1 异常的概念
在 Java 中,异常(Exception) 是指程序在运行过程中发生的非正常事件,它会中断程序的正常执行流程。
想象一下现实生活中的场景:当你开车去上班时,可能会遇到轮胎漏气、发动机故障等意外情况,这些情况会阻止你按计划到达公司。在程序中也是如此,比如:
- 试图打开一个不存在的文件
- 网络连接中断
- 除以零的运算
- 数组下标越界
这些情况都可以称为异常。Java 的异常处理机制提供了一种优雅的方式来处理这些意外情况,使程序能够继续运行或友好地终止。
12.1.2 异常类
Java 中的所有异常都是通过类来表示的,这些类统称为异常类。Java 提供了一个完善的异常类体系,所有异常类都直接或间接继承自Throwable
类。
Throwable
类有两个重要的子类:
- Error:表示严重的错误,通常是虚拟机相关的问题,如内存溢出(OutOfMemoryError),程序一般无法处理这类错误。
- Exception:表示程序可以处理的异常,是我们在编程中主要关注的类。
Exception
类又可以分为:
- Checked Exception(受检异常):在编译时就需要处理的异常,如果不处理,编译器会报错。如
IOException
、SQLException
等。 - Unchecked Exception(非受检异常):也称为运行时异常(RuntimeException),在编译时不需要强制处理,通常是由程序逻辑错误引起的。如
NullPointerException
、ArrayIndexOutOfBoundsException
等。
下面是异常类的继承关系图:
@startuml
title Java异常类体系
Throwable <|-- Error
Throwable <|-- Exception
Exception <|-- RuntimeException
Exception <|-- IOException
Exception <|-- SQLException
RuntimeException <|-- NullPointerException
RuntimeException <|-- ArrayIndexOutOfBoundsException
RuntimeException <|-- ArithmeticException
@enduml
12.2 异常处理
Java 提供了一套完整的异常处理机制,主要通过try
、catch
、finally
、throw
和throws
关键字来实现。
12.2.1 异常的抛出与捕获
异常处理的核心思想是抛出异常和捕获异常:
- 抛出异常:当程序执行过程中遇到异常情况时,会创建一个异常对象并将其抛出。
- 捕获异常:异常被抛出后,程序可以捕获这个异常并进行处理,而不是让程序直接崩溃。
形象地说,这就像生活中 "上报问题" 和 "解决问题" 的过程:员工遇到无法解决的问题(抛出异常),上报给经理(捕获异常),经理来处理这个问题。
12.2.2 try-catch-finally 语句
try-catch-finally
是 Java 中处理异常的基本结构,语法如下:
try {// 可能会发生异常的代码
} catch (异常类型1 异常对象名) {// 处理异常类型1的代码
} catch (异常类型2 异常对象名) {// 处理异常类型2的代码
} finally {// 无论是否发生异常,都会执行的代码
}
- try 块:包含可能会抛出异常的代码。
- catch 块:用于捕获并处理 try 块中抛出的异常。可以有多个 catch 块,分别处理不同类型的异常。
- finally 块:无论是否发生异常,都会执行的代码,通常用于释放资源。
执行流程示意图:
示例代码:
public class TryCatchFinallyDemo {public static void main(String[] args) {int a = 10;int b = 0;int[] arr = {1, 2, 3};try {// 可能发生异常的代码int result = a / b; // 会抛出ArithmeticExceptionSystem.out.println("数组的第4个元素是:" + arr[3]); // 会抛出ArrayIndexOutOfBoundsExceptionSystem.out.println("计算结果:" + result);} catch (ArithmeticException e) {// 处理算术异常System.out.println("发生算术异常:" + e.getMessage());e.printStackTrace(); // 打印异常堆栈信息} catch (ArrayIndexOutOfBoundsException e) {// 处理数组下标越界异常System.out.println("发生数组下标越界异常:" + e.getMessage());} finally {// 无论是否发生异常,都会执行System.out.println("finally块执行了,通常用于释放资源");}System.out.println("程序继续执行...");}
}
运行上述代码,输出结果:
12.2.3 用 catch 捕获多个异常
Java 7 及以上版本允许在一个catch
块中捕获多种类型的异常,使用|
分隔不同的异常类型。
示例代码:
import java.io.FileNotFoundException;
import java.io.IOException;public class MultiCatchDemo {public static void main(String[] args) {try {// 模拟可能抛出不同异常的操作int choice = Integer.parseInt(args[0]);if (choice == 1) {throw new FileNotFoundException("文件未找到");} else if (choice == 2) {throw new IOException("I/O操作失败");} else if (choice == 3) {throw new ArithmeticException("算术错误");}} catch (FileNotFoundException e) {// 先捕获子类异常System.out.println("处理文件未找到异常:" + e.getMessage());} catch (IOException e) {// 再捕获父类异常System.out.println("处理其他I/O异常:" + e.getMessage());} catch (ArithmeticException e) {// 捕获ArithmeticExceptionSystem.out.println("处理算术异常:" + e.getMessage());} catch (ArrayIndexOutOfBoundsException e) {// 捕获数组下标越界异常(当没有传入命令行参数时)System.out.println("请传入一个整数参数(1-3)");}System.out.println("程序结束");}
}
说明:
- 当一个
catch
块捕获多种异常类型时,这些异常类型不能有继承关系。 - 这种写法比多个
catch
块更简洁,尤其是当多种异常的处理逻辑相同时。 - 可以通过命令行参数来测试不同的异常情况:
java MultiCatchDemo 1
:测试 FileNotFoundExceptionjava MultiCatchDemo 2
:测试 IOExceptionjava MultiCatchDemo 3
:测试 ArithmeticExceptionjava MultiCatchDemo
:测试 ArrayIndexOutOfBoundsException
12.2.4 声明方法抛出异常
如果一个方法可能会抛出异常,但不想在方法内部处理,而是让调用者来处理,可以使用throws
关键字在方法声明处声明该方法可能抛出的异常。
语法:
修饰符 返回值类型 方法名(参数列表) throws 异常类型1, 异常类型2, ... {// 方法体
}
示例代码:
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;public class ThrowsDemo {// 声明方法可能抛出FileNotFoundException和IOExceptionpublic static void readFile(String fileName) throws FileNotFoundException, IOException {FileInputStream fis = new FileInputStream(fileName);int data = fis.read();while (data != -1) {System.out.print((char) data);data = fis.read();}fis.close();}public static void main(String[] args) {try {// 调用声明了抛出异常的方法,必须处理这些异常readFile("test.txt");} catch (FileNotFoundException e) {System.out.println("文件未找到:" + e.getMessage());} catch (IOException e) {System.out.println("文件读取错误:" + e.getMessage());}System.out.println("\n程序执行完毕");}
}
说明:
- 对于 checked exception,如果方法不处理,就必须在方法声明中用
throws
声明。 - 对于 unchecked exception(RuntimeException 及其子类),可以不用
throws
声明,编译器不会强制要求。 - 调用声明了异常的方法时,要么用
try-catch
处理这些异常,要么在当前方法中也用throws
声明继续向上抛出。
12.2.5 用 throw 语句抛出异常
throw
语句用于手动抛出一个具体的异常对象。通常在满足特定条件时,我们认为这是一个异常情况,就可以手动抛出异常。
语法:
throw 异常对象;
示例代码:
public class ThrowDemo {// 计算年龄的方法,如果年龄不合法则抛出异常public static void printAge(int birthYear) {int currentYear = 2023;int age = currentYear - birthYear;if (birthYear < 1900 || birthYear > currentYear) {// 手动抛出异常throw new IllegalArgumentException("出生年份不合法:" + birthYear);}System.out.println("今年" + age + "岁");}public static void main(String[] args) {try {printAge(2000); // 合法的出生年份printAge(2050); // 不合法的出生年份,会抛出异常printAge(1850); // 这行代码不会执行} catch (IllegalArgumentException e) {System.out.println("捕获到异常:" + e.getMessage());}System.out.println("程序继续执行");}
}
运行结果:
throw
与throws
的区别:
throw
用于方法内部,抛出的是一个具体的异常对象。throws
用于方法声明处,声明的是方法可能抛出的异常类型,可以是多个。
12.2.6 try-with-resources 语句
Java 7 引入了try-with-resources
语句,用于自动管理资源(如文件流、数据库连接等)。它确保在资源使用完毕后自动关闭资源,无需在finally
块中手动关闭。
要使用try-with-resources
,资源类必须实现AutoCloseable
接口(或其子类Closeable
接口)。
语法:
try (资源声明) {// 使用资源的代码
} catch (异常类型 异常对象名) {// 处理异常的代码
}
示例代码:
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;public class TryWithResourcesDemo {public static void main(String[] args) {// try-with-resources语句,资源会自动关闭try (FileInputStream fis = new FileInputStream("test.txt")) {int data = fis.read();while (data != -1) {System.out.print((char) data);data = fis.read();}} catch (FileNotFoundException e) {System.out.println("文件未找到:" + e.getMessage());} catch (IOException e) {System.out.println("文件读取错误:" + e.getMessage());}System.out.println("\n程序执行完毕");}
}
传统方式与 try-with-resources 对比:
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;// 传统方式需要在finally中手动关闭资源
public class TraditionalResourceHandling {public static void main(String[] args) {FileInputStream fis = null;try {fis = new FileInputStream("test.txt");// 使用资源,这里添加一些实际的读取操作示例int data = fis.read();while (data != -1) {System.out.print((char) data);data = fis.read();}} catch (FileNotFoundException e) {e.printStackTrace();} catch (IOException e) {e.printStackTrace();} finally {if (fis != null) {try {fis.close(); // 关闭资源可能也会抛出异常} catch (IOException e) {e.printStackTrace();}}}}
}
说明:
try-with-resources
语句可以声明多个资源,用分号分隔。- 资源的关闭顺序与声明顺序相反。
try-with-resources
语句也可以有catch
和finally
块,用于处理异常或执行必要的清理工作。
12.3 自定义异常类
Java 提供的异常类可能无法满足所有业务需求,这时我们可以自定义异常类。自定义异常类通常继承自Exception
(checked exception)或RuntimeException
(unchecked exception)。
自定义异常类的步骤:
- 创建一个类,继承
Exception
或RuntimeException
。 - 提供构造方法,通常至少提供两个构造方法:一个无参构造方法,一个带有详细信息的构造方法。
示例代码:
// 自定义异常类:用户年龄不合法异常
class InvalidAgeException extends Exception {// 无参构造方法public InvalidAgeException() {super();}// 带有详细信息的构造方法public InvalidAgeException(String message) {super(message);}
}// 自定义异常类:用户姓名为空异常(继承自RuntimeException)
class EmptyNameException extends RuntimeException {public EmptyNameException() {super();}public EmptyNameException(String message) {super(message);}
}// 使用自定义异常的示例
public class CustomExceptionDemo {// 注册用户的方法public static void registerUser(String name, int age) throws InvalidAgeException {if (name == null || name.trim().isEmpty()) {// 抛出unchecked异常,不需要在方法声明中throwsthrow new EmptyNameException("用户名不能为空");}if (age < 0 || age > 150) {// 抛出checked异常,需要在方法声明中throwsthrow new InvalidAgeException("年龄不合法:" + age + ",年龄必须在0-150之间");}System.out.println("用户注册成功:" + name + "," + age + "岁");}public static void main(String[] args) {try {registerUser("张三", 25); // 正常情况registerUser("", 30); // 姓名为空,会抛出EmptyNameExceptionregisterUser("李四", 200); // 年龄不合法,会抛出InvalidAgeException} catch (InvalidAgeException e) {System.out.println("注册失败:" + e.getMessage());} catch (EmptyNameException e) {System.out.println("注册失败:" + e.getMessage());}System.out.println("程序结束");}
}
运行结果:
何时需要自定义异常:
- 当 Java 内置异常不能准确描述业务中的异常情况时。
- 希望通过异常类型来区分不同的错误场景,便于异常处理。
- 需要在异常中包含特定的业务信息时。
12.4 断言
断言(Assertion)是 Java 1.4 引入的特性,用于在程序开发和测试阶段检查某些条件是否满足。如果断言失败,会抛出AssertionError
。
12.4.1 使用断言
断言的语法有两种形式:
- 简单形式
assert 布尔表达式;
如果布尔表达式的值为false
,则抛出AssertionError
。
2.带消息的形式:
assert 布尔表达式 : 消息表达式;
如果布尔表达式的值为false
,则抛出AssertionError
,并将消息表达式的值作为错误消息。
12.4.2 开启和关闭断言
默认情况下,Java 虚拟机(JVM)是关闭断言功能的。要开启断言,需要使用-ea
(或-enableassertions
)参数。
开启断言的方式:
- 对所有类开启断言:
java -ea 类名
- 对特定包开启断言:
java -ea:包名... 类名
- 对特定类开启断言:
java -ea:类名 类名
关闭断言的方式:
- 使用
-da
(或-disableassertions
)参数,用法与-ea
类似。
在 IDE(如 Eclipse、IntelliJ IDEA)中,可以在运行配置中设置 VM 参数来开启或关闭断言。
12.4.3 何时使用断言
断言主要用于:
- 检查程序内部的 invariants(不变量),即那些在程序正常执行时必须为真的条件。
- 检查方法的前置条件和后置条件。
- 检查私有方法的参数有效性(对于公共方法,应使用异常来处理无效参数)。
注意:
- 断言不应该用于检查程序运行时可能出现的预期错误,如用户输入错误。
- 断言可能会被关闭,因此不能依赖断言来处理程序的关键功能。
- 不要在断言表达式中包含有副作用的操作(如修改变量值),因为当断言关闭时,这些操作不会执行。
12.4.4 断言示例
public class AssertionDemo {// 计算三角形面积的方法(海伦公式)public static double calculateTriangleArea(double a, double b, double c) {// 检查前置条件:三角形的三条边必须为正数assert a > 0 && b > 0 && c > 0 : "三角形的边长必须为正数";// 检查前置条件:三角形任意两边之和大于第三边assert a + b > c && a + c > b && b + c > a : "不满足三角形两边之和大于第三边";double s = (a + b + c) / 2;double area = Math.sqrt(s * (s - a) * (s - b) * (s - c));// 检查后置条件:面积必须为正数assert area > 0 : "计算出的面积必须为正数";return area;}public static void main(String[] args) {try {double area1 = calculateTriangleArea(3, 4, 5);System.out.println("直角三角形的面积:" + area1);// 测试无效的三角形(两边之和不大于第三边)double area2 = calculateTriangleArea(1, 1, 3);System.out.println("面积:" + area2);} catch (AssertionError e) {System.out.println("断言失败:" + e.getMessage());}}
}
运行说明:
- 当关闭断言运行时(默认情况),程序不会检查断言条件,可能会计算出不合理的结果。
- 当开启断言运行时(
java -ea AssertionDemo
),第二个计算会触发断言失败,输出:
直角三角形的面积:6.0
断言失败:不满足三角形两边之和大于第三边
12.5 小结
本章我们学习了 Java 中的异常处理机制,主要内容包括:
- 异常的概念:异常是程序运行时发生的非正常事件,会中断程序的正常执行。
- 异常类体系:所有异常类都继承自
Throwable
,主要分为Error
和Exception
两大类。Exception
又分为 checked exception 和 unchecked exception。 - 异常处理机制:
- 使用
try-catch-finally
语句捕获和处理异常。 - 使用
throws
声明方法可能抛出的异常。 - 使用
throw
手动抛出异常。 - 使用
try-with-resources
自动管理资源。
- 使用
- 自定义异常:当 Java 内置异常不能满足需求时,可以自定义异常类。
- 断言:用于开发和测试阶段检查某些条件是否满足,默认是关闭的。
掌握异常处理是编写健壮 Java 程序的关键。合理地使用异常处理机制,可以使程序在遇到错误时能够优雅地处理,而不是直接崩溃,同时也便于调试和维护。
编程练习
练习 1:除法计算器
编写一个程序,实现两个整数的除法运算。要求:- 处理除数为 0 的情况(ArithmeticException)。
- 处理输入非整数的情况(InputMismatchException)。
- 使用 try-catch-finally 结构,确保程序在任何情况下都能友好地提示用户。
练习 1 参考答案:
import java.util.InputMismatchException;
import java.util.Scanner;public class DivisionCalculator {public static void main(String[] args) {Scanner scanner = null;try {scanner = new Scanner(System.in);System.out.print("请输入被除数:");int dividend = scanner.nextInt();System.out.print("请输入除数:");int divisor = scanner.nextInt();int result = dividend / divisor;System.out.println(dividend + " / " + divisor + " = " + result);} catch (ArithmeticException e) {System.out.println("错误:除数不能为0");} catch (InputMismatchException e) {System.out.println("错误:请输入有效的整数");} finally {if (scanner != null) {scanner.close();System.out.println("资源已释放");}}System.out.println("程序结束");}
}
希望本章的内容能帮助你理解和掌握 Java 异常处理的相关知识。如果有任何疑问或建议,欢迎在评论区留言讨论!