Home برمجة جافا نظرة حول الدالة Equals
نظرة حول الدالة Equals

نظرة حول الدالة Equals

287
5

عندما تكون دوال الـ equals ليست جميعها equals !

من المعلوم أن أي كلاس تقوم بكتابه ويرث Extends كلاس اخر فهذا الكلاس الإبن subclass سوف يرث من الكلاس ال superclass ، وبما أن أي كلاس في جافا يرث الكلاس Object فان الكلاسات التي تقوم بكتابتها سوف ترث ما يمكن وراثته من الObject.

يحتوي الكلاس Object على العديد من الدوال القابلة لاعادة تعريفها non final methods في الكلاسات الابناء subclasses، ولكن هناك أمور عليك مراعاتها Contract والا فلن تحصل على التصرف الصحيح وقت استدعاء هذه الدوال.

أحد الدوال المثيرة للاهتمام هي الدالة equals الموجودة في الكلاس Object ، حيث تختبر هل الكائن يعتبر مساوى Equals لكائن أخر، والى وقت قريب كانت ساحات الحوار والمقالات تنتاول هذا الموضوع من وجهه نظر المبرمجين، وحتى ان هناك دراسه قديمة في 2007 وجدت ان أغلب تعاريف ال equals المنتشرة ليست صحيحة (الورقة العلمية Declarative Object Identity Using Relation Types )

في الوضع الطبيعي عندما تقوم بعمل اي كلاس بدون اي وراثه صريحة (سوف ترث تلقائياً من Object ) وتكون الدالة equals هي نفسها ما توجد في Object والتي تحدد هل المؤشران يشيران لنفس الكائن Identity test وهذا منطقى لأنه اذا تساوى المؤشران Identity فهذا يعني انهم لديهم نفس القيمة state. الكود التالي يبين الكود الموجود في الدالة equals في الكلاس Object:

 

هناك حالات قليله يجدي معها ال Identity test مثلاً عندما أختبر هل الكائن PrintStream مساوي مع أخر (Identity)، لكن في غالب الحالات تحتاج ان تطبق المساواة على قيمة ذلك الكائن state based equality. لذلك في هذه الحالات عليك ان تقوم باعادة التعريف.

كتوحيد للمصطلحات بهذه المقاله: عندما نقول Identity test (فنحن نقصد المساواة عبر المؤشر وهو ما يوجد في Object.equals) وعندما نقول logical equivalence أو حتى state based equals (فنحن نقصد مساواة القيم في الكائن وهو ما يتطلب اعادة التعريف).

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

فعندما لا تعرفها سوف تحصل على equals تقوم ب Identity test، وهذا ينبغي فعله في حال:
1. كل كائن في الكلاس يمثل مهمه Entity/Activity وليس قيمة مثلاً الكلاس Thread أو PrintStream فالدالة equals الموجودة في الObject سوف تؤدي المطلوب لهذا النوع من الكلاسات.

2. لا تحتاج أن يتواجد مفهوم المساواة بالقيمة logical equivalence (أو ما يعرف ب stated based equals) مثلاً الكلاس Random لا يقوم باعادة تعريف هذه الدالة لأنه لا يحتاجها في الغالب ويكتفى ب object.equals التي ورثها.

3. اذا كان الأب superclass لدية equals وكانت ملائمة لجميع الابناء ، على سبيل المثال Set ترث من AbstractSet و List ترث من AbstractList و Map ترث من AbstractMap وكل من هذه الأبناء لا تقوم باعادة تعريف الدالة equals والسبب ان الأباء لديهم تعريف ملائم لهم.

4. الكلاسات التي تستخدم instance control مثل الSingleton ، وال Enum حيث أن ال Object.equals لهذه الكلاسات هو بمثابه ال state based equality. ( الكلاسات التي تسمى instance control فهي الكلاسات التي تستخدم static factory method لانشاء الكائن بدلاً من دالة البناء constructor لأن بها تحكم اكثر – اقرأ هنا للمزيد – )

