JavaRush /مدونة جافا /Random-AR /يساوي وعقود hashCode أو أيا كان
Aleksandr Zimin
مستوى
Санкт-Петербург

يساوي وعقود hashCode أو أيا كان

نشرت في المجموعة
الغالبية العظمى من مبرمجي Java، بالطبع، يعرفون أن الأساليب equalsمرتبطة hashCodeارتباطًا وثيقًا ببعضها البعض، وأنه من المستحسن تجاوز كلتا الطريقتين في فصولهما باستمرار. يعرف عدد أقل قليلاً سبب ذلك وما هي العواقب المحزنة التي يمكن أن تحدث في حالة انتهاك هذه القاعدة. أقترح النظر في مفهوم هذه الأساليب وتكرار الغرض منها وفهم سبب ارتباطها بهذا الشكل. لقد كتبت هذا المقال، مثل المقال السابق حول تحميل الفئات، لنفسي من أجل الكشف أخيرًا عن جميع تفاصيل المشكلة وعدم العودة إلى مصادر الطرف الثالث. لذلك، سأكون سعيدا بالنقد البناء، لأنه إذا كانت هناك فجوات في مكان ما، فيجب القضاء عليها. تبين أن المقالة، للأسف، طويلة جدًا.

يساوي تجاوز القواعد

مطلوب طريقة equals()في Java لتأكيد أو نفي حقيقة أن كائنين من نفس الأصل متساويان منطقيًا . أي أنه عند مقارنة كائنين، يحتاج المبرمج إلى فهم ما إذا كانت الحقول المهمة لهما متكافئة أم لا . ليس من الضروري أن تكون جميع الحقول متطابقة، لأن الطريقة equals()تعني المساواة المنطقية . لكن في بعض الأحيان لا توجد حاجة خاصة لاستخدام هذه الطريقة. كما يقولون، أسهل طريقة لتجنب المشاكل باستخدام آلية معينة هي عدم استخدامها. تجدر الإشارة أيضًا إلى أنه بمجرد فسخ العقد، equalsفإنك تفقد السيطرة على فهم كيفية تفاعل الأشياء والهياكل الأخرى مع الكائن الخاص بك. ومن ثم سيكون العثور على سبب الخطأ أمرًا صعبًا للغاية.

عندما لا لتجاوز هذه الطريقة

  • عندما يكون كل مثيل لفئة فريدًا.
  • وينطبق هذا إلى حد كبير على تلك الفئات التي توفر سلوكًا محددًا بدلاً من تصميمها للعمل مع البيانات. مثل، على سبيل المثال، مثل الطبقة Thread. بالنسبة لهم equals، يعد تنفيذ الطريقة التي يقدمها الفصل Objectأكثر من كافٍ. مثال آخر هو فئات التعداد ( Enum).
  • في حين أنه في الواقع لا يُطلب من الفصل تحديد معادلة حالاته.
  • على سبيل المثال، بالنسبة لفئة ما، java.util.Randomليست هناك حاجة على الإطلاق لمقارنة مثيلات الفئة مع بعضها البعض، وتحديد ما إذا كان بإمكانهم إرجاع نفس التسلسل من الأرقام العشوائية. ببساطة لأن طبيعة هذه الفئة لا تنطوي حتى على مثل هذا السلوك.
  • عندما يكون الفصل الذي تقوم بتوسيعه لديه بالفعل تطبيق خاص به للطريقة equalsوسلوك هذا التنفيذ يناسبك.
  • على سبيل المثال، بالنسبة للفئات Set، يتم التنفيذ في ، وعلى التوالي. ListMapequalsAbstractSetAbstractListAbstractMap
  • وأخيرًا، ليست هناك حاجة للتجاوز equalsعندما يكون نطاق فصلك هو privateأو package-privateأنت متأكد من أن هذه الطريقة لن يتم استدعاؤها أبدًا.

يساوي العقد

