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

การสร้าง hashCode

เขียนฟังก์ชันแฮชที่ถูกต้อง

การสร้าง hashCode เป็นบทเรียน Java Academy ฟรีบน CoddyKit นี่คือบทเรียนที่ 3 จากทั้งหมด 4 บทเรียน คุณสามารถอ่านบทเรียนทั้งหมดด้านล่างฟรี — จากนั้นลองปฏิบัติด้วยตัวคุณเองในเบราว์เซอร์พร้อมตัวแก้ไขโค้ดในตัวและติวเตอร์ AI ตลอด 24/7 บทเรียนนี้เป็นส่วนหนึ่งของเส้นทางการเรียน Java Academy และความก้าวหน้าของคุณจะซิงค์ข้ามเว็บและแอป CoddyKit คอร์ส Java Academy มีบทเรียนทั้งหมด 4 บทเรียน

เป้าหมายของ hashCode ที่ดี

hashCode() ที่ดีควร:

  • ส่งคืนค่าเดียวกันสำหรับอ็อบเจ็กต์ที่เท่ากัน (ตามสัญญา)
  • กระจายอ็อบเจ็กต์ที่ไม่เท่ากันไปยังค่าที่แตกต่างกันจำนวนมาก
  • คำนวณได้โดยมีต้นทุนต่ำ

hashCode ที่ไม่ดีซึ่งส่งคืนค่าคงที่ยังคงปฏิบัติตามสัญญา แต่จะทำให้แมปกลายเป็นรายการเชื่อมโยงที่ทำงานช้า

public class Main {
    public static void main(String[] args) {
        // Legal but terrible: every object collides
        System.out.println("constant hashCode is legal but kills performance");
    }
}

Objects.hash สำหรับกรณีทั่วไป

วิธีที่ง่ายและถูกต้องที่สุดคือ Objects.hash(field1, field2, ...)

เมธอดนี้จัดการค่าว่างและรวมฟิลด์ด้วยอัลกอริทึมมาตรฐาน ควรใช้ฟิลด์ชุดเดียวกันกับที่คุณเปรียบเทียบใน equals

import java.util.Objects;

public class Main {
    static class User {
        final String name; final int age;
        User(String name, int age) { this.name = name; this.age = age; }
        @Override public int hashCode() { return Objects.hash(name, age); }
    }
    public static void main(String[] args) {
        User a = new User("Ada", 36);
        User b = new User("Ada", 36);
        System.out.println(a.hashCode() == b.hashCode());
    }
}

ตัวคูณ 31 แบบดั้งเดิม

สำหรับการคำนวณแฮชด้วยตนเอง รูปแบบมาตรฐานคือคูณผลลัพธ์สะสมด้วย31 แล้วบวกค่าแฮชของแต่ละฟิลด์

31 เป็นจำนวนเฉพาะคี่ และ 31 * x ให้ผลเหมือนกับ (x << 5) - x ดังนั้น JVM จึงสามารถปรับให้เหมาะสมได้

public class Main {
    static class User {
        final String name; final int age;
        User(String name, int age) { this.name = name; this.age = age; }
        @Override public int hashCode() {
            int result = 17;
            result = 31 * result + (name == null ? 0 : name.hashCode());
            result = 31 * result + age;
            return result;
        }
    }
    public static void main(String[] args) {
        System.out.println(new User("Ada", 36).hashCode());
    }
}

การคำนวณแฮชของชนิดข้อมูลพื้นฐาน

ชนิดข้อมูลพื้นฐานแต่ละชนิดมีวิธีคำนวณแฮชที่แนะนำดังนี้:

  • int: ใช้ค่าของมันเอง
  • long: (int)(value ^ (value >>> 32))
  • boolean: 1 หรือ 0
  • double: Double.hashCode(value)
public class Main {
    public static void main(String[] args) {
        long id = 4_000_000_000L;
        int longHash = (int) (id ^ (id >>> 32));
        System.out.println("long hash: " + longHash);
        System.out.println("double hash: " + Double.hashCode(3.14));
        System.out.println("bool hash: " + Boolean.hashCode(true));
    }
}

การคำนวณแฮชของอาร์เรย์

อย่าเรียก hashCode() โดยตรงกับอาร์เรย์ เพราะเมธอดนี้ใช้เอกลักษณ์ของอ็อบเจ็กต์ ไม่ใช่เนื้อหา

ใช้ Arrays.hashCode(arr) สำหรับอาร์เรย์ชั้นเดียว หรือ Arrays.deepHashCode(arr) สำหรับอาร์เรย์ซ้อนกัน

import java.util.Arrays;

