JavaRush /مدونة جافا /Random-AR /يساوي وطرق hashCode: ممارسة الاستخدام

يساوي وطرق hashCode: ممارسة الاستخدام

نشرت في المجموعة
مرحبًا! سنتحدث اليوم عن طريقتين مهمتين في Java - equals()و hashCode(). هذه ليست المرة الأولى التي نلتقي بهم: في بداية دورة JavaRush كانت هناك محاضرة قصيرة حول - equals()اقرأها إذا نسيتها أو لم تراها من قبل. الأساليب تساوي &  رمز التجزئة: ممارسة الاستخدام - 1في درس اليوم سنتحدث عن هذه المفاهيم بالتفصيل - صدقوني، هناك الكثير لنتحدث عنه! وقبل أن ننتقل إلى شيء جديد، دعونا ننعش ذاكرتنا بشأن ما قمنا بتغطيته بالفعل :) كما تتذكر، فإن المقارنة المعتادة بين كائنين باستخدام عامل ==التشغيل " " هي فكرة سيئة، لأن " ==" يقارن بين المراجع. هذا هو مثالنا مع السيارات من محاضرة حديثة:
public class Car {

   String model;
   int maxSpeed;

   public static void main(String[] args) {

       Car car1 = new Car();
       car1.model = "Ferrari";
       car1.maxSpeed = 300;

       Car car2 = new Car();
       car2.model = "Ferrari";
       car2.maxSpeed = 300;

       System.out.println(car1 == car2);
   }
}
إخراج وحدة التحكم:

false
يبدو أننا أنشأنا كائنين متطابقين من الفئة Car: جميع الحقول الموجودة على الجهازين هي نفسها، ولكن نتيجة المقارنة لا تزال خاطئة. نحن نعرف السبب بالفعل: الروابط car1تشير car2إلى عناوين مختلفة في الذاكرة، لذا فهي غير متساوية. ما زلنا نريد مقارنة شيئين، وليس مرجعين. أفضل حل لمقارنة الكائنات هو equals().

يساوي () الأسلوب

ربما تتذكر أننا لا ننشئ هذه الطريقة من الصفر، بل نتجاوزها - ففي النهاية، equals()يتم تعريف الطريقة في الفصل Object. ومع ذلك، في شكله المعتاد فإنه قليل الفائدة:
public boolean equals(Object obj) {
   return (this == obj);
}
هذه هي الطريقة التي يتم بها تعريف الطريقة equals()في الفصل Object. نفس المقارنة بين الروابط. لماذا خلق هكذا؟ حسنًا، كيف يعرف منشئو اللغة أي الكائنات في برنامجك تعتبر متساوية وأيها ليست كذلك؟ :) هذه هي الفكرة الرئيسية للطريقة equals()- يحدد منشئ الفصل بنفسه الخصائص التي يتم من خلالها التحقق من مساواة كائنات هذه الفئة. من خلال القيام بذلك، يمكنك تجاوز الطريقة equals()في صفك. إذا كنت لا تفهم تمامًا معنى عبارة "أنت تحدد الخصائص بنفسك"، فلننظر إلى مثال. هنا فئة بسيطة من الأشخاص - Man.
public class Man {

   private String noseSize;
   private String eyesColor;
   private String haircut;
   private boolean scars;
   private int dnaCode;

public Man(String noseSize, String eyesColor, String haircut, boolean scars, int dnaCode) {
   this.noseSize = noseSize;
   this.eyesColor = eyesColor;
   this.haircut = haircut;
   this.scars = scars;
   this.dnaCode = dnaCode;
}

