data class ที่มี Array — equals กับ hashCode ถูกสร้างด้วยนิยามคนละอัน
data class ขายอยู่ข้อเดียว คือคุณประกาศ property แล้วได้ equals, hashCode,
toString, copy และ componentN มาฟรี
ถ้า property ตัวหนึ่งเป็น Array ของฟรีนั้นยังมาครบ แต่สองตัวในนั้นไม่ได้คุยกัน
ทรงที่พัง
หัวข้อที่มีชื่อว่า “ทรงที่พัง”สองอ็อบเจกต์ที่เนื้อในเหมือนกันทุกตัวอักษร ไม่เท่ากัน แต่ hash เท่ากัน
data class Tags(val items: Array<String>)
val a = Tags(arrayOf("tea", "cake"))val b = Tags(arrayOf("tea", "cake"))| ถาม | ได้ |
|---|---|
a == b | false |
a.hashCode() == b.hashCode() | true |
setOf(a, b).size | 2 |
a.items.contentEquals(b.items) | true |
ประกาศเดิมทุกอย่าง เปลี่ยนชนิดของ property เป็น List แล้ววัดใหม่
data class Names(val items: List<String>)| ถาม | ได้ |
|---|---|
c == d | true |
setOf(c, d).size | 1 |
เปิด bytecode ดูว่าใครเรียกอะไร
หัวข้อที่มีชื่อว่า “เปิด bytecode ดูว่าใครเรียกอะไร”คำอธิบายทั้งหมดอยู่ในสองบรรทัดนี้
$ javap -c -p Tags| method ที่ compiler สร้าง | มันเรียก |
|---|---|
equals | kotlin/jvm/internal/Intrinsics.areEqual:(Ljava/lang/Object;Ljava/lang/Object;)Z |
hashCode | java/util/Arrays.hashCode:([Ljava/lang/Object;)I |
Intrinsics.areEqual เรียก equals ของอ็อบเจกต์ · และ equals ของ Array บน JVM
คือการเทียบ reference · สอง array คนละตัวจึงไม่มีวันเท่ากัน
java.util.Arrays.hashCode ทำตรงข้าม มันเดินดูสมาชิกทีละตัวแล้วรวมเป็นเลขเดียว
· สอง array ที่เนื้อในเหมือนกันจึงได้เลขเดียวกันเสมอ
สัญญาของ equals กับ hashCode มีข้อเดียวที่ห้ามผิด คือ ถ้าเท่ากัน hash ต้องเท่ากัน
ทรงนี้ไม่ได้ผิดข้อนั้น เพราะมันไม่เคยเท่ากัน · ที่มันทำคือเดินไปอีกทาง
คือ hash เท่ากันทั้งที่ไม่เท่ากัน ซึ่งถูกกฎ แต่เป็นทรงที่แย่ที่สุดสำหรับ HashMap
สองตัวลงถังเดียวกันเพราะ hash ตรงกัน แล้วต้องถูกเทียบทีละคู่ในถังนั้น แล้วผลการเทียบบอกว่าคนละตัว · คุณจ่ายค่าการชนถัง โดยไม่ได้ประโยชน์ของการยุบซ้ำ
และปัญหาข้อที่สอง ซึ่งมีอยู่แล้ววันนี้
หัวข้อที่มีชื่อว่า “และปัญหาข้อที่สอง ซึ่งมีอยู่แล้ววันนี้”
hashCodeที่เทียบเนื้อใน แปลว่า hash ขยับได้ตลอดอายุของอ็อบเจกต์
Array แก้ค่าข้างในได้เสมอ แม้ property จะประกาศเป็น val
· val ห้ามเปลี่ยนว่าชี้ไป array ไหน มันไม่ได้ห้ามเปลี่ยนของข้างใน
val t = Tags(arrayOf("tea"))val set = hashSetOf(t)t.items[0] = "coffee" // อ็อบเจกต์เดิม · array เดิม · เนื้อในใหม่| ถาม | ได้ |
|---|---|
set.contains(t) ก่อนแก้ | true |
hashCode เปลี่ยนไหม | true |
set.contains(t) หลังแก้ | false |
set.size | 1 |
อ็อบเจกต์ยังอยู่ใน set แต่หาตัวเองไม่เจอ · size ยังเป็น 1
เพราะไม่มีใครถูกลบออก แต่ contains ไปหาผิดถัง เนื่องจาก hash เปลี่ยนไปแล้ว
เวลาคนเล่าเรื่องนี้ มักจบที่ “array ใน data class ใช้ reference equality ระวังด้วย”
แล้วแนะให้เขียน equals เองด้วย contentEquals
การเขียน equals เองไม่ได้แก้ข้อนี้เลย เพราะข้อนี้มาจาก hashCode
ซึ่งเป็น content-based อยู่แล้วตั้งแต่ต้น · แก้ equals ให้ตรงกับ hashCode
ได้ชนิดที่เท่ากันสมเหตุสมผลขึ้น แต่ hash ก็ยังขยับตามเนื้อในเหมือนเดิม
ชนิดที่ hash ขยับได้ ไม่ควรเป็น key ของ Map หรือสมาชิกของ Set ตั้งแต่แรก
ไม่ว่า equals จะเขียนดีแค่ไหน
ทำอย่างไร
หัวข้อที่มีชื่อว่า “ทำอย่างไร”เรียงจากทางที่ควรเลือกก่อน
- ใช้
ListแทนArrayถ้าไม่มีเหตุผลเรื่องหน่วยความจำที่วัดได้จริง ·Listที่ได้จากlistOfแก้ไม่ได้ และequalsของมันเทียบเนื้อใน - ถ้าต้องใช้
Arrayจริง ให้เขียนequalsกับhashCodeเอง และเขียนทั้งคู่ ในคอมมิตเดียวกัน · เขียนตัวเดียวคือการสร้างทรงที่หัวข้อแรกของหน้านี้วัดไว้ - อย่าเอาชนิดที่มี
Arrayไปเป็น key ของMapหรือสมาชิกของSet· ข้อนี้ไม่หายไปแม้เขียนequalsเองแล้ว ตราบใดที่เนื้อในยังแก้ได้
ทุกค่าในหน้านี้มาจาก kotlinc 2.4.10 กับ javap ตัวจริง
· แปลด้วย -Werror -Wextra -jvm-target 25 และไม่มีหน่วยเวลาสักตัว
python3 scripts/kotlin-bytecode-facts/probe.pynpm run verify:kotlin-bytecodeprobe มีช่องควบคุมคือชนิดที่ใช้ List ต้องเท่ากันและยุบเหลือหนึ่งตัว
· เปลี่ยน Names ให้ใช้ Array เหมือนกัน แล้วมันแดงทันที
- Kotlin — Data classes (เข้าถึง 2026-08-30)
- Kotlin — Arrays (เข้าถึง 2026-08-30)
- Java SE 25 —
Arrays.hashCode(เข้าถึง 2026-08-30)