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

แยก​ส่วน​ใน `case` และ​ข้อ​ที่​มัน​ไม่​เหมือน​ของ​เดิม

บท​ที่​แล้ว​ปิด​ท้าย​ด้วย case Card c -> "บัตรลงท้าย " + c.last4();

เรียก​ชื่อ​ตัวแปร​แล้ว​เรียก method ต่อ​อีก​ทอด · บท​นี้​ตัด​ทอด​นั้น​ทิ้ง แล้ว​ดู​ว่า​แลก​อะไร​ไป

ฝั่ง​หนึ่ง​เจ็ด​บรรทัด อีก​ฝั่ง​หนึ่ง​บรรทัด

record Pt(int x, int y) {}
record Line(Pt a, Pt b) {}

รูป​เดิม​ที่​ทุก​คน​เขียน​มา​ตั้งแต่ Java 8

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;
}

รูป​ใหม่​ที่​ใช้ record patternrecord patternการ​แยก​ช่อง​ข้อมูล​ของ `record` ออก​มา​เป็น​ตัวแปร​ใน​บรรทัด​เดียว​กับ​ที่​จับ​คู่​ชนิด · ซ้อน​กัน​ได้​หลาย​ชั้น และ compiler ตรวจ​ชนิด​ของ​ทุก​ชั้น​ให้

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))

deconstructiondeconstructionการ​รื้อ​ของ​ออก​เป็น​ชิ้น​ส่วน​ตาม​รูป​ที่​มัน​ถูก​ประกอบ​ขึ้น · ใช้ได้​เฉพาะ​กับ​ชนิด​ที่​ประกาศ​รูป​ของ​ตัว​เอง​ไว้ ซึ่ง​ใน Java คือ `record` เท่านั้น ทำงาน​ได้​เพราะ record ประกาศ​รูป​ของ​ตัว​เอง​ไว้​ใน​หัว · ชนิด​ที่​ไม่​ได้​ประกาศ​รูป แยก​ส่วน​แบบ​นี้​ไม่​ได้ · นี่​คือ​เหตุผล​ที่ Java แยก​ส่วน​ได้​เฉพาะ record

รูป​ใหม่​ไม่​ได้​เป็น​น้ำตาล​เคลือบ​ของ​รูป​เก่า มัน​แปล​ออก​มา​คนละ​แบบ

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

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

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

flowchart LR
  O["ค่าที่รับเข้ามา"]
  O --> P["viaPattern"]
  O --> I["viaInstanceof"]
  P --> BS["invokedynamic<br/>typeSwitch"] --> IDX["ได้เลขของ case ที่ตรง"] --> J["กระโดดไปกิ่งนั้น"]
  I --> C1["instanceof Line"] --> C2["แปลงชนิด"] --> C3["เรียก getter ทีละชั้น"]

คำ​บรรยาย​ภาพ: รูป​หนึ่ง​ถาม​ครั้ง​เดียว​ว่า​ตรง case ไหน อีก​รูป​ไล่​เงื่อนไข​เรียง​ลง​มา

ข้อ​ที่​ต่าง​กัน​จริง และ​มัน​ไม่​ได้​อยู่​ใน​ความ​สั้น

หัวข้อ​ที่​มีชื่อ​ว่า “ข้อ​ที่​ต่าง​กัน​จริง และ​มัน​ไม่​ได้​อยู่​ใน​ความ​สั้น”

รัน​ทั้ง​สอง​รูป​ด้วย null แล้ว​ผล​ไม่​เหมือน​กัน

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

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

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

`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 จาก​รูป​เก่า​มา​รูป​ใหม่ ความ​หมาย​เปลี่ยน​โดย​ไม่มี​อะไร​เตือน · นี่​คือ​ข้อ​เดียว​ใน​บท​นี้​ที่​ทำให้​ของ​ที่​เคย​ทำงาน​อยู่ พัง​ได้

type patterntype patternการ​เขียน `case ชนิด ชื่อ` แล้ว​ได้​ตัวแปร​ที่​แปลง​ชนิด​แล้ว​มา​ใช้​ทันที · แทน​การ​เขียน `instanceof` แล้ว​แปลง​ชนิด​ซ้ำ​อีก​บรรทัด กับ record pattern ต่าง​กัน​อย่างไร

หัวข้อ​ที่​มีชื่อ​ว่า “type pattern กับ record pattern ต่าง​กัน​อย่างไร”

ตัว​แรก​จับ​ชนิด ตัว​ที่​สอง​จับ​ชนิด​แล้ว​รื้อ​ออก​เป็น​ชิ้น

case Line l -> l.a().x();
case Line(Pt a, Pt ignored) -> a.x();
case Line(Pt(int x, int ignoredY), Pt ignoredB) -> x;

สาม​บรรทัด​นี้​จับ​ของ​อย่าง​เดียวกัน · ต่าง​กัน​ที่​รื้อ​ลึก​แค่​ไหน

รื้อ​ลึก​ขึ้น​แล้ว​ได้​สอง​อย่าง · ชื่อ​ที่​สื่อ​ความ​หมายก​ว่า l.a().x() · และ compiler ตรวจ​ชนิด​ของ​ทุก​ชั้น​ให้ ไม่ใช่​แค่​ชั้น​นอก​สุด

รื้อ​ลึก​เกิน​ไป​ก็​อ่าน​ยาก​ขึ้น · ไม่มี​กติกา​ตายตัว แต่​มี​เกณฑ์​ที่​ใช้ได้ คือ รื้อ​ถึง​ชั้น​ที่​คุณ​ใช้​ค่า​จริง แล้ว​หยุด

สาม​อย่าง​ที่​ยัง​ไม่มี และ​แต่ละ​อย่าง​มา​ถึง​เมื่อ​มี​เหตุผล​ให้​ต้อง​มี

  • ยัง​ไม่​ได้​ใช้​ชนิด​พื้น​ฐาน​ใน pattern case int i ยัง​เป็น preview ใน​รุ่น​นี้ · บท​ที่ 7 พูด​เรื่อง​ราคา​ของ preview
  • ยัง​ไม่​ได้​ใช้ when การก​รอง​เพิ่ม​ใน​กิ่ง​เดียวกัน​เป็น​ของ​ที่​มี​อยู่​แล้ว แต่​ไม่ใช่​เรื่อง​ที่​บท​นี้​วัด
  • ยัง​ไม่​ได้​แตะ Stream ซึ่ง​เป็น​ที่​ที่ Java 8 ให้​ของ​ไว้​เยอะ​ที่สุด · บท​ที่ 5

บท​นี้​ให้​ของ​ที่​สั้น​กว่า และ​เตือน​เรื่อง​ที่​ไม่มี​ใคร​เตือน

record pattern แปล​ออก​มา​คนละ​แบบ​กับ instanceof ที่​เขียน​มือ · มัน​ถาม typeSwitch ครั้ง​เดียว​แล้วก​ระ​โดด แทน​การ​ไล่​เงื่อนไข​เรียง​ลง​มา

และ​มัน​รับ null ไม่​เหมือน​กัน · รูป​เก่า​คืน​ค่า​ปกติ รูป​ใหม่​โยน exception · ถ้า​ย้าย code โดย​ไม่รู้​ข้อ​นี้ ของ​ที่​เคย​ทำงาน​จะ​พัง​เฉพาะ​ทาง​ที่​มี null วิ่ง​ผ่าน

บท​หน้า​เรา​ไป Stream ซึ่ง​เป็น​ของ​ที่ Java 8 ให้​ไว้​ครบ​ที่สุด — แล้ว​ดู​ว่า Gatherers เติม​อะไร​ที่ map กับ filter ทำ​ไม่​ได้


🔗 อ้างอิง​ต้นทาง​ของ​บท​นี้

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

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

เช็กความเข้าใจ — บทที่ 4

ข้อ 1 / 3

ย้ายจาก `if (o instanceof Line)` มาเป็น `switch` ที่มี pattern และมี `default` · แล้วส่ง `null` เข้าไป จะเกิดอะไรขึ้น