Skip to content

บทที่ 8 — Exception Handling

← บทที่ 7 | สารบัญ | บทที่ 9: Lambda + Stream →

ในโลกที่สมบูรณ์แบบโปรแกรมจะทำงานตามที่เขียนเสมอ แต่โลกจริงไม่ใช่:

  • ผู้ใช้กรอกข้อมูลผิด 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
ArrayIndexOutOfBoundsExceptionindex ของ array เกินขอบเขต
ClassCastExceptioncast type ผิด
NumberFormatExceptionparse string ผิด format
ArithmeticExceptionหารด้วย 0 (เฉพาะ int — double จะได้ Infinity)
IllegalArgumentExceptionargument ผิด
IllegalStateExceptionstate ของ 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 error

3.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 จะ crash

throw = "ออกจาก 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 (กริยา) vs throws (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 ต้องทำอย่างใดอย่างหนึ่งเสมอ:

  1. re-throw ขึ้นไปให้ caller จัดการ: throw e;
  2. 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;

⚠️ อย่าทิ้ง causecatch (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 ในครั้งเดียว:

java
try {
    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();    // 💥 NullPointerException

9.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));   // 42

11. สรุปบท

✅ 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

→ ไปบทที่ 9: Lambda + Stream + Optional