Java中的异常处理

介绍

异常是程序执行过程中可能发生的错误事件,会打断其正常流程。Java 提供了一种健壮且面向对象的异常处理方式,称为 Java 异常处理。

在 Java 中,异常可能源自不同的情况,例如用户输入错误的数据、硬件故障、网络连接失败或数据库服务器宕机。指定在特定异常情况下该做什么的代码称为异常处理。

抛出和捕获异常

当执行语句时发生错误时,Java 会创建一个异常对象。异常对象包含大量的调试信息,如方法层次结构、异常发生的行号和异常类型。

如果在方法中发生异常,则创建异常对象并将其交给运行时环境的过程称为“抛出异常”。程序的正常流程停止,Java运行时环境(JRE)会尝试找到异常的处理程序。异常处理程序是可以处理异常对象的代码块。

  • 查找异常处理程序的逻辑始于发生错误的方法中。
  • 如果没有找到适当的处理程序,则会转移到调用者方法。
  • 依此类推。

所以如果方法的调用栈是A->B->C,并且在方法C中引发了异常,则寻找适当的处理程序将从C->B->A移动。

如果找到适当的异常处理程序,则将异常对象传递给处理程序进行处理。处理程序被称为“捕获异常”。如果没有找到适当的异常处理程序,则程序将终止,并在控制台上打印有关异常的信息。

Java异常处理框架仅用于处理运行时错误。编译时错误必须由编写代码的开发人员修复,否则程序将无法执行。

Java异常处理关键字

Java提供了专门用于异常处理的关键字。

  1. throw – 我们知道,如果发生错误,就会创建一个异常对象,然后Java运行时开始处理它们。有时候,我们可能希望在我们的代码中显式生成异常。例如,在用户认证程序中,如果密码为null,我们应该向客户端抛出异常。关键字throw用于将异常抛给运行时进行处理。
  2. throws – 当我们在方法中抛出异常而不处理它时,我们必须在方法签名中使用throws关键字,以通知调用者可能由该方法抛出的异常。调用方法可能会处理这些异常,或者使用throws关键字将它们传播到其调用方法。我们可以在throws子句中提供多个异常,并且它还可以与main()方法一起使用。
  3. try-catch – 我们在代码中使用try-catch块进行异常处理。try是块的开始,catchtry块的末尾用于处理异常。我们可以在try块中有多个catch块。try-catch块也可以嵌套。catch块需要一个参数,应该是Exception类型。
  4. 最后 – finally 块是可选的,只能与 try-catch 块一起使用。由于异常会中止执行过程,我们可能会有一些资源未关闭,因此可以使用 finally 块。无论是否发生异常,finally 块始终会被执行。

异常处理示例

ExceptionHandling.java
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() 方法中,我使用 main() 方法中的 try-catch 块处理异常。当我不处理它时,我使用 main() 方法中的 throws 子句将其传播到运行时。
  • 由于异常,testException(-10) 永远不会被执行,然后执行 finally 块。

printStackTrace()Exception 类中用于调试目的的一种有用方法。

此代码将输出以下内容:

Output
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)

需要注意的一些重要点:

  • 我们不能在没有 try 语句的情况下使用 catchfinally 子句。
  • A try statement should have either catch block or finally 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`。

  1. 错误: `Error`是应用程序范围之外的异常情况,无法预料并且无法从中恢复。例如,硬件故障、Java虚拟机(JVM)崩溃或内存不足错误。这就是为什么我们有一个单独的`Error`层次结构,我们不应该尝试处理这些情况。一些常见的`Error`包括`OutOfMemoryError`和`StackOverflowError`。
  2. 检查异常: 检查异常是在程序中可以预料到的异常情况,我们可以尝试从中恢复。例如,FileNotFoundException。我们应该捕获此异常并为用户提供有用的消息,并适当记录以进行调试。Exception是所有检查异常的父类。如果我们抛出检查异常,必须在同一方法中捕获它,或者使用throws关键字将其传播到调用者。
  3. 运行时异常: 运行时异常是由糟糕的编程引起的。例如,尝试从数组中检索元素。在尝试检索元素之前,我们应该先检查数组的长度,否则可能在运行时抛出ArrayIndexOutOfBoundsException。RuntimeException是所有运行时异常的父类。如果在方法中抛出任何运行时异常,不需要在方法签名的throws子句中指定它们。通过更好的编程可以避免运行时异常。

异常类的一些有用方法

Java Exception 及其所有子类都不提供任何特定方法,所有方法都在基类Throwable中定义。Exception类被创建用于指定不同种类的Exception场景,以便我们可以轻松地识别根本原因并根据其类型处理ExceptionThrowable类实现了Serializable接口以实现互操作性。

Throwable类的一些有用方法包括:

  1. public String getMessage() – 此方法返回Throwable的消息String,并且在通过其构造函数创建异常时可以提供消息。
  2. public String getLocalizedMessage() – 提供此方法是为了让子类可以重写它以向调用程序提供特定于区域设置的消息。Throwable类对此方法的实现使用getMessage()方法返回异常消息。
  3. public synchronized Throwable getCause() – 此方法返回异常的原因,如果原因未知,则返回null
  4. public String toString() – 此方法以String格式返回关于Throwable的信息,返回的String包含Throwable类的名称和本地化消息。
  5. public void printStackTrace() – 此方法将堆栈跟踪信息打印到标准错误流,该方法被重载,我们可以传递PrintStreamPrintWriter作为参数来将堆栈跟踪信息写入文件或流。

Java 7自动资源管理和捕获块改进

如果您在单个try块中捕获了许多异常,您会注意到catch块代码主要由冗余代码组成以记录错误。在Java 7中,其中一个特性是改进了catch块,我们可以在单个catch块中捕获多个异常。以下是具有此功能的catch块的示例:

catch (IOException | SQLException ex) {
    logger.error(ex);
    throw new MyException(ex.getMessage());
}

有一些约束条件,例如异常对象是final的,我们不能在catch块内修改它,请阅读完整分析,详情请见Java 7 Catch Block Improvements

大多数情况下,我们使用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();
}

A Custom Exception Class Example

Java为我们提供了许多异常类供我们使用,但有时我们可能需要创建自己的自定义异常类。例如,为了通知调用者特定类型的异常,并提供适当的消息。我们可以有用于跟踪的自定义字段,例如错误代码。例如,假设我们编写了一个仅处理文本文件的方法,当其他类型的文件作为输入发送时,我们可以为调用者提供适当的错误代码。

首先,创建MyException

MyException.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;
	}
}

然后,创建一个CustomExceptionExample

CustomExceptionExample.java
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 子句中指定它。

这是一个设计决策。使用 Checked Exception 的好处是帮助开发人员理解可以预期的异常,并采取适当的措施来处理它们。

Java 异常处理的最佳实践

  • 使用具体的异常 – 异常层次结构的基类不提供任何有用的信息,这就是为什么 Java 有这么多异常类的原因,比如 IOException,它有进一步的子类如 FileNotFoundExceptionEOFException 等。我们应该总是抛出和捕获特定的异常类,以便调用者可以轻松知道异常的根本原因并处理它们。这样做可以更容易地调试,并帮助客户端应用程序适当地处理异常。
  • 尽早抛出异常或快速失败 – 我们应该尽早地抛出异常。考虑上面的 processFile() 方法,如果我们将 null 参数传递给此方法,将会得到以下异常:
Output
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");

	// ... 进一步处理
}

那么异常堆栈跟踪将指示异常发生的位置,并带有清晰的消息:

Output
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)
  • 晚捕获 – 由于Java强制要求处理已检查的异常或在方法签名中声明它,有时开发人员倾向于捕获异常并记录错误。但这种做法是有害的,因为调用程序不会收到任何关于异常的通知。我们只有在可以适当处理异常时才应捕获异常。例如,在上述方法中,我正在将异常抛回给调用方法来处理。其他可能希望以不同方式处理异常的应用程序可以使用相同的方法。在实现任何功能时,我们应始终将异常抛回给调用方,并让它们决定如何处理异常。
  • 关闭资源 – 由于异常会中止程序的处理,我们应在finally块中关闭所有资源,或者使用Java 7的try-with-resources增强功能,让Java运行时为您关闭资源。
  • 记录异常 – 我们应始终记录异常消息,并在抛出异常时提供清晰的消息,以便调用者能够轻松地了解异常的原因。我们应始终避免使用空的catch块,该块只消耗异常而不提供任何异常的有意义细节以进行调试。
  • 为多个异常使用单个catch块 – 大多数情况下,我们记录异常详情并向用户提供消息,在这种情况下,我们应使用Java 7的功能来处理单个catch块中的多个异常。这种方法将减少我们的代码量,并且看起来更清晰。
  • 使用自定义异常 – 在设计时定义异常处理策略总是更好的选择,而不是抛出和捕获多个异常,我们可以创建一个带有错误代码的自定义异常,并且调用程序可以处理这些错误代码。创建一个处理不同错误代码并使用它们的实用方法也是一个好主意。
  • 命名约定和打包 – 当您创建自定义异常时,请确保它以Exception结尾,以便从名称本身就清楚它是一个异常类。还要确保像Java开发工具包(JDK)中所做的那样对它们进行打包。例如,IOException是所有IO操作的基本异常。
  • 明智使用异常 – 异常是昂贵的,有时根本不需要抛出异常,我们可以向调用程序返回一个布尔变量,指示操作是否成功。这对于操作是可选的,并且您不希望程序因失败而被卡住很有帮助。例如,在从第三方网络服务更新数据库中的股票报价时,如果连接失败,我们可能希望避免抛出异常。
  • 记录抛出的异常 – 使用Javadoc @throws 清楚地指定方法抛出的异常是非常有帮助的,尤其是在为其他应用程序提供接口时。

结论

在本文中,您了解了Java中的异常处理。您学到了关于 throwthrows。您还了解了 try(以及 try-with-resources)、catchfinally 块。

Source:
https://www.digitalocean.com/community/tutorials/exception-handling-in-java