   //getters, setters, etc.
}
لنفترض أننا نكتب برنامجًا يحتاج إلى تحديد ما إذا كان هناك شخصان مرتبطان بتوأم، أم مجرد شخصين شبيهين. لدينا خمس خصائص: حجم الأنف، لون العين، تسريحة الشعر، وجود ندبات ونتائج الاختبار البيولوجي للحمض النووي (للبساطة - على شكل رقم كودي). أي من هذه الخصائص تعتقد أنها ستسمح لبرنامجنا بتحديد الأقارب التوأم؟ الأساليب تساوي &  رمز التجزئة: ممارسة الاستخدام - 2وبطبيعة الحال، فإن الاختبار البيولوجي فقط هو الذي يمكن أن يوفر الضمان. يمكن أن يكون لدى شخصين نفس لون العين، وتصفيفة الشعر، والأنف، وحتى الندوب - هناك الكثير من الناس في العالم، ومن المستحيل تجنب المصادفة. نحن بحاجة إلى آلية موثوقة: فقط نتيجة اختبار الحمض النووي هي التي تسمح لنا بالتوصل إلى نتيجة دقيقة. ماذا يعني هذا بالنسبة لطريقتنا equals()؟ نحن بحاجة إلى إعادة تعريفه في الفصل Manمع الأخذ بعين الاعتبار متطلبات برنامجنا. يجب أن تقارن الطريقة مجال int dnaCodeكائنين، وإذا كانا متساويين، فإن الكائنين متساويان.
@Override
public boolean equals(Object o) {
   Man man = (Man) o;
   return dnaCode == man.dnaCode;
}
هل هو حقا بهذه البساطة؟ ليس حقيقيًا. فاتنا شيئا. في هذه الحالة، بالنسبة لأشياءنا، قمنا بتحديد حقل "مهم" واحد فقط يتم من خلاله إثبات المساواة بينهما - dnaCode. تخيل الآن أنه لن يكون لدينا حقل واحد، بل 50 حقلاً "مهمًا". وإذا كانت جميع الحقول الخمسين لكائنين متساوية، فإن الكائنات متساوية. يمكن أن يحدث هذا أيضًا. المشكلة الرئيسية هي أن حساب المساواة بين 50 حقلاً هو عملية تستغرق وقتًا طويلاً وتستهلك الموارد. تخيل الآن أنه بالإضافة إلى الفصل الدراسي، Manلدينا فصل دراسي Womanيحتوي تمامًا على نفس الحقول الموجودة في Man. وإذا استخدم مبرمج آخر دروسك، فيمكنه بسهولة أن يكتب في برنامجه شيئًا مثل:
public static void main(String[] args) {

   Man man = new Man(........); //a bunch of parameters in the constructor

   Woman woman = new Woman(.........);//same bunch of parameters.

   System.out.println(man.equals(woman));
}
في هذه الحالة، لا فائدة من التحقق من قيم الحقل: نرى أننا ننظر إلى كائنات من فئتين مختلفتين، ولا يمكن أن تكونا متساويتين من حيث المبدأ! هذا يعني أننا بحاجة إلى وضع علامة اختيار على الطريقة equals()، وهي مقارنة كائنات من فئتين متطابقتين. من الجيد أننا فكرنا في هذا!
@Override
public boolean equals(Object o) {
   if (getClass() != o.getClass()) return false;
   Man man = (Man) o;
   return dnaCode == man.dnaCode;
}
لكن ربما نسينا شيئًا آخر؟ حسنًا... على الأقل، يجب أن نتأكد من أننا لا نقارن الشيء بنفسه! إذا كان المرجعان A وB يشيران إلى نفس العنوان في الذاكرة، فهما نفس الكائن، ولا نحتاج أيضًا إلى إضاعة الوقت في مقارنة 50 حقلاً.
@Override
public boolean equals(Object o) {
   if (this == o) return true;
   if (getClass() != o.getClass()) return false;
   Man man = (Man) o;
   return dnaCode == man.dnaCode;
}
بالإضافة إلى ذلك، لن يضر إضافة علامة اختيار إلى null: لا يمكن أن يكون أي كائن مساويًا لـ null، وفي هذه الحالة لا فائدة من إجراء عمليات فحص إضافية. مع أخذ كل هذا في الاعتبار، ستبدو طريقة equals()الفصل لدينا كما يلي:Man
@Override
public boolean equals(Object o) {
   if (this == o) return true;
   if (o == null || getClass() != o.getClass()) return false;
   Man man = (Man) o;
   return dnaCode == man.dnaCode;
}
نقوم بتنفيذ كافة الفحوصات الأولية المذكورة أعلاه. فإذا تبين أن:
  • نقارن بين كائنين من نفس الفئة
  • هذا ليس نفس الكائن
  • نحن لا نقارن كائننا معnull
