`record` เป็นสัญญา ไม่ใช่การพิมพ์น้อยลง
บทที่แล้วบอกว่า recordrecordชนิดที่ประกาศช่องข้อมูลไว้ในหัว แล้ว compiler สร้างส่วนที่เหลือให้ · มันเป็นสัญญาว่าค่าทั้งชุดคือตัวตนของ object ไม่ใช่แค่วิธีพิมพ์ให้สั้นลง เป็น final แล้วใน 25 · บทนี้ถามคำถามถัดไป
รับมาใช้แล้วได้อะไรกลับมา ที่ตรวจสอบได้ ไม่ใช่ที่รู้สึกได้
เทียบของสองอันที่หน้าตาทำงานเหมือนกัน
หัวข้อที่มีชื่อว่า “เทียบของสองอันที่หน้าตาทำงานเหมือนกัน”ฝั่งหนึ่งบรรทัดเดียว อีกฝั่งเก้าบรรทัด · แล้วดูว่า
javapเห็นอะไร
public record Money(long amount, String currency) {}public final class MoneyPojo { private final long amount; private final String currency; public MoneyPojo(long amount, String currency) { this.amount = amount; this.currency = currency; } public long amount() { return amount; } public String currency() { return currency; }}คนส่วนใหญ่หยุดที่ตรงนี้แล้วสรุปว่า record คือการพิมพ์น้อยลง · ซึ่งจริง แต่เป็นข้อที่เล็กที่สุด
สิ่งที่ javap เห็น
หัวข้อที่มีชื่อว่า “สิ่งที่ javap เห็น”เปิดดูของที่ออกมาจริง ไม่ใช่ของที่เราเขียน
javac --release 25 -d out Money.java MoneyPojo.javajavap -cp out Moneypublic final class Money extends java.lang.Record { public Money(long, java.lang.String); public final java.lang.String toString(); public final int hashCode(); public final boolean equals(java.lang.Object); public long amount(); public java.lang.String currency();}record | class ที่เขียนมือ | |
|---|---|---|
| member สาธารณะ | 6 | 3 |
equals · hashCode · toString | มีให้ · เป็น final | ไม่มี |
| หัว class | public final class | public final class |
| สืบทอดจาก | java.lang.Record | Object |
สามตัวที่ต่างกันคือ equals, hashCode และ toString
· ของที่เขียนมือไม่มีทั้งสามตัว และนั่นคือ bug ที่รอเวลา
สามตัวนั้นไม่ได้ถูกเขียนเป็น bytecode ทีละบรรทัด
หัวข้อที่มีชื่อว่า “สามตัวนั้นไม่ได้ถูกเขียนเป็น bytecode ทีละบรรทัด”ตรงนี้คือจุดที่
recordต่างจากการให้ IDE สร้างequalsให้
เปิดด้วย javap -v แล้วดูตาราง BootstrapMethods
javap -v -cp out Moneyทั้งสาม method เรียก invokedynamicinvokedynamicคำสั่งของ JVM ที่เลื่อนการตัดสินใจว่าจะเรียกอะไร ไปทำตอนรันครั้งแรก · `equals`, `hashCode` และ `toString` ของ `record` ใช้ทางนี้แทนการเขียน bytecode ไว้ทีละบรรทัด ไปที่ตัวเดียวกัน คือ ObjectMethods.bootstrapObjectMethods.bootstrapmethod ที่ JVM เรียกครั้งแรกเพื่อสร้างตัวจริงของสามอย่างนั้นให้ `record` · เห็นได้จาก `javap -v` ในตาราง `BootstrapMethods`
ตัวจริงของทั้งสามถูกสร้างตอนรันครั้งแรก จากรายชื่อช่องข้อมูลที่ฝังไว้ใน class file · ไม่ใช่จาก code ที่ใครพิมพ์ และไม่ใช่จาก code ที่ IDE พิมพ์ให้
ผลที่ตามมาคือข้อที่สำคัญที่สุดของบทนี้ · เพิ่มช่องข้อมูลเข้าไปหนึ่งช่อง แล้วทั้งสาม method เปลี่ยนตามทันที โดยไม่มีอะไรให้ลืมแก้
flowchart TB H["หัวของ record<br/>Money(long amount, String currency)"] CF["class file<br/>รายชื่อช่องข้อมูลถูกฝังไว้"] BS["ObjectMethods.bootstrap<br/>ทำงานตอนรันครั้งแรก"] E["equals"] HC["hashCode"] T["toString"] H --> CF --> BS BS --> E BS --> HC BS --> T IDE["equals ที่ IDE สร้างให้"] -.->|"สำเนาของรายชื่อ ณ วันที่กด"| OLD["ค้างอยู่กับ field ชุดเดิม"]
คำบรรยายภาพ: รายชื่อช่องข้อมูลมีที่เดียว และทั้งสาม method อ่านจากที่เดียวกันนั้น
ให้ IDE สร้าง equals ให้ก็ได้ code ที่ทำงานถูกเหมือนกัน · ในวันแรก
วันที่มีคนเพิ่ม field เข้าไป equals ที่ IDE สร้างไว้จะยังเทียบเฉพาะ field เดิม
· มันไม่แดง ไม่เตือน และผลลัพธ์ผิดเฉพาะกรณีที่ field ใหม่ต่างกันเท่านั้น
record ไม่มีช่องให้พลาดข้อนี้ เพราะไม่มีสำเนาของรายชื่อ field อยู่ที่ไหนเลย
· มีที่เดียวคือในหัวของ record เอง
canonical constructor คือทางเข้าเดียว
หัวข้อที่มีชื่อว่า “canonical constructor คือทางเข้าเดียว”และมันคือที่ที่ตรวจความถูกต้องได้ แบบที่ไม่มีทางเลี่ยง
public record Money(long amount, String currency) { public Money { if (amount < 0) throw new IllegalArgumentException("จำนวนเงินติดลบไม่ได้"); currency = currency.toUpperCase(); }}รูปที่ไม่มีวงเล็บนี้เรียกว่า compact form ของ canonical constructorcanonical constructorconstructor ที่รับช่องข้อมูลครบทุกช่องตามลำดับที่ประกาศ · ทางเข้าเดียวที่แท้จริงของ `record` และเป็นที่ที่ตรวจความถูกต้องได้
· มันทำงานก่อนที่ค่าจะถูกเก็บลง field · และคุณแก้ค่าที่กำลังจะถูกเก็บได้ด้วย
ซึ่งเป็นข้อที่ init ของ Kotlin ทำไม่ได้
ไม่มีทางสร้าง Money ขึ้นมาโดยไม่ผ่านตรงนี้ · ไม่มี setter ไม่มี constructor อื่น
สิ่งที่ยอมแลกไป
หัวข้อที่มีชื่อว่า “สิ่งที่ยอมแลกไป”ข้อนี้ต้องพูดออกมา เพราะมันตัดทางเลือกทิ้งจริง
record สืบทอดจาก java.lang.Recordjava.lang.Recordชนิดฐานที่ `record` ทุกตัวสืบทอด · ประกาศเองไม่ได้ และมันคือสิ่งที่ทำให้ `record` สืบทอดจากชนิดอื่นไม่ได้เลย อยู่แล้ว · และ Java สืบทอดได้ชั้นเดียว
record จึงสืบทอดจาก class อื่นไม่ได้เลย · ไม่มีข้อยกเว้น
- ทำ
implementsได้ตามปกติ · จึงใช้กับsealed interfaceได้ ซึ่งเป็นเรื่องของบทที่ 3 - field เพิ่มเติมนอกหัวไม่ได้ · ทุกอย่างที่
recordเก็บ ต้องอยู่ในวงเล็บบนสุด - ตัวมันเป็น
finalเสมอ · จึงไม่มีใครมาสืบทอดต่อได้ด้วย
ถ้าของที่คุณกำลังจะเขียนต้องการอย่างใดอย่างหนึ่งข้างบน record ไม่ใช่คำตอบ
· และนั่นไม่ใช่ข้อเสีย มันคือขอบเขตที่ประกาศไว้ชัด
สิ่งที่บทนี้ยังไม่ทำ
หัวข้อที่มีชื่อว่า “สิ่งที่บทนี้ยังไม่ทำ”สามอย่างที่ยังไม่มี และแต่ละอย่างมาถึงเมื่อมีเหตุผลให้ต้องมี
- ยังไม่ได้ใช้
recordกับsealedซึ่งเป็นที่ที่มันแสดงพลังจริง · บทที่ 3 - ยังไม่ได้แยกส่วน
recordในswitch· บทที่ 4 - ยังไม่พูดเรื่องการแปลง
recordลงตาราง เรื่องนั้นเป็นของ คอร์ส Kotlin กับ Spring Boot ซึ่งวัดไว้แล้ว
สรุปก่อนไปต่อ
หัวข้อที่มีชื่อว่า “สรุปก่อนไปต่อ”บทนี้เปลี่ยนคำถามจาก “พิมพ์สั้นลงไหม” เป็น “อะไรผูกกับอะไร”
record ให้ member สาธารณะมากกว่า class ที่เขียนมือสามตัว
· และสามตัวนั้นผูกกับรายชื่อช่องข้อมูลตลอดเวลา ไม่ได้ถูกคัดลอกไว้ตอนสร้าง
ราคาที่จ่ายคือสืบทอดไม่ได้ · ซึ่งเป็นราคาที่รู้ล่วงหน้าและตรวจได้ตอนแปล
บทหน้าเราเอา record ไปวางใน sealed interface แล้วดูว่า compiler บังคับอะไรให้
— และมีอยู่ช่องหนึ่งที่มันไม่บังคับ ซึ่งเป็นช่องที่ความผิดพลาดหลุดผ่านได้เงียบๆ
ตัวเลขและชื่อทุกตัวมาจาก javap บน class file จริง:
- วัดเมื่อ 2026-09-01 ·
javac 25.0.4.1·openjdk 25.0.4.1 2026-08-18 - รันซ้ำเองได้ด้วย
npm run verify:java-modernซึ่งแปลทั้งสองชนิดพร้อมกัน แล้วนับ member สาธารณะจากผลของjavapตรงๆ - JEP 395 — Records (เข้าถึง 2026-09-01)
— ข้อกำหนดว่า
recordเป็นfinalและสืบทอดจาก class อื่นไม่ได้ - ObjectMethods (เข้าถึง 2026-09-01) — bootstrap method ที่สร้างสามอย่างนั้นตอนรันครั้งแรก
เช็กความเข้าใจ — บทที่ 2
ข้อ 1 / 3`javap` บอกว่า `record` มี member สาธารณะ 6 ตัว ส่วน class ที่เขียนมือมี 3 · สามตัวที่ต่างกันคืออะไร