تجاوز equals و hashCode
تجاوز equals و hashCode
في الدرس الثالث تعلّمت كيفية تجاوز التوابع لكي يستبدل الصنف الفرعي سلوك الصنف الأب. اثنان من التوابع التي يحتاج كل صنف حقيقي تقريبًا إلى تجاوزها هما equals وhashCode، وكلاهما موروث من Object. فهم سبب الحاجة إلى تجاوزهما — والقواعد التي يجب اتباعها عند ذلك — معرفة أساسية في Java.
المساواة المرجعية مقابل المساواة القيمية
المعامل == في Java يتحقق من المساواة المرجعية: هل يشير المتغيران إلى نفس الكائن في الذاكرة؟ وهذا نادرًا ما يكون المقصود عند مقارنة كائنَين يمثّلان نفس المفهوم.
التابع equals، حين يُتجاوز بالشكل الصحيح، يُعبّر عن المساواة القيمية: هل يمثّل الكائنان نفس الشيء، حتى لو كانا نسختَين مختلفتَين؟
التابع equals الافتراضي من Object
إذا لم تتجاوز equals، تستخدم Java التطبيق الافتراضي من Object الذي يُطبّق == ببساطة. وبذلك، لا يساوي أي نسختان مستقلتان بعضهما مهما تطابقت حقولهما.
هذا نادرًا ما يكون الإجابة الصحيحة للكائنات النطاقية. ينبغي أن تُعدّ نقطتان في الموضع (3, 4) متساويتَين.
تجاوز equals بالطريقة الصحيحة
يشترط عقد equals (من توثيق Java) خمس خصائص: الانعكاسية (x.equals(x) دائمًا true)، التناظر (x.equals(y) يستلزم y.equals(x))، التعدّي، الاتساق (الاستدعاءات المتكررة تُعيد نفس النتيجة)، وأن x.equals(null) يُعيد دائمًا false.
getClass() بدلًا من instanceof؟ استخدام instanceof قد يكسر التناظر عند التعامل مع الأصناف الفرعية: قد يكون p1.equals(coloredPoint) صحيحًا بينما coloredPoint.equals(p1) خاطئًا. يضمن getClass() الاتساق في الاتجاهين. بالنسبة للـ records والأصناف المختومة القيمية، يكون instanceof مقبولًا؛ أما في تسلسلات الإرث العادية فيُفضَّل getClass().
لماذا يجب تجاوز hashCode أيضًا
تعتمد مجموعات Java — HashMap وHashSet وHashtable — على hashCode لوضع الكائنات في حاويات والعثور عليها. القاعدة صارمة:
- إذا كان
a.equals(b)يُعيدtrue، فيجب أن يكونa.hashCode()مساويًا لـb.hashCode(). - إذا كان
a.hashCode() != b.hashCode()، فيجب أن يكونa.equals(b)خاطئًا (لا يمكن أن يكونا متساويَين). - يُسمح لكائنَين غير متساويَين بامتلاك نفس رمز الهاش (تصادم)، لكن التصادمات الأقل تعني أداءً أفضل.
إذا تجاوزت equals دون تجاوز hashCode، قد يمتلك كائنان متساويان منطقيًا رمزَي هاش مختلفَين، فيعاملهما HashSet كإدخالَين منفصلَين.
تجاوز hashCode
أسلوب مباشر ومقاوم للتصادمات هو استخدام Objects.hash()، الذي يحسب هاشًا مدمجًا لجميع الحقول المستخدمة في equals:
مع وجود التابعَين معًا:
مثال متكامل
equals وhashCode وtoString الصحيحة تلقائيًا. record Point(int x, int y) {} يمنحك الثلاثة مجانًا. للأصناف العادية لا تزال بحاجة إلى كتابتها يدويًا أو توليدها بواسطة بيئة التطوير.
HashSet ثم غيّرت حقلًا يدخل في حسابه، لن تتمكن المجموعة من إيجاده بعد ذلك — سيضيع الكائن في الحاوية الخاطئة. اجعل الحقول المستخدمة في equals وhashCode غير قابلة للتغيير (أو على الأقل ثابتة) كلما أمكن ذلك.
الخلاصة
==يتحقق من المساواة المرجعية؛equalsيتحقق من المساواة القيمية.- التابع
equalsالافتراضي منObjectهو مجرد==— تجاوزه للكائنات النطاقية. - اتبع عقد
equalsالخماسي: الانعكاسية، التناظر، التعدّي، الاتساق، وعدم المساواة مع null. - تجاوز
hashCodeدائمًا عند تجاوزequals— يجب أن يتفقا. - استخدم
Objects.hash(field1, field2, ...)لتطبيقhashCodeنظيف وموثوق.