حسناً، تعرفنا على الحالات التي لا يجب بها تعريف الدالة equals ولكن متى يجب أن تقوم بتعريفها؟

عندما تريد تطبيق Logical Equality في الكلاس ( وليس Identity) ويكون الأب لا توجد به دالة equals مناسبة ويكون كلاسك هو Value Class (الكلاسات التي تسمى value classes هي التي تمثل قيمة مثل Date و Integer أو طالب أو مدير أو اي كلاس له قيمة) وتريد مساواة هذا الكائن مع أخر ففي الغالب انت تريد مساواة القيمة Logical Equality وليس Identity . أيضاً عملية اعادة تعريف الدالة equals تجب عندما تستخدم Collections تتطلب ان تكون لديك equals .

نأخذ مثال كلاس يمثل الموظف Employee ونريد أن تختبر هل الموظفين متساويين اذا كان لهم نفس الاسم والراتب وتاريخ التوظيف (بعض الأحيان خصوصاً في تطبيقات قواعد البيانات سوف تكون مساواة الموظف عن طريق ال Id -سوف تنتاول كيف يمكنك كتابه equals لهذا النوع في منتصف المقال- ولكن هذا المثال يشرح تطبيق فكرة الابتدائية من ال state based equals).

 

لاحظ أن الكلاس Employee لا يحتوي على اعادة تعريف override للدالة equals وبالتالي كل الاختبارات خاطئة (الموجودة في الدالة main) لأن لكل كائن موقع خاص في الذاكرة. لتصحيح هذا السلوك الخاطئ يجب تعريف الدالة equals داخل الكلاس Employee وليكن تعريفها كما يلي:

دالة equals في الكلاس Employee عن طريق استخدام getClass

 

هكذا عملت الدالة كما هو متوقع (وخرجت النتيجة الصحيحة)، تفاصيل هذه التعويذه ان بدت لك كذلك سوف تنتاولها فيما يلي ، ولكن لنلقي نظرة أخرى لدالة equals أخرى (استبدلها بالدالة اعلاه) :

قم بتشغيل المثال وستجد النتائج الصحيحه والى هنا الأمور تبدوا لا غبار عليها، سواء استخدمت طريقة getClass (والتي تتحقق من ان الكائن من نفس نوع الكائن الثاني وترجع Boolean على ذلك التحقق) او طريقة instanceof (والتي ترجع Boolean في حالة كائن الكائن المرسل احد ابناء الكائن الأخر ) فالنتيجة صحيحة.

ولكن لنقوم ببعض الاضافات الاخرى والتي توضح الفرق الكبير بين هذه الطريقتين (وهي لب هذا المقال) ولنقل ان لدينا كائن Manager يرث الموظف ولديه متغيرات state خاصه به ( بمعنى Subclass add new value to its super class).

 

كما هو واضح من نتيجة الكود اعلاه ان مساواة المدراء خاطئة (m1 و m5) بسبب أننا لم نقوم بتعريف الدالة equals في الكلاس Manager فسوف يتم تساوي اي مديرين في حالة تساوت بياناتهم التي ورثت من الموظف بدون النظر للstate الجديدة في المدير ، وبكل تأكيد هذا تصرف غير صحيح . (بنفس الأمر لو غيرنا طريقة equals من getClass في Employee الى instanceof سوف نحصل على نفس النتيجة والسسب اننا نحتاج لتعريف الدالة داخل Manager لكي تتحقق من تلك ال state الجديدة في المدير).

لنقوم بكتابه الدالة equals داخل Manager :

 

الآن لو شغلت الmain السابقة سوف تحصل على المخرج الصحيح بسبب وجود الدالة equals في ال manager والتي تنظر لتلك ال state الجديدة في الكلاس وتتحق منها.

الى هذه النقطة كنا نقوم بمساواه كائن مع كائن اخر من نفس النوع وكانت الأمور جيدة عندما نستخدم getClass أو نستخدم instanceof، ولكن ماذا لو أردت أن تساوي بين المدير والموظف ؟

اضف هذه للدالة main:

 

لو قمت بالتجربة في الكود الأخير وقمت بمساوة موظف مع مدير (وتستخدم getClass) سوف يفشل الاختبار وذلك لأنك في تعريف الدالة equals اشترطت لا يمكن ان يكون للكائنات من الكلاسات المختلفة ان يتساووا (وذلك من خلال الاختبار getClass والذي يعني انه لا يمكن ابدأ مساواة الكائنات المختلفة).

لكن في حال استخدمت instanceof بدلاً من getClass فسوف تتحقق المقارنه عندما نساوي الموظف p1 مع المدير m6 (وهو صحيح حيث لهم نفس البيانات وManger instanceof Employee) ولكن سوف تحصل على false عندما تقارن المدير m6 مع الموظف p1 حيث أن التعبير Employee instanceof Manger غير صحيح وبالتالي حصلت على false.

طريقة getClass صارمة ولا تسمح لك بمقارنه الكائنات المختلفة، وبالمقابل طريقة instanceof بدت واعدة بعض الشيء حيث سمحت بمقارنة الكائنات المختلفة (Mixing type) ولكن عندما تعكس عملية المقارنه كما في الأعلى فلن تحصل على المساواة، وهذا هو تجاوز Violate لأحد قوانين Contract الموجودة لأي كلاس يريد تعريف equals (تم تجاوز قانون Symmetric ، سنتحدث عنه بعد قليل).

القوانين الموجودة في equals يجب اتباعها بشكل حرفي والتأكد من ذلك، لماذا ؟ حتى لا تحصل على نتيجة خاطئة تسبب لك خلل في البرنامج، تخيل نفسك تدخل كائن في Collection ومن ثم تبحث عن هذا الكائن فلا تجده؟

لنقل C هو طلب Order وان هذه سلة الطلبات Shopping Cart، عندما تريد استخراج معلومات الطلب الذي وضعته على السله فلن تجده، هذا التصرف كفيل بجعل برنامجك غير قابل للاستخدام والسبب هو تجاوز قانون ال equals!.

لنلقى نظرة على معيار لغه الجافا في تعريف الدالة equals (الموجودة في Java Language Specification):

انقر هنا لمشاهدتها بشكل واضح

قد تبدوا هذه الشروط صعبة ولكنها ليست كذلك والأمر المهم هنا أنه يجب الالتزام بها والا قد تحصل على اداء غريب اثناء عمل البرنامج (كما حدث في مثال الShopping Cart اعلاه). لنتحدث عن كل من هذه القوانين على حده:

القانون الأول Reflexive: وهو ينص على أن يساوي الكائن نفسه، وفي الغالب اي كائن يساوي نفسه ويصعب تجاوز violate هذا القانون الا لو كنت متعمداً لهذا، لكن اذا قمت بذلك واضفت الكائن لأحد ال Collections فقد تحصل على اجابة غير موجود false عندما تسأل هل هذا الكائن موجود contains في ال collections.

القانون الثاني Symmetry: وهنا ينص أنه على الكائنين الذين يرغبوا بالتساووا أن يتفقوا في ذلك وان تكون المساواة من الطرفين وليس من طرف واحد، وهو بعكس الأول يحتاج للحذر والتأكد من عدم مخالفته عند تطبيق الequals .

المثال السابق عندما تساوى p1 مع m6 ولكن لم يتساوى m6 مع p1 هو تجاوز واضح لهذا القانون، كود تلك الدالة equals في ال manager باستخدام instanceof وهو مثال على تجاوز قانون symmetric

 

يمكنك جعل الدالة Manager.equals تتجاهل الbonus عندما تعمل ب Mixed type ، كما يلي:

مثال equals مع الblind وهو broken transitivity

 

لاحظ أن النتيجة (بعد تشغيل المثال) اصبحت صحيحه وانه لا يوجد تجاوز لل Symmetric ولكن للأسف هذا الحل ايضاً تجاوز القانون الثالث Transitivity.

