天天看點

Java異常處理的20個最佳實踐:提高程式設計效率與代碼品質

作者:架構師成長曆程

今天給大家講20 個異常處理的最佳實踐經驗,以免你以後在開發中采坑。

“好,那就不廢話了。開整。”

Java異常處理的20個最佳實踐:提高程式設計效率與代碼品質

#01、盡量不要捕獲 RuntimeException

阿裡出品的 Java 開發手冊open in new window上這樣規定:

盡量不要 catch RuntimeException,比如 NullPointerException、IndexOutOfBoundsException 等等,應該用預檢查的方式來規避。

正例:

if (obj != null) {
  //...
}
           

反例:

try { 
  obj.method(); 
} catch (NullPointerException e) {
  //...
}
           

“哦,那如果有些異常預檢查不出來呢?”。

“的确會存在這樣的情況,比如說 NumberFormatException,雖然也屬于 RuntimeException,但沒辦法預檢查,是以還是應該用 catch 捕獲處理。”我說。

#02、盡量使用 try-with-resource 來關閉資源

當需要關閉資源時,盡量不要使用 try-catch-finally,禁止在 try 塊中直接關閉資源。

反例:

public void doNotCloseResourceInTry() {
    FileInputStream inputStream = null;
    try {
        File file = new File("./tmp.txt");
        inputStream = new FileInputStream(file);
        inputStream.close();
    } catch (FileNotFoundException e) {
        log.error(e);
    } catch (IOException e) {
        log.error(e);
    }
}
           

“為什麼呢?”。

“原因也很簡單,因為一旦 close() 之前發生了異常,那麼資源就無法關閉。直接使用 try-with-resourceopen in new window 來處理是最佳方式。”我說。

public void automaticallyCloseResource() {
    File file = new File("./tmp.txt");
    try (FileInputStream inputStream = new FileInputStream(file);) {
    } catch (FileNotFoundException e) {
        log.error(e);
    } catch (IOException e) {
        log.error(e);
    }
}
           

“除非資源沒有實作 AutoCloseable 接口。”我補充道。

“那這種情況下怎麼辦呢?”。

“就在 finally 塊關閉流。”。

public void closeResourceInFinally() {
    FileInputStream inputStream = null;
    try {
        File file = new File("./tmp.txt");
        inputStream = new FileInputStream(file);
    } catch (FileNotFoundException e) {
        log.error(e);
    } finally {
        if (inputStream != null) {
            try {
                inputStream.close();
            } catch (IOException e) {
                log.error(e);
            }
        }
    }
}
           

#03、不要捕獲 Throwable

Java異常處理的20個最佳實踐:提高程式設計效率與代碼品質

Throwable 是 exception 和 error 的父類,如果在 catch 子句中捕獲了 Throwable,很可能把超出程式處理能力之外的錯誤也捕獲了。

public void doNotCatchThrowable() {
    try {
    } catch (Throwable t) {
        // 不要這樣做
    }
}
           

“到底為什麼啊?”三妹問。

“因為有些 error 是不需要程式來處理,程式可能也處理不了,比如說 OutOfMemoryError 或者 StackOverflowError,前者是因為 Java 虛拟機無法申請到足夠的記憶體空間時出現的非正常的錯誤,後者是因為線程申請的棧深度超過了允許的最大深度出現的非正常錯誤,如果捕獲了,就掩蓋了程式應該被發現的嚴重錯誤。”我說。

“打個比方,一匹馬隻能拉一車廂的貨物,拉兩車廂可能就挂了,但一 catch,就發現不了問題了。”我補充道。

#04、不要省略異常資訊的記錄

很多時候,由于疏忽大意,我們很容易捕獲了異常卻沒有記錄異常資訊,導緻程式上線後真的出現了問題卻沒有記錄可查。

public void doNotIgnoreExceptions() {
    try {
    } catch (NumberFormatException e) {
        // 沒有記錄異常
    }
}
           

應該把錯誤資訊記錄下來。

public void logAnException() {
    try {
    } catch (NumberFormatException e) {
        log.error("哦,錯誤竟然發生了: " + e);
    }
}
           

#05、不要記錄了異常又抛出了異常

這純屬畫蛇添足,并且容易造成錯誤資訊的混亂。

反例:

try {
} catch (NumberFormatException e) {
    log.error(e);
    throw e;
}
           

要抛出就抛出,不要記錄,記錄了又抛出,等于多此一舉。

反例:

public void wrapException(String input) throws MyBusinessException {
    try {
    } catch (NumberFormatException e) {
        throw new MyBusinessException("錯誤資訊描述:", e);
    }
}
           

這種也是一樣的道理,既然已經捕獲了,就不要在方法簽名上抛出了。

Java異常處理的20個最佳實踐:提高程式設計效率與代碼品質

#06、不要在 finally 塊中使用 return

阿裡出品的 Java 開發手冊open in new window上這樣規定:

try 塊中的 return 語句執行成功後,并不會馬上傳回,而是繼續執行 finally 塊中的語句,如果 finally 塊中也存在 return 語句,那麼 try 塊中的 return 就将被覆寫。

