عقد equals وhashCode
سبب وجوب توافقهما
عقد equals وhashCode درس مجاني في Java Academy على CoddyKit. هذا هو الدرس 2 من أصل 4. يمكنك قراءة الدرس كاملاً أدناه مجاناً — ثم تمرن عليه مباشرة في المتصفح باستخدام محرر أكواد مدمج ومدرس ذكاء اصطناعي متاح 24/7. هذا الدرس جزء من مسار التعلم في Java Academy، وتقدمك يتزامن عبر الويب وتطبيق CoddyKit. تتضمن دورة Java Academy 4 دروس في المجموع.
طريقتان، عقد واحد
يرث كل كائن في Java الدالتين equals() وhashCode() من Object.
عند إعادة تعريف إحداهما، يجب إعادة تعريف الأخرى في جميع الحالات تقريبًا. فهما تشكلان عقدًا ملزمًا تعتمد عليه المجموعات القائمة على التجزئة.
public class Main {
public static void main(String[] args) {
Object a = new Object();
Object b = new Object();
System.out.println(a.equals(b));
System.out.println(a.hashCode() == b.hashCode());
}
}القاعدة الأساسية
ينص العقد على ما يلي:
- إذا كانت
a.equals(b)تساوي true، فيجب أن تساويa.hashCode()قيمةb.hashCode(). - إذا اختلف رمزا التجزئة، فالكائنان غير متساويين حتمًا.
يجب أن تمتلك الكائنات المتساوية رموز تجزئة متساوية، أما العكس فليس مطلوبًا.
public class Main {
public static void main(String[] args) {
String s1 = "hello";
String s2 = "hel" + "lo";
System.out.println(s1.equals(s2));
System.out.println(s1.hashCode() == s2.hashCode());
}
}قد تتشارك الكائنات غير المتساوية رمز تجزئة واحدًا
يُسمح لكائنين غير متساويين بأن يتشاركا رمز تجزئة واحدًا. هذا مجرد تصادم، وتتعامل معه الخريطة باستخدام equals().
لذلك، فإن الحالة الوحيدة الممنوعة هي: كائنان متساويان لهما رمزا تجزئة مختلفان.
public class Main {
public static void main(String[] args) {
// Equal value, unequal objects, same hash is fine
System.out.println("FB".hashCode() == "Ea".hashCode());
System.out.println("FB".equals("Ea"));
}
}فئة تعيد تعريف equals فقط
إليك فئة معطوبة. فهي تعيد تعريف equals()، لكنها لا تعيد تعريف hashCode().
أصبح لنقطتين متساويتين منطقيًا الآن رمزا تجزئة مختلفان موروثان من Object، مما يخالف العقد.
public class Main {
static class Point {
final int x, y;
Point(int x, int y) { this.x = x; this.y = y; }
@Override public boolean equals(Object o) {
if (!(o instanceof Point p)) return false;
return x == p.x && y == p.y;
}
// BUG: no hashCode override
}
public static void main(String[] args) {
Point a = new Point(1, 2);
Point b = new Point(1, 2);
System.out.println("equals: " + a.equals(b));
System.out.println("same hash: " + (a.hashCode() == b.hashCode()));
}
}الخلل في HashSet
تفشل الفئة المعطوبة داخل HashSet. تبحث المجموعة في الحاوية التي اختارها رمز التجزئة الخاطئ، ولا تعثر مطلقًا على العنصر المتساوي.
وينتهي بك الأمر إلى عناصر مكررة كان ينبغي رفضها.
import java.util.HashSet;
import java.util.Set;
public class Main {
static class Point {
final int x, y;
Point(int x, int y) { this.x = x; this.y = y; }
@Override public boolean equals(Object o) {
if (!(o instanceof Point p)) return false;
return x == p.x && y == p.y;
}
}
public static void main(String[] args) {
Set<Point> set = new HashSet<>();
set.add(new Point(1, 2));
set.add(new Point(1, 2));
System.out.println("size = " + set.size());
}
}إصلاح المشكلة
أعد تعريف الطريقتين كلتيهما، واستمدهما من الحقول نفسها. عندها تتشارك الكائنات المتساوية رمز تجزئة واحدًا وتصل إلى الحاوية نفسها.
import java.util.HashSet;
import java.util.Objects;
import java.util.Set;
public class Main {
static class Point {
final int x, y;
Point(int x, int y) { this.x = x; this.y = y; }
@Override public boolean equals(Object o) {
if (!(o instanceof Point p)) return false;
return x == p.x && y == p.y;
}
@Override public int hashCode() { return Objects.hash(x, y); }
}
public static void main(String[] args) {
Set<Point> set = new HashSet<>();
set.add(new Point(1, 2));
set.add(new Point(1, 2));
System.out.println("size = " + set.size());
}
}يجب أن تكون equals انعكاسية
تلتزم equals() الصحيحة بأربع خصائص. أولها الانعكاسية: يجب أن تكون x.equals(x) مساوية لـ true.
قد يبدو هذا بديهيًا، لكن المنطق المخصص قد يخرقه إذا نسيت معالجة حالة المرجع نفسه.
public class Main {
public static void main(String[] args) {
String x = "java";
System.out.println(x.equals(x));
}
}التناظر والتعدي
قاعدتان إضافيتان:
- التناظر: إذا كانت
a.equals(b)صحيحة، فيجب أن تكونb.equals(a)صحيحة أيضًا. - التعدي: إذا كانت
a.equals(b)وb.equals(c)صحيحتين، فيجب أن تكونa.equals(c)صحيحة.
غالبًا ما يؤدي خلط الأنواع ضمن تسلسل هرمي للفئات إلى كسر التناظر، لذا قارن الفئات بعناية.
public class Main {
public static void main(String[] args) {
String a = "x", b = "x", c = "x";
boolean symmetric = a.equals(b) == b.equals(a);
boolean transitive = a.equals(b) && b.equals(c) && a.equals(c);
System.out.println("symmetric: " + symmetric);
System.out.println("transitive: " + transitive);
}
}الاتساق
القاعدة الرابعة هي الاتساق: تعيد الاستدعاءات المتكررة النتيجة نفسها ما دامت الكائنات لم تتغير.
لهذا السبب، ينبغي أن تستند equals وhashCode إلى حقول غير قابلة للتغيير. فتغيير حقل مستخدم في hashCode بعد الإدراج يؤدي إلى إفساد الخريطة.
import java.util.Objects;
public class Main {
public static void main(String[] args) {
// Immutable record gives consistent equals/hashCode automatically
record Id(int value) {}
Id id = new Id(7);
System.out.println(Objects.equals(id, new Id(7)));
System.out.println(id.hashCode() == new Id(7).hashCode());
}
}تتولى Records ذلك عنك
ينشئ Java record تلقائيًا الدالتين equals() وhashCode() اعتمادًا على جميع مكوّناته.
بالنسبة إلى البيانات الشبيهة بالقيم، يُفضّل استخدام Records. فعندها يتحقق العقد تلقائيًا ويظل متسقًا.
import java.util.HashSet;
import java.util.Set;
public class Main {
record Point(int x, int y) {}
public static void main(String[] args) {
Set<Point> set = new HashSet<>();
set.add(new Point(1, 2));
set.add(new Point(1, 2));
System.out.println("size = " + set.size());
}
}فخ المفتاح القابل للتغيير
إذا غيّرت حقلًا مستخدمًا في hashCode بعد وضع الكائن في خريطة، فلن تتمكن من العثور عليه بعد ذلك.
تبحث الخريطة في الحاوية القديمة، بينما يُجزّأ الكائن الآن إلى حاوية جديدة. ويصبح الإدخال شبحًا ضائعًا.
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
public class Main {
static class Box {
int id;
Box(int id) { this.id = id; }
@Override public boolean equals(Object o) {
return o instanceof Box b && b.id == id;
}
@Override public int hashCode() { return Objects.hash(id); }
}
public static void main(String[] args) {
Map<Box, String> m = new HashMap<>();
Box key = new Box(1);
m.put(key, "value");
key.id = 99; // mutated after insertion
System.out.println(m.get(key));
}
}اختبار سريع
اختبر مدى استيعابك للعقد.
مراجعة
لقد تعلّمت عقد equals/hashCode:
- يجب أن تمتلك الكائنات المتساوية رموز تجزئة متساوية.
- يجب أن تكون equals انعكاسية ومتناظرة ومتعدية ومتسقة.
- أسند الطريقتين كلتيهما إلى الحقول غير القابلة للتغيير نفسها.
- تنشئ Records تطبيقات صحيحة تلقائيًا.
بعد ذلك، ستكتب hashCode صحيحًا بنفسك.
import java.util.Objects;
public class Main {
public static void main(String[] args) {
System.out.println("Contract recap: " + Objects.hash(1, 2, 3));
}
}الأسئلة الشائعة
هل درس «عقد equals وhashCode» مجاني؟
نعم — نص درس «عقد equals وhashCode» كامل متاح مجاناً هنا على الويب. لتمرينه بشكل تفاعلي (محرر أكواد مدمج ومدرس ذكاء اصطناعي متاح 24/7) وفتح باقي دورة Java Academy، انتقل إلى CoddyKit PRO. تتضمن دورة Java Academy 4 دروس في المجموع.
ماذا ستتعلم في «عقد equals وhashCode»؟
سبب وجوب توافقهما تتمرن على Java Academy مع أكواد عملية تشغلها مباشرة في المتصفح، ومدرس ذكاء اصطناعي متاح 24/7 يجيب على أسئلتك أثناء عملك.
هل أحتاج إلى خبرة سابقة لأبدأ Java Academy؟
لا تُشترط خبرة سابقة. Java Academy على CoddyKit منظم للمبتدئين حتى المتقدمين، لذا يمكنك البدء من هنا أو من البداية والتقدم بسرعتك الخاصة. هذا هو الدرس 2 من أصل 4.
كم من الوقت يستغرق درس «عقد equals وhashCode»؟
معظم دروس CoddyKit تستغرق حوالي 5–10 دقائق. كل منها موجز وتفاعلي، لذا تحرز تقدماً مستمراً وتستأنف من حيث توقفت عبر الويب والتطبيق.
هل يمكنني كتابة وتشغيل أكواد في درس Java Academy هذا؟
نعم. كل درس في Java Academy يتضمن محرر أكواد مدمج، لذا تكتب وتشغل أكواداً حقيقية مباشرة في متصفحك وتحصل على تعليقات فورية من الذكاء الاصطناعي — بدون إعداد محلي.
جميع الدروس في هذه الدورة
- آلية عمل HashMap
- عقد equals وhashCode
- تنفيذ hashCode
- تحويل الأشجار والأداء