عند تجاوز طريقة ما، equalsيجب على المطور الالتزام بالقواعد الأساسية المحددة في مواصفات لغة Java.
  • الانعكاسية
  • لأي قيمة معينة ، يجب أن يعود xالتعبير . نظرا - بمعنى مثل هذاx.equals(x)true
    x != null
  • تناظر
  • لأية قيم معينة و xيجب أن تعود فقط في حالة إرجاعها . yx.equals(y)truey.equals(x)true
  • عبورية
  • لأية قيم معينة ، xوإذا yتم zإرجاعها x.equals(y)وإرجاعها true، y.equals(z)فيجب إرجاع القيمة . truex.equals(z)true
  • تناسق
  • لأي قيم معينة، xوسيقوم yالاستدعاء المتكرر x.equals(y)بإرجاع قيمة الاستدعاء السابق لهذه الطريقة، بشرط ألا تتغير الحقول المستخدمة لمقارنة الكائنين بين الاستدعاءات.
  • المقارنة فارغة
  • لأي قيمة معينة يجب أن تعود xالمكالمة . x.equals(null)false

يساوي انتهاك العقد

تعتمد العديد من الفئات، مثل تلك الموجودة في Java Collections Framework، على تنفيذ الطريقة equals()، لذا لا يجب إهمالها، لأن يمكن أن يؤدي انتهاك عقد هذه الطريقة إلى التشغيل غير العقلاني للتطبيق، وفي هذه الحالة سيكون من الصعب جدًا العثور على السبب. وفقا لمبدأ الانعكاسية ، يجب أن يكون كل كائن مساويا لنفسه. إذا تم انتهاك هذا المبدأ، فعندما نضيف كائنًا إلى المجموعة ثم نبحث عنه باستخدام الطريقة، contains()فلن نتمكن من العثور على الكائن الذي أضفناه للتو إلى المجموعة. ينص شرط التناظر على أن أي كائنين يجب أن يكونا متساويين بغض النظر عن الترتيب الذي تتم مقارنتهما به. على سبيل المثال، إذا كان لديك فئة تحتوي على حقل واحد فقط من نوع السلسلة، فسيكون من غير الصحيح مقارنة equalsهذا الحقل بسلسلة في إحدى الطرق. لأن في حالة المقارنة العكسية، ستقوم الطريقة دائمًا بإرجاع القيمة false.
// Нарушение симметричности
public class SomeStringify {
    private String s;

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o instanceof SomeStringify) {
            return s.equals(((SomeStringify) o).s);
        }
        // нарушение симметричности, классы разного происхождения
        if (o instanceof String) {
            return s.equals(o);
        }
        return false;
    }
}
//Правильное определение метода equals
@Override
public boolean equals(Object o) {
    if (this == o) return true;
    return o instanceof SomeStringify &&
            ((SomeStringify) o).s.equals(s);
}
ويترتب على شرط العبور أنه إذا كان أي اثنين من الأشياء الثلاثة متساويين، ففي هذه الحالة يجب أن تكون الثلاثة جميعها متساوية. يمكن انتهاك هذا المبدأ بسهولة عندما يكون من الضروري توسيع فئة أساسية معينة عن طريق إضافة مكون ذي معنى إليها . على سبيل المثال، إلى فئة Pointذات إحداثيات xوتحتاج yإلى إضافة لون النقطة عن طريق توسيعها. للقيام بذلك، سوف تحتاج إلى الإعلان عن فئة ColorPointمع الحقل المناسب color. وبالتالي، إذا قمنا بتسمية equalsالطريقة الأصلية في الفئة الموسعة، وافترضنا في الفئة الأم أن الإحداثيات فقط تتم xمقارنتها y، فسيتم اعتبار نقطتين من ألوان مختلفة ولكن بنفس الإحداثيات متساويتين، وهذا غير صحيح. وفي هذه الحالة من الضروري تعليم الفصل المشتق كيفية تمييز الألوان. للقيام بذلك، يمكنك استخدام طريقتين. لكن أحدهما سوف ينتهك قاعدة التناظر ، والثاني هو العبور .
// Первый способ, нарушая симметричность
// Метод переопределен в классе ColorPoint
@Override
public boolean equals(Object o) {
    if (!(o instanceof ColorPoint)) return false;
    return super.equals(o) && ((ColorPoint) o).color == color;
}
point.equals(colorPoint)في هذه الحالة، سيرجع الاستدعاء القيمة، وسترجع trueالمقارنة ، لأن تتوقع كائنًا من فئتها. وبالتالي، يتم انتهاك قاعدة التماثل. تتضمن الطريقة الثانية إجراء فحص "أعمى" في حالة عدم وجود بيانات حول لون النقطة، أي أن لدينا فئة . أو تحقق من اللون في حالة توفر معلومات عنه، أي قارن كائنًا من الفئة . colorPoint.equals(point)falsePointColorPoint
// Метод переопределен в классе ColorPoint
@Override
public boolean equals(Object o) {
    if (!(o instanceof Point)) return false;

    // Слепая проверка
    if (!(o instanceof ColorPoint))
        return super.equals(o);

    // Полная проверка, включая цвет точки
    return super.equals(o) && ((ColorPoint) o).color == color;
}
يتم انتهاك مبدأ العبور هنا على النحو التالي. لنفترض أن هناك تعريفًا للأشياء التالية:
ColorPoint p1 = new ColorPoint(1, 2, Color.RED);
Point p2 = new Point(1, 2);
ColorPoint p3 = new ColorPoint(1, 2, Color.BLUE);
p1.equals(p2)وهكذا، على الرغم من استيفاء المساواة p2.equals(p3)، p1.equals(p3)فإنه سيعود بالقيمة false. وفي الوقت نفسه، الطريقة الثانية، في رأيي، تبدو أقل جاذبية، لأن في بعض الحالات، قد تكون الخوارزمية معماة ولا تقوم بإجراء المقارنة بشكل كامل، وقد لا تعلم بذلك. قليلا من الشعر بشكل عام، كما أفهمها، لا يوجد حل ملموس لهذه المشكلة. هناك رأي من مؤلف موثوق يُدعى Kay Horstmann مفاده أنه يمكنك استبدال استخدام العامل instanceofباستدعاء أسلوب getClass()يُرجع فئة الكائن، وقبل البدء في مقارنة الكائنات نفسها، تأكد من أنها من نفس النوع ولا تنتبه إلى حقيقة أصلهم المشترك. وبالتالي، سيتم استيفاء قواعد التناظر والعبور . ولكن في الوقت نفسه، على الجانب الآخر من الحاجز يقف مؤلف آخر، لا يقل احتراما في دوائر واسعة، وهو جوشوا بلوخ، الذي يعتقد أن هذا النهج ينتهك مبدأ الاستبدال لباربرا ليسكوف. ينص هذا المبدأ على أن "رمز الاستدعاء يجب أن يتعامل مع الفئة الأساسية بنفس الطريقة التي يتعامل بها مع الفئات الفرعية دون معرفتها . " وفي الحل الذي اقترحه هورستمان، تم انتهاك هذا المبدأ بشكل واضح، لأنه يعتمد على التنفيذ. باختصار، من الواضح أن الأمر مظلم. تجدر الإشارة أيضًا إلى أن هورستمان يوضح قاعدة تطبيق منهجه ويكتب باللغة الإنجليزية البسيطة أنك بحاجة إلى اتخاذ قرار بشأن استراتيجية عند تصميم الفصول الدراسية، وإذا كان سيتم إجراء اختبار المساواة بواسطة الطبقة المتفوقة فقط، فيمكنك القيام بذلك عن طريق إجراء العملية . بخلاف ذلك، عندما تتغير دلالات التحقق اعتمادًا على الفئة المشتقة ويلزم نقل تنفيذ الطريقة إلى أسفل التسلسل الهرمي، يجب عليك استخدام الطريقة . يقترح جوشوا بلوخ بدوره التخلي عن الميراث واستخدام تكوين الكائن من خلال تضمين فئة في الفصل وتوفير طريقة وصول للحصول على معلومات حول هذه النقطة تحديدًا. سيؤدي هذا إلى تجنب خرق جميع القواعد، ولكن في رأيي، سيجعل فهم الكود أكثر صعوبة. الخيار الثالث هو استخدام التوليد التلقائي لطريقة التساوي باستخدام IDE. بالمناسبة، تقوم الفكرة بإعادة إنتاج جيل هورستمان، مما يسمح لك باختيار استراتيجية لتنفيذ طريقة ما في الطبقة المتفوقة أو في أحفادها. وأخيرًا، تنص قاعدة الاتساق التالية على أنه حتى لو لم تتغير الكائنات ، فإن استدعائها مرة أخرى يجب أن يعيد نفس القيمة كما كانت من قبل. القاعدة النهائية هي أنه لا ينبغي أن يكون أي كائن مساويًا لـ . كل شيء واضح هنا - هذا عدم يقين، هل الكائن يساوي عدم اليقين؟ ليس واضحا، أي . instanceofgetClass()ColorPointPointasPoint()xyx.equals(y)nullnullfalse