反例:

private int x = 0;
public int checkReturn() {
    try {
        return ++x;
    } finally {
        return ++x;
    }
}
           

“哦,确實啊,try 塊中 x 傳回的值為 1,到了 finally 塊中就傳回 2 了。”三妹說。

“是這樣的。”我點點頭。

#07、抛出具體定義的檢查性異常而不是 Exception

public void foo() throws Exception { //錯誤方式
}
           

一定要避免出現上面的代碼,它破壞了檢查性(checked)異常的目的。聲明的方法應該盡可能抛出具體的檢查性異常。

例如,如果一個方法可能會抛出 SQLException 異常,應該顯式地聲明抛出 SQLException 而不是 Exception 類型的異常。這樣可以讓其他開發者更好地了解代碼的意圖和異常處理的方式,并且可以根據 SQLException 的定義和文檔來确定異常的處理方式和政策。

#08、捕獲具體的子類而不是捕獲 Exception 類

try {
   someMethod();
} catch (Exception e) { //錯誤方式
   LOGGER.error("method has failed", e);
}
           

如果在 catch 塊中捕獲 Exception 類型的異常,會将所有異常都捕獲,進而可能會給程式帶來不必要的麻煩。具體來說,如果捕獲 Exception 類型的異常,可能會導緻以下問題:

  • 難以識别和定位異常:如果捕獲 Exception 類型的異常,可能會捕獲到一些不應該被處理的異常,進而導緻程式難以識别和定位異常。
  • 難以調試和排錯:如果捕獲 Exception 類型的異常,可能會使得調試和排錯變得更加困難,因為無法确定具體的異常類型和異常發生的原因。

下面舉一個例子來說明為什麼應該盡可能地捕獲具體的子類而不是 Exception 類型的異常。

假設我們有一個方法 readFromFile(String filePath),用于從指定檔案中讀取資料。在方法實作過程中,可能會出現兩種異常:FileNotFoundException 和 IOException。

如果在方法中使用以下 catch 塊來捕獲異常:

try {
    // 讀取資料的代碼
} catch (Exception e) {
    // 異常處理的代碼
}
           

這樣做會捕獲所有類型的異常,包括 Checked Exception 和 Unchecked Exception。這可能會導緻以下問題:

  • 發生 RuntimeException 類型的異常時,也會被捕獲,進而可能會掩蓋實際的異常資訊。
  • 在調試和排錯時,無法确定異常的具體類型和發生原因,進而增加了調試和排錯的難度。
  • 在程式運作時,可能會捕獲一些不需要處理的異常(如 NullPointerException、IllegalArgumentException 等),進而降低程式的性能和穩定性。

是以,為了更好地定位和處理異常,應該盡可能地捕獲具體的子類,例如:

try {
    // 讀取資料的代碼
} catch (FileNotFoundException e) {
    // 處理檔案未找到異常的代碼
} catch (IOException e) {
    // 處理輸入輸出異常的代碼
}
           

這樣做可以更準确地捕獲異常,進而提高程式的健壯性和穩定性。

Java異常處理的20個最佳實踐:提高程式設計效率與代碼品質

#09、自定義異常時不要丢失堆棧跟蹤

catch (NoSuchMethodException e) {
   throw new MyServiceException("Some information: " + e.getMessage());  //錯誤方式
}
           

這破壞了原始異常的堆棧跟蹤,正确的做法是:

catch (NoSuchMethodException e) {
   throw new MyServiceException("Some information: " , e);  //正确方式
}
           

例如,下面是一個自定義異常類,它重寫了 printStackTrace() 方法來列印堆棧跟蹤資訊:

public class MyException extends Exception {
    public MyException(String message, Throwable cause) {
        super(message, cause);
    }

    @Override
    public void printStackTrace() {
        System.err.println("MyException:");
        super.printStackTrace();
    }
}
           

這樣做可以保留堆棧跟蹤資訊,同時也可以提供自定義的異常資訊。在抛出 MyException 異常時,可以得到完整的堆棧跟蹤資訊,進而更好地定位和解決異常。

#10、finally 塊中不要抛出任何異常

try {
  someMethod();  //Throws exceptionOne
} finally {
  cleanUp();    //如果finally還抛出異常,那麼exceptionOne将永遠丢失
}
           

finally 塊用于定義一段代碼,無論 try 塊中是否出現異常,都會被執行。finally 塊通常用于釋放資源、關閉檔案等必須執行的操作。

如果在 finally 塊中抛出異常,可能會導緻原始異常被掩蓋。比如說上例中,一旦 cleanup 抛出異常,someMethod 中的異常将會被覆寫。

#11、不要在生産環境中使用printStackTrace()

