我想您可能遇到過這樣的情況:您運行代碼並以類似NullPointerExceptionClassCastException或更糟的方式結束......接下來是調試、分析、谷歌搜索等的漫長過程。異常是非常棒的:它們表明了問題的性質和發生的地方。如果您想重溫記憶並了解更多信息,請查看這篇文章:異常:已檢查、未檢查和自定義

也就是說,在某些情況下您可能需要創建自己的異常。例如,假設您的代碼需要從由於某種原因不可用的遠程服務請求信息。或者假設有人填寫銀行卡申請表並提供了一個電話號碼,無論是否偶然,該號碼已經與系統中的另一個用戶相關聯。

當然,這裡的正確行為仍然取決於客戶的要求和系統的體系結構,但我們假設您的任務是檢查電話號碼是否已在使用中,如果是則拋出異常。

讓我們創建一個例外:


public class PhoneNumberAlreadyExistsException extends Exception {

   public PhoneNumberAlreadyExistsException(String message) {
       super(message);
   }
}
    

接下來我們在執行檢查時使用它:


public class PhoneNumberRegisterService {
   List<String> registeredPhoneNumbers = Arrays.asList("+1-111-111-11-11", "+1-111-111-11-12", "+1-111-111-11-13", "+1-111-111-11-14");

   public void validatePhone(String phoneNumber) throws PhoneNumberAlreadyExistsException {
       if (registeredPhoneNumbers.contains(phoneNumber)) {
           throw new PhoneNumberAlreadyExistsException("The specified phone number is already in use by another customer!");
       }
   }
}
    

為了簡化我們的例子,我們將使用幾個硬編碼的電話號碼來表示一個數據庫。最後,讓我們嘗試使用我們的異常:


public class CreditCardIssue {
   public static void main(String[] args) {
       PhoneNumberRegisterService service = new PhoneNumberRegisterService();
       try {
           service.validatePhone("+1-111-111-11-14");
       } catch (PhoneNumberAlreadyExistsException e) {
           // Here we can write to logs or display the call stack
		e.printStackTrace();
       }
   }
}
    

現在是時候按Shift+F10(如果您使用的是 IDEA),即運行項目。這是您將在控制台中看到的內容:

exception.CreditCardIssue
exception.PhoneNumberAlreadyExistsException:指定的電話號碼已被其他客戶使用!
在 exception.PhoneNumberRegisterService.validatePhone(PhoneNumberRegisterService.java:11)

看著你!您創建了自己的異常,甚至對其進行了一些測試。祝賀您取得這一成就!我建議稍微試驗一下代碼以更好地理解它是如何工作的。

添加另一個檢查 - 例如,檢查電話號碼是否包含字母。您可能知道,在美國經常使用字母來使電話號碼更容易記住,例如 1-800-MY-APPLE。您的支票可以確保電話號碼只包含數字。

好的,所以我們已經創建了一個已檢查的異常。一切都會好起來的,但是……

編程社區分為兩個陣營——支持檢查異常的陣營和反對檢查異常的陣營。雙方都提出了有力的論據。兩者都包括一流的開發人員:Bruce Eckel 批評檢查異常,而 James Gosling 則為它們辯護。看來這件事永遠也解決不了了。也就是說,讓我們看看使用檢查異常的主要缺點。

檢查異常的主要缺點是它們必須被處理。這裡我們有兩個選擇:要么使用try-catch就地處理它,要么,如果我們在許多地方使用相同的異常,則使用throws拋出異常,並在頂級類中處理它們。

此外,我們最終可能會得到“樣板”代碼,即佔用大量空間但不會做太多繁重工作的代碼。

在處理大量異常的相當大的應用程序中會出現問題:頂級方法上的拋出列表很容易增長到包含十幾個異常。

public OurCoolClass() 拋出 FirstException、SecondException、ThirdException、ApplicationNameException...

開發人員通常不喜歡這樣,而是選擇了一個技巧:他們讓所有已檢查的異常繼承一個共同的祖先—— ApplicationNameException。現在他們還必須在處理程序中捕獲該(已檢查!)異常:


catch (FirstException e) {
    // TODO
}
catch (SecondException e) {
    // TODO
}
catch (ThirdException e) {
    // TODO
}
catch (ApplicationNameException e) {
    // TODO
}
    

這裡我們面臨另一個問題——我們應該在最後一個catch塊中做什麼?上面,我們已經處理了所有預期的情況,所以此時ApplicationNameException對我們來說無非是“異常:發生了一些無法理解的錯誤”。我們是這樣處理的:


catch (ApplicationNameException e) {
    LOGGER.error("Unknown error", e.getMessage());
}
    

最後,我們不知道發生了什麼。

但是我們不能像這樣一次拋出所有異常嗎?


public void ourCoolMethod() throws Exception {
// Do some work
}
    

是的,我們可以。但是“throws Exception”告訴我們什麼?那東西壞了。您必須從上到下調查所有內容,並長時間熟悉調試器才能理解原因。

