equals
קשורות hashCode
זו לזו, ושמומלץ לעקוף את שתי השיטות הללו במחלקות שלהן באופן עקבי. מספר מעט קטן יותר יודע מדוע זה כך ואיזה השלכות עצובות יכולות להתרחש אם הכלל הזה יופר. אני מציע לשקול את הרעיון של שיטות אלו, לחזור על מטרתן ולהבין מדוע הן כל כך קשורות. כתבתי לעצמי את המאמר הזה, כמו הקודם על טעינת שיעורים, על מנת לחשוף סוף סוף את כל פרטי הנושא ולא לחזור יותר למקורות צד שלישי. לכן, אשמח לביקורת בונה, כי אם יש פערים איפשהו, צריך לבטל אותם. המאמר, למרבה הצער, התברר כארוך למדי.
שווה לכללי ביטול
נדרשת שיטהequals()
ב-Java כדי לאשר או להכחיש את העובדה ששני אובייקטים מאותו מקור שווים מבחינה לוגית . כלומר, כאשר משווים בין שני אובייקטים, המתכנת צריך להבין האם השדות המשמעותיים שלהם שווים . אין צורך שכל השדות יהיו זהים, שכן השיטה equals()
מרמזת על שוויון לוגי . אבל לפעמים אין צורך מיוחד להשתמש בשיטה זו. כמו שאומרים, הדרך הקלה ביותר להימנע מבעיות בשימוש במנגנון מסוים היא לא להשתמש בו. כמו כן, יש לציין שברגע שאתה מפר חוזה, equals
אתה מאבד שליטה על ההבנה כיצד חפצים ומבנים אחרים יתקשרו עם האובייקט שלך. ובהמשך למצוא את הגורם לשגיאה יהיה קשה מאוד.
מתי לא לעקוף שיטה זו
- כאשר כל מופע של מחלקה הוא ייחודי. במידה רבה יותר, זה תקף לאותן מחלקות שמספקות התנהגות ספציפית ולא מתוכננות לעבוד עם נתונים. כגון, למשל, כמו הכיתה
- כאשר למעשה המחלקה אינה נדרשת לקבוע את השוויון של המקרים שלה. לדוגמה, עבור מחלקה
- כאשר לכיתה שאתה מרחיב כבר יש יישום משלה של השיטה
equals
וההתנהגות של יישום זה מתאימה לך. לדוגמה, עבור מחלקות - ולבסוף, אין צורך לעקוף
equals
כאשר היקף הכיתה שלך הואprivate
אוpackage-private
ואתה בטוח שהשיטה הזו לעולם לא תיקרא.
Thread
. עבורם equals
, היישום של השיטה שמספקת הכיתה Object
הוא די והותר. דוגמה נוספת היא מחלקות enum ( Enum
).
java.util.Random
אין צורך בכלל להשוות מופעים של המחלקה זה לזה, ולקבוע אם הם יכולים להחזיר את אותו רצף של מספרים אקראיים. פשוט כי האופי של המעמד הזה אפילו לא מרמז על התנהגות כזו.
Set
, List
, Map
היישום equals
הוא ב AbstractSet
, AbstractList
ובהתאמה AbstractMap
.
שווה חוזה
בעת עקיפת שיטה,equals
על המפתח לציית לכללים הבסיסיים המוגדרים במפרט שפת Java.
- רפלקסיביות עבור כל ערך נתון
- סִימֶטרִיָה עבור כל ערך נתון
- טרנזיטיביות עבור כל ערך נתון ,
- עֲקֵבִיוּת עבור כל ערך נתון,
- השוואה null עבור כל ערך נתון
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
. יחד עם זאת, השיטה השנייה, לדעתי, נראית פחות אטרקטיבית, כי במקרים מסוימים, האלגוריתם עלול להיות מסונוור ולא לבצע את ההשוואה במלואה, וייתכן שאינך יודע על כך. קצת שירה באופן כללי, למיטב הבנתי, אין פתרון קונקרטי לבעיה הזו. יש דעה של מחבר סמכותי אחד בשם קיי הורסטמן שאפשר להחליף את השימוש באופרטור instanceof
בקריאה לשיטה getClass()
שמחזירה את המחלקה של האובייקט ולפני שמתחילים להשוות בין האובייקטים עצמם, לוודא שהם מאותו סוג , ואל תשימו לב לעובדת מוצאם המשותף. לפיכך , כללי הסימטריה והטרנזיטיביות יתקיימו. אך במקביל, מעברו השני של המתרס עומד סופר נוסף, מכובד לא פחות בחוגים רחבים, יהושע בלוך, שסבור כי גישה זו מפרה את עקרון ההחלפה של ברברה ליסקוב. עיקרון זה קובע כי "קוד קורא חייב להתייחס למחלקה בסיס באותו אופן כמו מחלקות המשנה שלו מבלי לדעת זאת . " ובפתרון שהציע הורסטמן, עיקרון זה מופר בבירור, שכן הוא תלוי ביישום. בקיצור, ברור שהעניין אפל. יש לציין גם שהורסטמן מבהיר את הכלל ליישום גישתו וכותב באנגלית פשוטה שצריך להחליט על אסטרטגיה בעת תכנון שיעורים, ואם בדיקת השוויון תתבצע רק על ידי מחלקת העל, תוכל לעשות זאת על ידי ביצוע המבצע . אחרת, כאשר הסמנטיקה של הסימון משתנה בהתאם למחלקה הנגזרת ויש להזיז את יישום השיטה למטה בהיררכיה, עליך להשתמש בשיטה . יהושע בלוך, בתורו, מציע לנטוש את הירושה ולהשתמש בהרכב אובייקט על ידי הכללת מחלקה במחלקה ומתן שיטת גישה לקבלת מידע ספציפי על הנקודה. זה ימנע מהפרת כל הכללים, אבל, לדעתי, זה יקשה על הבנת הקוד. האפשרות השלישית היא להשתמש ביצירה האוטומטית של שיטת equals באמצעות ה-IDE. Idea, אגב, משחזר את דור הורסטמן, ומאפשר לך לבחור אסטרטגיה ליישום שיטה במחלקת על או בצאצאיה. לבסוף, כלל העקביות הבא קובע שגם אם האובייקטים לא משתנים, הקריאה אליהם שוב חייבת להחזיר את אותו ערך כמו קודם. הכלל הסופי הוא שאף אובייקט לא צריך להיות שווה ל . הכל ברור כאן - זו אי ודאות, האם האובייקט שווה לאי ודאות? לא ברור, כלומר . instanceof
getClass()
ColorPoint
Point
asPoint()
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
צורך להשתמש בשיטות השוואה של מחלקות העטיפה המתאימותFloat.compare()
וDouble.compare()
- עבור טיפוסים פרימיטיביים (למעט
- ולבסוף, ענה על שלוש שאלות: האם השיטה המיושמת סימטרית ? טרנזיטיבי ? מוסכם ? שני העקרונות האחרים ( רפלקסיביות וודאות ) מבוצעים בדרך כלל באופן אוטומטי .
כללים לעקוף HashCode
Hash הוא מספר שנוצר מאובייקט שמתאר את מצבו בנקודת זמן כלשהי. מספר זה משמש ב-Java בעיקר בטבלאות Hash כגוןHashMap
. במקרה זה, יש ליישם את פונקציית הגיבוב של השגת מספר המבוסס על אובייקט באופן שתבטיח חלוקה שווה יחסית של אלמנטים על פני טבלת הגיבוב. וגם כדי למזער את הסבירות להתנגשויות כאשר הפונקציה מחזירה את אותו ערך עבור מפתחות שונים.
חוזה hashCode
כדי ליישם פונקציית Hash, מפרט השפה מגדיר את הכללים הבאים:- קריאה למתודה
hashCode
פעם אחת או יותר על אותו אובייקט חייבת להחזיר את אותו ערך hash, בתנאי ששדות האובייקט המעורבים בחישוב הערך לא השתנו. - קריאה למתודה
hashCode
על שני אובייקטים צריכה תמיד להחזיר את אותו מספר אם האובייקטים שווים (קריאה למתודהequals
על אובייקטים אלו מחזירהtrue
). - קריאה למתודה
hashCode
על שני אובייקטים לא שווים חייבת להחזיר ערכי hash שונים. למרות שדרישה זו אינה מחייבת, יש לקחת בחשבון שליישום שלה תהיה השפעה חיובית על הביצועים של טבלאות גיבוב.
יש לעקוף את שיטות equals ו-hashCode יחד
בהתבסס על החוזים המתוארים לעיל, יוצא שכאשר אתה מחליף את השיטה בקוד שלךequals
, עליך תמיד לעקוף את השיטה hashCode
. מכיוון שלמעשה שני מופעים של מחלקה שונים מכיוון שהם נמצאים באזורי זיכרון שונים, יש להשוות אותם לפי כמה קריטריונים לוגיים. בהתאם לכך, שני אובייקטים שווים מבחינה לוגית חייבים להחזיר את אותו ערך hash. מה קורה אם רק אחת מהשיטות הללו נדחית?
-
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, והיא תחזיר את אותם ערכי hash עבור אובייקטים זהים מבחינה לוגית. ברור שבכך כבר הגדרנו מנגנון כלשהו להשוואה בין שני אובייקטים.לכן, הדוגמה מהפסקה הקודמת צריכה להתבצע באופן תיאורטי. אבל עדיין לא נוכל למצוא את החפץ שלנו בטבלת ה-hash. אמנם נהיה קרובים לכך, כי לכל הפחות נמצא סל שולחן חשיש שבתוכה ישכב החפץ.
כדי לחפש בהצלחה אובייקט בטבלת גיבוב, בנוסף להשוואת ערכי ה-hash של המפתח, נעשה שימוש גם בקביעת השוויון הלוגי של המפתח עם האובייקט המחפש. כלומר,
equals
אין דרך לעשות בלי לעקוף את השיטה.
אלגוריתם כללי לקביעת hashCode
כאן, נראה לי, אתה לא צריך לדאוג יותר מדי וליצור את השיטה ב-IDE האהוב עליך. מכיוון שכל ההזזות האלה של ביטים ימינה ושמאלה בחיפוש אחר יחס הזהב, כלומר התפלגות נורמלית - זה עבור בחורים עקשנים לחלוטין. באופן אישי, אני בספק אם אני יכול לעשות טוב יותר ומהיר יותר מאותו רעיון.במקום מסקנה
לפיכך, אנו רואים שלשיטותequals
תפקיד hashCode
מוגדר היטב בשפת ג'אווה ונועדו להשיג את השוויון הלוגי המאפיין שני אובייקטים. במקרה של השיטה, equals
יש לזה קשר ישיר להשוואת אובייקטים, במקרה של hashCode
עקיף, כאשר יש צורך, נניח, לקבוע את מיקומו המשוער של אובייקט בטבלאות גיבוב או מבני נתונים דומים על מנת לאפשר להגביר את מהירות החיפוש אחר חפץ. בנוסף לחוזים , equals
ישנה hashCode
דרישה נוספת הקשורה להשוואת חפצים. זוהי העקביות של שיטת compareTo
ממשק Comparable
עם equals
. דרישה זו מחייבת את היזם לחזור תמיד x.equals(y) == true
כאשר x.compareTo(y) == 0
. כלומר, אנו רואים שההשוואה הלוגית של שני אובייקטים לא צריכה לסתור בשום מקום באפליקציה וצריכה להיות תמיד עקבית.
GO TO FULL VERSION