如何在 Java 中指定和处理异常
在软件世界中,错误无时无刻不在发生。 它可能是无效的用户输入或没有响应的外部系统,或者是一个简单的编程错误。 在所有这些情况下,错误都发生在运行时,应用程序需要处理它们。 否则,它会崩溃并且无法处理进一步的请求。 Java 提供了一种强大的机制,允许我们在发生异常事件的地方或在调用堆栈中的较高层方法之一中处理异常事件。
在本文中,我们将涵盖以下主题:
- Java异常处理常用术语
- Java 中的已检查和未检查异常
- 如何处理异常
- 如何指定异常
- 如何知道是否处理或指定异常
在深入了解 Java 异常处理的细节之前,我们需要定义一些术语。
Java 异常处理:常用术语
调用栈
调用栈是为到达特定方法而调用的方法的有序列表。 在这篇文章的上下文中,这些是被调用以获取发生错误的方法的方法。
让我们看一个例子。 Method1 调用 Method2,Method2 调用 Method3。 调用堆栈现在包含以下三个条目:
- Method3
- Method2
- Method1
异常类和层次结构
异常类标识发生的错误类型。 例如,当字符串格式错误且无法转换为数字时,会抛出 NumberFormatException
。
与每个 Java 类一样,异常类是继承层次结构的一部分。 它必须扩展 java.lang.Exception 或其子类之一。
层次结构还用于对相似类型的错误进行分组。
IllegalArgumentException
就是一个例子。 它表示提供的方法参数无效,它是NumberFormatException
的超类。
我们还可以通过扩展 Exception
类或其任何子类来实现自己的异常类。 以下代码片段显示了一个自定义异常的简单示例。
public class MyBusinessException extends Exception {
private static final long serialVersionUID = 7718828512143293558L;
public MyBusinessException() {
super();
}
public MyBusinessException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) {
super(message, cause, enableSuppression, writableStackTrace);
}
public MyBusinessException(String message, Throwable cause) {
super(message, cause);
}
public MyBusinessException(String message) {
super(message);
}
public MyBusinessException(Throwable cause) {
super(cause);
}
}
异常对象
异常对象是异常类的实例。 当发生中断应用程序正常流程的异常事件时,它会被创建并交给 Java 运行时。 这称为“抛出异常”,因为在 Java 中我们使用关键字 throw
将异常交给运行环境。
当方法抛出异常对象时,运行环境会在调用堆栈中搜索处理它的一段代码。 我将在本文的如何处理异常部分中详细介绍异常处理。
Java 中的已检查和未检查异常
Java 支持已检查和未检查的异常。我们可以以类似的方式使用它们,并且有很多关于何时使用哪种异常的讨论。但这超出了本文的范围。现在,让我们按照 Oracle 的 Java 教程中介绍的方法进行操作。
我们应该对所有我们可以预见的异常事件使用已检查的异常,并且编写良好的应用程序应该能够处理。检查的异常扩展了 Exception 类。抛出检查异常或调用指定检查异常的方法的方法需要指定或处理它。
未经检查的异常扩展了 RuntimeException
。我们应该将它们用于无法预料的内部错误,而且大多数情况下,应用程序无法从中恢复。方法可以但不需要处理或指定未经检查的异常。抛出未检查异常的典型示例是:
-
变量未初始化导致
NullPointerException
-
API 使用不当导致
IllegalArgumentException
如何处理异常
Java 提供了两种不同的选项来处理异常。 我们可以使用 try-catch-finally
方法来处理各种异常。 或者可以使用 try-with-resource
方法,这样可以更轻松地清理资源。
Try-Catch-Finally
这是在 Java 中处理异常的经典方法。 它可以包括 3 个步骤:
-
包含可能引发异常的代码部分的
try
块, -
一个或多个处理异常的
catch
块,以及 -
在成功执行
try
块或处理抛出的异常后执行的finally
块。
try
块是必需的,可以在有或没有 catch
或 finally
块的情况下使用它。
try 块
让我们先谈谈 try
块。 它包含可能引发异常的代码部分。 如果我们的代码抛出不止一个异常,可以选择是否要:
-
为每个可能抛出异常的语句使用单独的
try
块或 -
对可能抛出多个异常的多个语句使用一个
try
块。
以下示例显示了一个包含三个方法调用的 try
块。
public void performBusinessOperation() {
try {
doSomething("A message");
doSomethingElse();
doEvenMore();
}
// see following examples for catch and finally blocks
}
public void doSomething(String input) throws MyBusinessException {
// do something useful ...
throw new MyBusinessException("A message that describes the error.");
}
public void doSomethingElse() {
// do something else ...
}
public void doEvenMore() throws NumberFormatException{
// do even more ...
}
正如我们在方法定义中看到的,只有第一个和第三个方法指定了一个异常。 第一个可能抛出 MyBusinessException
,doEvenMore
方法可能抛出 NumberFormatException
。
在下一步中,我们可以为每个要处理的异常类定义一个 catch
块和一个 finally
块。 需要指定所有未被任何 catch
块处理的已检查异常。
catch 块
我们可以在一个 catch
块中实现对一种或多种异常类型的处理。 正如我们在以下代码片段中看到的,catch
子句将异常作为参数获取。 我们可以通过参数名称在 catch
块中引用它。
public void performBusinessOperation() {
try {
doSomething("A message");
doSomethingElse();
doEvenMore();
} catch (MyBusinessException e) {
e.printStackTrace();
} catch (NumberFormatException e) {
e.printStackTrace();
}
}
前面的代码示例显示了两个 catch
块。 一个处理 MyBusinessException
,一个处理 NumberFormatException
。 两个块以相同的方式处理异常。 从 Java 7 开始,你可以只用一个 catch
块来做同样的事情。
public void performBusinessOperation() {
try {
doSomething("A message");
doSomethingElse();
doEvenMore();
} catch (MyBusinessException|NumberFormatException e) {
e.printStackTrace();
}
}
前面示例中的 catch
块的实现非常基础。 我只是调用 printStackTrace
方法,该方法将异常的类、消息和调用堆栈写入系统输出。
com.jiyik.example.MyBusinessException: A message that describes the error.
at com.stackify.example.TestExceptionHandling.doSomething(TestExceptionHandling.java:84)
at com.stackify.example.TestExceptionHandling.performBusinessOperation(TestExceptionHandling.java:25)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:497)
at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:47)
at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:44)
at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:271)
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:70)
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:50)
at org.junit.runners.ParentRunner$3.run(ParentRunner.java:238)
at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:63)
at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:236)
at org.junit.runners.ParentRunner.access$000(ParentRunner.java:53)
at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:229)
at org.junit.runners.ParentRunner.run(ParentRunner.java:309)
at org.eclipse.jdt.internal.junit4.runner.JUnit4TestReference.run(JUnit4TestReference.java:50)
at org.eclipse.jdt.internal.junit.runner.TestExecution.run(TestExecution.java:38)
at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:459)
at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:675)
at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.run(RemoteTestRunner.java:382)
at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.main(RemoteTestRunner.java:192)
在实际应用程序中,我们可能希望使用更高级的实现。 例如,我们可以向用户显示错误消息并请求不同的输入,或者可以将记录写入批处理的工作日志中。 有时,甚至可以捕获并忽略异常。
在生产中,我们还需要监控我们的应用程序及其异常处理。
finally 块
finally
块在 try
块成功执行后或 catch
块之一处理异常后执行。 因此,它是实现任何清理逻辑的好地方,例如关闭连接或 InputStream
。
我们可以在以下代码片段中看到此类清理操作的示例。 finally
块将被执行,即使 FileInputStream
的实例化抛出 FileNotFoundException
或文件内容的处理抛出任何其他异常。
FileInputStream inputStream = null;
try {
File file = new File("./tmp.txt");
inputStream = new FileInputStream(file);
// use the inputStream to read a file
} catch (FileNotFoundException e) {
e.printStackTrace();
} finally {
if (inputStream != null) {
try {
inputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
如你所见,finally
块提供了一个很好的选项来防止任何泄漏。 在 Java 7 之前,最好的做法是将所有清理代码放入 finally
块中。
Try-With-Resource
当 Java 7 引入 try-with-resource
语句时,情况发生了变化。 它会自动关闭所有实现 AutoCloseable
接口的资源。 大多数需要关闭的 Java 对象都是这种情况。
要使用此功能,我们唯一需要做的就是在 try
子句中实例化对象。 还需要处理或指定关闭资源时可能抛出的所有异常。
以下代码片段显示了前面的示例,其中使用了 try-with-resource
语句而不是 try-catch-finally
语句。
File file = new File("./tmp.txt");
try (FileInputStream inputStream = new FileInputStream(file);) {
// use the inputStream to read a file
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
如大家所见,
try-with-resource
语句更容易实现和阅读。 关闭FileInputStream
时可能抛出的IOException
的处理不需要嵌套的try-catch
语句。 它现在由try-with-resource
语句的 catch 块处理。
如何指定异常
如果我们不在方法内处理异常,它将在调用堆栈中传播。 如果是已检查的异常,我们还需要指定该方法可能会抛出异常。 我们可以通过在方法声明中添加一个 throws
子句来做到这一点。 因此,所有调用方法都需要自己处理或指定异常。
如果你想指出一个方法可能会抛出一个未经检查的异常,你也可以指定它。
public void doSomething(String input) throws MyBusinessException {
// do something useful ...
// if it fails
throw new MyBusinessException("A message that describes the error.");
}
处理或指定异常
通常,是否应该处理或指定异常取决于用例。 正如我们可能猜到的那样,很难提供适合所有用例的建议。
一般来说,我们需要问自己以下问题:
- 是否能够在当前方法中处理异常?
- 你能预料到你所在类所有用户的需求吗? 处理异常会满足这些需求吗?
如果对这两个问题的回答都是肯定的,那么你应该在当前方法中处理异常。 在所有其他情况下,最好指定它。 这允许你的类的调用者实现适合当前用例的处理。
总结
好的,这就是关于 Java 异常处理的全部内容。 我将在本系列的未来文章中详细介绍最佳实践和常见错误。
如我们所见,Java 为我们提供了两种一般类型的异常:已检查异常和未检查异常。
我们应该对应用程序可以预期和处理的所有异常事件使用已检查的异常。 需要决定是要在方法中处理它还是指定它。 我们可以使用 try-catch-finally
或 try-with-resource
块来处理它。 如果决定指定异常,它就会成为方法定义的一部分,并且异常需要由所有调用方法指定或处理。
对于无法预料的内部错误,应该使用未经检查的异常。 我们不需要处理或指定此类异常,但可以按照处理或指定已检查异常的相同方式来处理或指定。
相关文章
在 Java 中获取文件大小
发布时间:2023/05/01 浏览次数:139 分类:Java
-
Java 提供了不同的方法来获取文件的字节大小。 本教程演示了在 Java 中获取文件大小的不同方法。使用 Java IO 的文件类获取文件大小 Java IO 包的 File 类提供了以字节为单位获取文件大小的功能。
Java 中的文件分隔符
发布时间:2023/05/01 浏览次数:108 分类:Java
-
本篇文章介绍了 Java 中的文件分隔符。Java 中的文件分隔符 文件分隔符是用来分隔目录的字符; 例如,Unix 使用 /,Windows 使用 \ 作为文件分隔符。
Java 中的文件过滤器
发布时间:2023/05/01 浏览次数:193 分类:Java
-
本篇文章介绍如何在 Java 中使用 FileFilter。FileFilter 用于过滤具有特定扩展名的文件。 Java内置包IO和Apache Commons IO为FileFilter提供了类和接口来进行文件过滤操作。
Java 获取 ISO 8601 格式的当前时间戳
发布时间:2023/05/01 浏览次数:132 分类:Java
-
本篇文章介绍了 ISO 8601 日期格式、其重要性及其在 Java 中的使用。 它还列出了一些优点来强调为什么应该使用 ISO 格式来表示日期。
在 Java 中获取数组的子集
发布时间:2023/05/01 浏览次数:142 分类:Java
-
本篇文章介绍了几种在 Java 中获取数组子集的方法。使用 Arrays.copyOf() 方法获取数组的子集 使用 Arrays.copyOfRange() 方法获取数组的子集
用 Java 填充二维数组
发布时间:2023/05/01 浏览次数:110 分类:Java
-
二维数组是基于表结构的,即行和列,填充二维数组不能通过简单的添加到数组操作来完成。 本篇文章介绍如何在 Java 中填充二维数组。
计算 Java 数组中的重复元素
发布时间:2023/05/01 浏览次数:202 分类:Java
-
本篇文章介绍Java计算数组中重复元素的方法。计算 Java 数组中的重复元素。我们可以创建一个程序来计算数组中的重复元素。 该数组可以是未排序的,也可以是已排序的。
Java 中 List 和 Arraylist 的区别
发布时间:2023/05/01 浏览次数:90 分类:Java
-
表示为单个单元的一组单个对象称为集合。 在 Java 中,Collection 是一个具有多个已定义接口和类的框架,用于将一组对象表示为一个单元。 它允许我们操纵