您可能還會遇到一種有時稱為“異常吞噬”的構造:


try {
// Some code
} catch(Exception e) {
   throw new ApplicationNameException("Error");
}
    

這裡沒有太多要添加的解釋 - 代碼使一切變得清晰,或者更確切地說,它使一切變得不清楚。

當然,您可能會說您不會在實際代碼中看到這一點。好吧,讓我們深入了解java.net包中URL類的內部(代碼)。想知道就關注我吧!

這是URL類中的構造之一:


public URL(String spec) throws MalformedURLException {
   this(null, spec);
}
    

如您所見,我們有一個有趣的已檢查異常 — MalformedURLException。這是它可能被拋出的時間(我引用):
“如果沒有指定協議,或者找到未知協議,或者 spec 為空,或者解析的 URL 不符合相關協議的特定語法。”

那是:

  1. 如果沒有指定協議。
  2. 發現未知協議。
  3. 規範為null
  4. URL 不符合相關協議的特定語法。

讓我們創建一個創建URL對象的方法:


public URL createURL() {
   URL url = new URL("https://codegym.cc");
   return url;
}
    

只要您在 IDE 中編寫這些行(我在 IDEA 中編碼,但即使在 Eclipse 和 NetBeans 中也可以),您將看到:

這意味著我們需要拋出異常,或者將代碼包裝在try-catch塊中。現在,我建議選擇第二個選項來可視化正在發生的事情:


public static URL createURL() {
   URL url = null;
   try {
       url = new URL("https://codegym.cc");
   } catch(MalformedURLException e) {
  e.printStackTrace();
   }
   return url;
}
    

如您所見,代碼已經相當冗長了。我們在上面提到了這一點。這是使用未經檢查的異常的最明顯原因之一。

我們可以通過擴展Java 中的RuntimeException創建一個未經檢查的異常。

未經檢查的異常是從Error類或RuntimeException類繼承的。許多程序員認為這些異常可以在我們的程序中處理,因為它們表示我們無法期望在程序運行時從中恢復的錯誤。

當發生未經檢查的異常時,通常是由於代碼使用不當,傳入的參數為 null 或其他無效參數所致。

好吧,讓我們編寫代碼:


public class OurCoolUncheckedException extends RuntimeException {
   public OurCoolUncheckedException(String message) {
       super(message);
   }

   public OurCoolUncheckedException(Throwable cause) {
       super(cause);
   }
  
   public OurCoolUncheckedException(String message, Throwable throwable) {
       super(message, throwable);
   }
}
    

請注意,我們為不同的目的製作了多個構造函數。這讓我們可以為異常提供更多功能。例如,我們可以讓異常給我們一個錯誤代碼。首先,讓我們創建一個枚舉來表示我們的錯誤代碼:


public enum ErrorCodes {
   FIRST_ERROR(1),
   SECOND_ERROR(2),
   THIRD_ERROR(3);

   private int code;

   ErrorCodes(int code) {
       this.code = code;
   }

   public int getCode() {
       return code;
   }
}
    

現在讓我們向異常類添加另一個構造函數:


public OurCoolUncheckedException(String message, Throwable cause, ErrorCodes errorCode) {
   super(message, cause);
   this.errorCode = errorCode.getCode();
}
    

別忘了添加一個字段(我們差點忘了):


private Integer errorCode;
    

當然,還有一種獲取此代碼的方法:


public Integer getErrorCode() {
   return errorCode;
}
    

讓我們看一下整個班級,以便我們檢查和比較:

public class OurCoolUncheckedException extends RuntimeException {
   private Integer errorCode;

   public OurCoolUncheckedException(String message) {
       super(message);
   }

   public OurCoolUncheckedException(Throwable cause) {
       super(cause);
   }

   public OurCoolUncheckedException(String message, Throwable throwable) {

       super(message, throwable);
   }

   public OurCoolUncheckedException(String message, Throwable cause, ErrorCodes errorCode) {
       super(message, cause);
       this.errorCode = errorCode.getCode();
   }
   public Integer getErrorCode() {
       return errorCode;
   }
}
    

噠噠!我們的異常完成了!如您所見,這裡沒有什麼特別複雜的。讓我們實際檢查一下:


   public static void main(String[] args) {
       getException();
   }
   public static void getException() {
       throw new OurCoolUncheckedException("Our cool exception!");
   }
    

當我們運行我們的小應用程序時,我們會在控制台中看到如下內容:

現在讓我們利用我們添加的額外功能。我們將在之前的代碼中添加一些內容:


public static void main(String[] args) throws Exception {

   OurCoolUncheckedException exception = getException(3);
   System.out.println("getException().getErrorCode() = " + exception.getErrorCode());
   throw exception;

}

