Java 中的异常处理
介绍
异常是程序执行过程中可能发生的错误事件,会破坏程序的正常流程。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
用于处理异常。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");
}
}
}
- 该
testException()
方法使用关键字抛出异常throw
。方法签名使用关键字throws
让调用者知道它可能抛出的异常类型。 - 在方法中,我使用方法中的块
main()
来处理异常。当我不处理它时,我会使用方法中的子句将其传播到运行时。try-catch
main()
throws
main()
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)
需要注意的一些要点:
- 如果没有声明,就不能有
catch
或finally
子句try
。 - 一个
try
语句应该有catch
一个块或finally
一个块,也可以同时有这两个块。 - 我们不能在
try-catch-finally
块之间写任何代码。 - 我们可以用
catch
一个try
语句来包含多个块。 try-catch
块可以像语句一样嵌套if-else
。- 我们只能有一个
finally
带有语句的块try-catch
。
Java 异常层次结构
如前所述,当引发异常时,就会创建一个异常对象。Java 异常是分层的,使用继承对不同类型的异常进行分类。Throwable
是 Java 异常层次结构的父类,它有两个子对象 -Error
和Exception
。Exception
进一步分为 CheckedException
和 Runtime Exception
。
- 错误:
Error
是超出应用程序范围的异常情况,无法预测和恢复。例如,硬件故障、Java 虚拟机 (JVM) 崩溃或内存不足错误。这就是为什么我们有单独的Error
s 层次结构,我们不应该尝试处理这些情况。一些常见的Error
s 是OutOfMemoryError
和StackOverflowError
。 - Checked 异常:Checked
Exception
是我们可以预见程序中并尝试从中恢复的异常情况。例如,FileNotFoundException
。我们应该捕获此异常并向用户提供有用的消息,并正确记录它以用于调试目的。是Exception
所有 Checked 的父类Exception
。如果我们抛出 CheckedException
,我们必须catch
在同一个方法中抛出它,或者我们必须使用关键字将其传播给调用者throws
。 - 运行时异常:运行时
Exception
异常是由糟糕的编程引起的。例如,尝试从数组中检索元素。在尝试检索元素之前,我们应该先检查数组的长度,否则它可能会ArrayIndexOutOfBoundException
在运行时抛出。RuntimeException
是所有运行时的父类Exception
。如果我们在方法中throw
使用任何运行时Exception
,则不需要在方法签名throws
子句中指定它们。通过更好的编程可以避免运行时异常。
异常类的一些有用方法
JavaException
及其所有子类均不提供任何特定方法,所有方法均在基类中定义 - Throwable
。Exception
创建这些类是为了指定不同类型的Exception
场景,以便我们可以轻松识别根本原因并Exception
根据其类型进行处理。Throwable
该类实现了Serializable
接口以实现互操作性。
该类的一些有用方法Throwable
包括:
- public String getMessage()
String
–此方法返回的消息Throwable
,并且可以在通过其构造函数创建异常时提供该消息。 - public String getLocalizedMessage() – 提供此方法是为了使子类可以重写它,以便向调用程序提供特定于语言环境的消息。
Throwable
此方法的类实现使用该getMessage()
方法返回异常消息。 - public synchronized Throwable getCause() – 此方法返回异常的原因,或者
null
返回原因未知的情况。 - public String toString()
Throwable
– 此方法返回有关格式的信息String
,返回String
包含类的名称Throwable
和本地化消息。 - public void printStackTrace() – 此方法将堆栈跟踪信息打印到标准错误流,此方法是重载的,我们可以传递
PrintStream
或PrintWriter
作为参数将堆栈跟踪信息写入文件或流。
Java 7 自动资源管理和 Catch 块改进
如果您catch
在单个块中捕获大量异常try
,您会注意到catch
块代码主要由用于记录错误的冗余代码组成。在 Java 7 中,其中一个功能是改进的catch
块,我们可以在单个块中捕获多个异常。以下是具有此功能的块catch
的示例:catch
catch (IOException | SQLException ex) {
logger.error(ex);
throw new MyException(ex.getMessage());
}
存在一些限制,例如异常对象是最终的,我们不能在块内修改它,请阅读Java 7 Catch Block 改进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 提供了很多异常类供我们使用,但有时我们可能需要创建自己的自定义异常类。例如,使用适当的消息通知调用者有关特定类型的异常。我们可以有自定义字段用于跟踪,例如错误代码。例如,假设我们编写了一个仅处理文本文件的方法,因此当发送其他类型的文件作为输入时,我们可以向调用者提供适当的错误代码。
首先,创建MyException
:
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;
}
}
然后,创建一个CustomExceptionExample
:
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
子句中指定它。
这是一个设计决定。使用 CheckedException
的优点是可以帮助开发人员了解可以预期哪些异常,并采取适当的措施来处理它们。
Java 中异常处理的最佳实践
- 使用特定异常– 异常层次结构的基类不提供任何有用信息,这就是 Java 有如此多异常类的原因,例如,还有
IOException
进一步的子类,如FileNotFoundException
,EOFException
等等。我们应该始终throw
使用catch
特定的异常类,以便调用者可以轻松了解异常的根本原因并进行处理。这使得调试更容易,并帮助客户端应用程序适当地处理异常。 - 尽早抛出或快速失败
throw
– 我们应该尽早尝试异常。考虑上述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)
在调试时,我们必须仔细查看堆栈跟踪以确定异常的实际位置。如果我们更改实现逻辑以尽早检查这些异常,如下所示:
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
}
然后异常堆栈跟踪将以清晰的消息指示发生异常的位置:
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 强制要么处理已检查的异常,要么在方法签名中声明它,因此有时开发人员倾向于处理
catch
异常并记录错误。但这种做法是有害的,因为调用程序不会收到任何异常通知。我们catch
只在能够适当处理异常时才处理异常。例如,在上面的方法中,我将throw
异常返回给调用者方法来处理它。其他可能希望以不同方式处理异常的应用程序可以使用相同的方法。在实现任何功能时,我们应始终将throw
异常返回给调用者并让他们决定如何处理它。 - 关闭资源——由于异常会停止程序的处理,我们应该在 finally 块中关闭所有资源,或者使用 Java 7
try-with-resources
增强功能让 Java 运行时为您关闭它。 - 记录异常– 我们应该始终记录异常消息,并在
throw
处理异常时提供清晰的消息,以便调用者轻松了解异常发生的原因。我们应该始终避免使用空块,catch
因为空块只会消耗异常,而不会提供任何有意义的异常详细信息以供调试。 - 单个 catch 块处理多个异常– 大多数情况下,我们会记录异常详细信息并向用户提供消息,在这种情况下,我们应该使用 Java 7 功能在单个
catch
块中处理多个异常。这种方法将减少我们的代码大小,并且看起来也更简洁。 - 使用自定义异常– 在设计时定义异常处理策略总是更好的选择,我们可以创建一个带有错误代码的自定义异常,而不是
throw
反复处理catch
多个异常,然后调用程序可以处理这些错误代码。创建一个实用方法来处理不同的错误代码并使用它们也是一个好主意。 - 命名约定和打包– 创建自定义异常时,请确保其以 结尾,
Exception
这样从名称本身就可以清楚地看出它是一个异常类。此外,请确保像在 Java 开发工具包 (JDK) 中那样打包它们。例如,IOException
是所有 IO 操作的基本异常。 - 明智地使用异常– 异常代价高昂,有时根本不需要抛出异常,我们可以向调用程序返回一个布尔变量来指示操作是否成功。当操作是可选的,并且您不希望程序因失败而卡住时,这很有用。例如,在从第三方 Web 服务更新数据库中的股票报价时,我们可能希望避免在连接失败时抛出异常。
- 记录抛出的异常– 使用 Javadoc
@throws
明确指定方法抛出的异常。当您为其他应用程序提供接口时,这非常有用。
结论
在本文中,您了解了 Java 中的异常处理。您了解了throw
和throws
。您还了解了try
(和try-with-resources
)、catch
和finally
块。