تحويل الأشجار والأداء
كيفية تعامل Java 8+ مع التصادمات
تحويل الأشجار والأداء درس مجاني في Java Academy على CoddyKit. هذا هو الدرس 4 من أصل 4. يمكنك قراءة الدرس كاملاً أدناه مجاناً — ثم تمرن عليه مباشرة في المتصفح باستخدام محرر أكواد مدمج ومدرس ذكاء اصطناعي متاح 24/7. هذا الدرس جزء من مسار التعلم في Java Academy، وتقدمك يتزامن عبر الويب وتطبيق CoddyKit. تتضمن دورة Java Academy 4 دروس في المجموع.
مشكلة التصادم
قبل Java 8، كانت الحاوية التي تحتوي على تصادمات كثيرة تتحول إلى قائمة مرتبطة طويلة. وكان البحث في تلك الحاوية يتدهور إلى O(n).
كان بإمكان مهاجم استغلال ذلك باستخدام مفاتيح مُعدّة خصيصًا للتسبب في حجب الخدمة، إذ تتجزأ جميعها إلى حاوية واحدة.
public class Main {
public static void main(String[] args) {
// All these strings can be made to collide in one bucket
System.out.println("FB".hashCode() == "Ea".hashCode());
}
}التحويل إلى شجرة في Java 8
أضافت Java 8 ميزة التحويل إلى شجرة. فعندما تحتوي حاوية واحدة على إدخالات كثيرة جدًا، تتحول القائمة المرتبطة إلى شجرة حمراء-سوداء متوازنة.
وعندها يصبح البحث في تلك الحاوية O(log n) بدلًا من O(n).
import java.util.HashMap;
import java.util.Map;
public class Main {
public static void main(String[] args) {
Map<Integer, Integer> m = new HashMap<>();
for (int i = 0; i < 1000; i++) m.put(i, i);
System.out.println("Lookups stay fast: " + m.get(742));
}
}الحد الفاصل: TREEIFY_THRESHOLD
الثابت TREEIFY_THRESHOLD قيمته 8. تتحول الحاوية إلى شجرة عندما تصل إلى 8 إدخالات.
لكن هناك شرطًا ثانيًا: يجب أن يكون حجم الجدول أيضًا على الأقل MIN_TREEIFY_CAPACITY (64)، وإلا فستغيّر الخريطة حجمها بدلًا من ذلك.
public class Main {
public static void main(String[] args) {
int TREEIFY_THRESHOLD = 8;
int MIN_TREEIFY_CAPACITY = 64;
System.out.println("Treeify when bucket size >= " + TREEIFY_THRESHOLD);
System.out.println("...and table capacity >= " + MIN_TREEIFY_CAPACITY);
}
}غيّر الحجم أولًا، ثم حوّل إلى شجرة
إذا تجاوزت حاوية سعتها بينما كان الجدول لا يزال صغيرًا، أي أقل من 64، فإن HashMap تغيّر حجم الجدول أولًا.
عادةً ما تؤدي عملية تغيير الحجم إلى إعادة توزيع الإدخالات وإزالة نقطة الاختناق، لذلك لا يُستخدم التحويل إلى شجرة إلا كحل أخير لتوزيعات التجزئة السيئة فعلًا.
import java.util.HashMap;
import java.util.Map;
public class Main {
public static void main(String[] args) {
Map<Integer, Integer> m = new HashMap<>(16);
for (int i = 0; i < 50; i++) m.put(i, i);
// Many resizes happened before any treeify would
System.out.println("size = " + m.size());
}
}التحويل من شجرة
الأشجار ليست دائمة. فإذا أدت عمليات الحذف إلى تقليص حاوية إلى أقل من UNTREEIFY_THRESHOLD (6)، تعود الشجرة إلى قائمة مرتبطة.
تمنع الفجوة بين 8، حد التحويل إلى شجرة، و6، حد التحويل منها، التبديل المتكرر عند الحد الفاصل.
public class Main {
public static void main(String[] args) {
System.out.println("TREEIFY_THRESHOLD = 8");
System.out.println("UNTREEIFY_THRESHOLD = 6");
System.out.println("Gap prevents flip-flopping at the edge");
}
}تحتاج الأشجار إلى Comparable أو ترتيب الهوية
يجب أن ترتّب الشجرة الحمراء-السوداء إدخالاتها. تقارن HashMap رموز التجزئة أولًا؛ وعند التعادل تستخدم Comparable إذا كانت المفاتيح تطبّقه، وإلا فتستخدم كسر تعادل ثابتًا يعتمد على أسماء الفئات والهوية.
توفر المفاتيح التي تطبّق Comparable، مثل String أو Integer، أوضح ترتيب للشجرة.
public class Main {
public static void main(String[] args) {
System.out.println("String is Comparable: " + ("a" instanceof Comparable));
System.out.println("Integer is Comparable: " + (Integer.valueOf(1) instanceof Comparable));
}
}الأثر العملي
بالنسبة إلى معظم البرامج الحقيقية ذات رموز التجزئة الجيدة، لن ترى التحويل إلى شجرة مطلقًا. إذ تبقى الحاويات قصيرة.
التحويل إلى شجرة شبكة أمان تحدّ من أسوأ حالة للبحث إلى O(log n)، حتى عندما تكون التجزئة سيئة أو عدائية.
import java.util.HashMap;
import java.util.Map;
public class Main {
public static void main(String[] args) {
Map<String, Integer> m = new HashMap<>();
m.put("alpha", 1);
m.put("beta", 2);
m.put("gamma", 3);
// Tiny buckets, plain linked lists, no trees needed
System.out.println(m.get("beta"));
}
}يفرض hashCode الثابت التحويل إلى أشجار
إذا أعدت hashCode ثابتًا عمدًا، فسيصل كل مفتاح إلى حاوية واحدة. وعندما تكون السعة 64 أو أكثر، تتحول تلك الحاوية إلى شجرة.
يوضح ذلك شبكة الأمان، لكنه مؤشر على تصميم سيئ. أصلح hashCode بدلًا من ذلك.
import java.util.HashMap;
import java.util.Map;
public class Main {
static class Bad implements Comparable<Bad> {
final int v;
Bad(int v) { this.v = v; }
@Override public int hashCode() { return 1; } // forces collisions
@Override public boolean equals(Object o) { return o instanceof Bad b && b.v == v; }
@Override public int compareTo(Bad o) { return Integer.compare(v, o.v); }
}
public static void main(String[] args) {
Map<Bad, Integer> m = new HashMap<>();
for (int i = 0; i < 100; i++) m.put(new Bad(i), i);
System.out.println("All in one bucket, still works: " + m.get(new Bad(50)));
}
}تكلفة الأشجار من الذاكرة
تكون عقد الأشجار أكبر من عقد القوائم المرتبطة العادية، لأنها تخزّن مراجع إلى الأصل واليسار واليمين واللون.
وهذا سبب آخر يجعل التحويل إلى شجرة حلًا احتياطيًا لا الوضع الافتراضي: فالأشجار تستبدل الذاكرة بالسرعة في أسوأ الحالات.
public class Main {
public static void main(String[] args) {
System.out.println("Node: hash, key, value, next");
System.out.println("TreeNode: + parent, left, right, prev, red flag");
System.out.println("=> trees cost more memory per entry");
}
}كيفية تجنّب التحويل إلى شجرة
يكاد لا يكون من المرغوب فيه الاعتماد على التحويل إلى شجرة. تجنّبه باتباع ما يلي:
- كتابة
hashCode()جيد التوزيع. - استخدام الأنواع المدمجة أو Records كمفاتيح.
- تحديد حجم الخريطة مسبقًا لتقليل التصادمات.
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
public class Main {
record Key(int a, int b) {}
public static void main(String[] args) {
Map<Key, Integer> m = new HashMap<>(256);
for (int i = 0; i < 200; i++) m.put(new Key(i, i * 31), i);
System.out.println("Even distribution, fast lookups: " + m.get(new Key(10, 310)));
}
}ملخص الأداء
تكاليف عمليات HashMap:
- تجزئة جيدة: O(1) في المتوسط.
- حاوية مرتبطة: O(n) لكل حاوية في أسوأ الحالات.
- حاوية محوّلة إلى شجرة: O(log n) لكل حاوية.
يحدّ التحويل إلى شجرة من أسوأ الحالات، لكن hashCode الجيد يبقيك ضمن O(1).
import java.util.HashMap;
import java.util.Map;
public class Main {
public static void main(String[] args) {
Map<Integer, Integer> m = new HashMap<>(1 << 14);
for (int i = 0; i < 10000; i++) m.put(i, i);
System.out.println("10k entries, O(1) get: " + m.get(9999));
}
}اختبار سريع
اختبر معرفتك بالتحويل إلى شجرة.
مراجعة
لقد تعلّمت كيفية تعامل HashMap الحديثة مع التصادمات:
- تتحول الحاويات إلى أشجار عند 8 إدخالات عندما تكون السعة 64 على الأقل.
- توفر الأشجار بحثًا في أسوأ الحالات بتعقيد O(log n).
- تعود الحاويات إلى قوائم عند أقل من 6 إدخالات.
- يعني hashCode الجيد أنك نادرًا ما ستفعّل شبكة الأمان هذه.
لقد أتممت دورة بنية HashMap الداخلية.
public class Main {
public static void main(String[] args) {
System.out.println("Treeification course complete");
}
}الأسئلة الشائعة
هل درس «تحويل الأشجار والأداء» مجاني؟
نعم — نص درس «تحويل الأشجار والأداء» كامل متاح مجاناً هنا على الويب. لتمرينه بشكل تفاعلي (محرر أكواد مدمج ومدرس ذكاء اصطناعي متاح 24/7) وفتح باقي دورة Java Academy، انتقل إلى CoddyKit PRO. تتضمن دورة Java Academy 4 دروس في المجموع.
ماذا ستتعلم في «تحويل الأشجار والأداء»؟
كيفية تعامل Java 8+ مع التصادمات تتمرن على Java Academy مع أكواد عملية تشغلها مباشرة في المتصفح، ومدرس ذكاء اصطناعي متاح 24/7 يجيب على أسئلتك أثناء عملك.
هل أحتاج إلى خبرة سابقة لأبدأ Java Academy؟
لا تُشترط خبرة سابقة. Java Academy على CoddyKit منظم للمبتدئين حتى المتقدمين، لذا يمكنك البدء من هنا أو من البداية والتقدم بسرعتك الخاصة. هذا هو الدرس 4 من أصل 4.
كم من الوقت يستغرق درس «تحويل الأشجار والأداء»؟
معظم دروس CoddyKit تستغرق حوالي 5–10 دقائق. كل منها موجز وتفاعلي، لذا تحرز تقدماً مستمراً وتستأنف من حيث توقفت عبر الويب والتطبيق.
هل يمكنني كتابة وتشغيل أكواد في درس Java Academy هذا؟
نعم. كل درس في Java Academy يتضمن محرر أكواد مدمج، لذا تكتب وتشغل أكواداً حقيقية مباشرة في متصفحك وتحصل على تعليقات فورية من الذكاء الاصطناعي — بدون إعداد محلي.
جميع الدروس في هذه الدورة
- آلية عمل HashMap
- عقد equals وhashCode
- تنفيذ hashCode
- تحويل الأشجار والأداء