Java שווה לחוזי () ו- hashCode ()

1. סקירה כללית

במדריך זה נציג שתי שיטות המשתייכות מקרוב זו לזו: שווים() ו hashCode (). אנו נתמקד במערכת היחסים שלהם זה עם זה, כיצד לעקוף אותם נכון, ולמה עלינו לעקוף את שניהם או אף אחד מהם.

2. שווים()

ה לְהִתְנַגֵד מחלקה מגדירה את שניהם שווים() ו hashCode () שיטות - מה שאומר ששתי שיטות אלה מוגדרות באופן מרומז בכל מחלקת Java, כולל אלה שאנו יוצרים:

סכום הכיתה {סכום int; מחרוזת מטבע קוד; }
הכנסה מכסף = כסף חדש (55, "דולר"); הוצאות כסף = כסף חדש (55, "דולר ארה"ב"); מאוזן בוליאני = הכנסה. שווה (הוצאות)

היינו מצפים הכנסה. שוויון (הוצאות) לחזור נָכוֹן. אבל עם כֶּסֶף בכיתה במתכונתה הנוכחית, לא.

יישום ברירת המחדל של שווים() בכיתה לְהִתְנַגֵד אומר ששוויון זהה לזהות אובייקט. וגם הַכנָסָה ו הוצאות הם שני מקרים מובחנים.

2.1. שׁוֹלֵט שווים()

בואו נעקוף את שווים() שיטה כך שהיא לא תתחשב רק בזהות אובייקט, אלא גם בערך של שני המאפיינים הרלוונטיים:

@ עקוף בוליאני ציבורי שווה (אובייקט o) אם (o == זה) יחזיר נכון; אם (! (למשל כסף)) להחזיר שקר; כסף אחר = (כסף) o; currencyCodeEquals בוליאני = (this.currencyCode == null && other.currencyCode == null) 

2.2. שווים() חוֹזֶה

Java SE מגדיר חוזה שההטמעה שלנו של שווים() השיטה חייבת למלא. רוב הקריטריונים הם שכל ישר. ה שווים() השיטה חייבת להיות:

  • רפלקסיבי: אובייקט חייב להיות שווה לעצמו
  • סימטרי: x.equals (y) חייב להחזיר את אותה תוצאה כמו y.equals (x)
  • מעבר: אם x.equals (y) ו y.equals (z) אז גם x.equals (z)
  • עִקבִי: הערך של שווים() צריך להשתנות רק אם נכס הכלול ב שווים() שינויים (אין אפשרות לאקראיות)

אנו יכולים לחפש את הקריטריונים המדויקים ב- Java SE Docs עבור ה- לְהִתְנַגֵד מעמד.

2.3. מפר שווים() סימטריה עם ירושה

אם הקריטריונים ל שווים() האם השכל הישר כזה הוא, כיצד נוכל בכלל להפר אותו? נו, הפרות מתרחשות לרוב, אם אנו מרחיבים מעמד שדרס שווים(). בואו ניקח בחשבון א שובר כיתה שמרחיבה את שלנו כֶּסֶף מעמד:

בכיתה WrongVoucher מאריך כסף {חנות מיתרים פרטית; @ Override בוליאני ציבורי שווה (אובייקט o) // שיטות אחרות}

במבט ראשון, ה שובר הכיתה והביטול שלה עבור שווים() נראה שהם נכונים. ושניהם שווים() שיטות מתנהגות נכון כל עוד אנו משווים כֶּסֶף ל כֶּסֶף אוֹ שובר ל שובר. אך מה קורה אם נשווה בין שני האובייקטים הללו?

כסף מזומן = כסף חדש (42, "דולר ארה"ב"); שובר WrongVoucher = WrongVoucher חדש (42, "USD", "אמזון"); voucher.equals (cash) => false // כצפוי. cash.equals (שובר) => נכון // זה לא בסדר.

זה מפר את קריטריוני הסימטריה של שווים() חוֹזֶה.

2.4. קְבִיעָה שווים() סימטריה עם קומפוזיציה

כדי להימנע ממלכודת זו, עלינו לעשות זאת מעדיפים הרכב לעומת ירושה.

במקום לסווג משנה כֶּסֶף, בואו ניצור a שובר כיתה עם א כֶּסֶף תכונה:

שובר מחלקה {ערך כסף פרטי; חנות מיתרים פרטית; שובר (סכום int, currencyCode מחרוזת, חנות מחרוזות) {this.value = כסף חדש (סכום, currencyCode); this.store = חנות; } @ Override בוליאני ציבורי שווה (אובייקט o) // שיטות אחרות}

ועכשיו, שווים יעבוד בצורה סימטרית כנדרש החוזה.

3. hashCode ()

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

לקבלת פרטים נוספים, עיין במדריך שלנו ל hashCode ().

3.1. hashCode () חוֹזֶה

Java SE מגדירה גם חוזה עבור hashCode () שיטה. מבט מעמיק בו מראה כמה קשורים זה לזה hashCode () ו שווים() הם.

כל שלושת הקריטריונים בחוזה של hashCode () להזכיר במובנים מסוימים את שווים() שיטה:

  • עקביות פנימית: הערך של hashCode () עשוי להשתנות רק אם נכס שנמצא ב שווים() שינויים
  • שווה עקביות: אובייקטים השווים זה לזה חייבים להחזיר את אותו hashCode
  • התנגשויות: לאובייקטים לא שווים יכול להיות אותו hashCode

3.2. הפרת העקביות של hashCode () ו שווים()

לקריטריונים השנייה של חוזה שיטות hashCode יש תוצאה חשובה: אם אנו עוקפים שווה ל- (), עלינו לעקוף גם את hashCode (). וזאת ללא ספק ההפרה הנפוצה ביותר בנוגע לחוזים של שווים() ו hashCode () שיטות.

בואו נראה דוגמה כזו:

צוות כיתה {מחרוזת עיר; מחלקת מיתרים; @ Override הציבור הסופי בוליאני שווה (אובייקט o) {// יישום}}

ה קְבוּצָה עוקף מחלקה בלבד שווים(), אך הוא עדיין משתמש במשתמע ביישום ברירת המחדל של hashCode () כהגדרתו ב לְהִתְנַגֵד מעמד. וזה מחזיר אחרת hashCode () לכל מקרה של הכיתה. זה מפר את הכלל השני.

עכשיו אם ניצור שניים קְבוּצָה חפצים, שניהם עם העיר "ניו יורק" והמחלקה "שיווק", הם יהיו שווים, אך הם יחזירו hashCodes שונים.

3.3. מפת גיבוב מפתח עם עקביות hashCode ()

אבל מדוע הפרת החוזה היא שלנו קְבוּצָה בכיתה בעיה? ובכן, הבעיה מתחילה כאשר מעורבים כמה אוספים מבוססי-חשיש. בואו ננסה להשתמש שלנו קְבוּצָה כיתה כמפתח של א מפת גיבוב:

מנהיגי מפות = HashMap חדש (); leader.put (צוות חדש ("ניו יורק", "פיתוח"), "אן"); leader.put (צוות חדש ("בוסטון", "פיתוח"), "בריאן"); leader.put (צוות חדש ("בוסטון", "שיווק"), "צ'רלי"); צוות myTeam = צוות חדש ("ניו יורק", "פיתוח"); מחרוזת myTeamLeader = leader.get (myTeam);

היינו מצפים myTeamLeader להחזיר את "אן". אבל עם הקוד הנוכחי, זה לא.

אם אנו רוצים להשתמש במקרים של קְבוּצָה כיתה כ מפת גיבוב עלינו לעקוף את המקשים hashCode () שיטה כך שתעמוד בחוזה: עצמים שווים מחזירים אותו דבר hashCode.

בואו נראה יישום לדוגמא:

@ Override ציבורי סופי int hashCode () {int תוצאה = 17; אם (עיר! = null) {תוצאה = 31 * תוצאה + city.hashCode (); } אם (מחלקה! = null) {תוצאה = 31 * תוצאה + department.hashCode (); } להחזיר תוצאה; }

לאחר שינוי זה, leaders.get (myTeam) מחזירה את "אן" כצפוי.

4. מתי אנו עוקפים שווים() ו hashCode ()?

באופן כללי, אנו רוצים לעקוף את שניהם או אף אחד מהם. בדיוק ראינו בסעיף 3 את ההשלכות הלא רצויות אם נתעלם מכלל זה.

עיצוב מונחה תחום יכול לעזור לנו להחליט נסיבות מתי עלינו להשאיר אותן. בשיעורי ישויות - לאובייקטים בעלי זהות מהותית - יישום ברירת המחדל הגיוני לעיתים קרובות.

למרות זאת, עבור אובייקטים ערכיים, אנו בדרך כלל מעדיפים שוויון על סמך תכונותיהם. כך רוצים לעקוף שווים() ו hashCode (). זכור את שלנו כֶּסֶף בכיתה מסעיף 2: 55 דולר שווה 55 דולר - גם אם מדובר בשני מקרים נפרדים.

5. עוזרי יישום

בדרך כלל איננו כותבים את יישום השיטות הללו ביד. כפי שניתן לראות, יש לא מעט מלכודות.

דרך נפוצה אחת היא לתת ל- IDE שלנו ליצור את שווים() ו hashCode () שיטות.

ל- Apache Commons Lang ו- Google Guava יש שיעורי עוזר במטרה לפשט את כתיבת שתי השיטות.

פרויקט לומבוק מספק גם @EqualsAndHashCode ביאור. שים לב שוב איך שווים() ו hashCode () "לך ביחד" ואפילו תהיה ביאור משותף.

6. אימות החוזים

אם אנו רוצים לבדוק אם היישומים שלנו עומדים בחוזי Java SE וגם בכמה שיטות עבודה מומלצות, אנו יכולים להשתמש בספריית EqualsVerifier.

בואו נוסיף את התלות של מבחן EqualsVerifier Maven:

 nl.jqno.equalsverifier מבחן שווה ערך 3.0.3 

בואו נאמת את זה שלנו קְבוּצָה הכיתה עוקבת אחר שווים() ו hashCode () חוזים:

@Test הציבור בטל שווה ל- HashCodeContracts () {EqualsVerifier.forClass (Team.class). Verify (); }

ראוי לציין זאת EqualsVerifier בודק את שניהם שווים() ו hashCode () שיטות.

EqualsVerifier מחמיר בהרבה מחוזה Java SE. לדוגמה, זה מוודא שהשיטות שלנו לא יכולות לזרוק א NullPointerException. כמו כן, היא אוכפת ששתי השיטות, או הכיתה עצמה, הן סופיות.

חשוב להבין זאת תצורת ברירת המחדל של EqualsVerifier מאפשר רק שדות בלתי ניתנים לשינוי. זהו בדיקה מחמירה יותר ממה שמאפשר חוזה Java SE. זה עומד בהמלצה של Design-Driven Design כדי להפוך אובייקטים ערכיים לבלתי משתנים.

אם אנו מוצאים כמה מהאילוצים המובנים מיותרים, נוכל להוסיף א לדכא (אזהרה. SPECIFIC_WARNING) שלנו EqualsVerifier שִׂיחָה.

7. מסקנה

במאמר זה דנו ב שווים() ו hashCode () חוזים. עלינו לזכור:

  • עקוף תמיד hashCode () אם נעקוף שווים()
  • לבטל שווים() ו hashCode () לאובייקטים ערכיים
  • היו מודעים למלכודות של הרחבת שיעורים שדרסו שווים() ו hashCode ()
  • שקול להשתמש ב- IDE או בספריית צד שלישי ליצירת ה- שווים() ו hashCode () שיטות
  • שקול להשתמש ב- EqualsVerifier כדי לבדוק את היישום שלנו

לבסוף, ניתן למצוא את כל דוגמאות הקוד ב- GitHub.