public class Main {
    public static void main(String[] args) {
        int[] a = {1, 2, 3};
        int[] b = {1, 2, 3};
        System.out.println("identity equal: " + (a.hashCode() == b.hashCode()));
        System.out.println("content equal: " + (Arrays.hashCode(a) == Arrays.hashCode(b)));
    }
}

ทำให้ equals และ hashCode สอดคล้องกัน

ฟิลด์ที่ใช้ใน hashCode() ต้องเป็นสับเซตของฟิลด์ที่ใช้ใน equals() (ควรเป็นชุดเดียวกันทุกประการ)

หาก equals เปรียบเทียบฟิลด์มากกว่า hashCode อ็อบเจ็กต์ที่เท่ากันสองตัวก็ยังคงมีค่าแฮชเดียวกัน ซึ่งทำได้ตามสัญญา แต่หาก hashCode ใช้ฟิลด์ที่ equals ไม่ได้พิจารณา คุณจะละเมิดสัญญา

import java.util.Objects;

public class Main {
    static class Coord {
        final int x, y;
        Coord(int x, int y) { this.x = x; this.y = y; }
        @Override public boolean equals(Object o) {
            return o instanceof Coord c && c.x == x && c.y == y;
        }
        @Override public int hashCode() { return Objects.hash(x, y); }
    }
    public static void main(String[] args) {
        Coord a = new Coord(3, 4), b = new Coord(3, 4);
        System.out.println(a.equals(b) && a.hashCode() == b.hashCode());
    }
}

การเก็บค่าแฮชไว้ใช้ซ้ำ

สำหรับอ็อบเจ็กต์ที่ไม่เปลี่ยนแปลงและมีต้นทุนการคำนวณแฮชสูง คุณสามารถเก็บผลลัพธ์ไว้ในฟิลด์ได้

String ทำเช่นนี้ภายในอยู่แล้ว ควรทำก็ต่อเมื่ออ็อบเจ็กต์ไม่เปลี่ยนแปลงอย่างแท้จริง เพื่อไม่ให้ค่าที่เก็บไว้ล้าสมัย

import java.util.Objects;

public class Main {
    static final class Key {
        final String a, b;
        private int hash; // 0 until computed
        Key(String a, String b) { this.a = a; this.b = b; }
        @Override public int hashCode() {
            int h = hash;
            if (h == 0) { h = Objects.hash(a, b); hash = h; }
            return h;
        }
    }
    public static void main(String[] args) {
        Key k = new Key("x", "y");
        System.out.println(k.hashCode());
        System.out.println(k.hashCode());
    }
}

การกระจายมีความสำคัญ

hashCode ที่กระจายได้ดีจะกระจายคีย์ไปยังช่องต่าง ๆ อย่างสม่ำเสมอ ลองนับจำนวนค่าแฮชที่แตกต่างกันสำหรับกลุ่มอ็อบเจ็กต์ชุดหนึ่ง

ยิ่งมีค่าที่แตกต่างกันมาก การชนกันก็ยิ่งน้อยลง และแมปก็ยิ่งทำงานเร็วขึ้น

import java.util.HashSet;
import java.util.Objects;
import java.util.Set;

public class Main {
    record Pair(int a, int b) {}
    public static void main(String[] args) {
        Set<Integer> hashes = new HashSet<>();
        for (int i = 0; i < 100; i++) {
            hashes.add(Objects.hash(i, i * 7));
        }
        System.out.println("distinct hashes: " + hashes.size());
    }
}

ตัวอย่างการกระจายแบบ Bad

การบวกฟิลด์โดยไม่คูณทำให้เกิดการชนกัน: (1,2) และ (2,1) ต่างก็มีค่าแฮชเป็น 3

ตัวคูณ 31 แก้ปัญหาความสมมาตรนี้ เพราะลำดับจึงมีผล

public class Main {
    static int badHash(int a, int b) { return a + b; }
    static int goodHash(int a, int b) { return 31 * a + b; }
    public static void main(String[] args) {
        System.out.println("bad (1,2): " + badHash(1, 2) + ", (2,1): " + badHash(2, 1));
        System.out.println("good (1,2): " + goodHash(1, 2) + ", (2,1): " + goodHash(2, 1));
    }
}

ควรใช้ระเบียนสำหรับชนิดข้อมูลค่า

สำหรับโครงสร้างที่ใช้เก็บข้อมูลล้วน ๆ record จะสร้าง hashCode ที่ถูกต้องและกระจายได้ดีโดยอัตโนมัติ

ควรเขียน hashCode ด้วยตนเองก็ต่อเมื่อคุณต้องการความหมายการทำงานแบบกำหนดเอง หรือไม่สามารถใช้ระเบียนได้

