0Pricing
Java Academy · บทเรียน

สัญญาของ 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 ในทันที — ไม่ต้องติดตั้งในเครื่องของคุณ

บทเรียนทั้งหมดในหลักสูตรนี้

  1. การทำงานของ HashMap
  2. สัญญาของ equals/hashCode
  3. การสร้าง hashCode
  4. การเปลี่ยนเป็นต้นไม้และประสิทธิภาพ
← กลับไปที่ Java Academy