0Pricing
Java Academy · درس

تنفيذ hashCode

كتابة دوال تجزئة صحيحة

تنفيذ hashCode درس مجاني في Java Academy على CoddyKit. هذا هو الدرس 3 من أصل 4. يمكنك قراءة الدرس كاملاً أدناه مجاناً — ثم تمرن عليه مباشرة في المتصفح باستخدام محرر أكواد مدمج ومدرس ذكاء اصطناعي متاح 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, ...).

فهو يتعامل مع القيم null ويدمج الحقول باستخدام خوارزمية قياسية. استخدم الحقول نفسها التي تقارنها في 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());
    }
}

مثال على توزيع سيئ

يؤدي جمع الحقول من دون الضرب إلى حدوث تصادمات: إذ ينتج عن (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));
    }
}

تفضيل Records لأنواع القيم

بالنسبة إلى حوامل البيانات البحتة، ينشئ record hashCode صحيحًا وجيد التوزيع تلقائيًا.

لا تكتب hashCode يدويًا إلا إذا احتجت إلى دلالات مخصصة أو لم تتمكن من استخدام record.

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، وفضّل استخدام Records.

بعد ذلك، تعرّف على كيفية تحويل 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» كامل متاح مجاناً هنا على الويب. لتمرينه بشكل تفاعلي (محرر أكواد مدمج ومدرس ذكاء اصطناعي متاح 24/7) وفتح باقي دورة Java Academy، انتقل إلى CoddyKit PRO. تتضمن دورة Java Academy 4 دروس في المجموع.

ماذا ستتعلم في «تنفيذ hashCode»؟

كتابة دوال تجزئة صحيحة تتمرن على Java Academy مع أكواد عملية تشغلها مباشرة في المتصفح، ومدرس ذكاء اصطناعي متاح 24/7 يجيب على أسئلتك أثناء عملك.

هل أحتاج إلى خبرة سابقة لأبدأ Java Academy؟

لا تُشترط خبرة سابقة. Java Academy على CoddyKit منظم للمبتدئين حتى المتقدمين، لذا يمكنك البدء من هنا أو من البداية والتقدم بسرعتك الخاصة. هذا هو الدرس 3 من أصل 4.

كم من الوقت يستغرق درس «تنفيذ hashCode»؟

معظم دروس CoddyKit تستغرق حوالي 5–10 دقائق. كل منها موجز وتفاعلي، لذا تحرز تقدماً مستمراً وتستأنف من حيث توقفت عبر الويب والتطبيق.

هل يمكنني كتابة وتشغيل أكواد في درس Java Academy هذا؟

نعم. كل درس في Java Academy يتضمن محرر أكواد مدمج، لذا تكتب وتشغل أكواداً حقيقية مباشرة في متصفحك وتحصل على تعليقات فورية من الذكاء الاصطناعي — بدون إعداد محلي.

جميع الدروس في هذه الدورة

  1. آلية عمل HashMap
  2. عقد equals وhashCode
  3. تنفيذ hashCode
  4. تحويل الأشجار والأداء
← العودة إلى Java Academy