在 Java 中,printStackTrace() 方法用于将異常的堆棧跟蹤資訊輸出到标準錯誤流中。這個方法對于調試和排錯非常有用。但在生産環境中,不應該使用 printStackTrace() 方法,因為它可能會導緻以下問題:

  • printStackTrace() 方法将異常的堆棧跟蹤資訊輸出到标準錯誤流中,這可能會暴露敏感資訊,如檔案路徑、使用者名、密碼等。
  • printStackTrace() 方法會将堆棧跟蹤資訊輸出到标準錯誤流中,這可能會影響程式的性能和穩定性。在高并發的生産環境中,大量的異常堆棧跟蹤資訊可能會導緻系統崩潰或出現意外的行為。
  • 由于生産環境中往往是多線程、分布式的複雜系統,printStackTrace() 方法輸出的堆棧跟蹤資訊可能并不完整或準确。

在生産環境中,應該使用日志系統來記錄異常資訊,例如 log4jopen in new window、slf4jopen in new window、logbackopen in new window 等。日志系統可以将異常資訊記錄到檔案或資料庫中,而不會暴露敏感資訊,也不會影響程式的性能和穩定性。同時,日志系統也提供了更多的功能,如級别控制、滾動日志、郵件通知等。

例如,可以使用 logback 記錄異常資訊,如下所示:
try {
    // some code
} catch (Exception e) {
    logger.error("An error occurred: ", e);
}
           

#12、對于不打算處理的異常,直接使用 try-finally,不用 catch

try {
  method1();  // 會調用 Method 2
} finally {
  cleanUp();    //do cleanup here
}
           

如果 method1 正在通路 Method 2,而 Method 2 抛出一些你不想在 Method 1 中處理的異常,但是仍然希望在發生異常時進行一些清理,可以直接在 finally 塊中進行清理,不要使用 catch 塊。

#13、記住早 throw 晚 catch 原則

“早 throw, 晚 catch” 是 Java 中的一種異常處理原則。這個原則指的是在代碼中盡可能早地抛出異常,以便在異常發生時能夠及時地處理異常。同時,在 catch 塊中盡可能晚地捕獲異常,以便在捕獲異常時能夠獲得更多的上下文資訊,進而更好地處理異常。

來舉個 “早 throw” 例子,如果一個方法需要傳遞參數,并且該參數必須滿足一定的條件,如果參數不符合條件,則應該立即抛出異常,而不是在方法中進行其他操作。這可以確定異常在發生時能夠及時被處理,避免更嚴重的問題。

再來舉個“晚 catch”的例子,如果一個方法調用了其他方法,可能會抛出異常,如果在方法内部立即捕獲異常,則可能會導緻對異常的處理不充分。

來看這段代碼:

public class ExceptionDemo1 {
    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
        String str = sc.nextLine();
        try {
            int num = parseInt(str);
            System.out.println("轉換結果:" + num);
        } catch (NumberFormatException e) {
            System.out.println("轉換失敗:" + e.getMessage());
        }
    }

    public static int parseInt(String str) {
        if (str == null || "".equals(str)) {
            throw new NullPointerException("字元串為空");
        }
        if (!str.matches("\\d+")) {
            throw new NumberFormatException("字元串不是數字");
        }
        return Integer.parseInt(str);
    }
}
           

這個示例中,定義了一個 parseInt() 方法,用于将字元串轉換為整數。在該方法中,首先檢測字元串是否為空,如果為空,則立即抛出 NullPointerException 異常。然後,檢測字元串是否為數字,如果不是數字,則抛出 NumberFormatException 異常。最後,使用 Integer.parseInt() 方法将字元串轉換為整數,并傳回。

在示例的 main() 方法中,調用 parseInt() 方法,并使用 try-catch 塊捕獲可能抛出的 NumberFormatException 異常。如果轉換成功,則輸出轉換結果,否則輸出轉換失敗資訊。

這個示例使用了 “早 throw, 晚 catch” 的原則,在 parseInt() 方法中盡可能早地抛出異常,在 main() 方法中盡可能晚地捕獲異常,以便在捕獲異常時能夠獲得更多的上下文資訊,進而更好地處理異常。

運作該示例,輸入一個數字字元串,可以看到輸出轉換結果。如果輸入一個非數字字元串,則輸出轉換失敗資訊。

#14、隻抛出和方法相關的異常

相關性對于保持代碼的整潔非常重要。一種嘗試讀取檔案的方法,如果抛出 NullPointerException,那麼它不會給使用者提供有價值的資訊。相反,如果這種異常被包裹在自定義異常中,則會更好。NoSuchFileFoundException 則對該方法的使用者更有用。

public class Demo {
    public static void main(String[] args) {
        try {
            int result = divide(10, 0);
            System.out.println("The result is: " + result);
        } catch (ArithmeticException e) {
            System.err.println("Error: " + e.getMessage());
        }
    }

    public static int divide(int a, int b) throws ArithmeticException {
        if (b == 0) {
            throw new ArithmeticException("Division by zero");
        }
        return a / b;
    }
}
           

在該示例中,隻抛出了和方法相關的異常 ArithmeticException,這可以使代碼更加清晰和易于維護。

#15、切勿在代碼中使用異常來進行流程控制

在代碼中使用異常來進行流程控制會導緻代碼的可讀性、可維護性和性能出現問題。

public class Demo {
    public static void main(String[] args) {
        String input = "1,2,3,a,5";
        String[] values = input.split(",");
        for (String value : values) {
            try {
                int num = Integer.parseInt(value);
                System.out.println(num);
            } catch (NumberFormatException e) {
                System.err.println(value + " is not a valid number");
            }
        }
    }
}
           

雖然這個示例可以正确地處理輸入字元串中的非數字字元,但是它使用異常進行流程控制,這就導緻代碼變得混亂、難以了解。應該使用其他合适的控制結構open in new window(如 if、switch、循環等)來管理程式的流程。

#16、盡早驗證使用者輸入以在請求處理的早期捕獲異常

Java異常處理的20個最佳實踐:提高程式設計效率與代碼品質

例如:在使用者注冊的業務中,如果按照這樣來做:

  1. 驗證使用者
  2. 插入使用者
  3. 驗證位址
  4. 插入位址
  5. 如果出問題復原一切

這是不正确的做法,它會使資料庫在各種情況下處于不一緻的狀态,應該首先驗證所有内容,然後再進行資料庫更新。正确的做法是:

  1. 驗證使用者
  2. 驗證位址
  3. 插入使用者
  4. 插入位址
  5. 如果問題復原一切

舉個例子,我們用 JDBC 的方式往資料庫插入資料,那麼最好是先 validate 再 insert,而不是 validateUserInput、insertUserData、validateAddressInput、insertAddressData。

Connection conn = null;
try {
    // Connect to the database
    conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/mydatabase", "username", "password");

    // Start a transaction
    conn.setAutoCommit(false);

    // Validate user input
    validateUserInput();

    // Insert user data
    insertUserData(conn);

    // Validate address input
    validateAddressInput();

    // Insert address data
    insertAddressData(conn);

    // Commit the transaction if everything is successful
    conn.commit();

} catch (SQLException e) {
    // Rollback the transaction if there is an error
    if (conn != null) {
        try {
            conn.rollback();
        } catch (SQLException ex) {
            System.err.println("Error: " + ex.getMessage());
        }
    }
    System.err.println("Error: " + e.getMessage());
} finally {
    // Close the database connection
    if (conn != null) {
        try {
            conn.close();
        } catch (SQLException e) {
            System.err.println("Error: " + e.getMessage());
        }
    }
}
           

#17、一個異常隻能包含在一個日志中

不要這樣做:

log.debug("Using cache sector A");
log.debug("Using retry sector B");
           

在單線程環境中,這樣看起來沒什麼問題,但如果在多線程環境中,這兩行緊挨着的代碼中間可能會輸出很多其他的内容,導緻問題查起來會很難受。應該這樣做:

LOGGER.debug("Using cache sector A, using retry sector B");
           

#18、将所有相關資訊盡可能地傳遞給異常

有用的異常消息和堆棧跟蹤非常重要,如果你的日志不能定位異常位置,那要日志有什麼用呢?

// Log exception message and stack trace
LOGGER.debug("Error reading file", e);
           

應該盡量把 String message, Throwable cause 異常資訊和堆棧都輸出。

#19、終止掉被中斷線程

while (true) {
  try {
    Thread.sleep(100000);
  } catch (InterruptedException e) {} //别這樣做
  doSomethingCool();
}
           

InterruptedException 提示應該停止程式正在做的事情,比如事務逾時或線程池被關閉等。

應該盡最大努力完成正在做的事情,并完成目前執行的線程,而不是忽略 InterruptedException。修改後的程式如下:

while (true) {
  try {
    Thread.sleep(100000);
  } catch (InterruptedException e) {
    break;
  }
}
doSomethingCool();
           

#20、對于重複的 try-catch,使用模闆方法

類似的 catch 塊是無用的,隻會增加代碼的重複性,針對這樣的問題可以使用模闆方法。

例如,在嘗試關閉資料庫連接配接時的異常處理。

class DBUtil{
    public static void closeConnection(Connection conn){
        try{
            conn.close();
        } catch(Exception ex){
            //Log Exception - Cannot close connection
        }
    }
}
           

這類的方法将在應用程式很多地方使用。不要把這塊代碼放的到處都是,而是定義上面的方法,然後像下面這樣使用它:

public void dataAccessCode() {
    Connection conn = null;
    try{
        conn = getConnection();
        ....
    } finally{
        DBUtil.closeConnection(conn);
    }
}
           

“好了,三妹,關于異常處理實踐就先講這 20 條吧,實際開發中你還會碰到其他的一些坑,自己踩一踩可能印象更深刻一些。”我說。

“那萬一到時候我工作後被上司罵了怎麼辦?”三妹委屈地說。

“新人嘛,總要寫幾個 bug 才能對得起新人這個稱号嘛。”我輕描淡寫地說。

“好吧。”三妹無奈地歎了口氣。

01、什麼是異常

“二哥,今天就要學習異常了嗎?”三妹問。

“是的。隻有正确地處理好異常,才能保證程式的可靠性,是以異常的學習還是很有必要的。”我說。