public class Main {
    record Money(long cents, String currency) {}
    public static void main(String[] args) {
        Money a = new Money(1099, "USD");
        Money b = new Money(1099, "USD");
        System.out.println(a.equals(b));
        System.out.println(a.hashCode() == b.hashCode());
    }
}

ประกอบทุกอย่างเข้าด้วยกัน

คลาสข้อมูลค่าที่สมบูรณ์: มีฟิลด์ที่ไม่เปลี่ยนแปลง, equals และ hashCode ที่ใช้ฟิลด์ชุดเดียวกัน และ toString ที่เรียบร้อย

import java.util.Objects;

public class Main {
    static final class Version {
        final int major, minor, patch;
        Version(int major, int minor, int patch) {
            this.major = major; this.minor = minor; this.patch = patch;
        }
        @Override public boolean equals(Object o) {
            return o instanceof Version v && v.major == major && v.minor == minor && v.patch == patch;
        }
        @Override public int hashCode() { return Objects.hash(major, minor, patch); }
        @Override public String toString() { return major + "." + minor + "." + patch; }
    }
    public static void main(String[] args) {
        Version v = new Version(2, 1, 0);
        System.out.println(v + " hash=" + v.hashCode());
    }
}

ตรวจสอบความเข้าใจ

ทดสอบทักษะการเขียน hashCode ของคุณ

ทบทวน

คุณได้เรียนรู้วิธีเขียน hashCode ให้ถูกต้องดังนี้:

  • ใช้ Objects.hash(...) สำหรับกรณีทั่วไป
  • ใช้รูปแบบตัวคูณ 31สำหรับการคำนวณแฮชด้วยตนเอง
  • คำนวณแฮชของอาร์เรย์ด้วย Arrays.hashCode ไม่ใช่วิธีเริ่มต้น
  • ทำให้ฟิลด์ของ hashCode สอดคล้องกับ equals และควรใช้ระเบียน

ต่อไป มาดูกันว่า Java 8+ แปลงช่องที่มีการชนกันมากเป็นต้นไม้อย่างไร

import java.util.Objects;

public class Main {
    public static void main(String[] args) {
        System.out.println("hashCode recap done: " + Objects.hash("done"));
    }
}

คำถามที่พบบ่อย

บทเรียน “การสร้าง hashCode” ฟรีหรือไม่

ใช่ — ข้อความเต็มของ “การสร้าง hashCode” ฟรีให้อ่านที่นี่บนเว็บ เพื่อปฏิบัติแบบโต้ตอบ (ตัวแก้ไขโค้ดในตัวและติวเตอร์ AI ตลอด 24/7) และปลดล็อคส่วนที่เหลือของคอร์ส Java Academy ให้อัปเกรดเป็น CoddyKit PRO คอร์ส Java Academy มีบทเรียนทั้งหมด 4 บทเรียน

คุณจะเรียนรู้อะไรในบทเรียน “การสร้าง hashCode”

เขียนฟังก์ชันแฮชที่ถูกต้อง คุณปฏิบัติ Java Academy ด้วยโค้ดที่ใช้งานได้จริงที่คุณเรียกใช้โดยตรงในเบราว์เซอร์ และติวเตอร์ AI ตลอด 24/7 ตอบคำถามของคุณขณะที่คุณไปผ่านบทเรียน

คุณต้องมีประสบการณ์ก่อนที่จะเริ่มเรียน Java Academy หรือไม่

ไม่จำเป็นต้องมีประสบการณ์มาก่อน Java Academy บน CoddyKit ออกแบบมาสำหรับผู้เริ่มต้นไปจนถึงผู้เรียนขั้นสูง คุณสามารถเริ่มต้นที่นี่หรือเริ่มจากตัวแรกและเรียนด้วยความเร็วของคุณเอง นี่คือบทเรียนที่ 3 จากทั้งหมด 4 บทเรียน

บทเรียน “การสร้าง hashCode” ใช้เวลานานแค่ไหน

บทเรียน CoddyKit ส่วนใหญ่ใช้เวลาประมาณ 5–10 นาที แต่ละบทเรียนจึงสั้นและเป็นแบบโต้ตอบ คุณสามารถก้าวหน้าอย่างต่อเนื่องและกลับมาเรียนต่อจากตรงที่เพิ่งหยุดบนเว็บและแอปได้เลย

ฉันเขียนและรันโค้ดในบทเรียน Java Academy นี้ได้ไหม

ได้ บทเรียน Java Academy ทุกบทมีตัวแก้ไขโค้ดในตัว คุณจึงเขียนและรันโค้ดจริงได้เลยในเบราว์เซอร์ และได้รับข้อเสนอแนะจาก AI ในทันที — ไม่ต้องติดตั้งในเครื่องของคุณ

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

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