前言
这里是分享 Java 相关内容的专刊,每日一更。
本期将为大家带来以下内容:
- 异常处理
- 捕获与处理异常
- 自定义异常
- try-with-resources
异常处理
什么是异常
异常 是程序在运行时出现的意外情况或错误,它中断了程序的正常执行流程。换句话说,异常就是程序无法按预期执行时,由系统或程序员明确抛出的信号,表示程序遇到了某种问题。
在软件开发中,我们总是试图编写可靠的代码,但是不可避免地会遇到一些问题,比如:
- 用户输入无效数据(如本该输入数字时输入了字母)。
- 系统资源不可用(如文件找不到或数据库无法连接)。
- 程序内部逻辑错误(如除以零、数组越界等)。
这些问题在运行时触发的情况被称为 异常,异常的本质就是程序运行时的非预期行为。
在 Java 中,当程序遇到错误时,会生成一个 异常对象,该对象封装了错误信息。然后,通过抛出这个异常对象,程序将异常信息传递给调用栈的上层方法,以便处理该错误。如果该异常没有被程序处理,最终会导致程序终止。
举个简单的例子:
int a = 10;
int b = 0;
int result = a / b; // 此处会触发异常:ArithmeticException (除以零)
在上面的代码中,当 b
为 0 时,Java 将抛出一个 ArithmeticException
(算术异常),因为除以零在数学上是未定义的。这种情况下,程序会中断,并输出异常的详细信息。
异常层次结构
在 Java 中,异常(Exception)是程序在运行时遇到问题时用来报告错误的机制。为了更好地管理这些错误,Java 使用了一个“异常层次结构”,把不同的异常分类。
Throwable/ \Error Exception/ \RuntimeException CheckedException
Throwable:是 Java 中所有错误和异常的根类,分为两大子类:Error
和 Exception
。
Error:表示无法恢复的严重错误,通常是 JVM 层面的问题,如内存溢出(OutOfMemoryError
),程序不应该试图处理这些错误。
Exception:表示程序运行中的可预期问题,程序员可以捕捉并处理。分为:
- RuntimeException(非受检异常):如
NullPointerException
、IndexOutOfBoundsException
等。 - Checked Exception(受检异常):如
IOException
、SQLException
等。
Checked 异常与 Unchecked 异常
类别 | Checked Exception(受检异常) | Unchecked Exception(非受检异常) |
---|---|---|
定义 | 在编译时由编译器强制要求处理的异常。 | 即运行时异常,编译器不要求强制处理。 |
继承关系 | 继承自 Exception 类,**但不包括 **RuntimeException 。 | 继承自 RuntimeException 类。 |
处理方式 | 必须通过 throws 声明抛出,或使用 try-catch 块显式捕获。 | 编译器不强制要求处理,程序可以选择捕获或忽略。 |
典型场景 | 与外部资源交互时,可能发生不可避免的异常。 | 通常由程序逻辑错误或编程不当导致。 |
常见示例 | IOException (文件或网络 I/O 错误)SQLException (数据库操作异常) | NullPointerException (空指针异常)ArrayIndexOutOfBoundsException (数组越界异常) |
开发者处理需求 | 调用方法时必须显式处理(捕获或抛出)。 | 可以选择处理或忽略,程序运行时会抛出异常。 |
是否可预测 | 通常可预测的异常,发生在程序运行时与外部资源交互的过程中。 | 通常是不可预测的程序错误,往往是编程不当导致。 |
强制性 | 是。必须处理或声明。 | 否。编译时不会检查。 |
异常与错误
类别 | Exception(异常) | Error(错误) |
---|---|---|
定义 | 程序运行过程中可能发生的可预期问题,通常是程序或外部环境的异常情况。 | 严重的、不可恢复的问题,通常是 JVM 层面的问题,无法通过程序处理。 |
继承关系 | 继承自 Throwable 类,且进一步分为 Checked 和 Unchecked 异常。 | 继承自 Throwable 类,通常不建议捕获或处理。 |
常见场景 | 通常与外部资源交互时出现,如文件操作、数据库访问。 | 通常由系统资源耗尽、内存溢出或 JVM 崩溃引发。 |
处理方式 | 开发者可以通过 try-catch 块捕获并处理,或者声明抛出。 | 程序一般不应捕获 Error ,且大部分情况下无法恢复。 |
是否可恢复 | 是。异常通常可以通过适当的处理机制进行恢复。 | 否。错误通常无法恢复,意味着程序已经处于不可继续执行的状态。 |
是否可预测 | 通常是可预测的,且在程序中可以预见并进行处理。 | 不可预测的系统级问题,通常与 JVM 和硬件资源相关。 |
常见示例 | IOException (输入输出异常) SQLException (数据库异常) | OutOfMemoryError (内存溢出) StackOverflowError (栈溢出) |
开发者处理需求 | 必须通过异常处理机制处理或声明,编译器强制要求处理受检异常。 | 一般不建议处理,且大多数情况下无法处理或恢复。 |
捕获与处理异常
try-catch
try-catch
是 Java 中处理异常的核心机制。try
块包含可能抛出异常的代码,而 catch
块用于捕获并处理该异常。
try {int result = 10 / 0; // 抛出 ArithmeticException
} catch (ArithmeticException e) {System.out.println("发生了除以零错误: " + e.getMessage());
}
try
块:包含了可能会导致异常的代码。如果在 try
中发生异常,程序的执行将跳转到 catch
。
catch
块:用于捕获并处理异常。它可以包含多个 catch
,以处理不同类型的异常。
finally
finally
块在异常处理结束后执行,无论是否发生异常,finally
中的代码都会执行。通常用于释放资源,如关闭文件、数据库连接等。
try {// 打开文件或数据库连接
} catch (IOException e) {System.out.println("文件处理出错: " + e.getMessage());
} finally {// 关闭文件或释放资源
}
如果 try
或 catch
块中有 return
语句,finally
块仍然会在 return
语句之前执行。即使方法已经准备返回结果,finally
依然会先执行,然后再返回结果。
例如:
public static int testFinally() {try {System.out.println("In try block");return 1; // 尝试返回 1} catch (Exception e) {System.out.println("In catch block");return 2; // 捕获异常时尝试返回 2} finally {System.out.println("In finally block");return 3; // 覆盖前面的 return 语句,最终返回 3}
}public static void main(String[] args) {System.out.println(testFinally());
}
输出:
In try block
In finally block
3
在上面的例子中,try
块中的 return 1
尝试返回 1,但在执行 return
前,finally
块中的 return 3
覆盖了原本的返回值,最终返回了 3。这说明即使 try
或 catch
中有 return
,finally
依然会执行,且如果 finally
中也包含 return
,它会覆盖之前的返回值。
多重异常捕获与处理
Java 支持为一个 try
块提供多个 catch
块,每个 catch
块处理一种特定类型的异常。多个 catch
允许开发者对不同的异常类型做出不同的处理。
try {// 代码可能抛出多个异常
} catch (IOException e) {System.out.println("IO 异常: " + e.getMessage());
} catch (SQLException e) {System.out.println("SQL 异常: " + e.getMessage());
}
多异常捕获
Java 7 引入了多异常捕获机制,允许在同一个 catch
块中处理多个异常类型:
catch (IOException | SQLException e) {System.out.println("发生异常: " + e.getMessage());
}
这不仅简化了代码,也减少了重复代码的编写,提高了可读性。
throw
关键字
throw
用来手动抛出一个异常。当你确定程序中某些地方会发生特定的错误时,可以用 throw
抛出相应的异常,通知调用者这里有问题需要处理。
使用方法:
throw new ExceptionType("异常描述");
ExceptionType
是异常的类型,比如 IllegalArgumentException
、NullPointerException
等等。
"异常描述"
是你自己定义的错误信息,便于理解异常原因。
举个例子:
public class Example {public static void main(String[] args) {int age = -1;if (age < 0) {throw new IllegalArgumentException("年龄不能为负数");}}
}
这个例子中,如果 age
小于 0,就会抛出一个 IllegalArgumentException
异常,并显示错误信息 “年龄不能为负数”。
throws
关键字
throws
用在方法声明上,表示这个方法可能会抛出某种类型的异常。它告诉调用这个方法的代码:小心,这个方法可能会抛出异常,你要负责处理这个异常。
使用方法:
返回类型 方法名(参数) throws 异常类型1, 异常类型2, ... {// 方法体
}
你可以在方法声明中列出可能抛出的异常类型,用逗号分隔。
如果一个方法抛出了异常而没有用 throws
关键字声明,编译器会报错。
举个例子:
public void readFile(String filePath) throws FileNotFoundException {File file = new File(filePath);if (!file.exists()) {throw new FileNotFoundException("文件未找到:" + filePath);}
}
这个方法 readFile
可能抛出 FileNotFoundException
,所以在方法声明时使用 throws
来提醒调用者:你在调用我这个方法时要小心,有可能会出现文件找不到的异常。
throw
与 throws
的区别
项 | throw | throws |
---|---|---|
定义 | 用于显式抛出异常 | 用于声明可能抛出的异常类型 |
使用位置 | 在方法内部 | 在方法签名中 |
语法 | throw new ExceptionType("message"); | public void methodName() throws ExceptionType |
目的 | 触发一个异常 | 表示方法可能会抛出某些异常,调用者需处理 |
例子 | throw new IOException(); | public void readFile() throws IOException |
处理 | 直接抛出异常,程序可能终止 | 允许调用者捕获并处理异常 |
自定义异常
自定义异常是在 Java 中自己定义的异常类,用于处理程序中特定的错误或业务逻辑。虽然 Java 已经内置了许多常见的异常类(如 NullPointerException
, ArrayIndexOutOfBoundsException
等),但有时这些标准异常不能很好地描述你程序中的错误。此时,开发者可以通过创建自己的异常类,专门用来处理和表示特定的异常情况。
自定义异常类一般继承自 Exception
或 RuntimeException
。 例如:
public class CustomException extends Exception {public CustomException(String message) {super(message);}
}
try-with-resources
try-with-resources
是 Java 中用来自动关闭资源的一种机制。它特别适用于那些需要手动关闭的资源,比如文件、数据库连接、网络连接等。传统的 try-catch-finally
需要我们在 finally
代码块中显式地关闭这些资源,但 try-with-resources
可以帮我们自动完成这一过程,确保资源不会因为程序中的错误而忘记关闭。
具体怎么理解呢?
在程序中,像文件读写、数据库访问这类操作都需要 打开某些资源(例如打开文件或建立数据库连接),而这些资源一旦使用完毕就需要关闭。如果忘记关闭,可能会导致内存泄漏或资源耗尽。
传统做法中,我们会在 finally
块里手动关闭资源。比如:
BufferedReader br = null;
try {br = new BufferedReader(new FileReader("example.txt"));// 进行文件读取操作
} catch (IOException e) {e.printStackTrace();
} finally {if (br != null) {try {br.close(); // 手动关闭资源} catch (IOException e) {e.printStackTrace();}}
}
上面的代码中,finally
块确保了文件在使用完后一定会被关闭,即使中间发生了异常。但这样写比较繁琐。
使用 try-with-resources
怎么做?
使用 try-with-resources
后,代码变得更简单,因为资源会自动关闭:
try (BufferedReader br = new BufferedReader(new FileReader("example.txt"))) {// 进行文件读取操作
} catch (IOException e) {e.printStackTrace();
}
优势:
- 自动关闭资源:当
try
块结束时(无论是正常结束还是发生异常),BufferedReader
会自动关闭,无需手动在finally
中编写close()
。 - 简化代码:减少了显式关闭资源的代码,避免写太多的
try-catch-finally
,代码更加简洁明了。
使用 try-with-resources
的资源,必须实现 AutoCloseable
接口(很多常见的资源类,比如 BufferedReader
、FileInputStream
等都已经实现了这个接口),这确保了它们具有 close()
方法可以被自动调用。
本期小知识
很多人认为 finally
块中的代码总是会执行,但有极少数情况会导致 finally
块不会执行:
- JVM 关闭(例如调用
System.exit(0)
)。 - 线程被强制中断或崩溃。
- 程序因断电或操作系统强制关闭等外部因素终止。