...ثم ننتقل إلى مقارنة الخصائص الهامة. في حالتنا، مجالات dnaCodeكائنين. عند تجاوز طريقة ما equals()، تأكد من الالتزام بهذه المتطلبات:
  1. الانعكاسية.

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

    if (this == o) return true;

  2. تناظر.

    إذا a.equals(b) == true، فيجب b.equals(a)أن يعود true.
    طريقتنا تلبي هذا المطلب أيضًا.

  3. عبورية.

    إذا كان هناك كائنان يساويان كائنًا ثالثًا، فيجب أن يكونا متساويين لبعضهما البعض.
    إذا a.equals(b) == trueو a.equals(c) == true، فيجب أن يعود الشيك b.equals(c)أيضًا صحيحًا.

  4. الدوام.

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

  5. عدم المساواة مع null.

    بالنسبة لأي كائن، يجب أن يُرجع الشيك a.equals(null)خطأ.
    وهذا ليس مجرد مجموعة من "التوصيات المفيدة"، ولكنه عقد صارم للطرق المنصوص عليها في وثائق أوراكل

طريقة hashCode ().

الآن دعونا نتحدث عن الطريقة hashCode(). لماذا هو مطلوب؟ بالضبط لنفس الغرض - مقارنة الأشياء. ولكن لدينا بالفعل equals()! لماذا طريقة أخرى؟ الجواب بسيط: تحسين الإنتاجية. تقوم دالة التجزئة، التي يتم تمثيلها بالطريقة في Java hashCode()، بإرجاع قيمة رقمية ذات طول ثابت لأي كائن. في حالة Java، تقوم الطريقة hashCode()بإرجاع رقم 32 بت من النوع int. تعد مقارنة رقمين مع بعضهما البعض أسرع بكثير من مقارنة كائنين باستخدام الطريقة equals()، خاصة إذا كانت تستخدم العديد من الحقول. إذا كان برنامجنا سيقارن الكائنات، فمن الأسهل بكثير القيام بذلك عن طريق رمز التجزئة، وفقط إذا كانت متساوية hashCode()- تابع المقارنة بواسطة equals(). هذه، بالمناسبة، هي الطريقة التي تعمل بها هياكل البيانات القائمة على التجزئة، على سبيل المثال، تلك التي تعرفها HashMap! يتم تجاوز هذه الطريقة hashCode()من equals()قبل المطور نفسه. وكما هو الحال بالنسبة إلى equals()، فإن الطريقة hashCode()لها متطلبات رسمية محددة في وثائق Oracle:
  1. إذا كان هناك كائنان متساويان (أي أن الطريقة equals()تُرجع صحيحًا)، فيجب أن يكون لهما نفس رمز التجزئة.

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

  2. إذا تم استدعاء الأسلوب hashCode()عدة مرات على نفس الكائن، فيجب أن يُرجع نفس الرقم في كل مرة.

  3. القاعدة 1 لا تعمل في الاتجاه المعاكس. يمكن أن يكون لكائنين مختلفين نفس رمز التجزئة.

