היי! היום נדבר על שתי שיטות חשובות ב-Java: equals() ו- hashCode() . זו לא הפעם הראשונה שאנחנו פוגשים אותם: קורס CodeGym מתחיל בשיעור קצר
על equals() - קרא אותו אם שכחת אותו או לא ראית אותו קודם... בשיעור של היום, אנחנו' אדבר על מושגים אלה בפירוט. ותאמין לי, יש לנו על מה לדבר! אבל לפני שנעבור לחדש, בואו נרענן את מה שכבר סיקרנו :) כזכור, בדרך כלל זה רעיון רע להשוות בין שני אובייקטים באמצעות האופרטור == , כי == משווה הפניות. הנה הדוגמה שלנו עם מכוניות משיעור שנערך לאחרונה:
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
נראה שיצרנו שני אובייקטי רכב זהים : הערכים של השדות התואמים של שני אובייקטי המכונית זהים, אך תוצאת ההשוואה עדיין שקרית. אנחנו כבר יודעים את הסיבה: הפניות car1 ו- car2 מצביעות על כתובות זיכרון שונות, כך שהן אינן שוות. אבל אנחנו עדיין רוצים להשוות בין שני האובייקטים, לא שני הפניות. הפתרון הטוב ביותר להשוואת אובייקטים הוא שיטת equals() .
שיטת 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.
}
נניח שאנחנו כותבים תוכנית שצריכה לקבוע אם שני אנשים הם תאומים זהים או פשוט דומים. יש לנו חמישה מאפיינים: גודל אף, צבע עיניים, סגנון שיער, נוכחות של צלקות ותוצאות בדיקת DNA (למען הפשטות, אנו מייצגים זאת כקוד שלם). איזה מהמאפיינים האלה לדעתך יאפשר לתוכנית שלנו לזהות תאומים זהים? כמובן שרק בדיקת DNA יכולה לספק ערבות. לשני אנשים יכולים להיות אותו צבע עיניים, תספורת, אף ואפילו צלקות - יש הרבה אנשים בעולם, ואי אפשר להבטיח שאין דופלגנרים בחוץ. אבל אנחנו צריכים מנגנון אמין: רק התוצאה של בדיקת 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(.........); // The 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
. לכן, אם פרמטר השיטה הוא 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()
השיטה, הקפד לעמוד בדרישות הבאות:
-
רפלקסיביות.
כאשר
equals()
השיטה משמשת כדי להשוות אובייקט כלשהו עם עצמו, היא חייבת להחזיר אמת.
כבר עמדנו בדרישה הזו. השיטה שלנו כוללת:if (this == o) return true;
-
סִימֶטרִיָה.
אם
a.equals(b) == true
, אזb.equals(a)
חייב לחזורtrue
.
השיטה שלנו עונה גם על דרישה זו. -
טרנזיטיביות.
אם שני אובייקטים שווים לאובייקט שלישי כלשהו, אז הם חייבים להיות שווים זה לזה.
אםa.equals(b) == true
וa.equals(c) == true
, אזb.equals(c)
חייב גם להחזיר אמת. -
הַתמָדָה.
התוצאה של
equals()
חייבת להשתנות רק כאשר השדות המעורבים משתנים. אם הנתונים של שני האובייקטים אינם משתנים, התוצאה שלequals()
חייבת להיות תמיד זהה. -
אי שוויון עם
null
.עבור כל אובייקט,
a.equals(null)
חייב להחזיר false
זה לא רק קבוצה של כמה "המלצות שימושיות", אלא חוזה קפדני , המפורט בתיעוד של Oracle
GO TO FULL VERSION