equals
مرتبطة hashCode
ارتباطًا وثيقًا ببعضها البعض، وأنه من المستحسن تجاوز كلتا الطريقتين في فصولهما باستمرار. يعرف عدد أقل قليلاً سبب ذلك وما هي العواقب المحزنة التي يمكن أن تحدث في حالة انتهاك هذه القاعدة. أقترح النظر في مفهوم هذه الأساليب وتكرار الغرض منها وفهم سبب ارتباطها بهذا الشكل. لقد كتبت هذا المقال، مثل المقال السابق حول تحميل الفئات، لنفسي من أجل الكشف أخيرًا عن جميع تفاصيل المشكلة وعدم العودة إلى مصادر الطرف الثالث. لذلك، سأكون سعيدا بالنقد البناء، لأنه إذا كانت هناك فجوات في مكان ما، فيجب القضاء عليها. تبين أن المقالة، للأسف، طويلة جدًا.
يساوي تجاوز القواعد
مطلوب طريقةequals()
في Java لتأكيد أو نفي حقيقة أن كائنين من نفس الأصل متساويان منطقيًا . أي أنه عند مقارنة كائنين، يحتاج المبرمج إلى فهم ما إذا كانت الحقول المهمة لهما متكافئة أم لا . ليس من الضروري أن تكون جميع الحقول متطابقة، لأن الطريقة equals()
تعني المساواة المنطقية . لكن في بعض الأحيان لا توجد حاجة خاصة لاستخدام هذه الطريقة. كما يقولون، أسهل طريقة لتجنب المشاكل باستخدام آلية معينة هي عدم استخدامها. تجدر الإشارة أيضًا إلى أنه بمجرد فسخ العقد، equals
فإنك تفقد السيطرة على فهم كيفية تفاعل الأشياء والهياكل الأخرى مع الكائن الخاص بك. ومن ثم سيكون العثور على سبب الخطأ أمرًا صعبًا للغاية.
عندما لا لتجاوز هذه الطريقة
- عندما يكون كل مثيل لفئة فريدًا. وينطبق هذا إلى حد كبير على تلك الفئات التي توفر سلوكًا محددًا بدلاً من تصميمها للعمل مع البيانات. مثل، على سبيل المثال، مثل الطبقة
- في حين أنه في الواقع لا يُطلب من الفصل تحديد معادلة حالاته. على سبيل المثال، بالنسبة لفئة ما،
- عندما يكون الفصل الذي تقوم بتوسيعه لديه بالفعل تطبيق خاص به للطريقة
equals
وسلوك هذا التنفيذ يناسبك. على سبيل المثال، بالنسبة للفئات - وأخيرًا، ليست هناك حاجة للتجاوز
equals
عندما يكون نطاق فصلك هوprivate
أوpackage-private
أنت متأكد من أن هذه الطريقة لن يتم استدعاؤها أبدًا.
Thread
. بالنسبة لهم equals
، يعد تنفيذ الطريقة التي يقدمها الفصل Object
أكثر من كافٍ. مثال آخر هو فئات التعداد ( Enum
).
java.util.Random
ليست هناك حاجة على الإطلاق لمقارنة مثيلات الفئة مع بعضها البعض، وتحديد ما إذا كان بإمكانهم إرجاع نفس التسلسل من الأرقام العشوائية. ببساطة لأن طبيعة هذه الفئة لا تنطوي حتى على مثل هذا السلوك.
Set
، يتم التنفيذ في ، وعلى التوالي. List
Map
equals
AbstractSet
AbstractList
AbstractMap
يساوي العقد
عند تجاوز طريقة ما،equals
يجب على المطور الالتزام بالقواعد الأساسية المحددة في مواصفات لغة Java.
- الانعكاسية لأي قيمة معينة ، يجب أن يعود
- تناظر لأية قيم معينة و
- عبورية لأية قيم معينة ،
- تناسق لأي قيم معينة،
- المقارنة فارغة لأي قيمة معينة يجب أن تعود
x
التعبير . نظرا - بمعنى مثل هذاx.equals(x)
true
x != null
x
يجب أن تعود فقط في حالة إرجاعها . y
x.equals(y)
true
y.equals(x)
true
x
وإذا y
تم z
إرجاعها x.equals(y)
وإرجاعها true
، y.equals(z)
فيجب إرجاع القيمة . true
x.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)
false
Point
ColorPoint
// Метод переопределен в классе 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. بالمناسبة، تقوم الفكرة بإعادة إنتاج جيل هورستمان، مما يسمح لك باختيار استراتيجية لتنفيذ طريقة ما في الطبقة المتفوقة أو في أحفادها. وأخيرًا، تنص قاعدة الاتساق التالية على أنه حتى لو لم تتغير الكائنات ، فإن استدعائها مرة أخرى يجب أن يعيد نفس القيمة كما كانت من قبل. القاعدة النهائية هي أنه لا ينبغي أن يكون أي كائن مساويًا لـ . كل شيء واضح هنا - هذا عدم يقين، هل الكائن يساوي عدم اليقين؟ ليس واضحا، أي . instanceof
getClass()
ColorPoint
Point
asPoint()
x
y
x.equals(y)
null
null
false
خوارزمية عامة لتحديد المساواة
- التحقق من المساواة بين مراجع الكائنات
this
ومعلمات الطريقةo
.if (this == o) return true;
- تحقق مما إذا كان الارتباط محددًا
o
، أي ما إذا كان كذلكnull
.
إذا تم استخدام عامل التشغيل في المستقبل عند مقارنة أنواع الكائنات،instanceof
فيمكن تخطي هذا العنصر، نظرًا لأن هذه المعلمة تعودfalse
في هذه الحالةnull instanceof Object
. - قارن بين أنواع الكائنات
this
باستخدامo
عامل تشغيلinstanceof
أو طريقةgetClass()
، مسترشدًا بالوصف أعلاه وحدسك الخاص. - إذا تم تجاوز أسلوب ما
equals
في فئة فرعية، فتأكد من إجراء مكالمةsuper.equals(o)
- تحويل نوع المعلمة
o
إلى الفئة المطلوبة. - قم بإجراء مقارنة بين جميع حقول الكائنات المهمة:
- للأنواع البدائية (باستثناء
float
وdouble
)، وذلك باستخدام عامل التشغيل==
- بالنسبة للحقول المرجعية تحتاج إلى استدعاء طريقتهم
equals
- بالنسبة للمصفوفات، يمكنك استخدام التكرار الدوري أو الطريقة
Arrays.equals()
- للأنواع
float
ومنdouble
الضروري استخدام طرق المقارنة لفئات التغليف المقابلةFloat.compare()
وDouble.compare()
- للأنواع البدائية (باستثناء
- وأخيرًا، أجب عن ثلاثة أسئلة: هل الطريقة المطبقة متماثلة ؟ متعد ؟ متفق ؟ عادة ما يتم تنفيذ المبدأين الآخرين ( الانعكاسية واليقين ) تلقائيًا.
قواعد تجاوز HashCode
التجزئة عبارة عن رقم تم إنشاؤه من كائن يصف حالته في وقت ما. يُستخدم هذا الرقم في Java بشكل أساسي في جداول التجزئة مثلHashMap
. في هذه الحالة، يجب تنفيذ وظيفة التجزئة للحصول على رقم بناءً على كائن بطريقة تضمن التوزيع المتساوي نسبيًا للعناصر عبر جدول التجزئة. وأيضًا لتقليل احتمالية حدوث تصادمات عندما تقوم الدالة بإرجاع نفس القيمة لمفاتيح مختلفة.
رمز تجزئة العقد
لتنفيذ دالة التجزئة، تحدد مواصفات اللغة القواعد التالية:- إن استدعاء الطريقة
hashCode
مرة واحدة أو أكثر على نفس الكائن يجب أن يعيد نفس قيمة التجزئة، بشرط ألا تتغير حقول الكائن المشاركة في حساب القيمة. - استدعاء أسلوب
hashCode
على كائنين يجب أن يُرجع دائمًا نفس الرقم إذا كان الكائنان متساويين (استدعاء أسلوبequals
على هذين الكائنين يُرجعtrue
). - استدعاء أسلوب
hashCode
على كائنين غير متساويين يجب أن يُرجع قيم تجزئة مختلفة. على الرغم من أن هذا المطلب ليس إلزاميًا، إلا أنه ينبغي اعتبار أن تنفيذه سيكون له تأثير إيجابي على أداء جداول التجزئة.
يجب تجاوز أساليب يساوي و hashCode معًا
بناءً على العقود الموضحة أعلاه، يترتب على ذلك أنه عند تجاوز الطريقة في التعليمات البرمجية الخاصة بكequals
، يجب عليك دائمًا تجاوز الطريقة hashCode
. نظرًا لأن مثيلين للفئة مختلفان في الواقع لأنهما في مناطق ذاكرة مختلفة، فيجب مقارنتهما وفقًا لبعض المعايير المنطقية. وبناء على ذلك، يجب أن يقوم كائنان متكافئان منطقيا بإرجاع نفس قيمة التجزئة. ماذا يحدث إذا تم تجاوز إحدى هذه الطرق فقط؟
-
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));
من الواضح أن الكائن الذي يتم وضعه والكائن الذي يتم البحث عنه هما كائنان مختلفان، على الرغم من أنهما متساويان منطقيًا. ولكن لديهم قيم تجزئة مختلفة لأننا انتهكنا العقد، يمكننا القول أننا فقدنا كائننا في مكان ما في أحشاء جدول التجزئة.
-
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
. أي أننا نرى أن المقارنة المنطقية بين كائنين لا ينبغي أن تتعارض في أي مكان في التطبيق ويجب أن تكون متسقة دائمًا.
GO TO FULL VERSION