القاعدة الثالثة مربكة بعض الشيء. كيف يمكن أن يكون هذا؟ التفسير بسيط للغاية. hashCode()ترجع الطريقة int. intهو رقم 32 بت. لديها عدد محدود من القيم - من -2,147,483,648 إلى +2,147,483,647. بمعنى آخر، هناك ما يزيد قليلاً عن 4 مليارات اختلاف في الرقم int. تخيل الآن أنك تقوم بإنشاء برنامج لتخزين البيانات حول جميع الأشخاص الذين يعيشون على الأرض. سيكون لكل شخص كائن صفي خاص به Man. ~ 7.5 مليار شخص يعيشون على الأرض. بمعنى آخر، بغض النظر عن مدى جودة الخوارزمية Manالتي نكتبها لتحويل الكائنات إلى أرقام، فلن يكون لدينا أرقام كافية. لدينا فقط 4.5 مليار خيار، والعديد من الأشخاص أكثر. وهذا يعني أنه بغض النظر عن مدى صعوبة محاولتنا، فإن رموز التجزئة ستكون هي نفسها بالنسبة لبعض الأشخاص المختلفين. يُطلق على هذا الموقف (رموز التجزئة لكائنين مختلفين متطابقين) اسم الاصطدام. أحد أهداف المبرمج عند تجاوز طريقة ما hashCode()هو تقليل عدد الاصطدامات المحتملة قدر الإمكان. كيف ستبدو طريقتنا hashCode()في الفصل Manمع مراعاة كل هذه القواعد؟ مثله:
@Override
public int hashCode() {
   return dnaCode;
}
متفاجئ؟ :) بشكل غير متوقع، ولكن إذا نظرت إلى المتطلبات، سترى أننا ملتزمون بكل شيء. الكائنات التي تكون عوائدنا equals()صحيحة ستكون متساوية في hashCode(). إذا كان الكائنان لدينا Manمتساويين في القيمة equals(أي أن لهما نفس القيمة dnaCode)، فستعيد طريقتنا نفس الرقم. دعونا ننظر إلى مثال أكثر تعقيدا. لنفترض أن برنامجنا يجب أن يختار السيارات الفاخرة لعملاء هواة الجمع. يعد التجميع أمرًا معقدًا، وله العديد من الميزات. يمكن لسيارة من عام 1963 أن تكلف 100 مرة أكثر من نفس السيارة من عام 1964. يمكن أن تكلف السيارة الحمراء من عام 1970 100 مرة أكثر من السيارة الزرقاء من نفس النوع من نفس العام. الأساليب تساوي &  رمز التجزئة: ممارسة الاستخدام - 4في الحالة الأولى، مع الفصل Man، تجاهلنا معظم الحقول (أي خصائص الشخص) باعتبارها غير ذات أهمية واستخدمنا الحقل فقط للمقارنة dnaCode. نحن هنا نعمل مع منطقة فريدة جدًا، ولا يمكن أن تكون هناك تفاصيل بسيطة! هنا هو فصلنا LuxuryAuto:
public class LuxuryAuto {

   private String model;
   private int manufactureYear;
   private int dollarPrice;

   public LuxuryAuto(String model, int manufactureYear, int dollarPrice) {
       this.model = model;
       this.manufactureYear = manufactureYear;
       this.dollarPrice = dollarPrice;
   }