خوارزمية عامة لتحديد المساواة

  1. التحقق من المساواة بين مراجع الكائنات thisومعلمات الطريقة o.
    if (this == o) return true;
  2. تحقق مما إذا كان الارتباط محددًا o، أي ما إذا كان كذلك null.
    إذا تم استخدام عامل التشغيل في المستقبل عند مقارنة أنواع الكائنات، instanceofفيمكن تخطي هذا العنصر، نظرًا لأن هذه المعلمة تعود falseفي هذه الحالة null instanceof Object.
  3. قارن بين أنواع الكائنات thisباستخدام oعامل تشغيل instanceofأو طريقة getClass()، مسترشدًا بالوصف أعلاه وحدسك الخاص.
  4. إذا تم تجاوز أسلوب ما equalsفي فئة فرعية، فتأكد من إجراء مكالمةsuper.equals(o)
  5. تحويل نوع المعلمة oإلى الفئة المطلوبة.
  6. قم بإجراء مقارنة بين جميع حقول الكائنات المهمة:
    • للأنواع البدائية (باستثناء floatو double)، وذلك باستخدام عامل التشغيل==
    • بالنسبة للحقول المرجعية تحتاج إلى استدعاء طريقتهمequals
    • بالنسبة للمصفوفات، يمكنك استخدام التكرار الدوري أو الطريقةArrays.equals()
    • للأنواع floatومن doubleالضروري استخدام طرق المقارنة لفئات التغليف المقابلة Float.compare()وDouble.compare()
  7. وأخيرًا، أجب عن ثلاثة أسئلة: هل الطريقة المطبقة متماثلة ؟ متعد ؟ متفق ؟ عادة ما يتم تنفيذ المبدأين الآخرين ( الانعكاسية واليقين ) تلقائيًا.