القانون الثالث Transitivity: وهو ينص على انه في حال تساوى الكائن A مع كائن أخرB و كان B يساوي C فإن A يجب أن يساوى C ايضاً عليك بفهم هذا القانون لأنه يسهل كسره، فمثلاً تريد اضافه قيمة جديدة في الابن اضافة لما هو موجود في الأب.

نعود للمثال السابق، حيث تم حل مشكلة ال Symmetric ولكن في حال قمت بعمل مدير اخرm7 يختلف عن المدير m6 وكل منهم يساوي p1 فإن هذا يعتبر تجاوز لهذا القانون، حيث انه m7 يساوي p1 و p1 يساوي m6 وبالتالي حسب القانون يجب أن يساوى m7 ال m6 (وان كان منطقياً الكائن m7 لا يساوي m6، لكن هكذا تجاوز للقانون ونحن نعلم خطورة تجاوز هذه القوانين في البرامج).

هكذا اذا استخدمنا instanceof في الابناء الذين لديهم state جديده سوف نحصل على symmetric problem ، واذا حاولت غض النظر عن هذه الstate عندما أكتشف ان النوع يرجع للكلاس الأب فسوف احصل على Transitivity problem .

ما هو الحل ؟ ان تمنع ال Mixing type وتجعل ال equals للكائنات من نفس النوع من خلال getClass ، أو ان تستخدم instanceof وسوف تكسر القاعدة بمجرد وجود أبناء يعيدوا تعريف equals ( وهي مشكلة معروفة في اللغات OOP تسمى Equivalence Relation وأنه لا يمكنك اضافه كلاس ابن له state جديدة (باستخدام instanceof) بدون تجاوز قوانين ال equals ).

وكقاعدة استخدم دائماً getClass الا في الحالتين :
1) أن يكون تعريف الequals الموجود في الكلاس الأب يعمل على جميع الأبناء (مثلاً لو كان لكل موظف رقم id ، بالتالي الدالة equals فقط تحتاج ان تقارن هذا ال id فقط، وهكذا بما أن جميع الابناء يرثوا هذا ال id اذا فلا حاجة لدالة equals لدى الأبناء) في هذه الحالة يمكن استخدام instanceof ويفضل جعل الدالة final حتى لا يتم عمل override لها.

2) ان يكون الكلاس الابن يرث الاب ولا يضيف اي state جديدة ، مثلاً لديك كلاس Point (به نقطتين x و y) وقمت بعمل كلاس CounterPoint يرث النقطة ولا يحتوى على اي قيمة جديدة ولكن به عداد static Counter لعدد الكائنات من هذا الكلاس. لنفرض اننا ارسلنا كائن ال CounterPoint الى دالة contains في List of Points فاذا كان الequals تستخدم getClass فسوف ترجع false بغض النظر عن القيم المتساوية ولكن اذا استخدمت instance of فسوف تعمل الدالة.

هناك طريقة اخرى تمكنك من استخدام instanceof في الكلاسات بدون حصول تجاوز للقوانين وذلك عن طريق استخدام Compositions بدلاً من الوراثه Inheritance والمثال التالي يوضح في الكلاس Manager حيث يحتوي على ال Employee ويقوم بعمل انشاء له داخلياً وبنفس الأمر في المقارنه، عيب هذه الطريقة انه يستوجب عليك عمل دوال الget في الManager لكل دالة موجودة في ال Employee تريد الوصول لها من خلال الManager ، ولكن بهذه الطريقة قمت بعملية equals باستخدام instanceof بدون اي تجاوز للقوانين:

الحل الأخير والأكثر مرونه وهو يسمح للكلاس الابن بان لا يتساوي مع الأب في حال كان هناك state في الكلاس (وهكذا يمنع ال symmetric problem ويمنع ال transitivity problem) وفي نفس الوقت يعطي الابن قابلية التساوي مع الأب في حال لا يوجد اي state فقط كل ما علينا تزويد الكلاس الذي نريده أن لا يتساوي مع الأب (بالدالة تضع هذا الشرط وليكن اسمها canEqual).

