โหมดมืด
บทที่ 8 — Exception Handling
ในโลกที่สมบูรณ์แบบโปรแกรมจะทำงานตามที่เขียนเสมอ แต่โลกจริงไม่ใช่:
- ผู้ใช้กรอกข้อมูลผิด format
- ไฟล์ที่จะอ่านไม่มีอยู่
- network ขาด
- หารด้วย 0
- array index เกิน
Exception = "ความผิดปกติที่เกิดขึ้นตอน run" — Java มีกลไก (mechanism — วิธีการ/ระบบ) จัดการเรื่องนี้
หลังจบบท คุณจะ:
- เข้าใจว่า exception ทำงานยังไง (stack trace)
- ใช้ try/catch/finally จับ exception
- เข้าใจ checked vs unchecked exception
- สร้าง custom exception ของตัวเอง
- ใช้ try-with-resources จัดการ resource
- รู้ best practice — เมื่อไหร่ throw, เมื่อไหร่ catch
1. Exception คืออะไร — ตัวอย่าง
java
public class Main {
public static void main(String[] args) {
int[] arr = {1, 2, 3};
System.out.println(arr[10]); // 💥 error ตรงนี้!
}
}ผลลัพธ์:
text
Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: Index 10 out of bounds for length 3
at Main.main(Main.java:4)อ่านได้ว่า:
thread "main"= ชื่อของเส้นทางการทำงานหลักของโปรแกรม (มีแค่เส้นเดียวตอนนี้ — thread คือเส้นทางการทำงานของโปรแกรม จะเรียนละเอียดบทที่ 12)- มี exception ชนิด
ArrayIndexOutOfBoundsException - Message:
Index 10 out of bounds for length 3 at Main.main(Main.java:4)= error เกิดที่บรรทัด 4 ในไฟล์ Main.java ใน method main
นี่คือ stack trace (ร่องรอยการเรียก method — แสดงลำดับที่โปรแกรมเดินทางก่อนเกิด error) — แสดงว่า exception เกิดที่ไหน และเรียกจากไหนมาบ้าง
โปรแกรม crash — JVM stop ทันที
2. ตัวอย่าง exception ที่เจอบ่อย
| Exception | เกิดเมื่อ |
|---|---|
NullPointerException (NPE) | เรียก method/field บน null |
ArrayIndexOutOfBoundsException | index ของ array เกินขอบเขต |
ClassCastException | cast type ผิด |
NumberFormatException | parse string ผิด format |
ArithmeticException | หารด้วย 0 (เฉพาะ int — double จะได้ Infinity) |
IllegalArgumentException | argument ผิด |
IllegalStateException | state ของ object ผิด |
IOException | อ่าน/เขียนไฟล์ผิด |
ConcurrentModificationException | แก้ collection ระหว่าง iterate |
3. try / catch / finally
java
try {
// โค้ดที่อาจ throw exception
int[] arr = {1, 2, 3};
System.out.println(arr[10]);
} catch (ArrayIndexOutOfBoundsException e) {
// ทำเมื่อ exception เกิด
System.out.println("Caught: " + e.getMessage());
} finally {
// ทำเสมอ (มี exception หรือไม่)
System.out.println("Cleanup");
}ผลลัพธ์:
text
Caught: Index 10 out of bounds for length 3
Cleanupโปรแกรม ไม่ crash — เพราะ exception ถูก catch
3.1 catch หลายชนิด
java
try {
// ...
} catch (NumberFormatException e) {
System.out.println("Bad number format");
} catch (ArithmeticException e) {
System.out.println("Math error");
} catch (Exception e) {
System.out.println("Other error");
}Java เช็คจากบนลงล่าง — ใช้ catch ตัวแรกที่ match
⚠️ เรียงจาก specific → general — ไม่งั้น catch ทั่วไปจะ block ตัวล่าง:
java
catch (Exception e) { ... } // จับทุกอย่าง
catch (NumberFormatException e) { ... } // ❌ unreachable — compile error3.2 multi-catch (Java 7+)
java
try {
// ...
} catch (NumberFormatException | ArithmeticException e) {
System.out.println("Number or math error");
}3.3 finally — ทำเสมอ
java
try {
// เปิด file
return result; // แม้ return อยู่ใน try, finally ก็ run (result เป็นตัวแปรตัวอย่าง)
} catch (Exception e) {
// ...
} finally {
// ปิด file — run แม้เกิด exception
}ใช้สำหรับ cleanup — ปิด file, ปิด connection, ฯลฯ
3.4 ⚠️ Finally Pitfalls
Pitfall 1: return ใน finally — กลบ return/exception ของ try
java
public int f() {
try {
return 1;
} finally {
return 2; // ❌ ชนะ → ได้ 2!
}
}
public int g() {
try {
throw new RuntimeException("oops");
} finally {
return 0; // ❌ exception หาย → ได้ 0
}
}กฎ: ห้าม return หรือ throw ใน finally — กลบทุกอย่างของ try/catch แบบเงียบ
Pitfall 2: System.exit() ใน try → finally ไม่ run
java
try {
System.exit(0);
} finally {
cleanup(); // ❌ ไม่ถูกเรียก
}📌 Pitfall เรื่อง thread (
InterruptedException+Thread.sleepในfinally) — จะอธิบายในบทที่ 12 (Concurrency) ตอนเรียน thread แล้ว ตอนนี้ยังไม่ต้องรู้
4. Throw — โยน exception ขึ้นมาเอง
java
public static void divide(int a, int b) {
if (b == 0) {
throw new ArithmeticException("Cannot divide by zero");
}
System.out.println(a / b);
}
divide(10, 0); // throw → ถ้าไม่ catch จะ crashthrow = "ออกจาก method ทันที + ส่ง exception ขึ้นไปให้ caller จัดการ"
5. Exception hierarchy
📌 แผนภาพแสดงเฉพาะ exception หลัก — exception อื่น ๆ เช่น
ConcurrentModificationExceptionและIllegalStateExceptionก็เป็น RuntimeException เช่นกัน (ดูตารางใน section 2)
5.1 Checked vs Unchecked
สรุปสั้น ๆ ก่อน: Checked = Java บังคับให้เขียน try/catch (ถ้าไม่เขียน compile ไม่ผ่าน), Unchecked = ไม่บังคับ แต่ถ้าเกิดขึ้นโปรแกรมหยุดทันที
🐍 ถ้ามาจาก Python/JS: ภาษาเหล่านั้นไม่มี checked exception — ทุก exception เหมือนกันหมด ถ้าไม่ try ก็ crash Java ต่างจากนี้ — มี 2 ประเภท:
- Checked = compiler บังคับให้คุณ "รับรู้" ว่า method นี้อาจพัง (ต้อง try หรือ declare throws ไม่งั้น compile ไม่ผ่าน)
- Unchecked = เหมือน Python/JS (ไม่บังคับอะไร — เป็น bug ที่ควรแก้)
📖
throw(กริยา) vsthrows(declaration) — 2 คำคล้ายกันแต่คนละหน้าที่:
throw new IOException(...)= โยนตอนนี้ (อยู่ใน method body)void readFile() throws IOException= ประกาศว่า method อาจโยน (อยู่หลัง parameter list)
Unchecked (RuntimeException + descendants)
- ไม่บังคับ catch — compiler ไม่บ่น (เหมือน Python/JS)
- เกิดจาก programmer error — ควรแก้โค้ดไม่ใช่ catch
- ตัวอย่าง: NullPointerException, IllegalArgumentException
Checked (Exception แต่ไม่ใช่ RuntimeException) — เป็น subclass ของ Exception แต่ไม่ใช่ subclass ของ RuntimeException
- บังคับ catch หรือ throws — ถ้าไม่ catch และไม่ declare throws → compile ไม่ผ่าน (ไม่ใช่ warning)
- เกิดจาก external — เช่น file ไม่มี, network ขาด (สถานการณ์ที่ "คาดได้ + นอกการควบคุม")
- ตัวอย่าง: IOException, SQLException
5.2 ตัวอย่าง checked exception
💡 Note: ตัวอย่างนี้ใช้
FileReaderเพื่ออธิบาย checked exception — วิธีอ่านไฟล์แบบที่ใช้งานจริงจะสอนบทที่ 11 ข้อควรรู้: FileReader ใช้ platform default charset — ก่อน Java 18: Windows อาจเป็น CP1252/TIS-620 แทน UTF-8 ทำให้อ่านพัง; Java 18+ (JEP 400) default เป็น UTF-8 แล้วทั้งบน Windows และ Linux
⚠️ อย่าใช้ FileReader ใน production โดยตรง — ใช้
Files.readString(Path.of("data.txt"), StandardCharsets.UTF_8)หรือFiles.newBufferedReader(Path.of("data.txt"), StandardCharsets.UTF_8)แทน (บทที่ 11)💡 Java 11+:
FileReaderมี constructor รับCharsetโดยตรง:new FileReader("data.txt", StandardCharsets.UTF_8)— ใช้แทน default charset ได้โดยไม่ต้องเปลี่ยน class ถ้าต้องการใช้FileReaderเป็นการเฉพาะ
📌 โค้ดด้านล่างเป็น fragment แสดงแนวคิด — ต้องวางไว้ใน class เช่น
public class Example { ... }ก่อนนำไปรัน
java
import java.io.FileReader;
import java.io.IOException;
// ต้องอยู่ใน class ก่อน เช่น: public class Example {
public static void readFile() {
FileReader reader = new FileReader("data.txt"); // ❌ compile error
}Error:
text
unreported exception FileNotFoundException; must be caught or declaredแปลว่า: "มี exception (FileNotFoundException) ที่ยังไม่ได้รายงาน — ต้อง catch หรือ declare throws"
แก้แบบที่ 1: catch
java
import java.io.FileReader;
import java.io.FileNotFoundException;
// ต้องอยู่ใน class และ method ก่อน เช่น: public static void readFile() {
try {
FileReader reader = new FileReader("data.txt");
// ❌ resource leak — reader ไม่ถูกปิด ดูตัวอย่าง try-with-resources ที่ §7
} catch (FileNotFoundException e) {
System.out.println("File not found");
}แก้แบบที่ 2: throws (โยนต่อให้ caller)
java
public static void readFile() throws IOException { // IOException ครอบ FileNotFoundException + IO error อื่น
FileReader reader = new FileReader("data.txt");
// อ่านไฟล์อาจ throw IOException เช่นกัน — declare ครอบไปเลย
}throws = "method นี้อาจ throw exception นี้ — ผู้เรียกจัดการเอง"
5.3 ทำไมต้องมีทั้ง 2 แบบ
Checked บังคับเขียนโค้ดจัดการ error ที่ "คาดได้ + นอกการควบคุม" (file/network) Unchecked ไม่บังคับเพราะ "ควรเป็น bug ที่โค้ดแก้ ไม่ใช่ try/catch"
Debate: ภาษาใหม่ ๆ (Kotlin, C#) ไม่มี checked exception — ถือว่ายุ่งยากเกินไป Java ยังมี — ทำให้บางทีรู้สึกซ้ำซ้อน
6. Custom Exception
สร้าง exception ของเราเอง:
java
public class InsufficientFundsException extends RuntimeException {
public InsufficientFundsException(String message) {
super(message);
}
public InsufficientFundsException(String message, Throwable cause) {
super(message, cause); // ให้ caller wrap exception อื่นได้
}
}
// ใช้
class Account {
private double balance = 100;
public void withdraw(double amount) {
if (amount > balance) {
throw new InsufficientFundsException(
"Tried to withdraw " + amount + ", balance is " + balance);
}
balance -= amount;
}
}ทำไม custom:
- ชื่อบอกความหมายชัด
- catch แยกได้
- เพิ่ม field ที่ใช้ได้ (เช่น
amount,accountId)
Checked vs unchecked custom
java
// Unchecked — extends RuntimeException
public class InsufficientFundsException extends RuntimeException { ... }
// Checked — extends Exception
public class AccountClosedException extends Exception { ... }Default ใช้ unchecked — ทำให้โค้ดสะอาดกว่า
💡 ข้อถกเถียง: Effective Java (Item 71) แนะนำให้ใช้ checked สำหรับ error ที่ "caller กู้คืนได้จริง" (recoverable) เช่น file ไม่เจอ → ลองสร้าง path ใหม่ ส่วน unchecked สำหรับ programmer error ที่ caller แก้อะไรไม่ได้
boilerplate = โค้ดซ้ำ ๆ ที่ต้องเขียนทุกครั้งโดยไม่ค่อยเพิ่มคุณค่า เช่น try/catch ที่บังคับเขียนทั้งที่ไม่รู้จะทำอะไรกับ exception นั้น
ในทางปฏิบัติ codebase ใหม่ ๆ มักเลือก unchecked เพราะลด boilerplate แบบนี้ — แต่ก็ไม่ใช่ all-or-nothing
7. try-with-resources
ปัญหา: ต้องปิด resource (file, connection) เสมอ แม้เกิด exception
แบบเก่า:
java
FileReader reader = null;
try {
reader = new FileReader("data.txt");
// ...
} catch (IOException e) {
// handle
} finally {
if (reader != null) {
try {
reader.close();
} catch (IOException e) {
// ignore
}
}
}แบบใหม่ (Java 7+):
📌 ถ้าจะรันตัวอย่างนี้จริง ต้องสร้างไฟล์
data.txtก่อน — ใน terminal พิมพ์:echo hello > data.txt(Windows CMD/PowerShell) หรือecho hello > data.txt(macOS/Linux)
java
try (FileReader reader = new FileReader("data.txt")) {
// ...
} catch (IOException e) {
// handle
}
// reader.close() ถูกเรียกอัตโนมัติJava จะเรียก .close() ให้อัตโนมัติเมื่อออกจาก try — ปลอดภัยมาก
ใช้กับ class ที่ implement AutoCloseable
หลาย resource
java
try (
FileReader reader = new FileReader("input.txt");
FileWriter writer = new FileWriter("output.txt")
) {
// ...
}ปิดทั้งสองอัตโนมัติ (ปิด writer ก่อน reader — ตามลำดับกลับด้านของการเปิด)
7.5 Suppressed Exceptions (exception ที่ถูกเก็บกดไว้ — แนบไว้ในตัวแม่ ไม่ทิ้งหายไป) — เมื่อ exception ซ้อน exception
📌 คำว่า "suppress" ในที่นี้หมายถึงการ "ซ่อน/เก็บไว้แนบกับ exception หลัก" — ต่างจาก
@SuppressWarningsที่ใช้ปิดคำเตือน compiler
ปัญหา: ถ้า try body throw exception แล้ว .close() ก็ throw ด้วย → exception ไหนชนะ?
java
// โค้ดนี้เป็นโครงอธิบายแนวคิด — ต้องอยู่ใน method ที่ declare throws IOException
// หมายเหตุ: throws IOException ที่ method signature จำเป็นด้วย 2 เหตุผล:
// 1. new FileReader("a.txt") ใน constructor throw FileNotFoundException (extends IOException) — checked exception
// 2. try-with-resources เรียก r.close() โดยปริยาย ซึ่ง close() ของ FileReader throw IOException ด้วย
// ไม่ใช่แค่เพราะ close() เพียงอย่างเดียว
void example() throws IOException {
try (FileReader r = new FileReader("a.txt")) {
throw new RuntimeException("read failed");
// ออกจาก try → r.close() ถูกเรียก → close() เองก็ throw IOException
}
}Java เลือก exception จาก body เป็นหลัก แล้วเก็บ exception จาก close() เป็น suppressed ใน exception แม่:
java
try {
doRiskyWork();
} catch (Exception e) {
System.err.println("Main: " + e.getMessage());
for (Throwable s : e.getSuppressed()) { // ดูตัวที่ถูก suppress
System.err.println(" Also: " + s.getMessage());
}
}Stack trace อัตโนมัติจะแสดง:
text
RuntimeException: read failed
at Main.main(Main.java:5)
Suppressed: IOException: close failed
at ...ไม่หายเงียบเหมือนสมัยก่อน (Java 6) ที่ exception จาก close() กลบ exception จาก body
7.6 ปิด resource เอง? — ใช้ addSuppressed()
java
Exception primary = null;
try {
work();
} catch (Exception e) {
primary = e;
} finally {
try {
resource.close();
} catch (Exception closeEx) {
if (primary != null) primary.addSuppressed(closeEx);
else throw closeEx;
}
}
if (primary != null) throw primary;— ปกติไม่ต้องเขียนเอง ใช้ try-with-resources แทน
8. Best Practice
8.1 ❌ อย่า catch แล้วเงียบ
java
try {
// ...
} catch (Exception e) {
// empty — ไม่ทำอะไรเลย
}ทำให้ bug หาไม่เจอ — ต้อง log เสมอ ทำได้สองวิธีตามระดับ:
วิธีที่ 1 — สำหรับมือใหม่หรือโปรเจกต์ที่ยังไม่มี library เพิ่มเติม:
java
} catch (Exception e) {
System.err.println("Error: " + e.getMessage()); // ✅ รันได้ทันทีไม่ต้องติดตั้งอะไร
e.printStackTrace(); // แสดง stack trace (dev เท่านั้น)
}วิธีที่ 2 — สำหรับโปรเจกต์จริง (แนะนำก่อน deploy production):
java
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
// ประกาศ logger ไว้ใน class (ใช้แทน System.err ใน production)
// SLF4J/Logback คือ library สำหรับ logging (บันทึก log) — จะเรียนละเอียดบทที่ 15
// ⚠️ ต้องเพิ่ม SLF4J dependency ใน Maven/Gradle ก่อน — ถ้ายังไม่มีให้ใช้วิธีที่ 1 ชั่วคราว
private static final Logger log = LoggerFactory.getLogger(MyClass.class);
// ใน catch block:
} catch (Exception e) {
log.error("Failed to process", e); // ✅ เก็บ stack trace + timestamp + level ได้
}⚠️ อย่าใช้
e.printStackTrace()ใน production code — anti-pattern (รูปแบบที่ควรหลีกเลี่ยง): เขียนลงSystem.errตรง ๆ (เปลี่ยน destination ยาก), ไม่มี timestamp/level, เมื่อหลาย thread throw exception พร้อมกัน stack trace อาจ interleave (ปนกัน) ทำให้อ่านยาก — ใน dev เพื่อ quick debug พอรับได้ แต่ตั้งใจแทนด้วย logger ก่อน commit เสมอ
8.2 ❌ อย่า catch Exception ทั่วไปทุกที่ และ ห้าม catch Throwable/Error เด็ดขาด
ห้าม catch Error — มันคือสัญญาณว่า JVM พังเกินกู้
Throwable รวม Error (เช่น OutOfMemoryError, StackOverflowError) — สิ่งเหล่านี้ JVM บอกว่าพังจนกู้ไม่ได้ ถ้า catch แล้วทำเป็นปกติต่อ → ระบบจะอยู่ในสถานะที่ไม่แน่นอน → bug หายากกว่าเดิม
📌 ขั้นสูง: LinkageError, VirtualMachineError และ framework boundary (ข้ามได้ตอนนี้)
LinkageError, VirtualMachineError และการ catch ใน top-level executor/framework boundary — จะอธิบายในบทที่ 12
⚠️ InterruptedException — ห้าม catch แล้วเงียบ: ถ้าต้อง catch
InterruptedExceptionต้องทำอย่างใดอย่างหนึ่งเสมอ:
- re-throw ขึ้นไปให้ caller จัดการ:
throw e;- restore interrupted status:
Thread.currentThread().interrupt();catch แล้วไม่ทำอะไรเลยเป็น bug ร้ายแรงในงาน concurrent — ทำให้ thread ไม่สามารถหยุดได้ตามต้องการ (รายละเอียดเต็มบทที่ 12)
java
// ❌ ห้าม
try { ... } catch (Throwable t) { /* ... */ }
try { ... } catch (Error e) { /* ... */ }
// ✅ เฉพาะใน framework boundary เท่านั้น ที่ catch Throwable เพื่อ "log + crash" — เช่น top-level executor
try {
task.run();
} catch (Throwable t) {
log.error("Fatal in worker", t);
// re-throw หรือ shutdown — อย่าทำงานต่อเงียบ ๆ
throw t;
}java
try {
// ...
} catch (Exception e) { // จับทุกอย่าง รวม NullPointerException ที่ควรแก้
// ...
}catch แค่ที่คุณจัดการได้ — ส่วนอื่น throw ขึ้นไป
8.3 ✅ Throw exception ที่บอกความหมาย
java
// ❌
if (id < 0) throw new RuntimeException("bad");
// ✅
if (id < 0) throw new IllegalArgumentException("ID must be positive, got: " + id);8.4 ✅ Wrap exception (exception chaining)
📌 exception chaining = ห่อ exception ซ้อนกัน เพื่อเก็บสาเหตุต้นตอไว้ใน
cause(สาเหตุดั้งเดิม) — ดู stack trace ได้ครบทั้ง chain
📌
ConfigExceptionเป็น custom exception ที่ต้องสร้างเอง (extends RuntimeException) — ดู section 6
java
try {
parseConfig();
} catch (IOException e) {
throw new ConfigException("Failed to load config", e); // เก็บ original
}new ConfigException(msg, cause) เก็บ exception เดิมเป็น cause (สาเหตุ) — debug ง่ายขึ้น
ดู cause + chain
java
try {
work();
} catch (Exception e) {
Throwable cause = e.getCause(); // exception ที่ถูก wrap
while (cause != null) { // ไล่ chain
System.err.println("Caused by: " + cause);
cause = cause.getCause();
}
}Stack trace แสดง chain อัตโนมัติ:
text
ConfigException: Failed to load config
at Main.main(Main.java:5)
Caused by: java.io.IOException: file not found
at ...
Caused by: java.nio.file.NoSuchFileException: /etc/app.yaml
at ...initCause() — สำหรับ exception ที่ constructor ไม่รับ cause
java
SQLException ex = new SQLException("query failed");
ex.initCause(originalException); // เซ็ตทีหลังได้
throw ex;⚠️ อย่าทิ้ง cause —
catch (IOException e) { throw new MyException("oops"); }ทำให้สาเหตุจริงหาย
8.5 ✅ Fail fast (ล้มให้เร็ว — เจอปัญหาแล้ว error ทันทีตรงต้นทาง ไม่ปล่อยให้ลามไปไกล) — check input ก่อน
📌 fail fast ในที่นี้หมายถึง "ตรวจสอบ input เร็ว ๆ ก่อนทำงานจริง" — ต่างจาก fail-fast iterator (บทที่ 7) ที่หมายถึงการโยน exception ทันทีเมื่อ collection ถูกแก้ระหว่าง iterate
java
public void process(User user) {
if (user == null) throw new IllegalArgumentException("user cannot be null");
if (user.getId() < 0) throw new IllegalArgumentException("invalid id");
// ...
}ดีกว่าให้ NullPointerException เกิดลึก ๆ หา bug ยาก
8.6 ❌ อย่าใช้ exception เป็น control flow (การควบคุมทิศทางการทำงานของโปรแกรม — เช่น if/loop)
java
// ❌ ใช้ exception แทน if
try {
int n = Integer.parseInt(s);
process(n);
} catch (NumberFormatException e) {
// do default
}Exception ช้า + เจตนาไม่ชัด — ตรวจก่อนถ้าได้
📌 สำหรับมือใหม่: ตัวอย่างด้านล่างใช้ regex (
\\d+) ซึ่งเรียนบทที่ 13 — ถ้ายังไม่ถึงบทนั้น ใช้วิธี try-catch ด้านล่างซึ่ง robust กว่าและไม่ต้องรู้ regex เลย
java
// \\d+ = regex แปลว่า "ตัวเลขล้วน 1 ตัวขึ้นไป" — เรียนละเอียดบทที่ 13
// ⚠️ \\d+ ตรวจเฉพาะ unsigned integer เท่านั้น (ไม่รองรับ "-5") และยังไม่ตรวจว่าเกิน Integer.MAX_VALUE
// ⚠️ ถ้า input อาจมี whitespace ให้ใช้ s.strip().matches("\\d+") แทน (Java 11+: strip() ดีกว่า trim() สำหรับ Unicode whitespace)
if (s != null && s.matches("\\d+")) { // null check ก่อน — `matches` บน null = NPE
int n = Integer.parseInt(s);
process(n);
} else {
// default
}📌 วิธีที่ robust กว่า: ใช้
try-catch Integer.parseIntโดยตรง — ตรวจทั้ง format ผิดและ overflow ในครั้งเดียว:javatry { int n = Integer.parseInt(s.strip()); // handle whitespace + format + overflow ครบ process(n); } catch (NumberFormatException e) { // default }
📌 เชื่อมกับ Checkpoint 8.3:
parseIntSafeใช้-1เป็น sentinel value — ใช้ได้เฉพาะกรณีที่รู้ว่า input ไม่มีค่าลบ ถ้า input ที่ถูกต้องอาจเป็นจำนวนลบได้ (เช่น-5) sentinel-1จะทำให้เกิด bug บทที่ 9 จะสอนOptional<Integer>ซึ่งแก้ปัญหานี้ได้สวยกว่า
9. NullPointerException — ศัตรูตลอดกาล
NPE คือ exception ที่เจอบ่อยที่สุดในโลก Java
java
String s = null;
int len = s.length(); // 💥 NullPointerException9.1 ป้องกันด้วย null check
java
if (s != null) {
int len = s.length();
}9.2 ใช้ Optional (Java 8+)
java
Optional<String> maybe = Optional.ofNullable(s);
maybe.ifPresent(value -> System.out.println(value.length()));(เดี๋ยวบทที่ 9 จะเรียน Optional ละเอียด)
9.3 Helpful NullPointerException (Java 14+, default Java 15+)
ก่อน Java 14 — NPE จะบอกแค่ "NullPointerException" + บรรทัด:
text
Exception in thread "main" java.lang.NullPointerException
at Main.main(Main.java:5)→ ต้องเดาเองว่าตัวไหนใน expression ที่ null
Java 14+ (default ตั้งแต่ 15) — บอกชัด:
text
Cannot invoke "User.getAddress().getCity().toUpperCase()"
because "user.getAddress()" is null
at Main.main(Main.java:5)ใช้ -XX:+ShowCodeDetailsInExceptionMessages (default แล้ว) — ไม่ต้องตั้งอะไรเพิ่ม
💡 ถ้า production ปิด flag นี้เพื่อ security (กลัวเปิดเผยโครงสร้าง object) — เปิดไว้บน dev/staging ก็พอ
9.4 ใช้ Objects.requireNonNull
java
public void setName(String name) {
this.name = Objects.requireNonNull(name, "name");
}ถ้า null จะ throw NPE พร้อม message ชัด — fail fast
10. Checkpoint
🛠️ Checkpoint 8.1 — Safe divide
เขียน method safeDivide(int a, int b) ที่:
- ถ้า b != 0 → return a/b
- ถ้า b == 0 → catch ArithmeticException → return 0
📖 เฉลย
java
public static int safeDivide(int a, int b) {
try {
return a / b;
} catch (ArithmeticException e) {
System.out.println("Cannot divide by zero, returning 0");
return 0;
}
}🛠️ Checkpoint 8.2 — Custom exception
สร้าง InvalidAgeException (extends RuntimeException) สร้าง method setAge(int age) ที่ throw exception นี้ถ้า age < 0 หรือ > 150
📖 เฉลย
java
public class InvalidAgeException extends RuntimeException {
public InvalidAgeException(String msg) {
super(msg);
}
}
class Person {
private int age;
public void setAge(int age) {
if (age < 0 || age > 150) {
throw new InvalidAgeException("Invalid age: " + age);
}
this.age = age;
}
}
// ===== ไฟล์ Main.java =====
public class Main {
public static void main(String[] args) {
try {
new Person().setAge(200);
} catch (InvalidAgeException e) {
System.out.println("Error: " + e.getMessage());
}
}
}🛠️ Checkpoint 8.3 — First parsable
เขียน method parseIntSafe(String s) ที่:
- พยายาม parseInt
- ถ้า NumberFormatException → return -1
ลองเขียน method parseFirstValid(String[] inputs) ที่ลองแต่ละ input — return ตัวแรกที่ parse สำเร็จ
📖 เฉลย
java
public static int parseIntSafe(String s) {
try {
return Integer.parseInt(s);
} catch (NumberFormatException e) {
return -1;
}
}
// ⚠️ ข้อจำกัด: -1 เป็น sentinel value — ใช้ได้เฉพาะกรณีที่รู้ว่า input ไม่มีค่าลบ
// ถ้า input อาจเป็น "-1" จริง ๆ จะ skip ไปผิด — Optional<Integer> (บทที่ 9) แก้ปัญหานี้ได้สวยกว่า
public static int parseFirstValid(String[] inputs) {
for (String s : inputs) {
int n = parseIntSafe(s);
if (n != -1) return n;
}
return -1; // ไม่มีตัวไหน parse ได้
}
// test
String[] inputs = {"abc", "12.5", "42", "100"};
System.out.println(parseFirstValid(inputs)); // 4211. สรุปบท
✅ Exception = ความผิดปกติตอน run — มี stack trace บอกที่เกิด ✅ try/catch จับ, finally ทำเสมอ, throw โยน, throws ที่หัว method (signature) ✅ Checked (IOException ฯลฯ) บังคับ catch/throws — Unchecked (RuntimeException) ไม่บังคับ ✅ Custom exception extends RuntimeException (ปกติ) — ตั้งชื่อบอกความหมาย ✅ try-with-resources — ปิด resource อัตโนมัติ ✅ Best practice: catch เฉพาะที่จัดการได้, fail fast, อย่าใช้ exception เป็น control flow, chain cause ✅ NPE คือศัตรู — ใช้ null check, Optional, Objects.requireNonNull