قواعد تجاوز HashCode

التجزئة عبارة عن رقم تم إنشاؤه من كائن يصف حالته في وقت ما. يُستخدم هذا الرقم في Java بشكل أساسي في جداول التجزئة مثل HashMap. في هذه الحالة، يجب تنفيذ وظيفة التجزئة للحصول على رقم بناءً على كائن بطريقة تضمن التوزيع المتساوي نسبيًا للعناصر عبر جدول التجزئة. وأيضًا لتقليل احتمالية حدوث تصادمات عندما تقوم الدالة بإرجاع نفس القيمة لمفاتيح مختلفة.

رمز تجزئة العقد

لتنفيذ دالة التجزئة، تحدد مواصفات اللغة القواعد التالية:
  • إن استدعاء الطريقة hashCodeمرة واحدة أو أكثر على نفس الكائن يجب أن يعيد نفس قيمة التجزئة، بشرط ألا تتغير حقول الكائن المشاركة في حساب القيمة.
  • استدعاء أسلوب hashCodeعلى كائنين يجب أن يُرجع دائمًا نفس الرقم إذا كان الكائنان متساويين (استدعاء أسلوب equalsعلى هذين الكائنين يُرجع true).
  • استدعاء أسلوب hashCodeعلى كائنين غير متساويين يجب أن يُرجع قيم تجزئة مختلفة. على الرغم من أن هذا المطلب ليس إلزاميًا، إلا أنه ينبغي اعتبار أن تنفيذه سيكون له تأثير إيجابي على أداء جداول التجزئة.

يجب تجاوز أساليب يساوي و hashCode معًا