“那到底什麼是異常呢?”三妹問。

“異常是指中斷程式正常執行的一個不确定的事件。當異常發生時,程式的正常執行流程就會被打斷。一般情況下,程式都會有很多條語句,如果沒有異常處理機制,前面的語句一旦出現了異常,後面的語句就沒辦法繼續執行了。”

“有了異常處理機制後,程式在發生異常的時候就不會中斷,我們可以對異常進行捕獲,然後改變程式執行的流程。”

“除此之外,異常處理機制可以保證我們向使用者提供友好的提示資訊,而不是程式原生的異常資訊——使用者根本了解不了。”

“不過,站在開發者的角度,我們更希望看到原生的異常資訊,因為這有助于我們更快地找到 bug 的根源,反而被過度包裝的異常資訊會幹擾我們的視線。”

“Java 語言在一開始就提供了相對完善的異常處理機制,這種機制大大降低了編寫可靠程式的門檻,這也是 Java 之是以能夠流行的原因之一。”

“那導緻程式抛出異常的原因有哪些呢?”三妹問。

比如說:

  • 程式在試圖打開一個不存在的檔案;
  • 程式遇到了網絡連接配接問題;
  • 使用者輸入了糟糕的資料;
  • 程式在處理算術問題時沒有考慮除數為 0 的情況;

等等等等。

挑個最簡單的原因來說吧。

public class Demo {
    public static void main(String[] args) {
        System.out.println(10/0);
    }
}
           

這段代碼在運作的時候抛出的異常資訊如下所示:

Exception in thread "main" java.lang.ArithmeticException: / by zero
	at com.itwanger.s41.Demo.main(Demo.java:8)
           

“你看,三妹,這個原生的異常資訊對使用者來說,顯然是不太容易了解的,但對于我們開發者來說,簡直不要太直白了——很容易就能定位到異常發生的根源。”

#02、Exception和Error的差別

“哦,我知道了。下一個問題,我經常看到一些文章裡提到 Exception 和 Error,二哥你能幫我解釋一下它們之間的差別嗎?”三妹問。

“這是一個好問題呀,三妹!”

從單詞的釋義上來看,error 為錯誤,exception 為異常,錯誤的等級明顯比異常要高一些。

從程式的角度來看,也的确如此。

Error 的出現,意味着程式出現了嚴重的問題,而這些問題不應該再交給 Java 的異常處理機制來處理,程式應該直接崩潰掉,比如說 OutOfMemoryError,記憶體溢出了,這就意味着程式在運作時申請的記憶體大于系統能夠提供的記憶體,導緻出現的錯誤,這種錯誤的出現,對于程式來說是緻命的。

Exception 的出現,意味着程式出現了一些在可控範圍内的問題,我們應當采取措施進行挽救。

比如說之前提到的 ArithmeticException,很明顯是因為除數出現了 0 的情況,我們可以選擇捕獲異常,然後提示使用者不應該進行除 0 操作,當然了,更好的做法是直接對除數進行判斷,如果是 0 就不進行除法運算,而是告訴使用者換一個非 0 的數進行運算。

#03、checked和unchecked異常

“三妹,還能想到其他的問題嗎?”

“嗯,不用想,二哥,我已經提前做好預習工作了。”三妹自信地說,“異常又可以分為 checked 和 unchecked,它們之間又有什麼差別呢?”

“哇,三妹,果然又是一個好問題呢。”

checked 異常(檢查型異常)在源代碼裡必須顯式地捕獲或者抛出,否則編譯器會提示你進行相應的操作;而 unchecked 異常(非檢查型異常)就是所謂的運作時異常,通常是可以通過編碼進行規避的,并不需要顯式地捕獲或者抛出。

“我先畫一幅思維導圖給你感受一下。”

Java異常處理的20個最佳實踐:提高程式設計效率與代碼品質

首先,Exception 和 Error 都繼承了 Throwable 類。換句話說,隻有 Throwable 類(或者子類)的對象才能使用 throw 關鍵字抛出,或者作為 catch 的參數類型。

面試中經常問到的一個問題是,NoClassDefFoundError 和 ClassNotFoundException 有什麼差別?

“三妹你知道嗎?”

“不知道,二哥,你解釋下呗。”

它們都是由于系統運作時找不到要加載的類導緻的,但是觸發的原因不一樣。

  • NoClassDefFoundError:程式在編譯時可以找到所依賴的類,但是在運作時找不到指定的類檔案,導緻抛出該錯誤;原因可能是 jar 包缺失或者調用了初始化失敗的類。
  • ClassNotFoundException:當動态加載 Class 對象的時候找不到對應的類時抛出該異常;原因可能是要加載的類不存在或者類名寫錯了。

其次,像 IOException、ClassNotFoundException、SQLException 都屬于 checked 異常;像 RuntimeException 以及子類 ArithmeticException、ClassCastException、ArrayIndexOutOfBoundsException、NullPointerException,都屬于 unchecked 異常。

unchecked 異常可以不在程式中顯示處理,就像之前提到的 ArithmeticException 就是的;但 checked 異常必須顯式處理。

