equals ของ record ไม่ใช่ code ที่ใครเขียน และนั่นคือประเด็นทั้งหมด
record ถูกขายด้วยประโยคว่าพิมพ์สั้นกว่า
จริง · แต่ถ้าเหตุผลมีแค่นั้น การให้ IDE สร้าง equals ให้ก็จบเรื่องแล้ว
เทียบของสองอันที่หน้าตาทำงานเหมือนกัน
หัวข้อที่มีชื่อว่า “เทียบของสองอันที่หน้าตาทำงานเหมือนกัน”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; }}แปลทั้งคู่แล้วเปิดดูด้วย javap
public 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 | ไม่มี |
| สืบทอดจาก | java.lang.Record | Object |
ของที่เขียนมือไม่มีทั้งสามตัว · มันจึงเทียบกันด้วยที่อยู่ในหน่วยความจำ ซึ่งเป็น bug ที่รอเวลา
สามตัวนั้นไม่ได้ถูกเขียนเป็น bytecode ทีละบรรทัด
หัวข้อที่มีชื่อว่า “สามตัวนั้นไม่ได้ถูกเขียนเป็น bytecode ทีละบรรทัด”เปิดด้วย javap -v แล้วดูตาราง BootstrapMethods
ทั้งสาม method เรียก invokedynamic ไปที่ตัวเดียวกัน คือ ObjectMethods.bootstrap
ตัวจริงของทั้งสามถูกสร้างตอนรันครั้งแรก จากรายชื่อช่องข้อมูลที่ฝังไว้ใน class file · ไม่ใช่จาก code ที่ใครพิมพ์ และไม่ใช่จาก code ที่ IDE พิมพ์ให้
ทำไมข้อนี้ถึงสำคัญกว่าความสั้น
หัวข้อที่มีชื่อว่า “ทำไมข้อนี้ถึงสำคัญกว่าความสั้น”ให้ IDE สร้าง equals ให้ก็ได้ code ที่ทำงานถูกเหมือนกัน · ในวันแรก
วันที่มีคนเพิ่ม field เข้าไป equals ที่ IDE สร้างไว้จะยังเทียบเฉพาะ field เดิม
- มันไม่แดง
- มันไม่เตือน
- ผลลัพธ์ผิดเฉพาะกรณีที่ field ใหม่ต่างกันเท่านั้น
record ไม่มีช่องให้พลาดข้อนี้ เพราะ ไม่มีสำเนาของรายชื่อ field อยู่ที่ไหนเลย
· มีที่เดียวคือในหัวของ record เอง
เครื่องมือที่สร้าง code ให้ ทำงานครั้งเดียวตอนคุณกด · ผลของมันเป็นสำเนา
กลไกที่ผูกไว้ตลอด อ่านจากต้นทางทุกครั้ง · ไม่มีสำเนาให้ล้าสมัย
คำถามที่ใช้แยกสองอย่างนี้คือ ถ้าฉันเพิ่ม field เข้าไป ต้องไปแก้อะไรอีกไหม · ถ้าคำตอบคือต้อง แปลว่าคุณกำลังดูแลสำเนาอยู่
canonical constructor คือทางเข้าเดียว
หัวข้อที่มีชื่อว่า “canonical constructor คือทางเข้าเดียว”public record Money(long amount, String currency) { public Money { if (amount < 0) throw new IllegalArgumentException("จำนวนเงินติดลบไม่ได้"); currency = currency.toUpperCase(); }}รูปที่ไม่มีวงเล็บนี้ทำงานก่อนค่าจะถูกเก็บลง field · และแก้ค่าที่กำลังจะถูกเก็บได้ด้วย
ไม่มีทางสร้าง Money ขึ้นมาโดยไม่ผ่านตรงนี้ · ไม่มี setter ไม่มี constructor อื่น
ราคาที่จ่าย
หัวข้อที่มีชื่อว่า “ราคาที่จ่าย”record สืบทอดจาก java.lang.Record อยู่แล้ว และ Java สืบทอดได้ชั้นเดียว
มันจึงสืบทอดจาก class อื่นไม่ได้เลย · ไม่มีข้อยกเว้น
implementsได้ตามปกติ · จึงใช้กับsealed interfaceได้- field เพิ่มเติมนอกหัวไม่ได้ · ทุกอย่างที่มันเก็บต้องอยู่ในวงเล็บบนสุด
- ตัวมันเป็น
finalเสมอ
ถ้าของที่คุณกำลังจะเขียนต้องการอย่างใดอย่างหนึ่งข้างบน record ไม่ใช่คำตอบ
· และนั่นไม่ใช่ข้อเสีย มันคือขอบเขตที่ประกาศไว้ชัดและตรวจได้ตอนแปล
ตัวเลขและชื่อทุกตัวมาจาก 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)
- ObjectMethods (เข้าถึง 2026-09-01)
- เรื่องนี้เป็นบทที่ 2 ของ คอร์ส Java 8 ถึง 25