بناءً على العقود الموضحة أعلاه، يترتب على ذلك أنه عند تجاوز الطريقة في التعليمات البرمجية الخاصة بك equals، يجب عليك دائمًا تجاوز الطريقة hashCode. نظرًا لأن مثيلين للفئة مختلفان في الواقع لأنهما في مناطق ذاكرة مختلفة، فيجب مقارنتهما وفقًا لبعض المعايير المنطقية. وبناء على ذلك، يجب أن يقوم كائنان متكافئان منطقيا بإرجاع نفس قيمة التجزئة. ماذا يحدث إذا تم تجاوز إحدى هذه الطرق فقط؟
  1. equalsنعم / hashCodeلا

    لنفترض أننا حددنا طريقة بشكل صحيح equalsفي فصلنا، hashCodeوقررنا ترك الطريقة كما هي في الفصل Object. ومن ثم، من وجهة نظر الطريقة، equalsسيكون الكائنان متساويين منطقيًا، بينما من وجهة نظر الطريقة، hashCodeلن يكون هناك أي شيء مشترك بينهما. وبالتالي، من خلال وضع كائن في جدول التجزئة، فإننا نخاطر بعدم استعادته بالمفتاح.
    على سبيل المثال، مثل هذا:

    Map<Point, String> m = new HashMap<>();
    m.put(new Point(1, 1),Point A);
    // pointName == null
    String pointName = m.get(new Point(1, 1));

    من الواضح أن الكائن الذي يتم وضعه والكائن الذي يتم البحث عنه هما كائنان مختلفان، على الرغم من أنهما متساويان منطقيًا. ولكن لديهم قيم تجزئة مختلفة لأننا انتهكنا العقد، يمكننا القول أننا فقدنا كائننا في مكان ما في أحشاء جدول التجزئة.

  2. hashCodeنعم / equalsلا.

    ماذا يحدث إذا تجاوزنا الطريقة hashCodeوورثنا equalsتنفيذ الطريقة من الفصل Object؟ كما تعلم، تقوم equalsالطريقة الافتراضية ببساطة بمقارنة المؤشرات بالكائنات، وتحديد ما إذا كانت تشير إلى نفس الكائن. لنفترض أننا hashCodeكتبنا الطريقة وفقًا لجميع القواعد، أي أننا قمنا بإنشائها باستخدام IDE، وستعيد نفس قيم التجزئة للكائنات المتطابقة منطقيًا. ومن الواضح أننا بهذا نكون قد حددنا بالفعل بعض الآليات لمقارنة كائنين.

    ولذلك، ينبغي من الناحية النظرية تنفيذ المثال من الفقرة السابقة. لكننا ما زلنا غير قادرين على العثور على كائننا في جدول التجزئة. على الرغم من أننا سنكون قريبين من ذلك، لأنه على الأقل سنجد سلة جدول التجزئة التي سيقع فيها الكائن.

    للبحث بنجاح عن كائن في جدول التجزئة، بالإضافة إلى مقارنة قيم التجزئة للمفتاح، يتم أيضًا استخدام تحديد المساواة المنطقية للمفتاح مع الكائن الذي تم البحث عنه. أي أنه equalsلا توجد طريقة للاستغناء عن تجاوز الطريقة.

خوارزمية عامة لتحديد رمز التجزئة

هنا، يبدو لي أنه لا داعي للقلق كثيرًا وإنشاء الطريقة في IDE المفضل لديك. لأن كل هذه التحولات في البتات إلى اليمين واليسار بحثًا عن النسبة الذهبية، أي التوزيع الطبيعي - هذا مخصص للرجال العنيدين تمامًا. أنا شخصياً أشك في أنني أستطيع القيام بعمل أفضل وأسرع من نفس الفكرة.

بدلا من الاستنتاج

وهكذا نرى أن الأساليب equalsتلعب hashCodeدورًا محددًا جيدًا في لغة Java وهي مصممة للحصول على خاصية المساواة المنطقية بين كائنين. في حالة الطريقة، equalsيكون لذلك علاقة مباشرة بمقارنة الكائنات، في حالة hashCodeالطريقة غير المباشرة، عندما يكون من الضروري، على سبيل المثال، تحديد الموقع التقريبي للكائن في جداول التجزئة أو هياكل البيانات المشابهة من أجل زيادة سرعة البحث عن كائن. بالإضافة إلى العقود ، equalsهناك hashCodeشرط آخر يتعلق بمقارنة الأشياء. هذا هو تناسق طريقة compareToالواجهة Comparableمع equals. يُلزم هذا المطلب المطور بالعودة دائمًا x.equals(y) == trueعند x.compareTo(y) == 0. أي أننا نرى أن المقارنة المنطقية بين كائنين لا ينبغي أن تتعارض في أي مكان في التطبيق ويجب أن تكون متسقة دائمًا.

مصادر

جافا فعالة، الطبعة الثانية جوشوا بلوخ. ترجمة مجانية لكتاب جيد جدًا. جافا، مكتبة المحترفين. المجلد 1. الأساسيات. كاي هورستمان. القليل من النظرية والمزيد من الممارسة. لكن كل شيء لا يتم تحليله بقدر كبير من التفصيل مثل تحليل بلوخ. على الرغم من وجود وجهة نظر على نفس يساوي (). هياكل البيانات في الصور. HashMap مقالة مفيدة للغاية عن جهاز HashMap في Java. بدلًا من البحث في المصادر.
تعليقات
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION