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

`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 คือ​การ​พิมพ์​น้อย​ลง · ซึ่ง​จริง แต่​เป็น​ข้อ​ที่​เล็ก​ที่สุด

เปิด​ดู​ของ​ที่​ออก​มา​จริง ไม่ใช่​ของ​ที่​เรา​เขียน

Terminal window
javac --release 25 -d out Money.java MoneyPojo.java
javap -cp out Money
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ไม่มี
หัว classpublic final classpublic final class
สืบทอด​จากjava.lang.RecordObject

สาม​ตัว​ที่​ต่าง​กัน​คือ equals, hashCode และ toString · ของ​ที่​เขียน​มือ​ไม่มี​ทั้ง​สาม​ตัว และ​นั่น​คือ bug ที่​รอ​เวลา

สาม​ตัว​นั้น​ไม่​ได้​ถูก​เขียน​เป็น bytecode ที​ละ​บรรทัด

หัวข้อ​ที่​มีชื่อ​ว่า “สาม​ตัว​นั้น​ไม่​ได้​ถูก​เขียน​เป็น bytecode ที​ละ​บรรทัด”

ตรง​นี้​คือ​จุด​ที่ record ต่าง​จาก​การ​ให้ IDE สร้าง equals ให้

เปิด​ด้วย javap -v แล้ว​ดู​ตาราง BootstrapMethods

Terminal window
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 เอง

และ​มัน​คือ​ที่​ที่​ตรวจ​ความ​ถูก​ต้อง​ได้ แบบ​ที่​ไม่มี​ทาง​เลี่ยง

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 · สามตัวที่ต่างกันคืออะไร