什么是异常?
在程序运行过程中,发生的一些不正常的事件,它会中断程序的正常执行流程,这些事件就是异常。
- 算术异常:
int a = 10 / 0;// 除以零 - 空指针异常:
String str = null; str.length();// 在一个 null 对象上调用方法 - 数组越界异常:
int[] arr = new int[3]; arr[3] = 5;// 访问不存在的数组索引
如果没有异常处理机制,程序一旦遇到这些错误就会立即崩溃(终止执行),而 try-catch 机制允许我们优雅地处理这些错误,让程序在出错后依然可以继续执行或者给出友好的提示。
try-catch 的基本语法
try-catch 语句的基本结构如下:
try {
// 可能会抛出异常的代码块
// JVM 在这里监视代码的执行
} catch (异常类型1 e) {
// 当 try 块中发生 异常类型1 的异常时,执行的代码块
// e 是一个异常对象,包含了异常的信息
} catch (异常类型2 e) {
// 当 try 块中发生 异常类型2 的异常时,执行的代码块
} finally {
// 无论是否发生异常,也无论是否被 catch 捕获,此代码块总会执行
// 通常用于资源释放(如关闭文件、数据库连接等)
}
工作流程:
- 执行
try块:程序开始执行try块中的代码。 - 发生异常:如果在
try块的执行过程中,某个语句抛出了一个异常,程序会立即跳出try块。 - 匹配
catch块:JVM 会开始检查后面的catch块,从上到下寻找一个与抛出的异常类型匹配的catch块。- 匹配规则:
catch块中声明的异常类型与抛出的异常类型相同,或者是其父类。
- 匹配规则:
- 执行
catch块:一旦找到匹配的catch块,程序就会执行该catch块中的代码,执行完毕后,程序会继续执行try-catch-finally结构后面的代码。 - 未发生异常:
try块中的所有代码都正常执行完毕,没有抛出任何异常,那么所有的catch块都会被忽略,程序会直接执行finally块(如果存在)以及其后的代码。 finally块:finally块是可选的,但它保证总会被执行,它是进行清理工作的理想位置,比如关闭文件流、网络连接等。
代码示例
示例 1:基本的 try-catch
这个例子处理一个可能除以零的异常。
public class BasicTryCatch {
public static void main(String[] args) {
int a = 10;
int b = 0;
int result = 0;
System.out.println("开始计算...");
try {
// 这行代码可能会抛出 ArithmeticException
result = a / b;
System.out.println("计算结果是: " + result); // 这行代码不会执行
} catch (ArithmeticException e) {
// 当发生 ArithmeticException 时,执行这里的代码
System.out.println("捕获到异常: 不能除以零!");
// e.printStackTrace() 会打印出异常的详细跟踪信息,非常有助于调试
e.printStackTrace();
}
System.out.println("程序执行完毕,没有崩溃。");
}
}
输出:
开始计算...
捕获到异常: 不能除以零!
java.lang.ArithmeticException: / by zero
at BasicTryCatch.main(BasicTryCatch.java:10)
程序执行完毕,没有崩溃。
分析:
- 程序打印 "开始计算..."。
- 进入
try块,执行result = a / b;。 - 由于
b为 0,抛出ArithmeticException。 - 程序立即跳过
try块中剩余的代码。 catch (ArithmeticException e)块捕获到这个异常,打印提示信息并调用e.printStackTrace()。catch块执行完毕,程序继续执行System.out.println("程序执行完毕...");,程序正常结束。
示例 2:多个 catch 块
一个 try 块后面可以跟多个 catch 块,用来处理不同类型的异常。
重要提示: catch 块的顺序很重要,应该将更具体的异常放在前面,将更通用的异常(父类)放在后面,否则,编译器会报错。
public class MultipleCatch {
public static void main(String[] args) {
try {
// 假设我们从数组中获取一个元素,然后调用其方法
String[] names = { "Alice", "Bob" };
String name = names[5]; // 抛出 ArrayIndexOutOfBoundsException
int length = name.length(); // 如果上面不抛出异常,这里可能抛出 NullPointerException
} catch (ArrayIndexOutOfBoundsException e) {
// 先捕获更具体的异常
System.out.println("数组索引越界了!请检查数组大小。");
e.printStackTrace();
} catch (NullPointerException e) {
// 再捕获另一个具体的异常
System.out.println("对象为空,无法调用方法!");
e.printStackTrace();
} catch (Exception e) {
// 最后捕获所有异常的父类,作为最后的保障
System.out.println("发生了未知错误!");
e.printStackTrace();
}
System.out.println("程序继续执行...");
}
}
输出:
数组索引越界了!请检查数组大小。
java.lang.ArrayIndexOutOfBoundsException: Index 5 out of bounds for length 2
at MultipleCatch.main(MultipleCatch.java:8)
程序继续执行...
示例 3:finally 块
finally 块用于确保无论是否发生异常,某些代码都必须执行,最常见的用途是资源释放。
import java.io.FileWriter;
import java.io.IOException;
public class FinallyExample {
public static void main(String[] args) {
FileWriter writer = null; // 在 try 块外声明,以便 finally 块可以访问
try {
writer = new FileWriter("test.txt");
writer.write("Hello, Java!");
// 模拟一个可能发生的异常
// int a = 10 / 0;
System.out.println("文件写入成功。");
} catch (IOException e) {
System.out.println("捕获到 IO 异常!");
e.printStackTrace();
} finally {
// 这里的代码一定会执行
System.out.println("进入 finally 块,准备关闭文件流。");
if (writer != null) {
try {
writer.close(); // 释放资源
} catch (IOException e) {
e.printStackTrace();
}
}
}
System.out.println("程序执行完毕。");
}
}
输出(即使没有发生异常):
文件写入成功。
进入 finally 块,准备关闭文件流。
程序执行完毕。
输出(如果发生异常):
捕获到 IO 异常!
java.lang.ArithmeticException: / by zero
at FinallyExample.main(FinallyExample.java:12)
进入 finally 块,准备关闭文件流。
程序执行完毕。
可以看到,无论 try 块中是否发生异常,finally 块中的代码都被执行了,确保了 writer.close() 被调用。
异常的层次结构
理解 Java 的异常类层次结构对于正确使用 try-catch 至关重要。
java.lang.Object
java.lang.Throwable
java.lang.Error (错误)
- 通常由 JVM 或底层硬件问题引起,程序无法处理,如 OutOfMemoryError, StackOverflowError
java.lang.Exception (异常)
- IOException (及其子类,如 FileNotFoundException)
- SQLException
- RuntimeException (及其子类,如 NullPointerException, ArithmeticException, ArrayIndexOutOfBoundsException)
- 这类异常也叫“非受检异常”(Unchecked Exception),编译器不强制要求捕获
受检异常 vs. 非受检异常
| 类型 | 定义 | 特点 | 处理方式 |
|---|---|---|---|
| 受检异常 | Exception 类中除了 RuntimeException 及其子类以外的所有异常。 |
编译器会强制要求处理,要么用 try-catch 捕获,要么用 throws 关键字声明抛出给上层调用者处理。 |
必须处理。 |
| 非受检异常 | 包括 RuntimeException 及其所有子类,以及 Error 类及其子类。 |
编译器不强制要求处理,通常是由程序逻辑错误引起的,应该修正代码而不是捕获它。 | 可选处理,但推荐修复代码。 |
FileReader 构造函数会抛出 FileNotFoundException(受检异常),你必须处理它。
// 必须处理受检异常
try {
FileReader reader = new FileReader("non_existent_file.txt");
} catch (FileNotFoundException e) {
System.out.println("文件未找到,请检查路径。");
}
而 NullPointerException 是非受检异常,编译器不会提醒你,但你应该通过检查代码来避免它。
try-with-resources (Java 7+ 的新特性)
对于需要手动关闭的资源(如 FileInputStream, Connection, Socket等),Java 7 引入了一个非常方便的语法糖:try-with-resources。
只要一个资源实现了 AutoCloseable 接口(所有 Java I/O 相关的类都实现了它),你就可以在 try 语句中声明它。try 块执行完毕后(无论是否发生异常),这些资源都会被自动关闭,无需 finally 块。
示例:
import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;
public class TryWithResources {
public static void main(String[] args) {
// try-with-resources 语法
// BufferedReader 会在 try 块结束后自动关闭
try (BufferedReader reader = new BufferedReader(new FileReader("test.txt"))) {
String line;
while ((line = reader.readLine()) != null) {
System.out.println(line);
}
} catch (IOException e) {
System.out.println("读取文件时发生错误。");
e.printStackTrace();
}
// 这里不需要 finally 块来手动关闭 reader,它已经被自动关闭了
System.out.println("文件读取完毕,资源已自动释放。");
}
}
优点:
- 代码更简洁:省去了冗长的
finally块。 - 更安全:即使
try块中发生异常,资源也能被正确关闭。 - 可以同时管理多个资源:用分号隔开即可,它们会按照声明的相反顺序关闭。
try (FileInputStream fis = new FileInputStream("file1.txt");
FileOutputStream fos = new FileOutputStream("file2.txt")) {
// ...
} catch (IOException e) {
// ...
}
// fis 先被关闭,fos 才被关闭
| 关键点 | 描述 |
|---|---|
try |
放置可能抛出异常的代码。 |
catch |
捕获并处理特定类型的异常,可以有多个,顺序很重要(具体到通用)。 |
finally |
保证总会执行的代码块,用于资源清理。 |
| 异常类型 | Error (不处理), Exception (受检, 必须处理), RuntimeException (非受检, 最好修复代码)。 |
try-with-resources |
现代、自动化的资源管理方式,推荐使用。 |
掌握 try-catch 是编写健壮、可靠的 Java 程序的必备技能,它能让你的程序在面对意外情况时更加从容。