比如說下面這行代碼:

Class clz = Class.forName("com.itwanger.s41.Demo1");
           

如果沒做處理,比如說在 Intellij IDEA 環境下,就會提示你這行代碼可能會抛出 java.lang.ClassNotFoundException。

Java異常處理的20個最佳實踐:提高程式設計效率與代碼品質

建議你要麼使用 try-catch 進行捕獲:

try {
    Class clz = Class.forName("com.itwanger.s41.Demo1");
} catch (ClassNotFoundException e) {
    e.printStackTrace();
}
           

注意列印異常堆棧資訊的 printStackTrace() 方法,該方法會将異常的堆棧資訊列印到标準的控制台下,如果是測試環境,這樣的寫法還 OK,如果是生産環境,這樣的寫法是不可取的,必須使用日志架構把異常的堆棧資訊輸出到日志系統中,否則可能沒辦法跟蹤。

要麼在方法簽名上使用 throws 關鍵字抛出:

public class Demo1 {
    public static void main(String[] args) throws ClassNotFoundException {
        Class clz = Class.forName("com.itwanger.s41.Demo1");
    }
}
           

這樣做的好處是不需要對異常進行捕獲處理,隻需要交給 Java 虛拟機來處理即可;壞處就是沒法針對這種情況做相應的處理。

“二哥,針對 checked 異常,我在知乎上看到一個文章,說 Java 中的 checked 很沒有必要,這種異常在編譯期要麼 try-catch,要麼 throws,但又不一定會出現異常,你覺得這樣的設計有意義嗎?”三妹提出了一個很尖銳的問題。

“哇,這種問題問的好。”我不由得對三妹心生敬佩。

“的确,checked 異常在業界是有争論的,它假設我們捕獲了異常,并且針對這種情況作了相應的處理,但有些時候,根本就沒法處理。”我說,“就拿上面提到的 ClassNotFoundException 異常來說,我們假設對其進行了 try-catch,可真的出現了 ClassNotFoundException 異常後,我們也沒多少的可操作性,再 Class.forName() 一次?”

另外,checked 異常也不相容函數式程式設計,後面如果你寫 Lambda/Stream 代碼的時候,就會體驗到這種苦澀。

當然了,checked 異常并不是一無是處,尤其是在遇到 IO 或者網絡異常的時候,比如說進行 Socket 連結,我大緻寫了一段:

public class Demo2 {
    private String mHost;
    private int mPort;
    private Socket mSocket;
    private final Object mLock = new Object();

    public void run() {
    }

    private void initSocket() {
        while (true) {
            try {
                Socket socket = new Socket(mHost, mPort);
                synchronized (mLock) {
                    mSocket = socket;
                }
                break;
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}
           

當發生 IOException 的時候,socket 就重新嘗試連接配接,否則就 break 跳出循環。意味着如果 IOException 不是 checked 異常,這種寫法就略顯突兀,因為 IOException 沒辦法像 ArithmeticException 那樣用一個 if 語句判斷除數是否為 0 去規避。

或者說,強制性的 checked 異常可以讓我們在程式設計的時候去思考,遇到這種異常的時候該怎麼更優雅的去處理。顯然,Socket 程式設計中,肯定是會遇到 IOException 的,假如 IOException 是非檢查型異常,就意味着開發者也可以不考慮,直接跳過,交給 Java 虛拟機來處理,但我覺得這樣做肯定更不合适。

#04、關于 throw 和 throws

“二哥,你能告訴我 throw 和 throws 兩個關鍵字的差別嗎?”三妹問。

“throw 關鍵字,用于主動地抛出異常;正常情況下,當除數為 0 的時候,程式會主動抛出 ArithmeticException;但如果我們想要除數為 1 的時候也抛出 ArithmeticException,就可以使用 throw 關鍵字主動地抛出異常。”我說。

throw new exception_class("error message");
           

文法也非常簡單,throw 關鍵字後跟上 new 關鍵字,以及異常的類型還有參數即可。

舉個例子。

public class ThrowDemo {
    static void checkEligibilty(int stuage){
        if(stuage<18) {
            throw new ArithmeticException("年紀未滿 18 歲,禁止觀影");
        } else {
            System.out.println("請認真觀影!!");
        }
    }

    public static void main(String args[]){
        checkEligibilty(10);
        System.out.println("愉快地周末..");
    }
}
           

這段代碼在運作的時候就會抛出以下錯誤:

Exception in thread "main" java.lang.ArithmeticException: 年紀未滿 18 歲,禁止觀影
    at com.itwanger.s43.ThrowDemo.checkEligibilty(ThrowDemo.java:9)
    at com.itwanger.s43.ThrowDemo.main(ThrowDemo.java:16)
           

“throws 關鍵字的作用就和 throw 完全不同。”我說,“前面的小節裡已經講了 checked exception 和 unchecked exception,也就是檢查型異常和非檢查型異常;對于檢查型異常來說,如果你沒有做處理,編譯器就會提示你。”

Class.forName() 方法在執行的時候可能會遇到 java.lang.ClassNotFoundException 異常,一個檢查型異常,如果沒有做處理,IDEA 就會提示你,要麼在方法簽名上聲明,要麼放在 try-catch 中。

Java異常處理的20個最佳實踐:提高程式設計效率與代碼品質

“那什麼情況下使用 throws 而不是 try-catch 呢?”三妹問。

“假設現在有這麼一個方法 myMethod(),可能會出現 ArithmeticException 異常,也可能會出現 NullPointerException。這種情況下,可以使用 try-catch 來處理。”我回答。

public void myMethod() {
    try {
        // 可能抛出異常 
    } catch (ArithmeticException e) {
        // 算術異常
    } catch (NullPointerException e) {
        // 空指針異常
    }
}
           

“但假設有好幾個類似 myMethod() 的方法,如果為每個方法都加上 try-catch,就會顯得非常繁瑣。代碼就會變得又臭又長,可讀性就差了。”我繼續說。

“一個解決辦法就是,使用 throws 關鍵字,在方法簽名上聲明可能會抛出的異常,然後在調用該方法的地方使用 try-catch 進行處理。”

public static void main(String args[]){
    try {
        myMethod1();
    } catch (ArithmeticException e) {
        // 算術異常
    } catch (NullPointerException e) {
        // 空指針異常
    }
}
public static void myMethod1() throws ArithmeticException, NullPointerException{
    // 方法簽名上聲明異常
}
           

“好了,我來總結下 throw 和 throws 的差別,三妹,你記一下。”

1)throws 關鍵字用于聲明異常,它的作用和 try-catch 相似;而 throw 關鍵字用于顯式的抛出異常。

2)throws 關鍵字後面跟的是異常的名字;而 throw 關鍵字後面跟的是異常的對象。

示例。

throws ArithmeticException;
           
throw new ArithmeticException("算術異常");
           

3)throws 關鍵字出現在方法簽名上,而 throw 關鍵字出現在方法體裡。

