Java 中的异常处理是一个非常有趣的话题。异常是一种错误事件,它可能在程序执行期间发生并中断其正常流程。Java 提供了一种健壮且面向对象的方法来处理称为 Java 异常处理的异常场景。
为什么我们需要异常处理?
我们不喜欢例外,但我们总是要处理它们。好消息是 Java 中的异常处理非常健壮,易于理解和使用。java中的异常可以由用户输入错误数据、硬件故障、网络连接故障、数据库服务器宕机等不同的情况引起。指定在特定异常场景中做什么的代码称为异常处理。
Java如何处理异常?
Java 是一种面向对象的编程语言。因此,只要在执行语句时发生错误,它就会创建一个 异常对象。程序的正常流程停止, JRE尝试找到可以处理引发的异常的人。
异常对象包含了很多调试信息,如方法层次结构、异常发生的行号、异常类型等。 当方法中发生异常时,创建异常对象并将其交给运行时环境的过程称为 “抛出异常”。
一旦运行时收到异常对象,它就会尝试查找异常的处理程序。异常处理程序是可以处理异常对象的代码块。
查找异常处理程序的逻辑很简单——在发生错误的方法中开始搜索,如果没有找到合适的处理程序,则移动到调用方方法等等。因此,如果方法调用堆栈是A->B->C并且在方法 C 中引发异常,那么对 适当处理程序的搜索将从C->B->A 移动。
如果找到合适的异常处理程序,异常对象将传递给处理程序进行处理。处理程序被称为“捕获异常”。如果没有找到合适的异常处理程序,则程序终止并将有关异常的信息打印到控制台。
Java 异常处理框架仅用于处理运行时错误。编译时错误必须由编写代码的开发人员修复,否则程序将无法执行。
Java 异常处理关键字
Java 为异常处理目的提供了特定的关键字。
- throw—— 我们知道如果发生错误,就会创建一个异常对象,然后 Java 运行时开始处理它们。有时我们可能希望在我们的代码中显式地生成异常,例如在用户身份验证程序中,如果密码为空,我们应该向客户端抛出异常。throw 关键字用于向运行时抛出异常以进行处理。
- throws—— 当我们在一个方法中抛出一个异常并且不处理它时,我们必须在方法签名中使用 throws 关键字来让调用程序知道该方法可能抛出的异常。调用方方法可能会处理这些异常或使用 throws 关键字将它们传播到其调用方方法。我们可以在 throws 子句中提供多个异常,它也可以与 main() 方法一起使用。
- try-catch – 我们在代码中使用 try-catch 块进行异常处理。try 是块的开始,而 catch 是在 try 块的末尾以处理异常。我们可以有多个 catch 块和一个 try 块。try-catch 块也可以嵌套。catch 块需要一个类型为 Exception 的参数。
- finally – finally 块是可选的,只能与 try-catch 块一起使用。由于异常会停止执行过程,我们可能会打开一些不会关闭的资源,因此我们可以使用 finally 块。finally 块总是被执行,无论是否发生异常。
异常处理示例程序
package com.journaldev.exceptions;
import java.io.FileNotFoundException;
import java.io.IOException;
public class ExceptionHandling {
public static void main(String[] args) throws FileNotFoundException, IOException {
try{
testException(-5);
testException(-10);
}catch(FileNotFoundException e){
e.printStackTrace();
}catch(IOException e){
e.printStackTrace();
}finally{
System.out.println("Releasing resources");
}
testException(15);
}
public static void testException(int i) throws FileNotFoundException, IOException{
if(i < 0){
FileNotFoundException myException = new FileNotFoundException("Negative Integer "+i);
throw myException;
}else if(i > 10){
throw new IOException("Only supported for index 0 to 10");
}
}
}
输出:
java.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)
该testException()方法是投掷用throw关键字的异常。方法签名使用 throws 关键字让调用者知道它可能抛出的异常类型。
在 main() 方法中,我使用 main() 方法中的 try-catch 块处理异常。当我不处理它时,我使用 main 方法中的 throws 子句将它传播到运行时。
由于异常,testException(-10) 永远不会被执行,然后执行 finally 块。printStackTrace() 是 Exception 类中用于调试的有用方法之一。
需要注意的一些要点:
- 如果没有 try 语句,我们不能有 catch 或 finally 子句。
- try 语句应该有 catch 块或 finally 块,它可以有两个块。
- 我们不能在 try-catch-finally 块之间编写任何代码。
- 我们可以用一个 try 语句有多个 catch 块。
- try-catch 块可以嵌套,类似于 if-else 语句。
- 我们只能有一个带有 try-catch 语句的 finally 块。
Java 异常层次结构
如前所述,当引发异常时,就会创建一个异常对象。Java 异常是分层的,继承用于对不同类型的异常进行分类。Throwable是 Java Exceptions Hierarchy 的父类,它有两个子对象—— Error和Exception。异常进一步分为检查异常和运行时异常。
- 错误:错误是超出应用范围的异常情况,无法预测并从中恢复。例如,硬件故障、JVM 崩溃或内存不足错误。这就是为什么我们有一个单独的错误层次结构,我们不应该尝试处理这些情况。一些常见的错误是 OutOfMemoryError 和 StackOverflowError。
- Checked Exceptions:Checked Exceptions 是我们可以在程序中预测并尝试从中恢复的异常情况。例如,FileNotFoundException。我们应该捕获此异常并向用户提供有用的消息并正确记录它以进行调试。Exception 是所有 Checked Exceptions 的父类。如果我们抛出一个已检查的异常,我们必须在相同的方法中捕获它,或者我们必须使用 throws 关键字将它传播给调用者。
- 运行时异常:运行时异常是由错误的编程引起的。例如,尝试从 Array 中检索元素。在尝试检索元素之前,我们应该首先检查数组的长度,否则它可能会
ArrayIndexOutOfBoundException
在运行时抛出 。RuntimeException 是所有运行时异常的父类。如果我们在方法中抛出任何运行时异常,则不需要在方法签名 throws 子句中指定它们。通过更好的编程可以避免运行时异常。
异常类的一些有用方法
Java Exception 及其所有子类不提供任何特定方法,所有方法都在基类 Throwable 中定义。创建异常类是为了指定不同类型的异常场景,以便我们可以轻松识别根本原因并根据其类型处理异常。Throwable 类实现了 Serializable 接口以实现互操作性。
Throwable 类的一些有用方法是;
- public String getMessage() – 此方法返回 Throwable 的消息字符串,该消息可以在通过其构造函数创建异常时提供。
- public String getLocalizedMessage() – 提供此方法以便子类可以覆盖它以向调用程序提供特定于区域设置的消息。该方法的 Throwable 类实现使用getMessage()方法返回异常消息。
- public synchronized Throwable getCause() – 如果原因未知,此方法返回异常的原因或 null。
- public String toString() – 该方法以String格式返回Throwable的信息,返回的String包含Throwable类的名称和本地化的消息。
- public void printStackTrace() – 此方法将堆栈跟踪信息打印到标准错误流,此方法已重载,我们可以将 PrintStream 或 PrintWriter 作为参数传递给文件或流。
Java 7 自动资源管理和 Catch 块改进
如果您在单个 try 块中捕获大量异常,您会注意到 catch 块代码看起来非常难看,并且主要由记录错误的冗余代码组成。Java 7 的一项功能是改进了 catch 块,我们可以在单个 catch 块中捕获多个异常。具有此功能的 catch 块如下所示:
catch(IOException | SQLException ex){
logger.error(ex);
throw new MyException(ex.getMessage());
}
有一些限制,例如异常对象是最终的,我们不能在 catch 块内修改它.
大多数时候,我们使用 finally 块只是为了关闭资源。有时我们忘记关闭它们并在资源耗尽时获得运行时异常。这些异常很难调试,我们可能需要查看我们使用该资源的每个地方,以确保我们正在关闭它。Java 7 的改进之一是try-with-resources,我们可以在 try 语句本身中创建资源并在 try-catch 块中使用它。当执行从 try-catch 块中出来时,运行时环境会自动关闭这些资源。具有此改进的 try-catch 块的示例是:
try (MyResource mr = new MyResource()) {
System.out.println("MyResource created in try-with-resources");
} catch (Exception e) {
e.printStackTrace();
}
如何在 Java 中创建自定义异常类?
Java 提供了很多异常类供我们使用,但有时我们可能需要创建自己的自定义异常类。例如,通过适当的消息通知调用者特定类型的异常。我们可以使用自定义字段进行跟踪,例如错误代码。例如,假设我们编写了一个仅处理文本文件的方法,因此当其他类型的文件作为输入发送时,我们可以为调用者提供适当的错误代码。
这是自定义异常类的示例并显示其用法。
package com.journaldev.exceptions;
public class MyException extends Exception {
private static final long serialVersionUID = 4664456874499611218L;
private String errorCode="Unknown_Exception";
public MyException(String message, String errorCode){
super(message);
this.errorCode=errorCode;
}
public String getErrorCode(){
return this.errorCode;
}
}
package com.journaldev.exceptions;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
public class CustomExceptionExample {
public static void main(String[] args) throws MyException {
try {
processFile("file.txt");
} catch (MyException e) {
processErrorCodes(e);
}
}
private static void processErrorCodes(MyException e) throws MyException {
switch(e.getErrorCode()){
case "BAD_FILE_TYPE":
System.out.println("Bad File Type, notify user");
throw e;
case "FILE_NOT_FOUND_EXCEPTION":
System.out.println("File Not Found, notify user");
throw e;
case "FILE_CLOSE_EXCEPTION":
System.out.println("File Close failed, just log it.");
break;
default:
System.out.println("Unknown exception occured, lets log it for further debugging."+e.getMessage());
e.printStackTrace();
}
}
private static void processFile(String file) throws MyException {
InputStream fis = null;
try {
fis = new FileInputStream(file);
} catch (FileNotFoundException e) {
throw new MyException(e.getMessage(),"FILE_NOT_FOUND_EXCEPTION");
}finally{
try {
if(fis !=null)fis.close();
} catch (IOException e) {
throw new MyException(e.getMessage(),"FILE_CLOSE_EXCEPTION");
}
}
}
}
我们可以有一个单独的方法来处理我们从不同方法得到的不同类型的错误代码。其中一些被消耗掉是因为我们可能不想通知用户这一点,或者其中一些我们会退回来通知用户这个问题。
在这里,我扩展了 Exception 以便无论何时产生这个异常,它都必须在方法中处理或返回给调用程序。如果我们扩展 RuntimeException,则无需在 throws 子句中指定它。
这是一个设计决定,但我总是喜欢检查异常,因为我知道在调用任何方法时可以得到什么异常并采取适当的措施来处理它们。
Java 异常处理的最佳实践
- 使用特定异常 - 异常层次结构的基类不提供任何有用的信息,这就是为什么 Java 有这么多异常类的原因,例如 IOException 以及进一步的子类,如 FileNotFoundException、EOFException 等。我们应该始终抛出和捕获特定的异常类这样调用者就可以很容易地知道异常的根本原因并处理它们。这使得调试变得容易并帮助客户端应用程序适当地处理异常。
- 尽早抛出或快速失败 ——我们应该尽可能早地抛出异常。考虑上面的 processFile() 方法,如果我们将 null 参数传递给这个方法,我们将得到以下异常。
Exception 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)
在调试时,我们必须仔细查看堆栈跟踪以确定异常的实际位置。如果我们改变我们的实现逻辑来尽早检查这些异常,如下所示:
private static void processFile(String file) throws MyException {
if(file == null) throw new MyException("File name can't be null", "NULL_FILE_NAME");
//further processing
}
然后异常堆栈跟踪将如下所示,清楚地显示异常发生的位置并带有明确的消息。
com.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)
- Catch Late – 由于 java 强制处理已检查的异常或在方法签名中声明它,因此有时开发人员倾向于捕获异常并记录错误。但是这种做法是有害的,因为调用程序不会收到任何异常通知。只有当我们可以适当地处理它们时,我们才应该捕获异常。例如,在上面的方法中,我将异常抛出回调用方方法来处理它。可能希望以不同方式处理异常的其他应用程序可以使用相同的方法。在实现任何功能时,我们应该始终将异常抛回给调用者,并让他们决定如何处理它。
- 关闭资源 ——由于异常会停止程序的处理,我们应该关闭 finally 块中的所有资源或使用 Java 7 try-with-resources 增强让 java 运行时为您关闭它。
- 记录异常 ——我们应该总是记录异常消息,并在抛出异常时提供一个明确的消息,以便调用者很容易知道异常发生的原因。我们应该始终避免一个空的 catch 块,它只消耗异常而不为调试提供任何有意义的异常细节。
- 多个异常的单个 catch 块 ——大多数时候我们记录异常详细信息并向用户提供一条消息,在这种情况下,我们应该使用 Java 7 功能在单个 catch 块中处理多个异常。这种方法将减少我们的代码大小,它也会看起来更干净。
- 使用自定义异常 ——在设计时定义异常处理策略总是更好,而不是抛出和捕获多个异常,我们可以创建一个带有错误代码的自定义异常,调用程序可以处理这些错误代码。创建一个实用方法来处理不同的错误代码并使用它们也是一个好主意。
- 命名约定和包装 – 创建自定义异常时,请确保它以 Exception 结尾,以便从名称本身可以清楚地看出它是一个异常类。还要确保像在 JDK 中那样打包它们,例如,IOException 是所有 IO 操作的基本异常。
- 明智地使用异常 ——异常代价高昂,有时根本不需要抛出异常,我们可以向调用程序返回一个布尔变量来指示操作是否成功。这在操作是可选的并且您不希望程序因失败而卡住的情况下很有帮助。例如,在从第三方 Web 服务更新数据库中的股票报价时,我们可能希望避免在连接失败时抛出异常。
- 记录抛出的异常 - 使用 Javadoc
@throws
明确指定方法抛出的异常,这在您提供接口以供其他应用程序使用时非常有用。