المثال التالي يوضح كيفية استخدام الدالة canEqual في المثال Employee/Manager .

 

لاحظ ان الدالة تعمل بالضبط باسلوب getClass (تمنع مساواة الكائنات عندما تتبع لأنواع مختلفة، ولكنها تسمح ايضاُ بمساواة الكائنات عندما تريد ذلك) فقط كل ما عليك هو تعريف الدالة canEquals في الكلاس الذي تريد فيه المنع.

لو نظرنا لمكتبة جافا القياسية Standard Java Library تحتوى على حوالي 150 تعريف للدالة equals بعضها يستخدم getClass وبعضها يستخدم instance of وبعضها بدون فعل شيء وبعضها فيه تجاوز لقوانين الequals!

أحد الأمثلة لذلك في مكتبة جافا هو java.sql.Timestamp والذي يرث java.util.Date ويضيف متغير nanoseconds، لكن ال equals بها تخالف مبدأ ال Symmetry وسوف تسبب مشكلة اذا وضعتهم في Collection واحد أو حتى استخدمتهم بشكل مختلط Mixing them ، وتوثيق الكلاس يوضح ذلك. بالرغم انك سوف تكون في مأمن اذا لم تخلط بينهم Mixing لكن لا يوجد ما يمنع ذلك، هذا السلوك الخاطئ للكلاس Timestamp لا يجب ان يتكرر في كلاساتك.

