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 是該塊的開始,而 catch 是在 try 块的末尾以處理異常。我們可以在一個 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() 方法中,我使用 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異常層次結構的父類,它有兩個子對象 – ErrorExceptionException進一步分為已檢查的Exception和運行時Exception

  1. 錯誤Error是超出應用程序範圍的異常情況,不可能預測並從中恢復。例如,硬件故障,Java虛擬機(JVM)崩潰或內存不足錯誤。這就是為什麼我們有一個單獨的Error層次結構,我們不應該嘗試處理這些情況。一些常見的Error包括OutOfMemoryErrorStackOverflowError
  2. 檢查的異常: 檢查的Exception是我們可以預料到的程序中的異常情況,並試圖從中恢復。例如,FileNotFoundException。我們應該捕獲此異常並向用戶提供有用的消息,並對其進行適當的記錄以進行調試。Exception是所有檢查的Exception的父類。如果我們正在拋出一個檢查的Exception,我們必須在同一方法中catch它,或者我們必須使用throws關鍵字將其傳播給調用者。
  3. 運行時異常: 運行時Exception是由編程不良引起的。例如,試圖從數組中檢索元素。在嘗試檢索元素之前,我們應該先檢查數組的長度,否則它可能會在運行時拋出ArrayIndexOutOfBoundExceptionRuntimeException是所有運行時Exception的父類。如果我們在方法中throw任何運行時Exception,則不需要在方法簽名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自動資源管理和catch塊改進

如果您在單個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等。我們應該總是throwcatch特定的異常類,這樣呼叫者就能輕鬆知道異常的根本原因並處理它們。這使得調試更容易,並幫助客戶端應用程序適當地處理異常。
  • 早點拋出或快速失敗 – 我們應該盡可能早地throw異常。考慮上面的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)
  • Catch Late – 由于Java要求要么处理已检查的异常,要么在方法签名中声明它,有时开发人员倾向于catch异常并记录错误。但这种做法是有害的,因为调用程序不会收到任何有关异常的通知。我们只应在能够适当处理异常时才catch异常。例如,在上述方法中,我正在将异常抛回给调用方的方法来处理。其他可能希望以不同方式处理异常的应用程序也可以使用相同的方法。在实现任何功能时,我们应始终将异常抛回给调用方,并让它们决定如何处理异常。
  • 关闭资源 – 由于异常会中断程序的处理过程,我们应在finally块中关闭所有资源,或者使用Java 7的try-with-resources增强功能,让Java运行时为您关闭它。
  • 记录异常 – 我们应始终记录异常消息,并在throw异常时提供清晰的消息,以便调用者能够轻松了解异常原因。我们应始终避免空的catch块,它仅捕获异常而不提供任何有关异常的有意义的详细信息以进行调试。
  • 单一catch块处理多个异常 – 大多数情况下,我们记录异常详细信息并向用户提供消息,在这种情况下,我们应使用Java 7的功能,在单个catch块中处理多个异常。这种方法将减小代码体积,看起来也更清晰。
  • 使用自定义异常 – 在设计时定义异常处理策略总是更好的选择,而不是throwcatch多个异常,我们可以创建一个带有错误代码的自定义异常,调用程序可以处理这些错误代码。同时,创建一个处理不同错误代码并使用它们的实用方法也是个好主意。
  • 命名约定和打包 – 创建自定义异常时,请确保以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