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

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 == bfalse
a.hashCode() == b.hashCode()true
setOf(a, b).size2
a.items.contentEquals(b.items)true

ประกาศ​เดิม​ทุก​อย่าง เปลี่ยน​ชนิด​ของ property เป็น List แล้ว​วัด​ใหม่

data class Names(val items: List<String>)
ถามได้
c == dtrue
setOf(c, d).size1

คำ​อธิบาย​ทั้งหมด​อยู่​ใน​สอง​บรรทัด​นี้

$ javap -c -p Tags
method ที่ compiler สร้างมัน​เรียก
equalskotlin/jvm/internal/Intrinsics.areEqual:(Ljava/lang/Object;Ljava/lang/Object;)Z
hashCodejava/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.size1

อ็​อบ​เจ​กต์​ยัง​อยู่​ใน set แต่​หา​ตัว​เอง​ไม่​เจอ · size ยัง​เป็น 1 เพราะ​ไม่มี​ใคร​ถูก​ลบ​ออก แต่ contains ไป​หา​ผิด​ถัง เนื่องจาก hash เปลี่ยน​ไป​แล้ว

ข้อ​นี้​ไม่​ได้​เกิด​จาก​การ​ที่ equals เทียบ reference

เวลา​คน​เล่า​เรื่อง​นี้ มัก​จบ​ที่ “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.py
npm run verify:kotlin-bytecode

probe มี​ช่อง​ควบคุม​คือ​ชนิด​ที่​ใช้ List ต้อง​เท่า​กัน​และ​ยุบ​เหลือ​หนึ่ง​ตัว · เปลี่ยน Names ให้​ใช้ Array เหมือน​กัน แล้ว​มัน​แดง​ทันที