介绍
异常是程序执行过程中可能发生的错误事件,会打断其正常流程。Java 提供了一种健壮且面向对象的异常处理方式,称为 Java 异常处理。
在 Java 中,异常可能源自不同的情况,例如用户输入错误的数据、硬件故障、网络连接失败或数据库服务器宕机。指定在特定异常情况下该做什么的代码称为异常处理。
抛出和捕获异常
当执行语句时发生错误时,Java 会创建一个异常对象。异常对象包含大量的调试信息,如方法层次结构、异常发生的行号和异常类型。
如果在方法中发生异常,则创建异常对象并将其交给运行时环境的过程称为“抛出异常”。程序的正常流程停止,Java运行时环境(JRE)会尝试找到异常的处理程序。异常处理程序是可以处理异常对象的代码块。
- 查找异常处理程序的逻辑始于发生错误的方法中。
- 如果没有找到适当的处理程序,则会转移到调用者方法。
- 依此类推。
所以如果方法的调用栈是A->B->C
,并且在方法C
中引发了异常,则寻找适当的处理程序将从C->B->A
移动。
如果找到适当的异常处理程序,则将异常对象传递给处理程序进行处理。处理程序被称为“捕获异常”。如果没有找到适当的异常处理程序,则程序将终止,并在控制台上打印有关异常的信息。
Java异常处理框架仅用于处理运行时错误。编译时错误必须由编写代码的开发人员修复,否则程序将无法执行。
Java异常处理关键字
Java提供了专门用于异常处理的关键字。
- throw – 我们知道,如果发生错误,就会创建一个异常对象,然后Java运行时开始处理它们。有时候,我们可能希望在我们的代码中显式生成异常。例如,在用户认证程序中,如果密码为
null
,我们应该向客户端抛出异常。关键字throw
用于将异常抛给运行时进行处理。 - throws – 当我们在方法中抛出异常而不处理它时,我们必须在方法签名中使用
throws
关键字,以通知调用者可能由该方法抛出的异常。调用方法可能会处理这些异常,或者使用throws
关键字将它们传播到其调用方法。我们可以在throws
子句中提供多个异常,并且它还可以与main()
方法一起使用。 - try-catch – 我们在代码中使用
try-catch
块进行异常处理。try
是块的开始,catch
在try
块的末尾用于处理异常。我们可以在try
块中有多个catch
块。try-catch
块也可以嵌套。catch
块需要一个参数,应该是Exception
类型。 - 最后 –
finally
块是可选的,只能与try-catch
块一起使用。由于异常会中止执行过程,我们可能会有一些资源未关闭,因此可以使用finally
块。无论是否发生异常,finally
块始终会被执行。
异常处理示例
testException()
方法使用throw
关键字抛出异常。方法签名使用throws
关键字通知调用者它可能抛出的异常类型。- 在
main()
方法中,我使用main()
方法中的try-catch
块处理异常。当我不处理它时,我使用main()
方法中的throws
子句将其传播到运行时。 - 由于异常,
testException(-10)
永远不会被执行,然后执行finally
块。
printStackTrace()
是 Exception
类中用于调试目的的一种有用方法。
此代码将输出以下内容:
Outputjava.io.FileNotFoundException: Negative Integer -5
at com.journaldev.exceptions.ExceptionHandling.testException(ExceptionHandling.java:24)
at com.journaldev.exceptions.ExceptionHandling.main(ExceptionHandling.java:10)
Releasing resources
Exception in thread "main" java.io.IOException: Only supported for index 0 to 10
at com.journaldev.exceptions.ExceptionHandling.testException(ExceptionHandling.java:27)
at com.journaldev.exceptions.ExceptionHandling.main(ExceptionHandling.java:19)
需要注意的一些重要点:
- 我们不能在没有
try
语句的情况下使用catch
或finally
子句。 - A
try
statement should have eithercatch
block orfinally
block, it can have both blocks. - 我们不能在`try-catch-finally`块之间编写任何代码。
- 我们可以在单个`try`语句中有多个`catch`块。
- `try-catch`块可以嵌套,类似于`if-else`语句。
- 我们只能在一个`try-catch`语句中有一个`finally`块。
Java异常层次结构
如前所述,当异常被触发时,会创建一个异常对象。Java异常是分层的,继承用于对不同类型的异常进行分类。`Throwable`是Java异常层次结构的父类,它有两个子对象——`Error`和`Exception`。`Exception`进一步分为Checked `Exceptions`和Runtime `Exceptions`。
- 错误: `Error`是应用程序范围之外的异常情况,无法预料并且无法从中恢复。例如,硬件故障、Java虚拟机(JVM)崩溃或内存不足错误。这就是为什么我们有一个单独的`Error`层次结构,我们不应该尝试处理这些情况。一些常见的`Error`包括`OutOfMemoryError`和`StackOverflowError`。
- 检查异常: 检查异常是在程序中可以预料到的异常情况,我们可以尝试从中恢复。例如,FileNotFoundException。我们应该捕获此异常并为用户提供有用的消息,并适当记录以进行调试。Exception是所有检查异常的父类。如果我们抛出检查异常,必须在同一方法中捕获它,或者使用throws关键字将其传播到调用者。
- 运行时异常: 运行时异常是由糟糕的编程引起的。例如,尝试从数组中检索元素。在尝试检索元素之前,我们应该先检查数组的长度,否则可能在运行时抛出ArrayIndexOutOfBoundsException。RuntimeException是所有运行时异常的父类。如果在方法中抛出任何运行时异常,不需要在方法签名的throws子句中指定它们。通过更好的编程可以避免运行时异常。
异常类的一些有用方法
Java Exception
及其所有子类都不提供任何特定方法,所有方法都在基类Throwable
中定义。Exception
类被创建用于指定不同种类的Exception
场景,以便我们可以轻松地识别根本原因并根据其类型处理Exception
。Throwable
类实现了Serializable
接口以实现互操作性。
Throwable
类的一些有用方法包括:
- public String getMessage() – 此方法返回
Throwable
的消息String
,并且在通过其构造函数创建异常时可以提供消息。 - public String getLocalizedMessage() – 提供此方法是为了让子类可以重写它以向调用程序提供特定于区域设置的消息。
Throwable
类对此方法的实现使用getMessage()
方法返回异常消息。 - public synchronized Throwable getCause() – 此方法返回异常的原因,如果原因未知,则返回
null
。 - public String toString() – 此方法以
String
格式返回关于Throwable
的信息,返回的String
包含Throwable
类的名称和本地化消息。 - public void printStackTrace() – 此方法将堆栈跟踪信息打印到标准错误流,该方法被重载,我们可以传递
PrintStream
或PrintWriter
作为参数来将堆栈跟踪信息写入文件或流。
Java 7自动资源管理和捕获块改进
如果您在单个try
块中捕获了许多异常,您会注意到catch
块代码主要由冗余代码组成以记录错误。在Java 7中,其中一个特性是改进了catch
块,我们可以在单个catch
块中捕获多个异常。以下是具有此功能的catch
块的示例:
有一些约束条件,例如异常对象是final的,我们不能在catch
块内修改它,请阅读完整分析,详情请见Java 7 Catch Block Improvements。
大多数情况下,我们使用finally
块只是为了关闭资源。有时候我们会忘记关闭它们,当资源耗尽时会出现运行时异常。这些异常很难调试,我们可能需要查看每个使用该资源的地方,以确保我们关闭了它。在Java 7中,改进之一是try-with-resources
,我们可以在try
语句中创建资源,并在try-catch
块内部使用它。当执行离开try-catch
块时,运行时环境会自动关闭这些资源。以下是带有此改进的try-catch
块的示例:
A Custom Exception Class Example
Java为我们提供了许多异常类供我们使用,但有时我们可能需要创建自己的自定义异常类。例如,为了通知调用者特定类型的异常,并提供适当的消息。我们可以有用于跟踪的自定义字段,例如错误代码。例如,假设我们编写了一个仅处理文本文件的方法,当其他类型的文件作为输入发送时,我们可以为调用者提供适当的错误代码。
首先,创建MyException
:
然后,创建一个CustomExceptionExample
:
我们可以有一个单独的方法来处理我们从不同方法中获取的不同类型的错误代码。其中一些被消耗掉,因为我们可能不想通知用户,或者一些我们会退回以通知用户存在问题。
这里我正在扩展 Exception
,以便每当出现此异常时,必须在方法中处理它或将其返回给调用程序。如果我们扩展 RuntimeException
,就不需要在 throws
子句中指定它。
这是一个设计决策。使用 Checked Exception
的好处是帮助开发人员理解可以预期的异常,并采取适当的措施来处理它们。
Java 异常处理的最佳实践
- 使用具体的异常 – 异常层次结构的基类不提供任何有用的信息,这就是为什么 Java 有这么多异常类的原因,比如
IOException
,它有进一步的子类如FileNotFoundException
,EOFException
等。我们应该总是抛出和捕获特定的异常类,以便调用者可以轻松知道异常的根本原因并处理它们。这样做可以更容易地调试,并帮助客户端应用程序适当地处理异常。 - 尽早抛出异常或快速失败 – 我们应该尽早地抛出异常。考虑上面的
processFile()
方法,如果我们将null
参数传递给此方法,将会得到以下异常:
OutputException in thread "main" java.lang.NullPointerException
at java.io.FileInputStream.<init>(FileInputStream.java:134)
at java.io.FileInputStream.<init>(FileInputStream.java:97)
at com.journaldev.exceptions.CustomExceptionExample.processFile(CustomExceptionExample.java:42)
at com.journaldev.exceptions.CustomExceptionExample.main(CustomExceptionExample.java:12)
在调试时,我们必须仔细查看堆栈跟踪,以确定异常的实际位置。如果我们将实现逻辑更改为提前检查这些异常:
那么异常堆栈跟踪将指示异常发生的位置,并带有清晰的消息:
Outputcom.journaldev.exceptions.MyException: File name can't be null
at com.journaldev.exceptions.CustomExceptionExample.processFile(CustomExceptionExample.java:37)
at com.journaldev.exceptions.CustomExceptionExample.main(CustomExceptionExample.java:12)
- 晚捕获 – 由于Java强制要求处理已检查的异常或在方法签名中声明它,有时开发人员倾向于
捕获
异常并记录错误。但这种做法是有害的,因为调用程序不会收到任何关于异常的通知。我们只有在可以适当处理异常时才应捕获
异常。例如,在上述方法中,我正在将异常抛回给调用方法来处理。其他可能希望以不同方式处理异常的应用程序可以使用相同的方法。在实现任何功能时,我们应始终将异常抛回
给调用方,并让它们决定如何处理异常。 - 关闭资源 – 由于异常会中止程序的处理,我们应在finally块中关闭所有资源,或者使用Java 7的
try-with-resources
增强功能,让Java运行时为您关闭资源。 - 记录异常 – 我们应始终记录异常消息,并在抛出异常时提供清晰的消息,以便调用者能够轻松地了解异常的原因。我们应始终避免使用空的
catch
块,该块只消耗异常而不提供任何异常的有意义细节以进行调试。 - 为多个异常使用单个catch块 – 大多数情况下,我们记录异常详情并向用户提供消息,在这种情况下,我们应使用Java 7的功能来处理单个
catch
块中的多个异常。这种方法将减少我们的代码量,并且看起来更清晰。 - 使用自定义异常 – 在设计时定义异常处理策略总是更好的选择,而不是抛出和捕获多个异常,我们可以创建一个带有错误代码的自定义异常,并且调用程序可以处理这些错误代码。创建一个处理不同错误代码并使用它们的实用方法也是一个好主意。
- 命名约定和打包 – 当您创建自定义异常时,请确保它以
Exception
结尾,以便从名称本身就清楚它是一个异常类。还要确保像Java开发工具包(JDK)中所做的那样对它们进行打包。例如,IOException
是所有IO操作的基本异常。 - 明智使用异常 – 异常是昂贵的,有时根本不需要抛出异常,我们可以向调用程序返回一个布尔变量,指示操作是否成功。这对于操作是可选的,并且您不希望程序因失败而被卡住很有帮助。例如,在从第三方网络服务更新数据库中的股票报价时,如果连接失败,我们可能希望避免抛出异常。
- 记录抛出的异常 – 使用Javadoc
@throws
清楚地指定方法抛出的异常是非常有帮助的,尤其是在为其他应用程序提供接口时。
结论
在本文中,您了解了Java中的异常处理。您学到了关于 throw
和 throws
。您还了解了 try
(以及 try-with-resources
)、catch
和 finally
块。
Source:
https://www.digitalocean.com/community/tutorials/exception-handling-in-java