ข้าม​ไป​ยัง​เนื้อหา

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 switchstatement switch
enumแดงเขียว
sealedแดงแดง

ข้อความ​ของ​สาม​ช่อง​ที่​แดง

error: the switch expression does not cover all possible input values
error: the switch statement does not cover all possible input values

exhaustivenessexhaustivenessข้อ​ที่ 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 ก็​ไม่​ได้​พูด​อะไร​ตอน​แปล
นี่​คือ​รูป​ที่ code เก่า​ของ​คุณ​เขียน​ไว้​แทบ​ทั้งหมด

switch เหนือ enum แบบ​ไม่​ให้​ค่า​กลับ​มา คือ​รูป​ที่ Java 8 มี​ให้​ใช้​อยู่​แล้ว · โปร​เจ​กต์​ที่​เขียน​มา​ก่อน​ปี 2021 จึง​เต็ม​ไป​ด้วย​รูป​นี้

วัน​ที่​มี​คน​เพิ่ม​ค่า​ใหม่​เข้าไป​ใน enum ไม่มี​อะไร​บอก​คุณ​ว่า​มี switch กี่​ที่​ที่​ตกหล่น · ทั้งหมด​ยัง​แปล​ผ่าน ทั้งหมด​ยัง​รัน​ผ่าน และ​ทั้งหมด​เงียบ​ตรง​ค่า​ใหม่​นั้น

ทาง​แก้​ที่​ตรง​ที่สุด​ไม่ใช่​การ​ไป​เติม default ให้​ทุก​ที่ · default ทำให้​มัน​เงียบ​ต่อ​ไป แค่​เปลี่ยน​จาก​ไม่​ทำ​อะไร เป็น​ทำ​อย่าง​อื่น​แทน

ทาง​แก้​คือ​เปลี่ยน​ให้​เป็น expression switch เมื่อ​ทำได้ · เพราะ​รูป​นั้น​บังคับ​ให้​ครบ​เสมอ

เพราะ​ข้อมูล​ที่ 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 ปล่อยผ่าน