JavaRush /בלוג Java /Random-HE /equals & hashCode שיטות: תרגול השימוש

equals & hashCode שיטות: תרגול השימוש

פורסם בקבוצה
שלום! היום נדבר על שתי שיטות חשובות ב-Java - equals()ו hashCode(). זו לא הפעם הראשונה שאנחנו פוגשים אותם: בתחילת קורס JavaRush הייתה הרצאה קצרה על equals()- קרא אותה אם שכחתם אותה או לא ראיתם אותה בעבר. שיטות שווה &  hashCode: תרגול שימוש - 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().

שיטת 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 ביולוגית (למען הפשטות - בצורת מספר קוד). אילו מהמאפיינים האלה לדעתך יאפשרו לתוכנית שלנו לזהות קרובי משפחה תאומים? שיטות שווה &  hashCode: תרגול שימוש - 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)אמורה להחזיר אמת.

  4. קְבִיעוּת.

    תוצאות העבודה equals()אמורות להשתנות רק כשהשדות הכלולים בה משתנים. אם הנתונים של שני אובייקטים לא השתנו, תוצאות הבדיקה equals()צריכות להיות תמיד זהות.

  5. אי שוויון עם null.

    עבור כל אובייקט, הצ'ק a.equals(null)חייב להחזיר false.
    זה לא רק קבוצה של כמה "המלצות שימושיות", אלא חוזה קפדני של שיטות שנקבעו בתיעוד של אורקל

שיטת hashCode()

עכשיו בואו נדבר על השיטה hashCode(). למה זה נחוץ? בדיוק לאותה מטרה - השוואת חפצים. אבל כבר יש לנו את זה equals()! למה שיטה אחרת? התשובה פשוטה: לשפר את הפרודוקטיביות. פונקציית hash, המיוצגת על ידי השיטה , ב- Java hashCode(), מחזירה ערך מספרי באורך קבוע עבור כל אובייקט. במקרה של Java, השיטה hashCode()מחזירה מספר 32 סיביות מסוג int. השוואת שני מספרים זה עם זה היא הרבה יותר מהירה מהשוואת שני אובייקטים באמצעות השיטה equals(), במיוחד אם היא משתמשת בשדות רבים. אם התוכנית שלנו תשווה אובייקטים, הרבה יותר קל לעשות זאת על ידי קוד hash, ורק אם הם שווים על ידי hashCode()- המשך להשוואה על ידי equals(). כך, אגב, פועלים מבני נתונים מבוססי hash - למשל, זה שאתה מכיר HashMap! השיטה hashCode(), ממש כמו equals(), מעקפת על ידי המפתח עצמו. ובדיוק כמו עבור equals(), לשיטה hashCode()יש דרישות רשמיות המפורטות בתיעוד של Oracle:
  1. אם שני אובייקטים שווים (כלומר, השיטה equals()מחזירה true), עליהם להיות בעל אותו קוד hash.

    אחרת השיטות שלנו יהיו חסרות משמעות. בדיקה לפי hashCode(), כפי שאמרנו, צריכה לבוא קודם כל כדי לשפר את הביצועים. אם קודי ה-hash שונים, הסימון יחזיר false, למרות שהאובייקטים למעשה שווים (כפי שהגדרנו בשיטה equals()).

  2. אם מתודה hashCode()נקראת מספר פעמים על אותו אובייקט, היא צריכה להחזיר את אותו המספר בכל פעם.

  3. כלל 1 לא עובד הפוך. לשני אובייקטים שונים יכולים להיות אותו קוד hash.

