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

ย้าย​จาก instanceof มา​เป็น switch แล้ว null เปลี่ยน​ความ​หมาย

การ​ย้าย​จาก instanceof มา​เป็น switch ที่​มี pattern เป็นการ​เปลี่ยน​ที่​ดู​ปลอดภัย​ที่สุด อย่าง​หนึ่ง​ใน Java รุ่น​ใหม่

มัน​สั้น​กว่า · compiler ตรวจ​ให้​มากกว่า · และ​ผลลัพธ์​เหมือน​เดิม

ยกเว้น​ตอน​ที่​ค่าที่​รับ​เข้า​มา​เป็น null

record Pt(int x, int y) {}
record Line(Pt a, Pt b) {}
static int viaInstanceof(Object o) {
if (o instanceof Line) {
Line l = (Line) o;
Pt a = l.a();
Pt b = l.b();
return a.x() + a.y() + b.x() + b.y();
}
return 0;
}
static int viaPattern(Object o) {
return switch (o) {
case Line(Pt(int x1, int y1), Pt(int x2, int y2)) -> x1 + y1 + x2 + y2;
default -> 0;
};
}

ทั้ง​คู่​คืน 10 เมื่อ​รับ new Line(new Pt(1, 2), new Pt(3, 4))

viaPattern(null) → java.lang.NullPointerException
viaInstanceof(null) → 0

o instanceof Line เป็น​เท็จ​เมื่อ o เป็น null · รูป​เก่า​จึง​ตกลง​มา​ที่ return 0

แต่ switch ที่​มี pattern โยน NullPointerException เมื่อ​ค่าที่​รับ​เข้า​มา​เป็น null และ​ไม่มี case null เขียน​ไว้

`default` ไม่​ได้​รับ `null` และ​ข้อ​นี้​ทำให้​คน​ย้าย code พลาด​บ่อย

หลาย​คน​อ่าน default ว่า “ทุก​กรณี​ที่​เหลือ” แล้ว​สรุป​ว่า​มัน​รับ null ด้วย · มัน​ไม่​รับ

ถ้า​ต้องการ​ให้​รับ ต้อง​เขียน​ออก​มา​ให้​เห็น

return switch (o) {
case null -> 0;
case Line(Pt(int x1, int y1), Pt(int x2, int y2)) -> x1 + y1 + x2 + y2;
default -> 0;
};

การ​บังคับ​ให้​เขียน​ออก​มา​เป็นการ​ตัดสิน​ใจ​ของ​ภาษา ไม่ใช่​ความ​ผิด​พลาด · มัน​ทำให้​คน​อ่าน​เห็น​ว่า​ใคร​คิด​เรื่อง null แล้ว​บ้าง

แต่​ตอน​ย้าย code จาก​รูป​เก่า​มา​รูป​ใหม่ ความ​หมาย​เปลี่ยน​โดย​ไม่มี​อะไร​เตือน · ไม่มี warning ไม่มี lint ที่​เปิด​มา​เป็น​ค่า​ตั้ง​ต้น

เปิด​ด้วย javap -c แล้ว​นับ​คำ​สั่ง

viaPatternviaInstanceof
invokedynamic10
checkcast11
bootstrap ที่​เรียกtypeSwitch—

รูป​ใหม่​ถาม bootstrap method ว่า ค่า​นี้​ตรง​กับ case ที่​เท่าไร แล้ว​ค่อย​กระโดด · ไม่​ได้​ไล่ instanceof ที​ละ​อัน​เรียง​ลง​มา

ผล​คือ​เมื่อ​จำนวน case เพิ่ม​ขึ้น รูป​ใหม่​ไม่​ได้​ยาว​ขึ้น​ตาม​แบบ​เชิง​เส้น ส่วน​รูป​เก่า​ยาว​ขึ้น​ทุก​ครั้ง​ที่​เพิ่ม​เงื่อนไข

และ​มัน​อธิบาย​เรื่อง null ด้วย · การ​ถาม typeSwitch ต้อง​ถาม​ด้วย​ค่าที่​มี​อยู่​จริง · null ไม่ใช่​ค่าที่​จับ​คู่​ชนิด​กับ​อะไร​ได้ ภาษา​จึง​บังคับ​ให้​คุณ​บอก​ว่า​จะ​ทำ​อย่างไร​กับ​มัน

  • มี null วิ่ง​ผ่าน​ตรง​นี้​ได้​ไหม ถ้า​ไม่​แน่ใจ ให้​ถือว่า​ได้
  • ถ้า​ได้ กิ่ง​ไหน​ควร​รับ​มัน เขียน case null ออก​มา​ให้​เห็น · จะ​รวม​กับ​กิ่ง​อื่น​ก็ได้ เช่น case null, default ->
  • เทสต์​ที่​มี​อยู่ ส่ง null เข้าไป​หรือ​เปล่า ถ้า​ไม่ การ​ย้าย​นี้​จะ​เขียว​ทั้ง​ที่​ความ​หมาย​เปลี่ยน

ข้อ​สุดท้าย​คือ​ข้อ​ที่​ทำให้​ความ​ผิด​พลาด​นี้​หลุด​ขึ้น production ได้ · ไม่มี​ใคร​เขียน​เทสต์​ที่​ส่ง null ให้​ฟังก์ชัน​ที่​เคย​รับ null ได้​อย่าง​เงียบๆ อยู่​แล้ว


🔗 อ้างอิง​ต้นทาง

ทั้ง bytecode และ​ผล​ตอน​รัน มา​จาก​การ​แปล​และ​รัน​จริง:

  • วัด​เมื่อ 2026-09-01 · javac 25.0.4.1 · openjdk 25.0.4.1 2026-08-18
  • รัน​ซ้ำ​เอง​ได้​ด้วย npm run verify:java-modern ซึ่ง​นับ​คำ​สั่ง​จาก​ผล​ของ javap -c แล้ว​รัน​ทั้ง​สอง method ด้วย null
  • ตัวอย่าง​รอบ​แรก​ของ​ตัว​วัด​ตาย​ทั้ง​โปรแกรม​ตอน​รัน​ด้วย null จึง​รายงาน​ว่า​วัด​ไม่​ได้ · เปลี่ยน​ให้​จับ exception แล้ว​บันทึก​ชื่อ​ชนิด​แทน · ข้อ​ที่​โผล่​มา​จาก​ตรง​นั้น​คือ​เนื้อ​ของ​หน้า​นี้
  • JEP 440 — Record Patterns (เข้า​ถึง 2026-09-01)
  • JEP 441 — Pattern Matching for switch (เข้า​ถึง 2026-09-01) — กติกา​เรื่อง case null และ​เหตุ​ที่ default ไม่​รับ null
  • เรื่อง​นี้​เป็น​บท​ที่ 4 ของ คอร์ส Java 8 ถึง 25