สัญญาของ equals/hashCode
เหตุใดทั้งสองจึงต้องสอดคล้องกัน
สัญญาของ equals/hashCode เป็นบทเรียน Java Academy ฟรีบน CoddyKit นี่คือบทเรียนที่ 2 จากทั้งหมด 4 บทเรียน คุณสามารถอ่านบทเรียนทั้งหมดด้านล่างฟรี — จากนั้นลองปฏิบัติด้วยตัวคุณเองในเบราว์เซอร์พร้อมตัวแก้ไขโค้ดในตัวและติวเตอร์ AI ตลอด 24/7 บทเรียนนี้เป็นส่วนหนึ่งของเส้นทางการเรียน Java Academy และความก้าวหน้าของคุณจะซิงค์ข้ามเว็บและแอป CoddyKit คอร์ส Java Academy มีบทเรียนทั้งหมด 4 บทเรียน
สองเมธอด หนึ่งสัญญา
อ็อบเจ็กต์ Java ทุกตัวสืบทอด equals() และ hashCode() มาจาก Object
เมื่อคุณโอเวอร์ไรด์เมธอดหนึ่ง คุณมักจะต้องโอเวอร์ไรด์อีกเมธอดหนึ่งด้วย เมธอดทั้งสองเป็นสัญญาที่มีผลผูกพัน ซึ่งคอลเลกชันที่อาศัยแฮชต้องพึ่งพา
public class Main {
public static void main(String[] args) {
Object a = new Object();
Object b = new Object();
System.out.println(a.equals(b));
System.out.println(a.hashCode() == b.hashCode());
}
}กฎหลัก
สัญญาระบุไว้ว่า:
- หาก
a.equals(b)เป็นจริงa.hashCode()ต้องเท่ากับb.hashCode() - หากค่าแฮชแตกต่างกัน อ็อบเจ็กต์จะไม่เท่ากันอย่างแน่นอน
อ็อบเจ็กต์ที่เท่ากันต้องมีค่าแฮชเท่ากัน แต่ไม่จำเป็นต้องเป็นจริงในทางกลับกัน
public class Main {
public static void main(String[] args) {
String s1 = "hello";
String s2 = "hel" + "lo";
System.out.println(s1.equals(s2));
System.out.println(s1.hashCode() == s2.hashCode());
}
}อ็อบเจ็กต์ที่ไม่เท่ากันอาจมีค่าแฮชร่วมกัน
อ็อบเจ็กต์ที่ไม่เท่ากันสองตัวสามารถใช้ค่าแฮชเดียวกันได้ นั่นเป็นเพียงการชนกัน และแมปจะจัดการด้วย equals()
ดังนั้น สถานการณ์เดียวที่ห้ามเกิดขึ้นคือ อ็อบเจ็กต์ที่เท่ากันมีค่าแฮชแตกต่างกัน
public class Main {
public static void main(String[] args) {
// Equal value, unequal objects, same hash is fine
System.out.println("FB".hashCode() == "Ea".hashCode());
System.out.println("FB".equals("Ea"));
}
}คลาสที่โอเวอร์ไรด์เฉพาะ equals
นี่คือคลาสที่มีข้อบกพร่อง คลาสนี้โอเวอร์ไรด์ equals() แต่ไม่ได้โอเวอร์ไรด์ hashCode()
จุดสองจุดที่เท่ากันในเชิงตรรกะจึงมีค่าแฮชแตกต่างกันซึ่งสืบทอดมาจาก Object ทำให้ละเมิดสัญญา
public class Main {
static class Point {
final int x, y;
Point(int x, int y) { this.x = x; this.y = y; }
@Override public boolean equals(Object o) {
if (!(o instanceof Point p)) return false;
return x == p.x && y == p.y;
}
// BUG: no hashCode override
}
public static void main(String[] args) {
Point a = new Point(1, 2);
Point b = new Point(1, 2);
System.out.println("equals: " + a.equals(b));
System.out.println("same hash: " + (a.hashCode() == b.hashCode()));
}
}ข้อบกพร่องใน HashSet
คลาสที่มีข้อบกพร่องทำงานผิดพลาดใน HashSet เซตกำลังค้นหาในช่องที่เลือกจากค่าแฮชที่ไม่ถูกต้อง จึงไม่พบองค์ประกอบที่เท่ากัน
ผลลัพธ์คือมีรายการซ้ำที่ควรถูกปฏิเสธ
import java.util.HashSet;
import java.util.Set;
public class Main {
static class Point {
final int x, y;
Point(int x, int y) { this.x = x; this.y = y; }
@Override public boolean equals(Object o) {
if (!(o instanceof Point p)) return false;
return x == p.x && y == p.y;
}
}
public static void main(String[] args) {
Set<Point> set = new HashSet<>();
set.add(new Point(1, 2));
set.add(new Point(1, 2));
System.out.println("size = " + set.size());
}
}การแก้ไข
โอเวอร์ไรด์ทั้งสองเมธอดและคำนวณจากฟิลด์ชุดเดียวกัน จากนั้นอ็อบเจ็กต์ที่เท่ากันจะมีค่าแฮชเดียวกันและอยู่ในช่องเดียวกัน
import java.util.HashSet;
import java.util.Objects;
import java.util.Set;
public class Main {
static class Point {
final int x, y;
Point(int x, int y) { this.x = x; this.y = y; }
@Override public boolean equals(Object o) {
if (!(o instanceof Point p)) return false;
return x == p.x && y == p.y;
}
@Override public int hashCode() { return Objects.hash(x, y); }
}
public static void main(String[] args) {
Set<Point> set = new HashSet<>();
set.add(new Point(1, 2));
set.add(new Point(1, 2));
System.out.println("size = " + set.size());
}
}equals ต้องมีสมบัติสะท้อนตัวเอง
equals() ที่ถูกต้องต้องเป็นไปตามคุณสมบัติสี่ประการ ประการแรกคือสะท้อนตัวเอง: x.equals(x) ต้องเป็นจริง
ฟังดูเป็นเรื่อง очевид แต่ตรรกะแบบกำหนดเองอาจทำให้คุณสมบัตินี้ผิดพลาดได้ หากคุณลืมจัดการกรณีอ้างอิงเดียวกัน
public class Main {
public static void main(String[] args) {
String x = "java";
System.out.println(x.equals(x));
}
}สมมาตรและถ่ายทอด
กฎอีกสองข้อคือ:
- สมมาตร: หาก
a.equals(b)ก็ต้องเป็นb.equals(a) - ถ่ายทอด: หาก
a.equals(b)และb.equals(c)ก็ต้องเป็นa.equals(c)
การผสมชนิดข้อมูลข้ามลำดับชั้นของคลาสมักทำให้สมบัติสมมาตรผิดพลาด ดังนั้นควรเปรียบเทียบคลาสอย่างระมัดระวัง
public class Main {
public static void main(String[] args) {
String a = "x", b = "x", c = "x";
boolean symmetric = a.equals(b) == b.equals(a);
boolean transitive = a.equals(b) && b.equals(c) && a.equals(c);
System.out.println("symmetric: " + symmetric);
System.out.println("transitive: " + transitive);
}
}สอดคล้อง
กฎข้อที่สี่คือสอดคล้อง: การเรียกซ้ำต้องส่งคืนผลลัพธ์เดิมตราบใดที่อ็อบเจ็กต์ไม่เปลี่ยนแปลง
นี่คือเหตุผลที่คุณควรใช้ฟิลด์ที่ไม่เปลี่ยนแปลงเป็นพื้นฐานของ equals และ hashCode การเปลี่ยนค่าฟิลด์ที่ใช้ใน hashCode หลังจากเพิ่มอ็อบเจ็กต์ลงไปจะทำให้แมปเสียหาย
import java.util.Objects;
public class Main {
public static void main(String[] args) {
// Immutable record gives consistent equals/hashCode automatically
record Id(int value) {}
Id id = new Id(7);
System.out.println(Objects.equals(id, new Id(7)));
System.out.println(id.hashCode() == new Id(7).hashCode());
}
}ระเบียนจัดการให้คุณ
record ของ Java จะสร้าง equals() และ hashCode() โดยอัตโนมัติจากองค์ประกอบทั้งหมด
สำหรับข้อมูลที่มีลักษณะเป็นค่า ควรใช้ระเบียน สัญญาจะถูกปฏิบัติตามโดยไม่ต้องเขียนเอง และการทำงานจะยังสอดคล้องกัน
import java.util.HashSet;
import java.util.Set;
public class Main {
record Point(int x, int y) {}
public static void main(String[] args) {
Set<Point> set = new HashSet<>();
set.add(new Point(1, 2));
set.add(new Point(1, 2));
System.out.println("size = " + set.size());
}
}กับดักคีย์ที่เปลี่ยนแปลงได้
หากคุณเปลี่ยนค่าฟิลด์ที่ใช้ใน hashCode หลังจากเพิ่มอ็อบเจ็กต์ลงในแมป คุณจะไม่สามารถค้นหาอ็อบเจ็กต์นั้นได้อีก
แมปจะค้นหาในช่องเดิม แต่อ็อบเจ็กต์จะมีค่าแฮชไปอยู่ในช่องใหม่ รายการนั้นจึงกลายเป็นผีที่สูญหาย
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
public class Main {
static class Box {
int id;
Box(int id) { this.id = id; }
@Override public boolean equals(Object o) {
return o instanceof Box b && b.id == id;
}
@Override public int hashCode() { return Objects.hash(id); }
}
public static void main(String[] args) {
Map<Box, String> m = new HashMap<>();
Box key = new Box(1);
m.put(key, "value");
key.id = 99; // mutated after insertion
System.out.println(m.get(key));
}
}ตรวจสอบความเข้าใจ
ทดสอบความเข้าใจของคุณเกี่ยวกับสัญญานี้
ทบทวน
คุณได้เรียนรู้สัญญาของ equals/hashCode ดังนี้:
- อ็อบเจ็กต์ที่เท่ากันต้องมีค่าแฮชเท่ากัน
- equals ต้องมีสมบัติสะท้อนตัวเอง สมมาตร ถ่ายทอด และสอดคล้อง
- ใช้ฟิลด์ที่ไม่เปลี่ยนแปลงชุดเดียวกันเป็นพื้นฐานของทั้งสองเมธอด
- ระเบียนจะสร้างการใช้งานที่ถูกต้องให้โดยไม่ต้องเขียนเอง
ต่อไป คุณจะเขียน hashCode ที่ถูกต้องด้วยตนเอง
import java.util.Objects;
public class Main {
public static void main(String[] args) {
System.out.println("Contract recap: " + Objects.hash(1, 2, 3));
}
}คำถามที่พบบ่อย
บทเรียน “สัญญาของ equals/hashCode” ฟรีหรือไม่
ใช่ — ข้อความเต็มของ “สัญญาของ equals/hashCode” ฟรีให้อ่านที่นี่บนเว็บ เพื่อปฏิบัติแบบโต้ตอบ (ตัวแก้ไขโค้ดในตัวและติวเตอร์ AI ตลอด 24/7) และปลดล็อคส่วนที่เหลือของคอร์ส Java Academy ให้อัปเกรดเป็น CoddyKit PRO คอร์ส Java Academy มีบทเรียนทั้งหมด 4 บทเรียน
คุณจะเรียนรู้อะไรในบทเรียน “สัญญาของ equals/hashCode”
เหตุใดทั้งสองจึงต้องสอดคล้องกัน คุณปฏิบัติ Java Academy ด้วยโค้ดที่ใช้งานได้จริงที่คุณเรียกใช้โดยตรงในเบราว์เซอร์ และติวเตอร์ AI ตลอด 24/7 ตอบคำถามของคุณขณะที่คุณไปผ่านบทเรียน
คุณต้องมีประสบการณ์ก่อนที่จะเริ่มเรียน Java Academy หรือไม่
ไม่จำเป็นต้องมีประสบการณ์มาก่อน Java Academy บน CoddyKit ออกแบบมาสำหรับผู้เริ่มต้นไปจนถึงผู้เรียนขั้นสูง คุณสามารถเริ่มต้นที่นี่หรือเริ่มจากตัวแรกและเรียนด้วยความเร็วของคุณเอง นี่คือบทเรียนที่ 2 จากทั้งหมด 4 บทเรียน
บทเรียน “สัญญาของ equals/hashCode” ใช้เวลานานแค่ไหน
บทเรียน CoddyKit ส่วนใหญ่ใช้เวลาประมาณ 5–10 นาที แต่ละบทเรียนจึงสั้นและเป็นแบบโต้ตอบ คุณสามารถก้าวหน้าอย่างต่อเนื่องและกลับมาเรียนต่อจากตรงที่เพิ่งหยุดบนเว็บและแอปได้เลย
ฉันเขียนและรันโค้ดในบทเรียน Java Academy นี้ได้ไหม
ได้ บทเรียน Java Academy ทุกบทมีตัวแก้ไขโค้ดในตัว คุณจึงเขียนและรันโค้ดจริงได้เลยในเบราว์เซอร์ และได้รับข้อเสนอแนะจาก AI ในทันที — ไม่ต้องติดตั้งในเครื่องของคุณ
บทเรียนทั้งหมดในหลักสูตรนี้
- การทำงานของ HashMap
- สัญญาของ equals/hashCode
- การสร้าง hashCode
- การเปลี่ยนเป็นต้นไม้และประสิทธิภาพ