全面解析Java中常見Exception異常的錯誤排查與代碼修正
前言
適用對象
本課程適合已經(jīng)掌握J(rèn)ava基礎(chǔ)語法,初步了解異常處理概念,但希望系統(tǒng)掌握常見異常排查與修復(fù)能力的開發(fā)者。無論你是剛?cè)胄械男氯?,還是有一定經(jīng)驗的開發(fā)者,這門課程都將幫助你建立系統(tǒng)的異常排查思維,提升代碼質(zhì)量。
學(xué)習(xí)目標(biāo)
通過兩個課時的系統(tǒng)學(xué)習(xí),你將能夠:
- 識別 Java中10+種最常見的異常及其產(chǎn)生場景
- 分析 異常堆棧信息,快速定位問題根源
- 掌握 針對不同異常的系統(tǒng)性排查方法
- 運用 最佳實踐修復(fù)代碼,預(yù)防同類問題再次發(fā)生
- 建立 異常處理的正確思維模式
課程安排
- 第一課時(約60分鐘):異常基礎(chǔ)概念 + 運行時異常深度剖析(NullPointerException、ArrayIndexOutOfBoundsException、ClassCastException、ArithmeticException)
- 第二課時(約60分鐘):受檢異常深度剖析 + 復(fù)雜異常排查 + 綜合實戰(zhàn)演練
教學(xué)方式
每個異常都遵循“現(xiàn)象描述 → 出現(xiàn)場景 → 堆棧分析 → 排查方法 → 代碼修正 → 預(yù)防措施”的六步教學(xué)法,確保理論與實踐緊密結(jié)合。
第一部分:Java異常體系回顧(約10分鐘)
1.1 異常是什么
在深入具體異常之前,我們先理解異常的本質(zhì)。異常(Exception) 是程序運行過程中出現(xiàn)的打斷正常執(zhí)行流程的事件。它本質(zhì)上是一個對象,封裝了錯誤類型、錯誤描述、方法調(diào)用堆棧以及可能的底層原因。
1.2 Java異常體系結(jié)構(gòu)
java.lang.Object
|
java.lang.Throwable
|
---------------------
| |
java.lang.Error java.lang.Exception
|
-------------------------
| |
RuntimeException 其他 Exception
(運行時異常) (受檢異常)
- Error:JVM級別的嚴(yán)重錯誤,如
OutOfMemoryError、StackOverflowError,程序通常無法處理。 - Exception:程序可處理的異常,分為兩類:
- 受檢異常(Checked Exception):編譯時必須處理(捕獲或聲明拋出),如
IOException、SQLException。 - 運行時異常(RuntimeException):編譯時不強(qiáng)制處理,通常由程序邏輯錯誤導(dǎo)致,如
NullPointerException、ArrayIndexOutOfBoundsException。
- 受檢異常(Checked Exception):編譯時必須處理(捕獲或聲明拋出),如
1.3 異常信息解讀
一個典型的異常堆棧包含以下要素:
Exception java.lang.IllegalArgumentException: item quantity must be a number
at io.jzheaux.pluralsight.DeliController.orderSandwich (DeliController.java:45)
// …
Caused by java.lang.NumberFormatException: For input string: " 3"
at NumberFormatException.forInputString (NumberFormatException.java:67)
at Integer.parseInt (Integer.java:647)
...
- 異常類型:
IllegalArgumentException - 異常消息:
item quantity must be a number - 堆棧軌跡:從main開始到異常發(fā)生處的調(diào)用鏈
- Caused by:底層根本原因,通常是排查的關(guān)鍵入口
排查技巧:遇到復(fù)雜異常時,不要只看第一行,要順著堆棧往下找,尤其是“Caused by”部分,那里往往藏著真正的原因。
第二課時(上):運行時異常深度剖析(約30分鐘)
運行時異常(RuntimeException)是Java程序中最常見的異常類型,它們通常由代碼邏輯錯誤引起。下面我們將逐個剖析最常見的運行時異常。
2.1 NullPointerException(空指針異常)
現(xiàn)象描述
當(dāng)應(yīng)用程序試圖在需要對象的地方使用null引用時,拋出此異常。這是Java中最著名的異常,占據(jù)了異??倲?shù)的很大比例。
出現(xiàn)場景
場景一:直接調(diào)用null對象的方法或?qū)傩?/strong>
String text = null; int length = text.length(); // 拋出NullPointerException
場景二:自動拆箱時包裝類型為null
Boolean willVote = null;
if (willVote) { // 自動拆箱時拋出NullPointerException
System.out.println("可以投票");
}
場景三:方法參數(shù)或返回值未做空檢查
void parseDocument(Document doc) {
doc.getElements(); // 如果傳入的doc為null,拋出異常
}
String lookupElement(Document doc) {
Element element = doc.findElement("span");
return element.getValue(); // 如果element為null,拋出異常
}
場景四:數(shù)組元素未初始化
Person[] people = new Person[5]; people[0].getName(); // 數(shù)組元素默認(rèn)為null,拋出異常
堆棧分析示例
Exception in thread "main" java.lang.NullPointerException
at com.example.UserService.getUserAge(UserService.java:25)
at com.example.UserController.main(UserController.java:12)
從堆??梢钥闯?,UserService.java的第25行調(diào)用了某個null對象的方法。
排查方法流程圖