הכלל השלישי קצת מבלבל. איך זה יכול להיות? ההסבר די פשוט. השיטה hashCode()חוזרת int. intהוא מספר של 32 סיביות. יש לו מספר מוגבל של ערכים - מ-2,147,483,648 ל-+2,147,483,647. במילים אחרות, יש קצת יותר מ-4 מיליארד וריאציות של המספר int. כעת דמיינו שאתם יוצרים תוכנית לאחסון נתונים על כל האנשים החיים על פני כדור הארץ. לכל אדם יהיה אובייקט כיתתי משלו Man. ~7.5 מיליארד בני אדם חיים על פני כדור הארץ. במילים אחרות, לא משנה כמה טוב נכתוב אלגוריתם Manלהמרת אובייקטים למספרים, פשוט לא יהיו לנו מספיק מספרים. יש לנו רק 4.5 מיליארד אפשרויות, ועוד הרבה אנשים. זה אומר שלא משנה כמה ננסה, קודי ה-hash יהיו זהים עבור כמה אנשים שונים. מצב זה (קודי הגיבוב של שני אובייקטים שונים מתאימים) נקרא התנגשות. אחת המטרות של המתכנת בעת עקיפת שיטה hashCode()היא להפחית את מספר ההתנגשויות הפוטנציאלי ככל האפשר. איך תיראה השיטה שלנו hashCode()לכיתה Man, תוך התחשבות בכל הכללים הללו? ככה:
@Override
public int hashCode() {
   return dnaCode;
}
מוּפתָע? :) באופן בלתי צפוי, אבל אם תסתכל על הדרישות, תראה שאנחנו עומדים בכל. אובייקטים שעבורם שלנו equals()מחזיר אמת יהיו שווים ב hashCode(). אם שני האובייקטים שלנו Manשווים בערכם equals(כלומר, יש להם אותו ערך dnaCode), השיטה שלנו תחזיר את אותו מספר. בואו נסתכל על דוגמה מסובכת יותר. נניח שהתוכנית שלנו צריכה לבחור מכוניות יוקרה עבור לקוחות אספנים. איסוף הוא דבר מורכב, ויש בו תכונות רבות. מכונית משנת 1963 יכולה לעלות פי 100 יותר מאשר אותה מכונית משנת 1964. מכונית אדומה משנת 1970 יכולה לעלות פי 100 יותר ממכונית כחולה מאותו יצרן מאותה שנה. שיטות שווה &  hashCode: תרגול שימוש - 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, ואליו נוסיף את סכום שני השדות המספריים האחרים. יש טריק קטן ב-Java שמשמש להפחתת מספר ההתנגשויות: בעת חישוב קוד ה-hash, הכפל את תוצאת הביניים במספר ראשוני אי זוגי. המספר הנפוץ ביותר בשימוש הוא 29 או 31. לא ניכנס לפרטי המתמטיקה כרגע, אבל לעיון עתידי, זכור שכפל תוצאות הביניים במספר אי זוגי גדול מספיק עוזר "להפיץ" את תוצאות ה-hash לתפקד ולסיים עם פחות אובייקטים עם אותו hashcode. לשיטה שלנו hashCode()ב-LuxuryAuto זה ייראה כך:
@Override
public int hashCode() {
   int result = model == null ? 0 : model.hashCode();
   result = 31 * result + manufactureYear;
   result = 31 * result + dollarPrice;
   return result;
}
אתה יכול לקרוא עוד על כל המורכבויות של מנגנון זה בפוסט זה ב-StackOverflow , כמו גם בספרו של ג'ושוע בלוך " ג'אווה יעילה ". לבסוף, יש עוד נקודה חשובה ששווה להזכיר. בכל פעם בעת עקיפת 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(). אבל זה עדיין נלקח בחשבון בעת ​​חישוב קוד ה-hash. מה נקבל כתוצאה מכך? בואו ניצור שתי מכוניות ונבדוק את זה!
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()חייבים להיות אותו קוד hash. יש לנו משמעויות שונות עבורם. שגיאות כאלה יכולות להוביל להשלכות המדהימות ביותר, במיוחד כאשר עובדים עם אוספים המשתמשים ב-hash. לכן, בעת הגדרה מחדש equals()ויהיה hashCode()נכון להשתמש באותם שדות. ההרצאה יצאה די ארוכה, אבל היום למדת הרבה דברים חדשים! :) הגיע הזמן לחזור לפתרון בעיות!
הערות
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION