JavaRush /وبلاگ جاوا /Random-FA /روش های برابر و هش کد: تمرین استفاده

روش های برابر و هش کد: تمرین استفاده

در گروه منتشر شد
سلام! امروز در مورد دو روش مهم در جاوا صحبت خواهیم کرد - 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().

متد ()quals

ممکن است به یاد داشته باشید که ما این متد را از ابتدا ایجاد نمی کنیم، بلکه آن را لغو می کنیم - بالاخره متد 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.
}
فرض کنید در حال نوشتن برنامه‌ای هستیم که باید مشخص کنیم که آیا دو نفر با هم رابطه دارند یا فقط دوقلوها. ما پنج ویژگی داریم: اندازه بینی، رنگ چشم، مدل مو، وجود جای زخم و نتایج یک آزمایش بیولوژیکی DNA (برای سادگی - به صورت یک عدد رمز). به نظر شما کدام یک از این ویژگی ها به برنامه ما اجازه می دهد تا اقوام دوقلو را شناسایی کند؟ روش ها برابر است &  هش کد: تمرین استفاده - 2البته فقط آزمایش بیولوژیکی می تواند تضمین کند. دو نفر می توانند رنگ چشم، مدل مو، بینی و حتی زخم های یکسانی داشته باشند - افراد زیادی در جهان وجود دارند و اجتناب از تصادف غیرممکن است. ما به یک مکانیسم قابل اعتماد نیاز داریم: فقط نتیجه یک آزمایش DNA به ما امکان می دهد یک نتیجه دقیق بگیریم. این برای روش ما چه معنایی دارد equals()؟ ما باید آن را در یک کلاس Manبا در نظر گرفتن الزامات برنامه خود دوباره تعریف کنیم. این روش باید میدان int dnaCodeدو شی را با هم مقایسه کند و اگر برابر باشند، اشیاء برابر هستند.
@Override
public boolean equals(Object o) {
   Man man = (Man) o;
   return dnaCode == man.dnaCode;
}
آیا واقعا به همین سادگی است؟ نه واقعا. چیزی را از دست دادیم. در این مورد، ما برای اشیاء خود فقط یک میدان "مهم" تعریف کرده ایم که توسط آن برابری آنها ایجاد می شود - dnaCode. حال تصور کنید که ما نه 1، بلکه 50 فیلد «معنی دار» داشته باشیم و اگر همه 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)نیز باید true باشد.

  4. ماندگاری.

    نتایج کار equals()فقط زمانی باید تغییر کند که فیلدهای موجود در آن تغییر کند. اگر داده های دو شی تغییر نکرده باشد، نتایج بررسی equals()باید همیشه یکسان باشد.

  5. نابرابری با null.

    برای هر شی، چک a.equals(null)باید نادرست باشد.
    این فقط مجموعه‌ای از «توصیه‌های مفید» نیست، بلکه یک قرارداد سخت‌گیرانه از روش‌ها است که در اسناد اوراکل تجویز شده است.

متد hashCode().

حالا بیایید در مورد روش صحبت کنیم hashCode(). چرا نیاز است؟ دقیقاً برای همان هدف - مقایسه اشیاء. اما ما قبلا آن را داریم equals()! چرا روش دیگری؟ پاسخ ساده است: بهبود بهره وری. یک تابع هش که با متد , در جاوا نشان داده می شود hashCode()، مقدار عددی با طول ثابت را برای هر شی برمی گرداند. در مورد جاوا، این روش hashCode()یک عدد 32 بیتی از نوع را برمی گرداند int. مقایسه دو عدد با یکدیگر بسیار سریعتر از مقایسه دو شیء با استفاده از روش است equals()، به خصوص اگر از فیلدهای زیادی استفاده شود. اگر برنامه ما اشیاء را با هم مقایسه کند، انجام این کار با کد هش بسیار ساده‌تر است، و فقط در صورتی که با هم برابر باشند hashCode()- به مقایسه با equals(). به هر حال، ساختارهای داده مبتنی بر هش چگونه کار می کنند - برای مثال، ساختاری که شما می شناسید HashMap! این روش hashCode()، درست مانند equals()، توسط خود توسعه دهنده لغو می شود. و درست مانند برای equals()، این روش hashCode()دارای الزامات رسمی است که در اسناد اوراکل مشخص شده است:
  1. اگر دو شی با هم برابر باشند (یعنی متد equals()true را برمی گرداند)، باید کد هش یکسانی داشته باشند.

    در غیر این صورت روش های ما بی معنی خواهد بود. hashCode()همانطور که گفتیم، چک کردن توسط ، باید ابتدا برای بهبود عملکرد باشد. اگر کدهای هش متفاوت باشند، چک به صورت false برمی گردد، حتی اگر اشیا در واقع برابر باشند (همانطور که در روش تعریف کردیم 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()true برمی گرداند در برابر خواهند بود 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و مجموع دو فیلد عددی دیگر را به آن اضافه می کنیم. ترفند کوچکی در جاوا وجود دارد که برای کاهش تعداد برخوردها استفاده می شود: هنگام محاسبه کد هش، نتیجه میانی را در یک عدد اول فرد ضرب کنید. رایج ترین عددی که استفاده می شود 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 و همچنین در کتاب جاوای مؤثر جاشوا بلوخ اطلاعات بیشتری در مورد همه پیچیدگی‌های این مکانیسم بخوانید . در پایان، یک نکته مهم دیگر وجود دارد که قابل ذکر است. هر بار هنگام overriding 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