代碼修正與預(yù)防
修正方案一:參數(shù)校驗
// 錯誤代碼
void parseDocument(Document doc) {
doc.getElements();
}
// 修正代碼
void parseDocument(@NonNull Document doc) {
if (doc == null) {
throw new IllegalArgumentException("doc cannot be null");
}
doc.getElements();
}
修正方案二:使用守衛(wèi)語句
// 錯誤代碼
String lookupElement(Document doc) {
Element element = doc.findElement("span");
return element.getValue();
}
// 修正代碼
@Nullable String lookupElement(Document doc) {
Element element = doc.findElement("span");
if (element == null) {
return null; // 或者返回默認(rèn)值
}
return element.getValue();
}
修正方案三:使用Optional(Java 8+)
public Optional<String> lookupElement(Document doc) {
return Optional.ofNullable(doc.findElement("span"))
.map(Element::getValue);
}
修正方案四:數(shù)組元素初始化
Person[] people = new Person[5];
for (int i = 0; i < people.length; i++) {
people[i] = new Person(); // 確保每個元素都被初始化
}
預(yù)防措施 :
- 明確空值來源:是無效值(上游問題)還是有效值(可接受null)
- 使用@NonNull和@Nullable注解,讓IDE幫助檢查
- 遵循"盡早失敗"原則:在方法入口處就進(jìn)行參數(shù)校驗
- 謹(jǐn)慎處理返回值:明確方法是否可能返回null,并在文檔中說明
2.2 ArrayIndexOutOfBoundsException(數(shù)組下標(biāo)越界異常)
現(xiàn)象描述
當(dāng)試圖使用非法索引訪問數(shù)組元素時拋出,非法索引包括負(fù)數(shù)、0到數(shù)組長度減1范圍外的值。
出現(xiàn)場景
場景一:索引超出數(shù)組長度
int[] numbers = {1, 2, 3};
int value = numbers[3]; // 索引3超出范圍(有效索引0-2)
場景二:循環(huán)條件錯誤
int[] scores = {85, 90, 78, 92};
for (int i = 0; i <= scores.length; i++) { // 應(yīng)該是 i < scores.length
System.out.println(scores[i]); // 最后一次循環(huán)i=4,越界
}
場景三:索引為負(fù)數(shù)
int[] data = new int[10]; int index = -1; data[index] = 100; // 負(fù)索引越界
堆棧分析示例
Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: Index 5 out of bounds for length 5
at com.example.ArrayDemo.processArray(ArrayDemo.java:15)
at com.example.ArrayDemo.main(ArrayDemo.java:8)
異常信息直接告訴我們:試圖訪問索引5,但數(shù)組長度只有5(有效索引0-4)。
排查方法