   //... getters, setters, etc.
}
وهنا، عند المقارنة، يجب أن نأخذ في الاعتبار جميع المجالات. أي خطأ يمكن أن يكلف العميل مئات الآلاف من الدولارات، لذا من الأفضل أن تكون آمنًا:
@Override
public boolean equals(Object o) {
   if (this == o) return true;
   if (o == null || getClass() != o.getClass()) return false;

   LuxuryAuto that = (LuxuryAuto) o;

   if (manufactureYear != that.manufactureYear) return false;
   if (dollarPrice != that.dollarPrice) return false;
   return model.equals(that.model);
}
وفي طريقتنا equals()لم ننسى كل الفحوصات التي تحدثنا عنها سابقاً. لكننا الآن نقارن بين كل مجال من المجالات الثلاثة للأشياء التي لدينا. وفي هذا البرنامج يجب أن تكون المساواة مطلقة في كل مجال. ماذا عن hashCode؟
@Override
public int hashCode() {
   int result = model == null ? 0 : model.hashCode();
   result = result + manufactureYear;
   result = result + dollarPrice;
   return result;
}
الحقل modelفي صفنا عبارة عن سلسلة. هذا مناسب: تم ​​تجاوز Stringالطريقة hashCode()بالفعل في الفصل. نحسب رمز التجزئة للحقل modelونضيف إليه مجموع الحقلين الرقميين الآخرين. هناك خدعة صغيرة في Java تُستخدم لتقليل عدد الاصطدامات: عند حساب رمز التجزئة، اضرب النتيجة المتوسطة بعدد أولي فردي. الرقم الأكثر استخدامًا هو 29 أو 31. لن نخوض في تفاصيل العمليات الحسابية الآن، ولكن للرجوع إليها مستقبلاً، تذكر أن ضرب النتائج المتوسطة بعدد فردي كبير بدرجة كافية يساعد على "توزيع" نتائج التجزئة تعمل وينتهي الأمر بكائنات أقل بنفس رمز التجزئة. بالنسبة لطريقتنا hashCode()في LuxuryAuto ستبدو كما يلي:
@Override
public int hashCode() {
   int result = model == null ? 0 : model.hashCode();
   result = 31 * result + manufactureYear;
   result = 31 * result + dollarPrice;
   return result;
}
يمكنك قراءة المزيد عن كل تعقيدات هذه الآلية في هذا المنشور على StackOverflow ، وكذلك في كتاب جوشوا بلوخ " جافا الفعالة ". وأخيرا، هناك نقطة أخرى مهمة تستحق الذكر. في كل مرة عند التجاوز equals()، hashCode()قمنا باختيار حقول معينة من الكائن، والتي تم أخذها بعين الاعتبار في هذه الطرق. ولكن هل يمكننا أن نأخذ في الاعتبار المجالات المختلفة في equals()و hashCode()؟ من الناحية الفنية، نستطيع. لكن هذه فكرة سيئة، وإليكم السبب:
@Override
public boolean equals(Object o) {
   if (this == o) return true;
   if (o == null || getClass() != o.getClass()) return false;

   LuxuryAuto that = (LuxuryAuto) o;

   if (manufactureYear != that.manufactureYear) return false;
   return dollarPrice == that.dollarPrice;
}

@Override
public int hashCode() {
   int result = model == null ? 0 : model.hashCode();
   result = 31 * result + manufactureYear;
   result = 31 * result + dollarPrice;
   return result;
}
فيما يلي طرقنا equals()لفئة hashCode()LuxuryAuto. hashCode()ظلت الطريقة دون تغيير، equals()وقمنا بإزالة الحقل من الطريقة model. الآن النموذج ليس خاصية لمقارنة كائنين بواسطة equals(). ولكن لا يزال يؤخذ في الاعتبار عند حساب رمز التجزئة. ماذا سنحصل نتيجة لذلك؟ دعونا نصنع سيارتين ونتحقق من ذلك!
public class Main {

   public static void main(String[] args) {

       LuxuryAuto ferrariGTO = new LuxuryAuto("Ferrari 250 GTO", 1963, 70000000);
       LuxuryAuto ferrariSpider = new LuxuryAuto("Ferrari 335 S Spider Scaglietti", 1963, 70000000);

       System.out.println("Are these two objects equal to each other?");
       System.out.println(ferrariGTO.equals(ferrariSpider));

       System.out.println("What are their hash codes?");
       System.out.println(ferrariGTO.hashCode());
       System.out.println(ferrariSpider.hashCode());
   }
}

Эти два an object равны друг другу?
true
Какие у них хэш-codeы?
-1372326051
1668702472
خطأ! باستخدام مجالات مختلفة وقمنا equals()بانتهاك hashCode()العقد المبرم معهم! يجب أن يكون لكائنين متساويين equals()نفس رمز التجزئة. لقد حصلنا على معاني مختلفة بالنسبة لهم. يمكن أن تؤدي مثل هذه الأخطاء إلى عواقب لا تصدق، خاصة عند العمل مع المجموعات التي تستخدم التجزئة. لذلك، عند إعادة التعريف equals()، hashCode()سيكون من الصحيح استخدام نفس الحقول. اتضح أن المحاضرة كانت طويلة جدًا، لكنك تعلمت اليوم الكثير من الأشياء الجديدة! :) حان الوقت للعودة إلى حل المشاكل!
تعليقات
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION