Java入門(mén)異常處理最佳實(shí)踐
在 Java 開(kāi)發(fā)的道路上,異常處理是繞不開(kāi)的核心知識(shí)點(diǎn)。無(wú)論是新手調(diào)試代碼時(shí)遇到的NullPointerException,還是開(kāi)發(fā)企業(yè)級(jí)項(xiàng)目時(shí)處理的網(wǎng)絡(luò)、文件 IO 異常,掌握規(guī)范的異常處理方式,能讓我們的代碼更健壯、更易維護(hù),還能大幅降低線上問(wèn)題的排查成本。本文將從異常的核心概念出發(fā),逐步拆解 Java 異常體系、處理方式、執(zhí)行流程,最后手把手教你實(shí)現(xiàn)自定義異常,讓你徹底吃透 Java 異常處理。
一、什么是異常?打破程序的正常流程
異常是程序運(yùn)行過(guò)程中發(fā)生的非正常行為 / 錯(cuò)誤狀態(tài),它會(huì)打斷程序的正常執(zhí)行流程,需要開(kāi)發(fā)者通過(guò)特定方式進(jìn)行處理。
在開(kāi)發(fā)中,我們無(wú)法避免各類(lèi)異常場(chǎng)景:比如除數(shù)為 0 的算術(shù)錯(cuò)誤、訪問(wèn)數(shù)組不存在的下標(biāo)、調(diào)用空對(duì)象的方法、讀取不存在的文件、網(wǎng)絡(luò)請(qǐng)求超時(shí)等。這些場(chǎng)景無(wú)法通過(guò)普通的邏輯判斷完全規(guī)避,而 Java 為每一種異常場(chǎng)景都提供了對(duì)應(yīng)的類(lèi)來(lái)描述,讓我們能精準(zhǔn)定位和處理問(wèn)題。
常見(jiàn)異常示例
/**
* 常見(jiàn)運(yùn)行時(shí)異常演示
*/
public class CommonExceptionDemo {
public static void main(String[] args) {
// 1. 算術(shù)異常:ArithmeticException
int a = 10;
int b = 0;
// System.out.println(a / b);
// 2. 數(shù)組越界異常:ArrayIndexOutOfBoundsException
String[] strArr = {"Java", "Python", "C++"};
// System.out.println(strArr[5]);
// 3. 空指針異常:NullPointerException
String str = null;
// System.out.println(str.length());
// 4. 類(lèi)型轉(zhuǎn)換異常:ClassCastException
Object obj = new Integer(100);
// System.out.println((String) obj);
}
}取消上述代碼的注釋?zhuān)\(yùn)行后會(huì)看到控制臺(tái)拋出對(duì)應(yīng)的異常信息,包含異常類(lèi)型、異常原因和出錯(cuò)的代碼行,這也是 Java 異常給我們的核心調(diào)試線索。
二、Java 異常體系結(jié)構(gòu):理清繼承關(guān)系,不混淆概念
Java 為了對(duì)不同類(lèi)型的異常和錯(cuò)誤進(jìn)行分類(lèi)管理,設(shè)計(jì)了一套清晰的異常體系,其頂層類(lèi)是java.lang.Throwable,所有異常和錯(cuò)誤都直接或間接繼承自該類(lèi)。
核心繼承結(jié)構(gòu)
Throwable
├─ Error:JVM級(jí)別的嚴(yán)重錯(cuò)誤,無(wú)法通過(guò)代碼處理
│ ├─ StackOverflowError:棧溢出錯(cuò)誤(如遞歸無(wú)終止條件)
│ └─ OutOfMemoryError:內(nèi)存溢出錯(cuò)誤(OOM)
└─ Exception:程序級(jí)別的異常,開(kāi)發(fā)者可通過(guò)代碼處理
├─ 編譯時(shí)異常(受檢查異常 Checked Exception):編譯期必須處理
│ ├─ IOException:IO操作相關(guān)異常(如文件讀取、網(wǎng)絡(luò)請(qǐng)求)
│ ├─ SQLException:數(shù)據(jù)庫(kù)操作相關(guān)異常
│ └─ ClassNotFoundException:類(lèi)加載失敗異常
└─ 運(yùn)行時(shí)異常(非受檢查異常 Unchecked Exception):運(yùn)行期才會(huì)出現(xiàn),可按需處理
├─ NullPointerException:空指針異常
├─ ArrayIndexOutOfBoundsException:數(shù)組越界異常
├─ ArithmeticException:算術(shù)異常
└─ ClassCastException:類(lèi)型轉(zhuǎn)換異常
三大核心類(lèi)的區(qū)別
- Error:Java 虛擬機(jī)無(wú)法解決的嚴(yán)重問(wèn)題,屬于系統(tǒng)級(jí)錯(cuò)誤,一旦發(fā)生程序基本無(wú)法恢復(fù),只能提前預(yù)防。比如遞歸調(diào)用無(wú)終止條件會(huì)導(dǎo)致
StackOverflowError,創(chuàng)建大量對(duì)象未釋放會(huì)導(dǎo)致OutOfMemoryError。 - 編譯時(shí)異常(Checked Exception):在程序編譯階段就會(huì)被檢測(cè)到的異常,編譯器強(qiáng)制要求開(kāi)發(fā)者必須處理(捕獲或拋出),否則代碼無(wú)法通過(guò)編譯。比如讀取文件時(shí)的
FileNotFoundException,必須顯式處理。 - 運(yùn)行時(shí)異常(Unchecked Exception):繼承自
RuntimeException的異常,在程序運(yùn)行階段才會(huì)觸發(fā),編譯器不強(qiáng)制處理。這類(lèi)異常通常是由于開(kāi)發(fā)者的代碼邏輯錯(cuò)誤導(dǎo)致的,比如空指針、數(shù)組越界,建議通過(guò)優(yōu)化代碼邏輯避免,而非被動(dòng)處理。
重要區(qū)分:編譯期的語(yǔ)法錯(cuò)誤(如把System.out.println寫(xiě)成system.out.println)不屬于異常,只是代碼書(shū)寫(xiě)錯(cuò)誤,編譯器會(huì)直接提示,無(wú)法生成 class 文件;而異常是代碼編譯通過(guò)后,JVM 執(zhí)行時(shí)發(fā)生的錯(cuò)誤。
三、異常的處理方式:從防御式編程到核心關(guān)鍵字
在處理異常前,我們需要了解兩種編程思想:事前防御和事后處理,Java 異常處理的核心基于后者,同時(shí)提供了 5 個(gè)核心關(guān)鍵字:throw、throws、try、catch、finally,掌握這 5 個(gè)關(guān)鍵字,就能處理絕大多數(shù)異常場(chǎng)景。
3.1 兩種防御式編程思想
LBYL:事前防御型(Look Before You Leap)
在執(zhí)行操作前,對(duì)所有可能出現(xiàn)的問(wèn)題進(jìn)行充分檢查,正常流程和錯(cuò)誤處理流程混在一起,代碼可讀性差。
/**
* 事前防御型編程示例:用戶(hù)登陸
*/
public class LBYLDemo {
public static void main(String[] args) {
String username = "test";
String password = "123";
// 檢查用戶(hù)名是否為空
if (username == null || username.isEmpty()) {
System.out.println("錯(cuò)誤:用戶(hù)名為空");
return;
}
// 檢查密碼是否為空
if (password == null || password.isEmpty()) {
System.out.println("錯(cuò)誤:密碼為空");
return;
}
// 檢查用戶(hù)名密碼是否正確
if (!"admin".equals(username) || !"admin123".equals(password)) {
System.out.println("錯(cuò)誤:用戶(hù)名或密碼錯(cuò)誤");
return;
}
System.out.println("登陸成功");
}
}缺陷:代碼中大量的if判斷讓核心業(yè)務(wù)邏輯被淹沒(méi),后期維護(hù)難度大。
EAFP:事后處理型(It's Easier to Ask Forgiveness than Permission)
先執(zhí)行操作,遇到問(wèn)題再捕獲處理,將正常流程和錯(cuò)誤流程分離,代碼更清晰,這也是 Java 異常處理的核心思想。
/**
* 事后處理型編程示例:用戶(hù)登陸
*/
public class EAFPDemo {
public static void main(String[] args) {
String username = "test";
String password = "123";
try {
// 直接執(zhí)行核心業(yè)務(wù)邏輯,不做前置檢查
login(username, password);
System.out.println("登陸成功");
} catch (NullPointerException e) {
System.out.println("錯(cuò)誤:用戶(hù)名或密碼為空");
} catch (IllegalArgumentException e) {
System.out.println("錯(cuò)誤:" + e.getMessage());
}
}
private static void login(String username, String password) {
if (username == null || password == null) {
throw new NullPointerException();
}
if (!"admin".equals(username) || !"admin123".equals(password)) {
throw new IllegalArgumentException("用戶(hù)名或密碼錯(cuò)誤");
}
}
}優(yōu)勢(shì):核心業(yè)務(wù)邏輯login()方法簡(jiǎn)潔,錯(cuò)誤處理集中在catch塊,開(kāi)發(fā)者更關(guān)注正常流程,代碼可讀性和可維護(hù)性大幅提升。
3.2 手動(dòng)拋出異常:throw
在編寫(xiě)程序時(shí),如果檢測(cè)到非法的業(yè)務(wù)邏輯或參數(shù)錯(cuò)誤,需要主動(dòng)將錯(cuò)誤信息告知調(diào)用者,此時(shí)可以使用throw關(guān)鍵字手動(dòng)拋出一個(gè)指定的異常對(duì)象。
語(yǔ)法格式
throw new 異常類(lèi)名("異常產(chǎn)生的原因");
實(shí)戰(zhàn)示例:參數(shù)合法性校驗(yàn)
/**
* throw 手動(dòng)拋出異常示例:獲取集合指定下標(biāo)元素
*/
import java.util.List;
public class ThrowDemo {
public static <T> T getListElement(List<T> list, int index) {
// 校驗(yàn)集合是否為null
if (list == null) {
throw new NullPointerException("傳遞的集合對(duì)象為null,無(wú)法獲取元素");
}
// 校驗(yàn)下標(biāo)是否合法
if (index < 0 || index >= list.size()) {
throw new IndexOutOfBoundsException("傳遞的下標(biāo)" + index + "越界,集合長(zhǎng)度為" + list.size());
}
// 校驗(yàn)通過(guò),返回元素
return list.get(index);
}
public static void main(String[] args) {
List<String> list = List.of("Java", "異常", "處理");
// 正常獲取
System.out.println(getListElement(list, 1));
// 下標(biāo)越界,手動(dòng)拋出異常
// System.out.println(getListElement(list, 5));
// 集合為null,手動(dòng)拋出異常
// System.out.println(getListElement(null, 0));
}
}throw 使用注意事項(xiàng)
throw必須寫(xiě)在方法體內(nèi)部;- 拋出的對(duì)象必須是
Exception或其子類(lèi)的實(shí)例; - 拋出運(yùn)行時(shí)異常(如
NullPointerException),調(diào)用者可按需處理,編譯器不強(qiáng)制; - 拋出編譯時(shí)異常(如
IOException),調(diào)用者必須處理(捕獲或拋出),否則代碼無(wú)法編譯; - 異常一旦拋出,其后的代碼將不會(huì)執(zhí)行。
3.3 聲明異常:throws
如果當(dāng)前方法沒(méi)有能力處理拋出的異常,或者希望將異常處理的責(zé)任轉(zhuǎn)移給調(diào)用者,此時(shí)可以使用throws關(guān)鍵字在方法聲明處聲明該方法可能拋出的異常。
語(yǔ)法格式
修飾符 返回值類(lèi)型 方法名(參數(shù)列表) throws 異常類(lèi)型1, 異常類(lèi)型2... {
// 方法體,可能拋出異常
}
實(shí)戰(zhàn)示例:文件操作聲明編譯時(shí)異常
/**
* throws 聲明異常示例:文件讀取
*/
import java.io.File;
import java.io.FileReader;
import java.io.FileNotFoundException;
public class ThrowsDemo {
// 聲明文件未找到異常,交給調(diào)用者處理
public static FileReader openFile(String filePath) throws FileNotFoundException {
File file = new File(filePath);
// FileNotFoundException是編譯時(shí)異常,此處不處理,聲明后拋出
return new FileReader(file);
}
public static void main(String[] args) {
try {
// 調(diào)用聲明異常的方法,必須處理異常
FileReader fr = openFile("test.txt");
System.out.println("文件打開(kāi)成功");
fr.close();
} catch (FileNotFoundException e) {
System.out.println("異常原因:" + e.getMessage());
}
}
}throws 使用注意事項(xiàng)
throws必須跟在方法參數(shù)列表之后;- 聲明的異常必須是
Exception或其子類(lèi); - 方法內(nèi)部拋出多個(gè)異常時(shí),
throws后用逗號(hào)分隔多個(gè)異常類(lèi)型;若異常之間有父子關(guān)系,直接聲明父類(lèi)異常即可(如FileNotFoundException繼承自IOException,可直接聲明throws IOException); - 調(diào)用聲明了編譯時(shí)異常的方法,調(diào)用者必須處理(
try-catch捕獲或繼續(xù)throws拋出);調(diào)用聲明了運(yùn)行時(shí)異常的方法,編譯器不強(qiáng)制處理。
3.4 捕獲并處理異常:try-catch
throws只是將異常轉(zhuǎn)移給調(diào)用者,并未真正處理異常;而try-catch是 Java 中處理異常的核心方式,能捕獲異常并對(duì)其進(jìn)行處理,讓程序在發(fā)生異常后繼續(xù)執(zhí)行。
語(yǔ)法格式
try {
// 可能拋出異常的代碼塊(監(jiān)控區(qū))
} catch (異常類(lèi)型1 異常對(duì)象名) {
// 處理異常類(lèi)型1的代碼(捕獲區(qū))
} catch (異常類(lèi)型2 異常對(duì)象名) {
// 處理異常類(lèi)型2的代碼
}
// 可選:finally塊,下文單獨(dú)講解
實(shí)戰(zhàn)示例:多異常捕獲與處理
/**
* try-catch 捕獲異常示例:多異常處理
*/
public class TryCatchDemo {
public static void calculate(int a, int b, int[] arr) {
try {
System.out.println("a / b = " + (a / b));
System.out.println("數(shù)組下標(biāo)0的元素:" + arr[0]);
} catch (ArithmeticException e) {
// 處理算術(shù)異常
System.out.println("處理算術(shù)異常:" + e.getMessage());
} catch (NullPointerException e) {
// 處理空指針異常
System.out.println("處理空指針異常:數(shù)組對(duì)象為null");
} catch (ArrayIndexOutOfBoundsException e) {
// 處理數(shù)組越界異常
System.out.println("處理數(shù)組越界異常:" + e.getMessage());
}
}
public static void main(String[] args) {
// 測(cè)試1:除數(shù)為0
calculate(10, 0, new int[]{1,2});
System.out.println("===== 分割線 =====");
// 測(cè)試2:數(shù)組為null
calculate(10, 2, null);
System.out.println("===== 分割線 =====");
// 測(cè)試3:數(shù)組越界(空數(shù)組)
calculate(10, 2, new int[]{});
// 異常處理后,后續(xù)代碼正常執(zhí)行
System.out.println("程序執(zhí)行完成");
}
}運(yùn)行結(jié)果
處理算術(shù)異常:/ by zero ===== 分割線 ===== 處理空指針異常:數(shù)組對(duì)象為null ===== 分割線 ===== 處理數(shù)組越界異常:Index 0 out of bounds for length 0 程序執(zhí)行完成
可以看到,即使發(fā)生了異常,經(jīng)過(guò)try-catch處理后,程序的后續(xù)代碼依然能正常執(zhí)行,這也是異常處理的核心目的。
try-catch 使用關(guān)鍵注意事項(xiàng)
try塊中拋出異常的位置后續(xù)代碼不會(huì)執(zhí)行;- 異常捕獲遵循類(lèi)型匹配原則:只有
catch的異常類(lèi)型與try中拋出的異常類(lèi)型一致,或?yàn)槠?strong>父類(lèi),才能捕獲到異常; - 處理多個(gè)不同類(lèi)型的異常時(shí),需注意子類(lèi)異常在前,父類(lèi)異常在后,否則會(huì)出現(xiàn)語(yǔ)法錯(cuò)誤(父類(lèi)異常會(huì)捕獲所有子類(lèi)異常,后續(xù)的子類(lèi)異常
catch塊永遠(yuǎn)無(wú)法執(zhí)行); - 若多個(gè)異常的處理邏輯完全相同,可使用 ** 豎線 |** 合并捕獲,簡(jiǎn)化代碼:
catch (ArithmeticException | NullPointerException | ArrayIndexOutOfBoundsException e) { System.out.println("處理異常:" + e.getMessage()); } - 可以使用
Exception捕獲所有異常(因?yàn)?code>Exception是所有程序級(jí)異常的父類(lèi)),但不推薦:會(huì)掩蓋具體的異常類(lèi)型,不利于問(wèn)題排查,僅適用于通用的異常兜底處理。
3.5 必執(zhí)行的代碼塊:finally
在程序開(kāi)發(fā)中,有些代碼無(wú)論是否發(fā)生異常,都必須執(zhí)行,比如打開(kāi)的文件流、數(shù)據(jù)庫(kù)連接、網(wǎng)絡(luò)連接等資源的釋放,否則會(huì)造成資源泄漏。finally塊就是為了解決這個(gè)問(wèn)題,它配合try-catch使用,里面的代碼永遠(yuǎn)會(huì)被執(zhí)行。
語(yǔ)法格式
try {
// 可能拋出異常的代碼
} catch (異常類(lèi)型 e) {
// 處理異常的代碼
} finally {
// 無(wú)論是否發(fā)生異常,都會(huì)執(zhí)行的代碼(資源釋放為主)
}
核心場(chǎng)景:資源釋放(全新示例)
/**
* finally 塊示例:資源釋放(Scanner)
*/
import java.util.Scanner;
import java.util.InputMismatchException;
public class FinallyDemo {
public static int getIntInput() {
Scanner sc = new Scanner(System.in);
try {
System.out.print("請(qǐng)輸入一個(gè)整數(shù):");
// 嘗試獲取整數(shù)輸入
int num = sc.nextInt();
return num;
} catch (InputMismatchException e) {
System.out.println("輸入類(lèi)型錯(cuò)誤,不是整數(shù)");
return -1;
} finally {
// 無(wú)論是否輸入正確,都關(guān)閉Scanner,釋放資源
System.out.println("執(zhí)行finally塊:關(guān)閉Scanner資源");
sc.close();
}
}
public static void main(String[] args) {
int num = getIntInput();
System.out.println("獲取到的數(shù)字:" + num);
}
}測(cè)試結(jié)果 1:輸入正確整數(shù)
請(qǐng)輸入一個(gè)整數(shù):100 執(zhí)行finally塊:關(guān)閉Scanner資源 獲取到的數(shù)字:100
測(cè)試結(jié)果 2:輸入非整數(shù)
請(qǐng)輸入一個(gè)整數(shù):abc 輸入類(lèi)型錯(cuò)誤,不是整數(shù) 執(zhí)行finally塊:關(guān)閉Scanner資源 獲取到的數(shù)字:-1
可以看到,無(wú)論try塊中是否發(fā)生異常,finally塊的代碼都會(huì)執(zhí)行,完美解決了資源釋放的問(wèn)題。
finally 的特殊注意事項(xiàng)
finally塊的執(zhí)行時(shí)機(jī):在方法返回之前(即使try或catch中有return語(yǔ)句,也會(huì)先執(zhí)行finally塊,再執(zhí)行return);- 若
finally塊中也有return語(yǔ)句,會(huì)覆蓋try或catch中的return結(jié)果,強(qiáng)烈不建議在finally中寫(xiě)return(編譯器會(huì)給出警告); finally塊唯一不執(zhí)行的情況:程序執(zhí)行到try/catch塊時(shí),調(diào)用了System.exit(0)(強(qiáng)制終止 JVM),此時(shí) JVM 直接退出,所有代碼都不再執(zhí)行。
四、異常的處理流程:跟著調(diào)用棧走,理清執(zhí)行順序
要徹底理解異常處理,必須理清異常的傳播和處理流程,而核心就是方法調(diào)用棧:Java 中方法之間的調(diào)用關(guān)系會(huì)被 JVM 存儲(chǔ)在虛擬機(jī)棧中,當(dāng)發(fā)生異常時(shí),異常會(huì)沿著調(diào)用棧從下往上傳播,直到被捕獲處理,若最終無(wú)人處理,則由 JVM 接管,程序異常終止。
異常處理完整執(zhí)行流程
- 程序先執(zhí)行
try塊中的代碼; - 若
try塊中未發(fā)生異常,跳過(guò)catch塊,直接執(zhí)行finally塊,再執(zhí)行try-catch-finally后的代碼; - 若
try塊中發(fā)生異常,立即終止try塊后續(xù)代碼,匹配catch塊的異常類(lèi)型:- 找到匹配的異常類(lèi)型:執(zhí)行對(duì)應(yīng)
catch塊的處理代碼,再執(zhí)行finally塊,最后執(zhí)行后續(xù)代碼; - 未找到匹配的異常類(lèi)型:先執(zhí)行
finally塊,再將異常向上傳播給上層調(diào)用者;
- 找到匹配的異常類(lèi)型:執(zhí)行對(duì)應(yīng)
- 上層調(diào)用者重復(fù)步驟 3,若所有調(diào)用者都未處理異常,最終傳遞到
main方法; - 若
main方法也未處理異常,異常會(huì)被JVM 接管,JVM 會(huì)打印異常信息(類(lèi)型、原因、調(diào)用棧),并強(qiáng)制終止程序,main方法后續(xù)代碼不再執(zhí)行。
實(shí)戰(zhàn)示例:異常的向上傳播
/**
* 異常處理流程示例:異常向上傳播
*/
public class ExceptionFlowDemo {
// 方法3:拋出數(shù)組越界異常
public static void method3() {
int[] arr = {1,2,3};
System.out.println(arr[10]); // 拋出異常
}
// 方法2:調(diào)用method3,未處理異常
public static void method2() {
method3();
}
// 方法1:調(diào)用method2,捕獲并處理異常
public static void method1() {
try {
method2();
} catch (ArrayIndexOutOfBoundsException e) {
System.out.println("method1捕獲到異常:" + e.getMessage());
}
}
public static void main(String[] args) {
method1();
// 異常被處理,后續(xù)代碼正常執(zhí)行
System.out.println("main方法后續(xù)代碼執(zhí)行");
}
}運(yùn)行結(jié)果
method1捕獲到異常:Index 10 out of bounds for length 3
main方法后續(xù)代碼執(zhí)行
若刪除method1中的try-catch,異常會(huì)傳播到main方法,若main方法也不處理,JVM 會(huì)接管,程序終止,main方法后續(xù)代碼不再執(zhí)行。
五、自定義異常:貼合業(yè)務(wù)場(chǎng)景,讓異常更有意義
Java 內(nèi)置了豐富的異常類(lèi),但這些異常類(lèi)都是通用的,無(wú)法精準(zhǔn)描述實(shí)際開(kāi)發(fā)中的業(yè)務(wù)異常,比如用戶(hù)登陸時(shí)的 “用戶(hù)名不存在”、“密碼錯(cuò)誤”,訂單操作時(shí)的 “訂單不存在”、“庫(kù)存不足” 等。此時(shí)我們需要自定義異常類(lèi),貼合業(yè)務(wù)場(chǎng)景,讓異常信息更精準(zhǔn),便于問(wèn)題排查和業(yè)務(wù)處理。
5.1 自定義異常的實(shí)現(xiàn)步驟
Java 中自定義異常的核心是繼承,遵循以下兩步即可:
- 自定義異常類(lèi),繼承自
Exception(編譯時(shí)異常,受檢查)或RuntimeException(運(yùn)行時(shí)異常,非受檢查); - 實(shí)現(xiàn)帶 String 類(lèi)型參數(shù)的構(gòu)造方法,將異常原因通過(guò)
super()傳遞給父類(lèi)構(gòu)造方法(便于通過(guò)getMessage()獲取異常原因)。
5.2 實(shí)戰(zhàn)示例:用戶(hù)登陸業(yè)務(wù)的自定義異常
我們針對(duì)用戶(hù)登陸場(chǎng)景,自定義兩個(gè)業(yè)務(wù)異常:UserNameNotExistException(用戶(hù)名不存在)、PasswordErrorException(密碼錯(cuò)誤),并在業(yè)務(wù)代碼中拋出和處理。
步驟 1:實(shí)現(xiàn)自定義異常類(lèi)
/**
* 自定義異常:用戶(hù)名不存在(繼承Exception,編譯時(shí)異常)
*/
public class UserNameNotExistException extends Exception {
// 構(gòu)造方法,傳遞異常原因
public UserNameNotExistException(String message) {
super(message);
}
}
/**
* 自定義異常:密碼錯(cuò)誤(繼承Exception,編譯時(shí)異常)
*/
public class PasswordErrorException extends Exception {
public PasswordErrorException(String message) {
super(message);
}
}可選優(yōu)化:若希望自定義異常為運(yùn)行時(shí)異常,只需將父類(lèi)改為RuntimeException,編譯器不強(qiáng)制處理。
步驟 2:在業(yè)務(wù)代碼中拋出自定義異常
/**
* 用戶(hù)登陸業(yè)務(wù)類(lèi)
*/
public class UserLoginService {
// 模擬數(shù)據(jù)庫(kù)中的用戶(hù)信息
private static final String DB_USERNAME = "admin";
private static final String DB_PASSWORD = "admin123456";
/**
* 登陸方法,拋出自定義業(yè)務(wù)異常
* @param username 用戶(hù)名
* @param password 密碼
* @throws UserNameNotExistException 用戶(hù)名不存在
* @throws PasswordErrorException 密碼錯(cuò)誤
*/
public void login(String username, String password) throws UserNameNotExistException, PasswordErrorException {
// 校驗(yàn)用戶(hù)名
if (!DB_USERNAME.equals(username)) {
throw new UserNameNotExistException("用戶(hù)名[" + username + "]不存在");
}
// 校驗(yàn)密碼
if (!DB_PASSWORD.equals(password)) {
throw new PasswordErrorException("密碼錯(cuò)誤,請(qǐng)重新輸入");
}
}
}步驟 3:調(diào)用業(yè)務(wù)方法,處理自定義異常
/**
* 測(cè)試自定義異常:用戶(hù)登陸
*/
public class CustomExceptionTest {
public static void main(String[] args) {
UserLoginService loginService = new UserLoginService();
// 測(cè)試1:用戶(hù)名不存在
String username = "test";
String password = "admin123456";
try {
loginService.login(username, password);
System.out.println("登陸成功!");
} catch (UserNameNotExistException e) {
System.out.println("登陸失敗:" + e.getMessage());
// 可做后續(xù)處理,如跳轉(zhuǎn)到注冊(cè)頁(yè)面
} catch (PasswordErrorException e) {
System.out.println("登陸失?。? + e.getMessage());
// 可做后續(xù)處理,如提示密碼找回
}
// 測(cè)試2:密碼錯(cuò)誤
System.out.println("===== 分割線 =====");
username = "admin";
password = "123";
try {
loginService.login(username, password);
System.out.println("登陸成功!");
} catch (UserNameNotExistException | PasswordErrorException e) {
System.out.println("登陸失?。? + e.getMessage());
}
}
}運(yùn)行結(jié)果
登陸失敗:用戶(hù)名[test]不存在
===== 分割線 =====
登陸失?。好艽a錯(cuò)誤,請(qǐng)重新輸入
可以看到,自定義異常能精準(zhǔn)描述業(yè)務(wù)中的錯(cuò)誤場(chǎng)景,讓異常處理更貼合實(shí)際業(yè)務(wù),同時(shí)異常信息更直觀,便于開(kāi)發(fā)和運(yùn)維人員排查問(wèn)題。
5.3 自定義異常的選型建議
- 若希望編譯器強(qiáng)制處理該異常(如核心業(yè)務(wù)異常,必須顯式處理),讓自定義異常繼承
Exception(編譯時(shí)異常); - 若該異常可通過(guò)代碼邏輯避免,或希望簡(jiǎn)化代碼(不強(qiáng)制處理),讓自定義異常繼承
RuntimeException(運(yùn)行時(shí)異常); - 自定義異常的命名要見(jiàn)名知意,通常以
Exception結(jié)尾,如OrderNotExistException、StockNotEnoughException。
六、異常處理的最佳實(shí)踐
掌握了異常的基礎(chǔ)知識(shí)點(diǎn)后,更重要的是在實(shí)際開(kāi)發(fā)中遵循最佳實(shí)踐,讓異常處理更規(guī)范、更高效:
- 避免捕獲所有異常:不要直接捕獲
Exception,會(huì)掩蓋具體的異常類(lèi)型,不利于問(wèn)題排查,應(yīng)捕獲具體的異常類(lèi)型; - 不要忽略異常:不要在
catch塊中只寫(xiě)e.printStackTrace(),甚至空的catch塊,應(yīng)根據(jù)業(yè)務(wù)場(chǎng)景做具體處理(如記錄日志、提示用戶(hù)、重試操作); - 及時(shí)釋放資源:打開(kāi)的 IO 流、數(shù)據(jù)庫(kù)連接、網(wǎng)絡(luò)連接等資源,必須在
finally塊中釋放,或使用 Java7 的try-with-resources自動(dòng)釋放; - 異常信息要精準(zhǔn):拋出異常時(shí),填寫(xiě)清晰的異常原因(如
throw new NullPointerException("用戶(hù)信息對(duì)象為null,無(wú)法獲取用戶(hù)ID")),便于排查問(wèn)題; - 子類(lèi)方法拋出異常范圍不超過(guò)父類(lèi):繼承父類(lèi)并重寫(xiě)方法時(shí),子類(lèi)方法拋出的異常類(lèi)型不能是父類(lèi)方法異常的父類(lèi),也不能拋出更多的受檢查異常;
- 合理選擇自定義異常的父類(lèi):核心業(yè)務(wù)異常建議繼承
Exception,強(qiáng)制調(diào)用者處理;非核心異常建議繼承RuntimeException,簡(jiǎn)化代碼; - 使用日志框架記錄異常:實(shí)際開(kāi)發(fā)中,不要使用
e.printStackTrace(),應(yīng)使用 SLF4J/Logback 等日志框架記錄異常(可記錄異常級(jí)別、調(diào)用棧、業(yè)務(wù)上下文),便于線上問(wèn)題排查。
七、總結(jié)
Java 異常處理是保證程序健壯性的核心,其核心是事后處理的編程思想,通過(guò)throw、throws、try、catch、finally五個(gè)關(guān)鍵字實(shí)現(xiàn)異常的拋出、聲明、捕獲和處理。
本文從異常的概念出發(fā),理清了Throwable、Error、Exception的繼承關(guān)系,區(qū)分了編譯時(shí)異常和運(yùn)行時(shí)異常;然后詳細(xì)講解了異常處理的核心方式和執(zhí)行流程;最后通過(guò)實(shí)戰(zhàn)實(shí)現(xiàn)了貼合業(yè)務(wù)的自定義異常,并給出了開(kāi)發(fā)中的最佳實(shí)踐。
掌握異常處理的關(guān)鍵,不僅是記住語(yǔ)法和規(guī)則,更重要的是結(jié)合業(yè)務(wù)場(chǎng)景選擇合適的處理方式:讓程序在發(fā)生異常時(shí),既能精準(zhǔn)定位問(wèn)題,又能優(yōu)雅地處理異常,保證程序的正常運(yùn)行。希望本文能讓你對(duì) Java 異常處理有更全面、更深入的理解,在實(shí)際開(kāi)發(fā)中玩轉(zhuǎn)異常體系!
到此這篇關(guān)于Java入門(mén)異常處理最佳實(shí)踐的文章就介紹到這了,更多相關(guān)java異常處理內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Java多線程鎖機(jī)制相關(guān)原理實(shí)例解析
這篇文章主要介紹了Java多線程鎖機(jī)制相關(guān)原理實(shí)例解析,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2020-08-08
SpringBoot3解決跨域請(qǐng)求的方案小結(jié)
解決跨域請(qǐng)求,主要有JSONP,iframe,window.name,CORS等方式,其中CORS方式是最常用的跨域?qū)崿F(xiàn)方式,而且是對(duì)各種請(qǐng)求方法、各種數(shù)據(jù)請(qǐng)求類(lèi)型都是完美支持的,本文介紹了SpringBoot3解決跨域請(qǐng)求的方案小結(jié),需要的朋友可以參考下2024-07-07
Docker?快速部署Springboot項(xiàng)目超詳細(xì)最新版
這篇文章主要介紹了Docker?快速部署Springboot項(xiàng)目超詳細(xì)最新版的相關(guān)資料,本文給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2021-04-04
Java實(shí)現(xiàn)動(dòng)態(tài)數(shù)字時(shí)鐘
這篇文章主要為大家詳細(xì)介紹了Java實(shí)現(xiàn)動(dòng)態(tài)數(shù)字時(shí)鐘,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2019-12-12
Java事務(wù)失效八大場(chǎng)景詳細(xì)解析
在Java開(kāi)發(fā)中Spring框架提供了強(qiáng)大的事務(wù)管理功能,然而在實(shí)際開(kāi)發(fā)過(guò)程中,由于對(duì)框架特性的誤解或不當(dāng)使用,可能導(dǎo)致事務(wù)失效問(wèn)題,這篇文章主要介紹了Java事務(wù)失效八大場(chǎng)景的相關(guān)資料,需要的朋友可以參考下2025-10-10
在SpringBoot里自定義Spring MVC配置的三種方法
SpringBoot幫我們自動(dòng)配好了 MVC 的核心組件,但實(shí)際開(kāi)發(fā)中,光用默認(rèn)配置肯定不夠今天就教你 3 種自定義 Spring MVC 配置的方法,從簡(jiǎn)單到復(fù)雜,新手也能一步步跟著做,需要的朋友可以參考下2026-03-03

