JavaRush /وبلاگ جاوا /Random-FA /قراردادهای برابر و هش کد یا هر چیزی که هست
Aleksandr Zimin
مرحله
Санкт-Петербург

قراردادهای برابر و هش کد یا هر چیزی که هست

در گروه منتشر شد
البته اکثریت قریب به اتفاق برنامه نویسان جاوا می دانند که متدها ارتباط نزدیکی با یکدیگر equalsدارند و توصیه می شود که هر دوی این متدها را به طور مداوم در کلاس های خود لغو کنند. hashCodeتعداد کمی کوچکتر می دانند که چرا چنین است و اگر این قانون شکسته شود چه عواقب غم انگیزی ممکن است رخ دهد. من پیشنهاد می کنم مفهوم این روش ها را در نظر بگیریم، هدف آنها را تکرار کنیم و بفهمیم که چرا آنها تا این حد به هم متصل هستند. من این مقاله را مانند مقاله قبلی در مورد بارگذاری کلاس ها برای خودم نوشتم تا در نهایت تمام جزئیات موضوع را فاش کنم و دیگر به منابع شخص ثالث برنگردم. بنابراین، خوشحال می شوم که انتقاد سازنده داشته باشم، زیرا اگر در جایی خلاء وجود دارد، باید برطرف شود. متأسفانه مقاله بسیار طولانی شد.

برابر است با قوانین لغو

روشی equals()در جاوا برای تأیید یا رد این واقعیت مورد نیاز است که دو شی با منشأ منطقی برابر هستند . به این معنی که هنگام مقایسه دو شی، برنامه نویس باید بفهمد که آیا میدان های مهم آنها معادل هستند یا خیر . لازم نیست که همه فیلدها باید یکسان باشند، زیرا این روش equals()متضمن برابری منطقی است . اما گاهی اوقات نیاز خاصی به استفاده از این روش وجود ندارد. همانطور که می گویند، ساده ترین راه برای جلوگیری از مشکلات با استفاده از یک مکانیسم خاص، استفاده نکردن از آن است. همچنین لازم به ذکر است که به محض شکستن قرارداد، equalsکنترل درک نحوه تعامل اشیاء و ساختارهای دیگر با شی شما را از دست می دهید. و متعاقباً یافتن علت خطا بسیار دشوار خواهد بود.

چه زمانی نباید این روش را نادیده گرفت

  • زمانی که هر نمونه از یک کلاس منحصر به فرد است.
  • این امر تا حد زیادی در مورد آن دسته از کلاس هایی اعمال می شود که رفتار خاصی را ارائه می دهند تا اینکه برای کار با داده ها طراحی شوند. مثلاً کلاس Thread. برای آنها equals، پیاده سازی متد ارائه شده توسط کلاس Objectبیش از اندازه کافی است. مثال دیگر کلاس های enum ( Enum) است.
  • زمانی که در واقع کلاس نیازی به تعیین معادل بودن نمونه های خود ندارد.
  • به عنوان مثال، برای یک کلاس java.util.Randomاصلاً نیازی به مقایسه نمونه های کلاس با یکدیگر نیست و تعیین می کند که آیا آنها می توانند همان دنباله اعداد تصادفی را برگردانند یا خیر. صرفاً به این دلیل که ماهیت این طبقه حتی حاکی از چنین رفتاری نیست.
  • وقتی کلاسی که در حال گسترش آن هستید از قبل پیاده‌سازی متد خود را دارد equalsو رفتار این پیاده‌سازی برای شما مناسب است.
  • به عنوان مثال، برای کلاس ها ، Setپیاده سازی به ترتیب در ، و . ListMapequalsAbstractSetAbstractListAbstractMap
  • و در نهایت نیازی به override نیست equalsوقتی که محدوده کلاس شما privateیا package-privateو مطمئن هستید که این متد هرگز فراخوانی نخواهد شد.

برابر قرارداد

هنگام نادیده گرفتن یک روش، equalsتوسعه‌دهنده باید قوانین اساسی تعریف شده در مشخصات زبان جاوا را رعایت کند.
  • انعکاس پذیری
  • برای هر مقدار داده شده x، عبارت x.equals(x)باید برگردد true.
    داده شده - به این معنی کهx != null
  • تقارن
  • برای هر مقدار داده شده xو y، فقط در صورتی x.equals(y)باید برگردد که برگردد . truey.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()کنید که کلاس شی را برمی‌گرداند و قبل از شروع مقایسه خود اشیا، مطمئن شوید که آنها از یک نوع هستند. ، و به حقیقت منشأ مشترک آنها توجه نکنید. بنابراین، قوانین تقارن و گذرا برآورده خواهد شد. اما در همان زمان، در آن سوی سنگر نویسنده دیگری ایستاده است که در محافل وسیع مورد احترام نیست، جاشوا بلوخ، که معتقد است این رویکرد اصل جایگزینی باربارا لیسکوف را نقض می کند. این اصل بیان می‌کند که «کد فراخوانی باید با یک کلاس پایه مانند کلاس‌های فرعی آن بدون اطلاع از آن رفتار کند . » و در راه حل پیشنهادی هورستمن، این اصل به وضوح نقض شده است، زیرا به اجرا بستگی دارد. خلاصه واضح است که موضوع تاریک است. همچنین لازم به ذکر است که هورستمن قانون اعمال رویکرد خود را روشن می کند و به زبان انگلیسی ساده می نویسد که هنگام طراحی کلاس ها باید در مورد استراتژی تصمیم گیری کنید و اگر تست برابری فقط توسط سوپرکلاس انجام می شود، می توانید این کار را با انجام انجام دهید. عملیات instanceof. در غیر این صورت، زمانی که معنای چک بسته به کلاس مشتق شده تغییر می کند و پیاده سازی متد باید به پایین سلسله مراتب منتقل شود، باید از متد استفاده کنید getClass(). جاشوا بلوخ، به نوبه خود، پیشنهاد می کند که از وراثت صرف نظر شود و از ترکیب شی با گنجاندن یک ColorPointکلاس در کلاس Pointو ارائه یک روش دسترسی asPoint()برای به دست آوردن اطلاعات به طور خاص در مورد نقطه استفاده شود. این از شکستن همه قوانین جلوگیری می کند، اما، به نظر من، درک کد را دشوارتر می کند. گزینه سوم استفاده از تولید خودکار روش برابر با استفاده از IDE است. به هر حال، Idea نسل Horstmann را بازتولید می کند و به شما امکان می دهد یک استراتژی برای اجرای یک روش در یک سوپرکلاس یا در فرزندان آن انتخاب کنید. در نهایت، قانون سازگاری بعدی بیان می کند که حتی اگر اشیا تغییر xنکنند y، فراخوانی مجدد آنها x.equals(y)باید همان مقدار قبلی را برگرداند. قانون نهایی این است که هیچ شیئی نباید برابر باشد null. اینجا همه چیز روشن است null- این عدم قطعیت است، آیا شیء برابر با عدم قطعیت است؟ معلوم نیست یعنی false.

الگوریتم کلی برای تعیین مساوی

  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استفاده از روش های مقایسه کلاس های wrapper مربوطه Float.compare()وDouble.compare()
  7. و در نهایت به سه سوال پاسخ دهید: آیا روش اجرا شده متقارن است ؟ متعدی ؟ موافق ؟ دو اصل دیگر ( انعکاس و قطعیت ) معمولاً به طور خودکار انجام می شوند.

قوانین نادیده گرفتن HashCode

هش یک عدد تولید شده از یک شی است که وضعیت آن را در یک نقطه از زمان توصیف می کند. این عدد در جاوا عمدتاً در جداول هش مانند HashMap. در این حالت، تابع هش برای به دست آوردن عدد بر اساس یک شی باید به گونه ای اجرا شود که توزیع نسبتاً یکنواخت عناصر در جدول هش را تضمین کند. و همچنین برای به حداقل رساندن احتمال برخورد زمانی که تابع مقدار یکسانی را برای کلیدهای مختلف برمی گرداند.

هش کد قرارداد

برای پیاده سازی یک تابع هش، مشخصات زبان قوانین زیر را تعریف می کند:
  • فراخوانی یک متد hashCodeیک یا چند بار روی یک شی باید همان مقدار هش را برگرداند، مشروط بر اینکه فیلدهای شی که در محاسبه مقدار دخیل هستند تغییر نکرده باشند.
  • فراخوانی یک متد hashCodeبر روی دو شیء، همیشه باید همان عدد را برگرداند، در صورتی که اشیاء برابر باشند ( فراخوانی یک متد equalsبر روی این اشیاء برمی گردد true).
  • فراخوانی یک متد 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در مورد روش، equalsاین رابطه مستقیمی با مقایسه اشیاء دارد، در مورد hashCodeغیرمستقیم، زمانی که لازم است، مثلاً، مکان تقریبی یک شی را در جداول هش یا ساختارهای داده مشابه تعیین کنیم تا افزایش سرعت جستجوی یک شی علاوه بر قراردادها ، الزام دیگری نیز در رابطه با مقایسه اشیاء وجود دارد equals. hashCodeاین سازگاری یک روش compareToرابط Comparableبا یک است equals. این الزام توسعه‌دهنده را ملزم می‌کند که همیشه x.equals(y) == trueزمانی که x.compareTo(y) == 0. یعنی می بینیم که مقایسه منطقی دو شی نباید در هیچ کجای کاربرد متناقض باشد و همیشه باید سازگار باشد.

منابع

جاوا موثر، ویرایش دوم. جاشوا بلوخ. ترجمه رایگان یک کتاب بسیار خوب. جاوا، کتابخانه حرفه ای. جلد 1. مبانی. کی هورستمن کمی تئوری کمتر و عمل بیشتر. اما همه چیز به اندازه بلوخ با جزئیات تحلیل نمی شود. اگرچه یک view در برابر () یکسان وجود دارد. ساختار داده در تصاویر HashMap یک مقاله بسیار مفید در مورد دستگاه HashMap در جاوا. به جای نگاه کردن به منابع
نظرات
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION