equals
دارند و توصیه می شود که هر دوی این متدها را به طور مداوم در کلاس های خود لغو کنند. hashCode
تعداد کمی کوچکتر می دانند که چرا چنین است و اگر این قانون شکسته شود چه عواقب غم انگیزی ممکن است رخ دهد. من پیشنهاد می کنم مفهوم این روش ها را در نظر بگیریم، هدف آنها را تکرار کنیم و بفهمیم که چرا آنها تا این حد به هم متصل هستند. من این مقاله را مانند مقاله قبلی در مورد بارگذاری کلاس ها برای خودم نوشتم تا در نهایت تمام جزئیات موضوع را فاش کنم و دیگر به منابع شخص ثالث برنگردم. بنابراین، خوشحال می شوم که انتقاد سازنده داشته باشم، زیرا اگر در جایی خلاء وجود دارد، باید برطرف شود. متأسفانه مقاله بسیار طولانی شد.
برابر است با قوانین لغو
روشیequals()
در جاوا برای تأیید یا رد این واقعیت مورد نیاز است که دو شی با منشأ منطقی برابر هستند . به این معنی که هنگام مقایسه دو شی، برنامه نویس باید بفهمد که آیا میدان های مهم آنها معادل هستند یا خیر . لازم نیست که همه فیلدها باید یکسان باشند، زیرا این روش equals()
متضمن برابری منطقی است . اما گاهی اوقات نیاز خاصی به استفاده از این روش وجود ندارد. همانطور که می گویند، ساده ترین راه برای جلوگیری از مشکلات با استفاده از یک مکانیسم خاص، استفاده نکردن از آن است. همچنین لازم به ذکر است که به محض شکستن قرارداد، equals
کنترل درک نحوه تعامل اشیاء و ساختارهای دیگر با شی شما را از دست می دهید. و متعاقباً یافتن علت خطا بسیار دشوار خواهد بود.
چه زمانی نباید این روش را نادیده گرفت
- زمانی که هر نمونه از یک کلاس منحصر به فرد است. این امر تا حد زیادی در مورد آن دسته از کلاس هایی اعمال می شود که رفتار خاصی را ارائه می دهند تا اینکه برای کار با داده ها طراحی شوند. مثلاً کلاس
- زمانی که در واقع کلاس نیازی به تعیین معادل بودن نمونه های خود ندارد. به عنوان مثال، برای یک کلاس
- وقتی کلاسی که در حال گسترش آن هستید از قبل پیادهسازی متد خود را دارد
equals
و رفتار این پیادهسازی برای شما مناسب است. به عنوان مثال، برای کلاس ها ، - و در نهایت نیازی به override نیست
equals
وقتی که محدوده کلاس شماprivate
یاpackage-private
و مطمئن هستید که این متد هرگز فراخوانی نخواهد شد.
Thread
. برای آنها equals
، پیاده سازی متد ارائه شده توسط کلاس Object
بیش از اندازه کافی است. مثال دیگر کلاس های enum ( Enum
) است.
java.util.Random
اصلاً نیازی به مقایسه نمونه های کلاس با یکدیگر نیست و تعیین می کند که آیا آنها می توانند همان دنباله اعداد تصادفی را برگردانند یا خیر. صرفاً به این دلیل که ماهیت این طبقه حتی حاکی از چنین رفتاری نیست.
Set
پیاده سازی به ترتیب در ، و . List
Map
equals
AbstractSet
AbstractList
AbstractMap
برابر قرارداد
هنگام نادیده گرفتن یک روش،equals
توسعهدهنده باید قوانین اساسی تعریف شده در مشخصات زبان جاوا را رعایت کند.
- انعکاس پذیری برای هر مقدار داده شده
- تقارن برای هر مقدار داده شده
- گذرا برای هر مقدار داده شده
- ثبات برای هر مقدار داده شده،
- مقایسه صفر برای هر مقدار داده شده
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()
کنید که کلاس شی را برمیگرداند و قبل از شروع مقایسه خود اشیا، مطمئن شوید که آنها از یک نوع هستند. ، و به حقیقت منشأ مشترک آنها توجه نکنید. بنابراین، قوانین تقارن و گذرا برآورده خواهد شد. اما در همان زمان، در آن سوی سنگر نویسنده دیگری ایستاده است که در محافل وسیع مورد احترام نیست، جاشوا بلوخ، که معتقد است این رویکرد اصل جایگزینی باربارا لیسکوف را نقض می کند. این اصل بیان میکند که «کد فراخوانی باید با یک کلاس پایه مانند کلاسهای فرعی آن بدون اطلاع از آن رفتار کند . » و در راه حل پیشنهادی هورستمن، این اصل به وضوح نقض شده است، زیرا به اجرا بستگی دارد. خلاصه واضح است که موضوع تاریک است. همچنین لازم به ذکر است که هورستمن قانون اعمال رویکرد خود را روشن می کند و به زبان انگلیسی ساده می نویسد که هنگام طراحی کلاس ها باید در مورد استراتژی تصمیم گیری کنید و اگر تست برابری فقط توسط سوپرکلاس انجام می شود، می توانید این کار را با انجام انجام دهید. عملیات instanceof
. در غیر این صورت، زمانی که معنای چک بسته به کلاس مشتق شده تغییر می کند و پیاده سازی متد باید به پایین سلسله مراتب منتقل شود، باید از متد استفاده کنید getClass()
. جاشوا بلوخ، به نوبه خود، پیشنهاد می کند که از وراثت صرف نظر شود و از ترکیب شی با گنجاندن یک ColorPoint
کلاس در کلاس Point
و ارائه یک روش دسترسی asPoint()
برای به دست آوردن اطلاعات به طور خاص در مورد نقطه استفاده شود. این از شکستن همه قوانین جلوگیری می کند، اما، به نظر من، درک کد را دشوارتر می کند. گزینه سوم استفاده از تولید خودکار روش برابر با استفاده از IDE است. به هر حال، Idea نسل Horstmann را بازتولید می کند و به شما امکان می دهد یک استراتژی برای اجرای یک روش در یک سوپرکلاس یا در فرزندان آن انتخاب کنید. در نهایت، قانون سازگاری بعدی بیان می کند که حتی اگر اشیا تغییر 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
استفاده از روش های مقایسه کلاس های wrapper مربوطهFloat.compare()
وDouble.compare()
- برای انواع اولیه (به جز
- و در نهایت به سه سوال پاسخ دهید: آیا روش اجرا شده متقارن است ؟ متعدی ؟ موافق ؟ دو اصل دیگر ( انعکاس و قطعیت ) معمولاً به طور خودکار انجام می شوند.
قوانین نادیده گرفتن HashCode
هش یک عدد تولید شده از یک شی است که وضعیت آن را در یک نقطه از زمان توصیف می کند. این عدد در جاوا عمدتاً در جداول هش مانندHashMap
. در این حالت، تابع هش برای به دست آوردن عدد بر اساس یک شی باید به گونه ای اجرا شود که توزیع نسبتاً یکنواخت عناصر در جدول هش را تضمین کند. و همچنین برای به حداقل رساندن احتمال برخورد زمانی که تابع مقدار یکسانی را برای کلیدهای مختلف برمی گرداند.
هش کد قرارداد
برای پیاده سازی یک تابع هش، مشخصات زبان قوانین زیر را تعریف می کند:- فراخوانی یک متد
hashCode
یک یا چند بار روی یک شی باید همان مقدار هش را برگرداند، مشروط بر اینکه فیلدهای شی که در محاسبه مقدار دخیل هستند تغییر نکرده باشند. - فراخوانی یک متد
hashCode
بر روی دو شیء، همیشه باید همان عدد را برگرداند، در صورتی که اشیاء برابر باشند ( فراخوانی یک متدequals
بر روی این اشیاء برمی گرددtrue
). - فراخوانی یک متد
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
در مورد روش، equals
این رابطه مستقیمی با مقایسه اشیاء دارد، در مورد hashCode
غیرمستقیم، زمانی که لازم است، مثلاً، مکان تقریبی یک شی را در جداول هش یا ساختارهای داده مشابه تعیین کنیم تا افزایش سرعت جستجوی یک شی علاوه بر قراردادها ، الزام دیگری نیز در رابطه با مقایسه اشیاء وجود دارد equals
. hashCode
این سازگاری یک روش compareTo
رابط Comparable
با یک است equals
. این الزام توسعهدهنده را ملزم میکند که همیشه x.equals(y) == true
زمانی که x.compareTo(y) == 0
. یعنی می بینیم که مقایسه منطقی دو شی نباید در هیچ کجای کاربرد متناقض باشد و همیشه باید سازگار باشد.
GO TO FULL VERSION