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

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();
}
recordclass ที่​เขียน​มือ
member สาธารณะ63
equals · hashCode · toStringมี​ให้ · เป็น finalไม่มี
สืบทอด​จากjava.lang.RecordObject

ของ​ที่​เขียน​มือ​ไม่มี​ทั้ง​สาม​ตัว · มัน​จึง​เทียบ​กัน​ด้วย​ที่​อยู่​ใน​หน่วย​ความ​จำ ซึ่ง​เป็น 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 เข้าไป ต้อง​ไป​แก้​อะไร​อีก​ไหม · ถ้า​คำ​ตอบ​คือ​ต้อง แปล​ว่า​คุณ​กำลัง​ดูแล​สำเนา​อยู่

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