Java equals() ו- hashCode()

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

Java equals()

מחלקת Object מגדירה את שיטת equals() כך:

public boolean equals(Object obj) {
        return (this == obj);
}

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

  • עבור כל אובייקט x, x.equals(x) צריך להחזיר true.
  • עבור שני אובייקטים 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.equals(y) צריכות להחזיר את אותו התוצאה, אלא אם ורק אם יש שינוי באחת מתכונות האובייקט שמשמשות במימוש של equals().
  • מימוש של שיטת equals() במחלקת Object מחזיר true רק כאשר שני ההפניות מצביעות על אותו אובייקט.

Java hashCode()

שיטת hashCode() של אובייקט Java היא שיטה ילידית והיא מחזירה את ערך הקוד ההאש של האובייקט במספר שלם. החוזה הכללי של שיטת hashCode() הוא:

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

חשיבות שיטת equals() ו-hashCode()

שיטות hashCode() ו-equals() של Java משמשות ביישומים המבוססים על טבלת האש ב-java לאחסון ושליפת נתונים. הסברתי זאת בפירוט ב-איך HashMap עובד ב-java? היישום של equals() ו-hashCode() צריך לעקוב אחרי כללים אלה.

  • אם o1.equals(o2), אז o1.hashCode() == o2.hashCode() אמור תמיד להיות נכון.
  • אם o1.hashCode() == o2.hashCode נכון, זה לא אומר ש-o1.equals(o2) יהיה נכון.

מתי לדרוס את השיטות equals() ו־hashCode()?

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

package com.journaldev.java;

public class DataKey {

	private String name;
	private int id;

        // שיטות getter ו־setter

	@Override
	public String toString() {
		return "DataKey [name=" + name + ", id=" + id + "]";
	}

}
package com.journaldev.java;

import java.util.HashMap;
import java.util.Map;

public class HashingTest {

	public static void main(String[] args) {
		Map<DataKey, Integer> hm = getAllData();

		DataKey dk = new DataKey();
		dk.setId(1);
		dk.setName("Pankaj");
		System.out.println(dk.hashCode());

		Integer value = hm.get(dk);

		System.out.println(value);

	}

	private static Map<DataKey, Integer> getAllData() {
		Map<DataKey, Integer> hm = new HashMap<>();

		DataKey dk = new DataKey();
		dk.setId(1);
		dk.setName("Pankaj");
		System.out.println(dk.hashCode());

		hm.put(dk, 10);

		return hm;
	}

}

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

יישום של שיטות equals() ו־hashCode()

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

@Override
public int hashCode() {
	final int prime = 31;
	int result = 1;
	result = prime * result + id;
	result = prime * result + ((name == null) ? 0 : name.hashCode());
	return result;
}

@Override
public boolean equals(Object obj) {
	if (this == obj)
		return true;
	if (obj == null)
		return false;
	if (getClass() != obj.getClass())
		return false;
	DataKey other = (DataKey) obj;
	if (id != other.id)
		return false;
	if (name == null) {
		if (other.name != null)
			return false;
	} else if (!name.equals(other.name))
		return false;
	return true;
}

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

מה זה התנגשות פישוט

במונחים פשוטים מאוד, מימושי טבלת ההשמות ב־Java משתמשים בלוגיקה הבאה לפעולות get ו־put.

  1. ראשית, לזהות את "הדלי" לשימוש באמצעות קוד ה־hash של "המפתח".
  2. אם אין אובייקטים קיימים בדלי עם קוד hash זהה, אז הוסף את האובייקט לפעולת put והחזר null לפעולת get.
    • אם ישנם אובייקטים אחרים בדלי עם קוד גישה זהה, אז מתחיל להתקיים אמצעי equals() של ה"מפתח".
    • אם equals() מחזיר true וזו פעולת הכניסה, אז ערך האובייקט יוחלף.
    • אם equals() מחזיר true וזו פעולת קבלה, אז ערך האובייקט יוחזר.
    • אם equals() מחזיר false וזו פעולת קבלה, אז null יוחזר.

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

מה אם לא ניישם גם את hashCode() וגם את equals()?

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

מעשים מומלצים ליישום של המתודות equals() ו-hashCode()

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

אתה יכול להוריד את הקוד המלא מה־מאגר הקוד שלנו ב-GitHub.

Source:
https://www.digitalocean.com/community/tutorials/java-equals-hashcode