public static OurCoolUncheckedException getException(int errorCode) {
   return switch (errorCode) {
   case 1:
       return new OurCoolUncheckedException("Our cool exception! An error occurred: " + ErrorCodes.FIRST_ERROR.getCode(), new Throwable(), ErrorCodes.FIRST_ERROR);
   case 2:
       return new OurCoolUncheckedException("Our cool exception! An error occurred: " + ErrorCodes.SECOND_ERROR.getCode(), new Throwable(), ErrorCodes.SECOND_ERROR);
   default: // Since this is the default action, here we catch the third and any other codes that we have not yet added. You can learn more by reading Java switch statement
       return new OurCoolUncheckedException("Our cool exception! An error occurred: " + ErrorCodes.THIRD_ERROR.getCode(), new Throwable(), ErrorCodes.THIRD_ERROR);
}

}
    

您可以像處理對像一樣處理異常。當然,我相信您已經知道 Java 中的一切都是對象。

看看我們做了什麼。首先,我們更改了方法,它現在不會拋出異常,而是根據輸入參數簡單地創建一個異常。接下來,使用switch-case語句,我們生成具有所需錯誤代碼和消息的異常。在 main 方法中,我們獲取創建的異常,獲取錯誤代碼,然後拋出它。

讓我們運行它,看看我們在控制台上得到了什麼:

看——我們打印了從異常中得到的錯誤代碼,然後拋出異常本身。更重要的是,我們甚至可以準確地跟踪拋出異常的位置。根據需要,您可以將所有相關信息添加到消息中,創建額外的錯誤代碼,並為您的異常添加新功能。

嗯,你怎麼看?我希望一切順利!

一般來說,例外是一個相當廣泛的話題,而且不是很明確。還會有更多的爭論。例如,只有 Java 有檢查異常。在最流行的語言中,我還沒有看到使用它們的語言。

Bruce Eckel 在他的“Thinking in Java”一書的第 12 章中對異常寫得非常好 — 我建議您閱讀它!還可以看看 Horstmann 的“Core Java”的第一卷——它在第 7 章中也有很多有趣的東西。

一個小總結

  1. 將所有內容寫入日誌!在拋出的異常中記錄消息。這通常會對調試有很大幫助,並讓您了解發生了什麼。不要將catch塊留空,否則它只會“吞掉”異常,您將沒有任何信息來幫助您查找問題。

  2. 當涉及到異常時,一次捕獲所有異常是不好的做法(正如我的一位同事所說,“這不是口袋妖怪,它是 Java”),因此請避免 catch (Exception e) 或更糟的是 catch ( Throwable t )

  3. 儘早拋出異常。這是良好的 Java 編程習慣。當您研究像 Spring 這樣的框架時,您會發現它們遵循“快速失敗”原則。也就是說,它們盡可能早地“失敗”,以便能夠快速找到錯誤。當然,這帶來了一定的不便。但這種方法有助於創建更健壯的代碼。

  4. 在調用代碼的其他部分時,最好捕獲某些異常。如果被調用的代碼拋出多個異常,那麼只捕獲這些異常的父類是一種糟糕的編程習慣。例如,假設您調用拋出FileNotFoundExceptionIOException 的代碼。在調用此模塊的代碼中,最好編寫兩個 catch 塊來捕獲每個異常,而不是編寫一個catch 塊來捕獲Exception

  5. 僅當您可以為用戶和調試有效地處理異常時才捕獲異常。

  6. 不要猶豫,編寫您自己的異常。當然,Java 有很多現成的,適用於各種場合,但有時您仍然需要發明自己的“輪子”。但是您應該清楚地了解為什麼要這樣做,並確保標準的異常集不具備您所需要的。

  7. 當您創建自己的異常類時,請注意命名!您可能已經知道正確命名類、變量、方法和包非常重要。例外也不例外!:) 總是以單詞Exception結尾,異常的名稱應該清楚地傳達它所代表的錯誤類型。例如,FileNotFoundException

  8. 記錄您的例外情況。我們建議為異常編寫一個@throws Javadoc 標記。當您的代碼提供任何類型的接口時,這將特別有用。而且您還會發現以後更容易理解您自己的代碼。您怎麼看,如何確定MalformedURLException是關於什麼的?來自 Javadoc!是的,編寫文檔的想法並不是很吸引人,但相信我,當您在六個月後返回自己的代碼時,您會感謝自己的。

  9. 釋放資源並且不要忽略try-with-resources結構。

  10. 以下是總體總結:明智地使用異常。就資源而言,拋出異常是一個相當“昂貴”的操作。在許多情況下,使用簡單且“成本較低”的if-else可能更容易避免拋出異常,而是返回一個布爾變量來表示操作是否成功。

    將應用程序邏輯與異常聯繫起來也很誘人,而您顯然不應該這樣做。正如我們在文章開頭所說的那樣,異常是針對異常情況的,而不是預期的情況,並且有多種工具可以防止它們發生。特別是,有Optional來防止NullPointerException,或Scanner.hasNext等來防止IOExceptionread()方法可能拋出。