لاحظ أنك تستطيع اضافة قيمة جديدة للكلاس الابن في حال كان الأب abstract class ولن تكسر القوانين وهذا جيد في البرامج التي تحتاج لهيكل من الكلاسات Hierarchal (مثلاً لديك Shape بدون اي متغير والابن Circle (لديه radius) والابن Rectangle (لديه width, length) فلن تحصل على المشاكل بسبب انك لن تستطيع عمل كائن من superclass.

القانون الرابع Consistency: وهو ينص على انه في حال تساوى الكائنين فبنبغي ان يكونا كذلك حتى يتغيروا أو يتغير احد منهم.

القانون Non-Nullity: كل الكائنات يجب ان لا تساوى null والا سوف تحصل على NullPointerException عندما تستخدم null object لذلك يجب فحص اذا كان الكائن المرسل otherObject == null فارجع false. اذا كنت تستخدم instance of فلا حاجه لك بهذا الاختبار لأنه تقوم بذلك وترجع false مباشره اذا كان الكائن هو null.

الى هنا وصلنا لنهايه المقال وتعرفنا أن هناك طريقتين لفحص نوع الكلاس في الدالة equals، فإما من خلال استخدام instanceof وهكذا سيسمح كلاسك ب mixed type comparison بين الsuperclass و الsubclass أو من خلال استخدام getClass وهكذا سيتم اعتبار ان الكائنات من الكلاسات المختلفة غير متساوية ،

وهذه خلاصه لكيفية الوصول للدالة المناسبة :

اذا كنت تقوم بعمل كلاس جديد فكر في النقاط التالية:
هل الكلاس هو Entity Type ؟ فاذا كان الكلاس من هذا النوع فهو لا يحتاج لدالة equals والدالة equals التي يرثها من Object كافية وتقوم بالفعل الصحيح.

هل الكلاس هو Final Value Class ؟ في هذه الحالة يجب عليك باعادة تعريف الدالة equals حتى يؤدي الفعل المطلوب (ويمكن اختيار اي طريقة getClass أو instanceof حيث أنك تعرف لا يوجد subclass هنا) ، وهذه أحد فوائد الfinal class.

هل الكلاس كلاس قابل للوراثه Not-Final Value Class ؟ في هذه الحالة يجب اعادة تعريف equals (فاذا اخترت instanceof فيجب على جميع الابناء ان لا يقوموا باعادة تعريف equals مجدداً والا سيقدموا مشكلة transitivity، واذا اخترت getClass فلن يوجد كلاس ابن يمكن مقارنته مع الكلاس الأب).

اذا وجدت كلاس جاهز وتريد كتابه subclass له ، فيجب دراسه الequals في الكلاس الأب لأنها تؤثر على طريقة عمل الأبن بكل تأكيد :

هل الأب يسمح بمقارنه الكائنات المختلفة Mixed-type comparison permitted ؟ اذا وجدت الأب يقوم باستخدام instanceof ولا يجب عليك ان تقوم باعادة equals. (خطأ من الأب هو السماح لك بدون قيد –وضع final على الدالة equals-)

هل الأب لا يسمح بمقارنه الكائنات المختلفة Same-type comparison only؟ اذا وجدت الأب يستخدم getClass فهذا يعني ان الsuperclass object وال subclass object لا يتساووا، (مرة اخرى هذا يعني انك لو وضعت الأب في Set من الsubclass فسوف تحصل على false عندما تبحث عن الأب في هذه القائمة).

نقاط أخرى متعلقة بالدالة Equals ؟
عندما تتحق من القيم في الكائن:
استخدم علامه المساواة == للحقول العادية primitive data type
استخدم equals للكائنات، و Arrays.equals للمصفوفات
استخدم Dobule.compareDouble و Float.compareFloat لل Double و Float.
اذا كنت تختبر احد الأبناء فلا تنسى ان تختبر ما يوجد في الأب super.equals .
اذا احتوى الكائن على قيم قد تكون null وتريد ادخالها في الدالة equals ، فاستخدم ال Conditional Operator لذلك:

 

من الأخطاء الشائعه: هو تمرير كائن Employee للدالة equals بدلاً من Object وهذا يعتبر overloading وليس overriding ، لذلك يفضل دائماً استخدام @Overriding عندما تقوم بعمل override لأي دالة من الsuperclass ، وسوف يخرج المترجم رسالة خطأ اذا لم تتساوى المعاملات ولم تقم بعملية ال Overriding بشكل صحيح.

هناك نقاط متعلقه بهذه المقاله مثلاً الhashcode ولماذا الكائنات المتساوية يجب ان يكون لها نفس hashcode وكيف نحصل على ذلك؟ ماهي الcompareTo وهل هي تستخدم equals لتأدية عملها ؟ ولهذا مقال أخر ان شاء الله.

المصادر:
How to Write an Equality Method in Java
Secrets of equals
instanceof versus getClass in equals Methods
Effective Java Second Edition
Core Java, Volume I–Fundamentals

(287)

وجدي عصام مهندس برمجيات مهتم بعلوم الحاسب وبالأخص مجال الخوارزميات وهندسة البرمجيات وحماية التطبيقات،

Comment(5)

  1. بسم الله الرحمن الرحيم

    وضعت الاسئلة فى المنتدى وقلت ابارك على المدونة الجديدة -مبارك على المدونة ان شاء الله ربنا يتمها لكم على خير-امين
    ولا يحرمكم اجر نشر العلم فيها -امين

  2. شكراً للأخ فؤاد على التنبيه بأن اختبار getClass في الدالة equals في الManager مكررة ، لأنها تختبر في الدالة Employee.equals ولا يوجد داعي لتكرار الأختبار مره أخرى :

    فهذه تكون فقط في الsuperclass وليس في الchild :

    شكراً للأخ فؤاد مجدداً :).

    1. الشكر لك على هذا المقال الرائع الذي أتحفتنا به 🙂

      لم أكن أعلم بشأن المشكلة اللتي قد تصابح استخدام instanceof داخل الدالة equals، وأن getClass هي الاستخدام الأمثل هنا!

  3. مبارك على المدونة الجديدة .. أستمتعت جدا بقرآئتها.. بإنتظار المزيد عن لغة الجافا وغيرها من اللغات : )
    زآد الله في علمك .

LEAVE YOUR COMMENT

لن يتم نشر عنوان بريدك الإلكتروني. الحقول الإلزامية مشار إليها بـ *

مشاركة