switch ที่พลาดไม่ได้ กับช่องเดียวที่มันยังพลาดได้
คำแนะนำที่ได้ยินบ่อยคือ ใช้ sealedsealedชนิดที่ประกาศรายชื่อชนิดลูกไว้ครบตอนแปล · ทำให้ compiler รู้ว่ามีความเป็นไปได้กี่แบบ ซึ่งเป็นข้อมูลที่ `enum` ให้ไม่ได้เมื่อชนิดลูกถือข้อมูลต่างกัน แล้ว compiler จะบังคับให้ไล่ครบเอง
จริง · แต่ไม่ครบ · และช่องที่มันไม่บังคับ คือช่องที่ความผิดพลาดหลุดผ่านได้เงียบที่สุด
วัดเป็นตารางสองคูณสอง ไม่ใช่เทียบสองอัน
หัวข้อที่มีชื่อว่า “วัดเป็นตารางสองคูณสอง ไม่ใช่เทียบสองอัน”แกนหนึ่งคือชนิด อีกแกนคือรูปของ
switchเอง · สองแกนนี้ตัดสินคนละเรื่อง
ชนิดที่เทียบคือ enum กับ sealed interface ที่มีชนิดลูกสองตัว
enum Colour { RED, GREEN, BLUE }sealed interface S permits A, B {}record A(int v) implements S {}record B(String s) implements S {}รูปของ switch ที่เทียบคือ expression switchexpression switch`switch` ที่ให้ค่ากลับมา เขียนในตำแหน่งที่ต้องการค่า · รูปนี้ต้องครอบครบเสมอ ไม่ว่าชนิดจะเป็นอะไร เพราะไม่มีค่าไหนให้คืนเมื่อไม่มี case ตรง ซึ่งให้ค่ากลับมา
กับ statement switchstatement switch`switch` ที่ไม่ให้ค่ากลับมา · รูปนี้ไม่ถูกบังคับให้ครอบครบเมื่อชนิดเป็น `enum` · ค่าที่ไม่มี case รองรับจะผ่านไปเงียบๆ โดยไม่มีอะไรเกิดขึ้น ซึ่งไม่ให้
return switch (c) { case RED -> "r"; case GREEN -> "g"; };switch (c) { case RED -> System.out.println("r"); case GREEN -> System.out.println("g"); }ทั้งสองอันนี้ไล่ไม่ครบเหมือนกัน · แล้วเราแปลทั้งสี่ช่องด้วย javac --release 25 -Xlint:all
ผล · สามช่องแดง หนึ่งช่องเขียว
หัวข้อที่มีชื่อว่า “ผล · สามช่องแดง หนึ่งช่องเขียว”ช่องเดียวที่เขียวคือช่องที่คนใช้บ่อยที่สุด
| expression switch | statement switch | |
|---|---|---|
enum | แดง | เขียว |
sealed | แดง | แดง |
ข้อความของสามช่องที่แดง
error: the switch expression does not cover all possible input valueserror: the switch statement does not cover all possible input valuesexhaustivenessexhaustivenessข้อที่ compiler บังคับว่า `switch` ต้องครอบทุกความเป็นไปได้ · **มันบังคับบางรูปเท่านั้น** และช่องที่ไม่บังคับคือช่องที่ความผิดพลาดหลุดผ่านได้ ไม่ใช่กติกาข้อเดียวที่ใช้กับทุกกรณี · มันเป็นกติกาที่มีเงื่อนไข
flowchart TB
Q{"switch นี้ให้ค่ากลับมาไหม"}
Q -->|"ให้ · expression"| E["ต้องครบเสมอ<br/>ไม่ว่าชนิดจะเป็นอะไร"]
Q -->|"ไม่ให้ · statement"| T{"ชนิดเป็นอะไร"}
T -->|"sealed"| S["ต้องครบ"]
T -->|"enum"| G["ไม่ต้องครบ<br/>ค่าที่ไม่มี case ผ่านไปเงียบๆ"]
คำบรรยายภาพ: คำถามสองข้อ และมีเส้นทางเดียวที่ไปจบที่ช่องซึ่งไม่มีใครบังคับ
ช่องที่เขียว เขียวจนน่ากลัว
หัวข้อที่มีชื่อว่า “ช่องที่เขียว เขียวจนน่ากลัว”ไม่ใช่แค่แปลผ่าน · มันรันผ่านด้วย
เอาช่องนั้นมารันด้วยค่าที่ไม่มี case รองรับ
switch (c) { case RED -> System.out.println("r"); case GREEN -> System.out.println("g"); }เรียกด้วย Colour.BLUE แล้ว โปรแกรมไม่ทำอะไรเลย และจบด้วยรหัสออก 0
- ไม่มี exception
- ไม่มีข้อความบน stderr
-Xlint:allก็ไม่ได้พูดอะไรตอนแปล
switch เหนือ enum แบบไม่ให้ค่ากลับมา คือรูปที่ Java 8 มีให้ใช้อยู่แล้ว
· โปรเจกต์ที่เขียนมาก่อนปี 2021 จึงเต็มไปด้วยรูปนี้
วันที่มีคนเพิ่มค่าใหม่เข้าไปใน enum ไม่มีอะไรบอกคุณว่ามี switch กี่ที่ที่ตกหล่น
· ทั้งหมดยังแปลผ่าน ทั้งหมดยังรันผ่าน และทั้งหมดเงียบตรงค่าใหม่นั้น
ทางแก้ที่ตรงที่สุดไม่ใช่การไปเติม default ให้ทุกที่ · default ทำให้มันเงียบต่อไป
แค่เปลี่ยนจากไม่ทำอะไร เป็นทำอย่างอื่นแทน
ทางแก้คือเปลี่ยนให้เป็น expression switch เมื่อทำได้ · เพราะรูปนั้นบังคับให้ครบเสมอ
ทำไม sealed ถึงเข้มกว่า enum
หัวข้อที่มีชื่อว่า “ทำไม sealed ถึงเข้มกว่า enum”เพราะข้อมูลที่ compiler มีในมือ ต่างกันคนละเรื่อง
enum บอก compiler ว่ามีค่าอะไรบ้าง · แต่ Java ไม่ได้ถือว่ารายชื่อนั้นปิดตายสำหรับ
switch แบบไม่ให้ค่า · เพราะรูปนั้นมีมาก่อนที่กติกาเรื่องความครบจะถูกเพิ่มเข้ามา
และการทำให้มันเข้มขึ้นย้อนหลังจะทำให้ code ที่มีอยู่แปลไม่ผ่านทั้งโลก
sealed ไม่มีปัญหานั้น เพราะมันเป็นของใหม่ · กติกาของมันจึงเข้มได้ตั้งแต่วันแรก
นี่คือความเข้ากันได้ย้อนหลังที่มีราคา และราคานั้นคือช่องเขียวช่องนั้น
ทางที่ปลอดภัยที่สุดคือทำให้ผิดไม่ได้
หัวข้อที่มีชื่อว่า “ทางที่ปลอดภัยที่สุดคือทำให้ผิดไม่ได้”รวมสองอย่างที่บทที่ 2 กับบทนี้ให้มา
sealed interface Payment permits Card, Transfer, Cash {}record Card(String last4) implements Payment {}record Transfer(String bank) implements Payment {}record Cash() implements Payment {}
static String describe(Payment p) { return switch (p) { case Card c -> "บัตรลงท้าย " + c.last4(); case Transfer t -> "โอนผ่าน " + t.bank(); case Cash ignored -> "เงินสด"; };}record ทำ implements ได้ · จึงเข้ากับ sealed interface ได้พอดี
· และเมื่อเขียนเป็น expression switch การเพิ่มชนิดลูกตัวที่สี่จะทำให้ที่นี่แดงทันที
ไม่มี default และนั่นตั้งใจ · default จะทำให้ compiler เลิกบังคับ
สิ่งที่บทนี้ยังไม่ทำ
หัวข้อที่มีชื่อว่า “สิ่งที่บทนี้ยังไม่ทำ”สามอย่างที่ยังไม่มี และแต่ละอย่างมาถึงเมื่อมีเหตุผลให้ต้องมี
- ยังไม่ได้แยกส่วนของ
recordในcaseตอนนี้เรายังเรียกc.last4()อยู่ · บทที่ 4 ทำให้มันสั้นลงและตรวจได้มากขึ้น - ยังไม่ได้พูดถึง
MatchExceptionซึ่งโผล่เมื่อชนิดลูกถูกเพิ่มหลังจากที่ผู้เรียก ถูกแปลไปแล้ว · เป็นเรื่องของการ deploy ทีละส่วน - ยังไม่ได้กวาด code เก่า การหา
switchเหนือenumทั้งโปรเจกต์เป็นงานของเครื่องมือ ไม่ใช่ของสายตา
สรุปก่อนไปต่อ
หัวข้อที่มีชื่อว่า “สรุปก่อนไปต่อ”บทนี้แก้คำแนะนำที่ถูกครึ่งเดียว ให้เป็นข้อที่ใช้ตัดสินใจได้
sealed บังคับให้ไล่ครบจริง · แต่ enum ไม่ได้บังคับในรูปที่ไม่ให้ค่ากลับมา
· และรูปนั้นคือรูปที่ code เก่าเขียนไว้แทบทั้งหมด
สิ่งที่เอาไปใช้ได้ทันทีคือ เปลี่ยนเป็น expression switch เมื่อทำได้
ไม่ใช่เติม default ให้ทุกที่ ซึ่งทำให้เงียบต่อไป
บทหน้าเราแยกส่วนของ record ออกมาใน case เลย — และดูว่ามันแปลออกมาเป็นอะไร
ทั้งสี่ช่องถูกแปลจริงด้วย -Xlint:all และช่องที่เขียวถูกนำไปรันจริง:
- วัดเมื่อ 2026-09-01 ·
javac 25.0.4.1·openjdk 25.0.4.1 2026-08-18 - รันซ้ำเองได้ด้วย
npm run verify:java-modern· ร่างแรกของ probe ใช้ expression switch ทั้งสองชนิด แล้วสรุปว่าenumก็แดงเหมือนsealedซึ่งกว้างเกินไป · probe จับได้เองด้วยรหัสออก 1 - JEP 441 — Pattern Matching for switch
(เข้าถึง 2026-09-01) — เงื่อนไขที่
switchต้องครอบครบ และข้อยกเว้นของมัน - เว็บนี้มีบทความที่ถือข้ออ้างเรื่อง
defaultโดยเฉพาะอยู่แล้ว · switch ที่ไม่มี default
เช็กความเข้าใจ — บทที่ 3
ข้อ 1 / 3ในตารางสองคูณสอง ช่องไหนที่ javac ปล่อยผ่าน