4)throws 關鍵字在聲明異常的時候可以跟多個,用逗号隔開;而 throw 關鍵字每次隻能抛出一個異常。

#05、關于 try-catch-finally

“二哥,之前你講了異常處理機制,這一節講什麼呢?”三妹問。

“該講 try-catch-finally 了。”我說,“try 關鍵字後面會跟一個大括号 {},我們把一些可能發生異常的代碼放到大括号裡;try 塊後面一般會跟 catch 塊,用來處理發生異常的情況;當然了,異常不一定會發生,為了保證發不發生異常都能執行一些代碼,就會跟一個 finally 塊。”

“具體該怎麼用呀,二哥?”三妹問。

“别擔心,三妹,我一一來說明下。”我說。

try 塊的文法很簡單:

try{
// 可能發生異常的代碼
}
           

“注意啊,三妹,如果一些代碼确定不會抛出異常,就盡量不要把它包裹在 try 塊裡,因為加了異常處理的代碼執行起來要比沒有加的花費更多的時間。”

catch 塊的文法也很簡單:

try{
// 可能發生異常的代碼
}catch (exception(type) e(object)){
// 異常處理代碼
}
           

一個 try 塊後面可以跟多個 catch 塊,用來捕獲不同類型的異常并做相應的處理,當 try 塊中的某一行代碼發生異常時,之後的代碼就不再執行,而是會跳轉到異常對應的 catch 塊中執行。

如果一個 try 塊後面跟了多個與之關聯的 catch 塊,那麼應該把特定的異常放在前面,通用型的異常放在後面,不然編譯器會提示錯誤。舉例來說。

static void test() {
    int num1, num2;
    try {
        num1 = 0;
        num2 = 62 / num1;
        System.out.println(num2);
        System.out.println("try 塊的最後一句");
    } catch (ArithmeticException e) {
        // 算術運算發生時跳轉到這裡
        System.out.println("除數不能為零");
    } catch (Exception e) {
        // 通用型的異常意味着可以捕獲所有的異常,它應該放在最後面,
        System.out.println("異常發生了");
    }
    System.out.println("try-catch 之外的代碼.");
}
           

“為什麼 Exception 不能放到 ArithmeticException 前面呢?”三妹問。

“因為 ArithmeticException 是 Exception 的子類,它更具體,我們看到就這個異常就知道是發生了算術錯誤,而 Exception 比較泛,它隐藏了具體的異常資訊,我們看到後并不确定到底是發生了哪一種類型的異常,對錯誤的排查很不利。”我說,“再者,如果把通用型的異常放在前面,就意味着其他的 catch 塊永遠也不會執行,是以編譯器就直接提示錯誤了。”

“再給你舉個例子,注意看,三妹。”

static void test1 () {
    try{
        int arr[]=new int[7];
        arr[4]=30/0;
        System.out.println("try 塊的最後");
    } catch(ArithmeticException e){
        System.out.println("除數必須是 0");
    } catch(ArrayIndexOutOfBoundsException e){
        System.out.println("數組越界了");
    } catch(Exception e){
        System.out.println("一些其他的異常");
    }
    System.out.println("try-catch 之外");
}
           