代碼修正與預(yù)防
修正方案一:修正循環(huán)邊界
// 錯誤代碼
for (int i = 0; i <= scores.length; i++) {
System.out.println(scores[i]);
}
// 修正代碼
for (int i = 0; i < scores.length; i++) {
System.out.println(scores[i]);
}
// 更好的方式:使用增強(qiáng)for循環(huán)
for (int score : scores) {
System.out.println(score);
}
修正方案二:訪問前檢查索引
public int getElement(int[] array, int index) {
if (array == null) {
throw new IllegalArgumentException("array cannot be null");
}
if (index < 0 || index >= array.length) {
throw new IndexOutOfBoundsException(
"Index " + index + " out of bounds for length " + array.length);
}
return array[index];
}
預(yù)防措施:
- 優(yōu)先使用增強(qiáng)for循環(huán)處理數(shù)組遍歷
- 使用
Arrays工具類的方法進(jìn)行數(shù)組操作 - 動態(tài)計算索引時,添加邊界檢查
- **考慮使用
ArrayList**等集合類,它們提供了更安全的get()方法(也會拋出越界異常,但信息更明確)
2.3 ClassCastException(類型轉(zhuǎn)換異常)
現(xiàn)象描述
當(dāng)試圖將一個對象強(qiáng)制轉(zhuǎn)換為它不是實例的子類時拋出。這是使用繼承和多態(tài)時的常見問題。
出現(xiàn)場景
場景一:將父類對象強(qiáng)制轉(zhuǎn)換為子類類型
Object obj = new Object(); Integer num = (Integer) obj; // Object不能轉(zhuǎn)換為Integer
場景二:集合中元素類型不一致
List list = new ArrayList();
list.add("Hello");
list.add(123); // 混合類型
String first = (String) list.get(0); // 正常
String second = (String) list.get(1); // 拋出ClassCastException,123不能轉(zhuǎn)String
場景三:不正確的向下轉(zhuǎn)型
Animal animal = new Dog(); Cat cat = (Cat) animal; // Dog不能轉(zhuǎn)換為Cat
堆棧分析示例
Exception in thread "main" java.lang.ClassCastException: class java.lang.Integer cannot be cast to class java.lang.String
at com.example.GenericDemo.processList(GenericDemo.java:22)
at com.example.GenericDemo.main(GenericDemo.java:15)
異常信息明確告訴我們:試圖將Integer轉(zhuǎn)換為String,類型不兼容。
排查方法

代碼修正與預(yù)防
修正方案一:使用泛型
// 錯誤代碼
List list = new ArrayList();
list.add("Hello");
String s = (String) list.get(0);
// 修正代碼:使用泛型
List<String> list = new ArrayList<>();
list.add("Hello");
String s = list.get(0); // 無需強(qiáng)制轉(zhuǎn)換
修正方案二:使用instanceof檢查
Object obj = getSomeObject();
if (obj instanceof String) {
String str = (String) obj; // 安全轉(zhuǎn)換
// 處理字符串
} else if (obj instanceof Integer) {
Integer num = (Integer) obj; // 安全轉(zhuǎn)換
// 處理整數(shù)
}
修正方案三:Java 16+的Pattern Matching for instanceof
Object obj = getSomeObject();
if (obj instanceof String str) {
// 這里可以直接使用str變量
System.out.println(str.length());
} else if (obj instanceof Integer num) {
System.out.println(num + 10);
} else {
// 處理其他情況
}
預(yù)防措施:
- 始終使用泛型確保集合類型安全
- 在向下轉(zhuǎn)型前使用
instanceof檢查 - 遵循里氏替換原則,避免不必要的向下轉(zhuǎn)型
- 考慮使用多態(tài),而不是頻繁的類型轉(zhuǎn)換
2.4 ArithmeticException(算術(shù)異常)
現(xiàn)象描述
當(dāng)發(fā)生異常的算術(shù)條件時拋出,最常見的是整數(shù)除零。
出現(xiàn)場景
場景一:整數(shù)除零
int result = 10 / 0; // 拋出ArithmeticException
場景二:取模運算除零
int remainder = 10 % 0; // 拋出ArithmeticException
注意:浮點數(shù)除零不會拋出異常,會返回Infinity或NaN
double result = 10.0 / 0.0; // 返回 Infinity,不會拋出異常
堆棧分析示例
Exception in thread "main" java.lang.ArithmeticException: / by zero
at com.example.Calculator.divide(Calculator.java:10)
at com.example.Calculator.main(Calculator.java:5)
異常信息直接告訴我們問題:除零。
排查方法

代碼修正與預(yù)防
修正方案一:檢查除數(shù)
public int divide(int a, int b) {
if (b == 0) {
throw new IllegalArgumentException("除數(shù)不能為0");
}
return a / b;
}
修正方案二:使用Optional處理可能為0的情況
public Optional<Integer> safeDivide(int a, int b) {
if (b == 0) {
return Optional.empty();
}
return Optional.of(a / b);
}
修正方案三:使用浮點數(shù)運算(如果業(yè)務(wù)允許)
double result = 10.0 / 0.0; // 返回 Infinity,不會拋出異常
if (Double.isInfinite(result)) {
// 處理無窮大情況
}
預(yù)防措施:
- 在進(jìn)行除法或取模前,始終檢查除數(shù)是否為0
- **考慮使用
BigDecimal**進(jìn)行精確計算,它提供了更好的異常處理 - 從用戶輸入獲取除數(shù)時,必須進(jìn)行驗證
第二課時(中):運行時異常(續(xù))與常見受檢異常(約20分鐘)
2.5 NumberFormatException(數(shù)字格式異常)
現(xiàn)象描述
當(dāng)嘗試將字符串轉(zhuǎn)換為數(shù)字類型,但字符串格式不合法時拋出。
出現(xiàn)場景
場景一:字符串包含非數(shù)字字符
int num = Integer.parseInt("123abc"); // 拋出NumberFormatException
場景二:字符串包含空格或特殊符號
int num = Integer.parseInt(" 123 "); // 拋出NumberFormatException,空格未處理
場景三:數(shù)字超出類型范圍
int num = Integer.parseInt("2147483648"); // 超出int最大值,拋出異常
場景四:空字符串或null
int num = Integer.parseInt(""); // 拋出NumberFormatException
Integer.parseInt(null); // 拋出NullPointerException,注意這里是NPE
代碼修正與預(yù)防
修正方案一:數(shù)據(jù)清洗
// 錯誤代碼
String input = " 123 ";
int value = Integer.parseInt(input); // 拋出異常
// 修正代碼
String input = " 123 ";
input = input.trim(); // 去除前后空格
if (!input.isEmpty()) {
try {
int value = Integer.parseInt(input);
} catch (NumberFormatException e) {
// 處理異常
}
}
修正方案二:使用正則表達(dá)式預(yù)驗證
public int parsePostalCode(String input) {
// 預(yù)驗證:必須是5位數(shù)字
if (input == null || !input.matches("\\d{5}")) {
throw new IllegalArgumentException("郵政編碼必須是5位數(shù)字");
}
return Integer.parseInt(input); // 此時已保證安全
}
修正方案三:使用Apache Commons Lang的NumberUtils
import org.apache.commons.lang3.math.NumberUtils; String input = "123"; int value = NumberUtils.toInt(input, 0); // 失敗時返回默認(rèn)值0,不拋出異常
修正方案四:Java 8+的Optional + 異常處理
public Optional<Integer> tryParseInt(String input) {
try {
return Optional.of(Integer.parseInt(input.trim()));
} catch (NumberFormatException e) {
return Optional.empty();
}
}
預(yù)防措施 :
- 始終對輸入進(jìn)行清洗(trim、去除非數(shù)字字符)
- 解析前驗證格式,特別是來自外部系統(tǒng)的數(shù)據(jù)
- 考慮使用專門的驗證框架如Hibernate Validator
- 使用
try-catch包圍解析代碼,優(yōu)雅處理異常
2.6 IllegalArgumentException(非法參數(shù)異常)
現(xiàn)象描述
當(dāng)向方法傳遞了不合法或不適當(dāng)?shù)膮?shù)時拋出。這通常表示調(diào)用者的責(zé)任。
出現(xiàn)場景
場景一:參數(shù)值超出允許范圍
public void setAge(int age) {
if (age < 0 || age > 150) {
throw new IllegalArgumentException("年齡必須在0-150之間");
}
this.age = age;
}
場景二:參數(shù)格式錯誤
public void setEmail(String email) {
if (email == null || !email.contains("@")) {
throw new IllegalArgumentException("郵箱格式不正確");
}
this.email = email;
}
場景三:參數(shù)為null但方法不允許
public void processData(@NonNull Data data) {
if (data == null) {
throw new IllegalArgumentException("data cannot be null");
}
// 處理數(shù)據(jù)
}
排查方法
- 查看異常消息,通常會說明參數(shù)需要滿足什么條件
- 檢查調(diào)用代碼,確認(rèn)傳入的參數(shù)值
- 驗證參數(shù)來源,判斷是輸入錯誤還是上游數(shù)據(jù)問題
代碼修正
// 在方法開頭進(jìn)行參數(shù)校驗
public void registerUser(String username, String email, int age) {
// 參數(shù)校驗集中處理
if (username == null || username.trim().isEmpty()) {
throw new IllegalArgumentException("用戶名不能為空");
}
if (email == null || !email.matches("^[A-Za-z0-9+_.-]+@(.+)$")) {
throw new IllegalArgumentException("郵箱格式不正確");
}
if (age < 0 || age > 150) {
throw new IllegalArgumentException("年齡無效");
}
// 業(yè)務(wù)邏輯
}
2.7 IllegalStateException(非法狀態(tài)異常)
現(xiàn)象描述
當(dāng)方法在非法或不適當(dāng)?shù)臅r間被調(diào)用時拋出。通常表示被調(diào)用者的狀態(tài)不適合執(zhí)行請求的操作。
出現(xiàn)場景
場景一:對象未正確初始化
public class ConnectionPool {
private boolean initialized = false;
public void connect() {
if (!initialized) {
throw new IllegalStateException("連接池未初始化");
}
// 建立連接
}
}
場景二:迭代器越界
List<String> list = Arrays.asList("A", "B");
Iterator<String> it = list.iterator();
it.next(); // A
it.next(); // B
it.next(); // 拋出NoSuchElementException,但有時會被包裝為IllegalStateException
排查方法
- 閱讀異常消息,了解當(dāng)前對象應(yīng)該處于什么狀態(tài)
- 檢查對象初始化或配置代碼,確保在調(diào)用前已正確設(shè)置
- 檢查操作順序,確認(rèn)是否按正確步驟調(diào)用
代碼修正
public class FileProcessor {
private boolean opened = false;
public void open() {
// 打開文件
opened = true;
}
public void readData() {
if (!opened) {
throw new IllegalStateException("必須先調(diào)用open()方法打開文件");
}
// 讀取數(shù)據(jù)
}
}
2.8 IOException(輸入輸出異常)
現(xiàn)象描述
當(dāng)輸入輸出操作失敗或中斷時拋出。這是最典型的受檢異常,處理文件、網(wǎng)絡(luò)、流操作時經(jīng)常遇到。
出現(xiàn)場景
場景一:文件不存在(FileNotFoundException)
FileReader fr = new FileReader("nonexistent.txt"); // 拋出FileNotFoundException
場景二:讀取流時連接斷開
InputStream in = socket.getInputStream(); int data = in.read(); // 如果連接已關(guān)閉,可能拋出IOException
場景三:寫入磁盤空間不足
FileOutputStream fos = new FileOutputStream("largefile.bin");
byte[] data = new byte[1024];
fos.write(data); // 如果磁盤空間不足,拋出IOException
堆棧分析示例
java.io.FileNotFoundException: nonexistent.txt (系統(tǒng)找不到指定的文件)
at java.base/java.io.FileInputStream.open0(Native Method)
at java.base/java.io.FileInputStream.open(FileInputStream.java:219)
at java.base/java.io.FileInputStream.<init>(FileInputStream.java:157)
at com.example.FileReaderDemo.main(FileReaderDemo.java:8)
排查方法

代碼修正與預(yù)防
修正方案一:使用try-with-resources確保資源關(guān)閉
// 錯誤代碼:可能忘記關(guān)閉資源
public String readFile(String path) throws IOException {
FileReader fr = new FileReader(path);
BufferedReader br = new BufferedReader(fr);
return br.readLine();
// 沒有關(guān)閉資源,可能造成內(nèi)存泄漏
}
// 修正代碼:使用try-with-resources
public String readFile(String path) throws IOException {
try (FileReader fr = new FileReader(path);
BufferedReader br = new BufferedReader(fr)) {
return br.readLine();
} // 自動關(guān)閉
}
修正方案二:檢查文件存在性
public void processFile(String path) {
File file = new File(path);
if (!file.exists()) {
System.err.println("文件不存在: " + path);
return; // 或者拋出更友好的異常
}
try (BufferedReader br = new BufferedReader(new FileReader(file))) {
// 處理文件
} catch (IOException e) {
System.err.println("讀取文件時發(fā)生錯誤: " + e.getMessage());
e.printStackTrace();
}
}
修正方案三:多層異常處理
public void copyFile(String src, String dest) {
try (FileInputStream in = new FileInputStream(src);
FileOutputStream out = new FileOutputStream(dest)) {
byte[] buffer = new byte[1024];
int length;
while ((length = in.read(buffer)) > 0) {
out.write(buffer, 0, length);
}
} catch (FileNotFoundException e) {
System.err.println("源文件不存在或目標(biāo)目錄無法寫入: " + e.getMessage());
} catch (IOException e) {
System.err.println("復(fù)制過程中發(fā)生IO錯誤: " + e.getMessage());
}
}
預(yù)防措施:
- 始終使用try-with-resources或確保finally中關(guān)閉資源
- 操作前檢查文件和目錄狀態(tài)
- 為IO操作提供有意義的錯誤消息
- 考慮重試機(jī)制,特別是網(wǎng)絡(luò)相關(guān)的IO操作
2.9 ClassNotFoundException(類未找到異常)
現(xiàn)象描述
當(dāng)應(yīng)用程序試圖通過字符串名加載類,但在類路徑中找不到該類的定義時拋出。
出現(xiàn)場景
場景一:Class.forName()加載類
Class.forName("com.mysql.jdbc.Driver"); // 如果驅(qū)動jar不在類路徑中,拋出異常
場景二:類加載器加載類
ClassLoader.getSystemClassLoader().loadClass("com.example.MissingClass");
場景三:使用反射創(chuàng)建實例
Object obj = Class.forName("com.example.DynamicClass").newInstance();
堆棧分析示例
java.lang.ClassNotFoundException: com.mysql.jdbc.Driver
at java.base/java.net.URLClassLoader.findClass(URLClassLoader.java:476)
at java.base/java.lang.ClassLoader.loadClass(ClassLoader.java:589)
at java.base/java.lang.Class.forName0(Native Method)
at java.base/java.lang.Class.forName(Class.java:398)
排查方法

代碼修正與預(yù)防
修正方案一:確保JAR包在類路徑中
- 對于Maven項目:檢查pom.xml中的依賴
- 對于普通項目:確認(rèn)JAR文件在classpath下
修正方案二:捕獲并處理異常
try {
Class.forName("com.mysql.jdbc.Driver");
} catch (ClassNotFoundException e) {
// 提供友好的錯誤信息
throw new RuntimeException("MySQL驅(qū)動未找到,請檢查是否添加了mysql-connector-java依賴", e);
}
修正方案三:使用ServiceLoader模式(Java 6+)
ServiceLoader<Driver> drivers = ServiceLoader.load(Driver.class);
for (Driver driver : drivers) {
// 自動發(fā)現(xiàn)所有驅(qū)動實現(xiàn)
}
第二課時(下):綜合實戰(zhàn)與最佳實踐(約10分鐘)
3.1 復(fù)雜異常排查案例
案例:銀行轉(zhuǎn)賬系統(tǒng)中的異常鏈
public class BankingService {
public void transfer(String fromAccount, String toAccount, double amount)
throws BusinessException {
try {
Account from = accountRepository.findByNumber(fromAccount);
Account to = accountRepository.findByNumber(toAccount);
if (from == null || to == null) {
throw new IllegalArgumentException("賬戶不存在");
}
from.withdraw(amount);
to.deposit(amount);
transactionLog.log(fromAccount, toAccount, amount);
} catch (IllegalArgumentException e) {
throw new BusinessException("轉(zhuǎn)賬參數(shù)錯誤", e);
} catch (InsufficientBalanceException e) {
throw new BusinessException("余額不足", e);
} catch (Exception e) {
throw new BusinessException("轉(zhuǎn)賬失敗,請稍后重試", e);
}
}
}
異常排查思路
當(dāng)看到類似下面的異常堆棧時:
com.example.BusinessException: 轉(zhuǎn)賬失敗,請稍后重試
at com.example.BankingService.transfer(BankingService.java:45)
at com.example.BankingController.main(BankingController.java:18)
Caused by: java.sql.SQLException: Connection timed out
at com.mysql.jdbc.ConnectionImpl.createNewIO(ConnectionImpl.java:2189)
at com.mysql.jdbc.ConnectionImpl.<init>(ConnectionImpl.java:795)
at com.mysql.jdbc.NonRegisteringDriver.connect(NonRegisteringDriver.java:329)
at java.sql.DriverManager.getConnection(DriverManager.java:664)
at com.example.AccountRepository.findByNumber(AccountRepository.java:22)
... 5 more
排查步驟:
- 看頂層異常:
BusinessException,但消息太泛化,請稍后重試沒有實質(zhì)信息 - 看Caused by:
SQLException: Connection timed out,這才是真正原因 - 追溯源頭:
AccountRepository.java:22處的數(shù)據(jù)庫連接超時 - 根本原因:數(shù)據(jù)庫連接失敗
解決方案:
- 檢查數(shù)據(jù)庫服務(wù)是否運行
- 檢查網(wǎng)絡(luò)連接
- 檢查數(shù)據(jù)庫連接池配置
- 添加重試機(jī)制
教訓(xùn):包裝異常時不要丟失原始信息,提供具體的錯誤消息有助于排查。
3.2 異常處理最佳實踐總結(jié)
捕獲特定異常,而不是通用異常
// 不好的做法
try {
// 業(yè)務(wù)代碼
} catch (Exception e) {
// 捕獲所有異常,掩蓋了真正的問題
}
// 好的做法
try {
// 業(yè)務(wù)代碼
} catch (FileNotFoundException e) {
// 處理文件不存在
} catch (IOException e) {
// 處理其他IO錯誤
}
避免空的catch塊
// 絕對不要這樣做
try {
riskyOperation();
} catch (Exception e) {
// 空的catch塊,異常被吞噬
}
// 至少記錄異常
try {
riskyOperation();
} catch (Exception e) {
logger.error("操作失敗", e); // 記錄日志
throw e; // 或者重新拋出
}
使用try-with-resources自動關(guān)閉資源
// Java 7之前的方式
FileInputStream fis = null;
try {
fis = new FileInputStream("file.txt");
// 處理文件
} finally {
if (fis != null) {
try {
fis.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
// Java 7+ 推薦的方式
try (FileInputStream fis = new FileInputStream("file.txt")) {
// 處理文件
} // 自動關(guān)閉
使用自定義異常增強(qiáng)業(yè)務(wù)語義
// 自定義業(yè)務(wù)異常
public class InsufficientBalanceException extends Exception {
private double currentBalance;
private double requiredAmount;
public InsufficientBalanceException(double current, double required) {
super(String.format("余額不足:當(dāng)前余額%.2f,需要%.2f", current, required));
this.currentBalance = current;
this.requiredAmount = required;
}
// getters...
}
// 使用
public void withdraw(double amount) throws InsufficientBalanceException {
if (balance < amount) {
throw new InsufficientBalanceException(balance, amount);
}
balance -= amount;
}
方法重寫時遵守異常聲明規(guī)則
- 子類方法可以拋出與父類相同的異常、子類異常,或不拋出異常
- 子類方法不能拋出比父類更寬泛的受檢異常
class Parent {
public void process() throws IOException { }
}
class Child extends Parent {
@Override
public void process() throws FileNotFoundException { } // 允許,F(xiàn)ileNotFoundException是IOException的子類
// @Override
// public void process() throws Exception { } // 不允許,Exception比IOException更寬泛
}
記錄異常時包含上下文信息
try {
processOrder(orderId, userId);
} catch (OrderException e) {
// 記錄有用的上下文信息
logger.error("處理訂單失敗: orderId={}, userId={}", orderId, userId, e);
throw e;
}
不要用異常控制正常的程序流程
// 不好的做法:用異??刂屏鞒?
try {
Integer.parseInt(userInput);
// 是數(shù)字,繼續(xù)處理
} catch (NumberFormatException e) {
// 不是數(shù)字,執(zhí)行其他邏輯
}
// 好的做法:使用條件判斷
if (userInput.matches("\\d+")) {
int value = Integer.parseInt(userInput);
// 是數(shù)字,繼續(xù)處理
} else {
// 不是數(shù)字,執(zhí)行其他邏輯
}
異常處理的黃金法則總結(jié)
| 原則 | 說明 |
|---|---|
| 精準(zhǔn)捕獲 | 捕獲具體的異常類型,而不是籠統(tǒng)的Exception |
| 絕不吞噬 | 空的catch塊是萬惡之源,至少要記錄日志 |
| 及時釋放 | 使用try-with-resources或finally確保資源釋放 |
| 保留原始異常 | 包裝異常時要把原異常作為cause傳入 |
| 提供上下文 | 異常消息要包含有助于排查的信息 |
| 區(qū)分異常類型 | 可恢復(fù)用受檢異常,程序錯誤用運行時異常 |
| 文檔化 | 用javadoc的@throws說明方法可能拋出的異常 |
3.3 Java 7+ 多異常捕獲
從Java 7開始,可以使用|在一個catch塊中捕獲多個異常類型,減少代碼重復(fù):
try {
// 可能拋出多種異常的代碼
} catch (IOException | SQLException e) {
// 統(tǒng)一處理IO和SQL異常
logger.error("數(shù)據(jù)訪問錯誤", e);
throw e; // Java 7+ 支持更精確的重拋類型檢查
}
注意:多異常捕獲時,catch參數(shù)隱式為final,不能修改。
3.4 異常處理與事務(wù)管理
在企業(yè)級應(yīng)用中,異常處理與事務(wù)管理密切相關(guān)。通常:
- 運行時異常觸發(fā)事務(wù)回滾
- 受檢異常不自動觸發(fā)事務(wù)回滾(在Spring中可通過
rollbackFor配置)
@Service
public class AccountService {
@Transactional(rollbackFor = {BusinessException.class, RuntimeException.class})
public void transferMoney(String from, String to, double amount)
throws BusinessException {
try {
// 轉(zhuǎn)賬邏輯
} catch (InsufficientBalanceException e) {
// 業(yè)務(wù)異常,觸發(fā)事務(wù)回滾
throw new BusinessException("轉(zhuǎn)賬失敗", e);
}
}
}
課程總結(jié)(約5分鐘)
知識體系回顧
通過兩個課時的學(xué)習(xí),我們?nèi)娓采w了:
- 異?;A(chǔ):體系結(jié)構(gòu)、受檢與非受檢異常、異常信息解讀
- 運行時異常:
NullPointerException:空引用訪問 → 前置檢查、OptionalArrayIndexOutOfBoundsException:數(shù)組越界 → 邊界檢查、增強(qiáng)for循環(huán)ClassCastException:類型轉(zhuǎn)換錯誤 → instanceof檢查、泛型ArithmeticException:算術(shù)異常 → 除零檢查NumberFormatException:數(shù)字格式錯誤 → 輸入清洗、預(yù)驗證IllegalArgumentException/IllegalStateException:參數(shù)/狀態(tài)錯誤 → 前置校驗
- 受檢異常:
IOException及其子類 → try-with-resources、文件存在性檢查ClassNotFoundException→ 檢查類路徑、依賴管理
- 排查方法:堆棧分析、Caused by追蹤、異常鏈理解
- 最佳實踐:精準(zhǔn)捕獲、避免吞噬、及時釋放、保留上下文等8大原則
異常排查思維導(dǎo)圖
遇到異常時,按以下順序思考:
┌─────────────────────────────────────┐
│ 1. 看類型:是什么異常?屬于哪一類? │
├─────────────────────────────────────┤
│ 2. 看消息:異常說了什么?有什么線索? │
├─────────────────────────────────────┤
│ 3. 看堆棧:第一行自己的代碼在哪? │
├─────────────────────────────────────┤
│ 4. 看原因:有Caused by嗎?底層是什么? │
├─────────────────────────────────────┤
│ 5. 想來源:這個值從哪來的?誰傳的? │
├─────────────────────────────────────┤
│ 6. 想方案:怎么修復(fù)?如何預(yù)防? │
└─────────────────────────────────────┘
以上就是全面解析Java中常見Exception異常的錯誤排查與代碼修正的詳細(xì)內(nèi)容,更多關(guān)于Java常見Exception異常的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Java數(shù)據(jù)庫連接池技術(shù)的入門教程
這篇文章主要給大家介紹了關(guān)于Java數(shù)據(jù)庫連接池技術(shù)的相關(guān)資料,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2021-03-03
Java使用?Spire.PDF實現(xiàn)合并PDF文檔并進(jìn)行加密與壓縮處理
無論是需要將零散的業(yè)務(wù)報告匯總,還是將多份合同掃描件整合成一個文件,掌握合并?PDF?的技巧都是提升辦公效率的必備技能,本文將介紹如何在?Java?中使用?Spire.PDF?庫高效合并?PDF?文件,感興趣的小伙伴可以了解下2026-04-04
實戰(zhàn)干貨之基于SpringBoot的RabbitMQ多種模式隊列
RabbitMQ 是一個由Erlang語言開發(fā)的AMQP的開源實現(xiàn),支持多種客戶端。用于在分布式系統(tǒng)中存儲轉(zhuǎn)發(fā)消息,在易用性、擴(kuò)展性、高可用性等方面表現(xiàn)不俗,下文將帶你深入了解 RabbitMQ 多種模式隊列2021-09-09
Java通過PowerMockito和Mokito進(jìn)行單元測試的實現(xiàn)
PowerMockito和Mockito都是Java語言中的測試框架,用于進(jìn)行單元測試和集成測試,本文就來詳細(xì)的介紹一下通過PowerMockito和Mokito進(jìn)行單元測試,感興趣的可以了解一下2023-08-08