這段代碼在執行的時候,第一個 catch 塊會執行,因為除數為零;我再來稍微改動下代碼。

static void test1 () {
    try{
        int arr[]=new int[7];
        arr[9]=30/1;
        System.out.println("try 塊的最後");
    } catch(ArithmeticException e){
        System.out.println("除數必須是 0");
    } catch(ArrayIndexOutOfBoundsException e){
        System.out.println("數組越界了");
    } catch(Exception e){
        System.out.println("一些其他的異常");
    }
    System.out.println("try-catch 之外");
}
           

“我知道,二哥,第二個 catch 塊會執行,因為沒有發生算術異常,但數組越界了。”三妹沒等我把代碼運作起來就說出了答案。

“三妹,你說得很對,我再來改一下代碼。”

static void test1 () {
    try{
        int arr[]=new int[7];
        arr[9]=30/1;
        System.out.println("try 塊的最後");
    } catch(ArithmeticException | ArrayIndexOutOfBoundsException e){
        System.out.println("除數必須是 0");
    }
    System.out.println("try-catch 之外");
}
           

“當有多個 catch 的時候,也可以放在一起,用豎劃線 | 隔開,就像上面這樣。”我說。

“這樣不錯呀,看起來更簡潔了。”三妹說。

finally 塊的文法也不複雜。

try {
    // 可能發生異常的代碼
}catch {
   // 異常處理
}finally {
   // 必須執行的代碼
}
           

在沒有 try-with-resourcesopen in new window 之前,finally 塊常用來關閉一些連接配接資源,比如說 socket、資料庫連結、IO 輸入輸出流等。

OutputStream osf = new FileOutputStream( "filename" );
OutputStream osb = new BufferedOutputStream(opf);
ObjectOutput op = new ObjectOutputStream(osb);
try{
    output.writeObject(writableObject);
} finally{
    op.close();
}
           

“三妹,注意,使用 finally 塊的時候需要遵守這些規則。”

  • finally 塊前面必須有 try 塊,不要把 finally 塊單獨拉出來使用。編譯器也不允許這樣做。
  • finally 塊不是必選項,有 try 塊的時候不一定要有 finally 塊。
  • 如果 finally 塊中的代碼可能會發生異常,也應該使用 try-catch 進行包裹。
  • 即便是 try 塊中執行了 return、break、continue 這些跳轉語句,finally 塊也會被執行。

“真的嗎,二哥?”三妹對最後一個規則充滿了疑惑。

“來試一下就知道了。”我說。

static int test2 () {
    try {
        return 112;
    }
    finally {
        System.out.println("即使 try 塊有 return,finally 塊也會執行");
    }
}
           

來看一下輸出結果:

即使 try 塊有 return,finally 塊也會執行
           

“那,會不會有不執行 finally 的情況呀?”三妹很好奇。

“有的。”我斬釘截鐵地回答。

  • 遇到了死循環。
  • 執行了 System. exit() 這行代碼。

System.exit() 和 return 語句不同,前者是用來退出程式的,後者隻是回到了上一級方法調用。

“三妹,來看一下源碼的文檔注釋就全明白了!”

Java異常處理的20個最佳實踐:提高程式設計效率與代碼品質

至于參數 status 的值也很好了解,如果是異常退出,設定為非 0 即可,通常用 1 來表示;如果是想正常退出程式,用 0 表示即可。

#06、小結

Java 的異常處理是一種重要的機制,可以幫助我們處理程式執行期間發生的錯誤❎或異常。

異常分為兩類:Checked Exception 和 Unchecked Exception,其中 Checked Exception 需要在代碼中顯式地處理或聲明抛出,而 Unchecked Exception 不需要在代碼中顯式地處理或聲明抛出。異常處理通常使用 try-catch-finally 塊來處理,也可以使用 throws 關鍵字将異常抛出給調用者處理。

下面是 Java 異常處理的一些總結:

  • 使用 try-catch 塊捕獲并處理異常,可以避免程式因異常而崩潰。
  • 可以使用多個 catch 塊來捕獲不同類型的異常,并進行不同的處理。
  • 可以使用 finally 塊來執行一些必要的清理工作,無論是否發生異常都會執行。
  • 可以使用 throw 關鍵字手動抛出異常,用于在程式中明确指定某些異常情況。
  • 可以使用 throws 關鍵字将異常抛出給調用者處理,用于在方法簽名中聲明可能會出現的異常。
  • Checked Exception 通常是由于外部因素導緻的問題,需要在代碼中顯式地處理或聲明抛出。
  • Unchecked Exception 通常是由于程式内部邏輯或資料異常導緻的,可以不處理或者在需要時進行處理。
  • 在處理異常時,應該根據具體的異常類型進行處理,例如可以嘗試重新打開檔案、重建立立網絡連接配接等操作。
  • 異常處理應該根據具體的業務需求和設計原則進行,避免過度捕獲和處理異常,進而降低程式的性能和可維護性。

繼續閱讀