التعدادات والسجلّات والأنواع المختومة

التعدادات المتقدمة

15 دقيقة الدرس 3 من 13

التعدادات المتقدمة

في الدرسين السابقين تعرّفت على صياغة التعدادات وكيفية إرفاق الحقول والمُنشئات والدوال بالثوابت. يذهب هذا الدرس أبعد من ذلك في ثلاثة محاور تُميّز مبرمج Java المتوسط عن المبتدئ: أجسام الدوال الخاصة بكل ثابت، ومجموعتا EnumSet وEnumMap المتخصّصتان. وعلى طول الطريق ستفهم آلية عمل values() وvalueOf() من الداخل.

أجسام الدوال الخاصة بكل ثابت

يستطيع كل ثابت في التعداد أن يُعيد تعريف دالة مجرّدة (أو غير مجرّدة) مُعلَنة في جسم التعداد. تُسمّى هذه التقنية الجسم الخاص بكل ثابت، وتجعل كل ثابت يحمل تنفيذه الخاص بدلًا من تكرار عبارات switch الضخمة في أكواد الاستدعاء.

انظر مثال نظام معالجة الطلبات حيث تحسب كل شركة شحن تكلفتها بأسلوب مختلف:

public enum Carrier { STANDARD { @Override public double shippingCost(double weightKg) { return 2.00 + weightKg * 0.50; } }, EXPRESS { @Override public double shippingCost(double weightKg) { return 5.00 + weightKg * 1.20; } }, OVERNIGHT { @Override public double shippingCost(double weightKg) { return 12.00 + weightKg * 2.50; } }; // كل ثابت مُلزَم بتنفيذ هذه الدالة لأنها مجرّدة. public abstract double shippingCost(double weightKg); }

الاستخدام واضح — المُستدعي لا يحتاج أن يعرف مع أي شركة شحن يتعامل:

Carrier carrier = Carrier.EXPRESS; double cost = carrier.shippingCost(3.5); // 5.00 + 3.5 * 1.20 = 9.20 System.out.println("Cost: " + cost);
لماذا لا نستخدم switch؟ عبارة switch على تعداد داخل دالة مساعدة هشّة: إضافة ثابت جديد يعني البحث عن كل switch وتحديثه. أما مع الأجسام الخاصة بالثوابت فتُضيف الثابت وتنفيذه معًا — يجبرك المترجم على توفير الدالة فلا يُنسى شيء.

يمكنك أيضًا إعادة تعريف دالة غير مجرّدة في بعض الثوابت فقط، وترك البقية تعتمد على التنفيذ الافتراضي:

public enum LogLevel { DEBUG, INFO, WARN, ERROR { @Override public void log(String message) { // أضف علامة واضحة لكل خطأ. System.err.println("!!! ERROR !!! " + message); } }; // التنفيذ الافتراضي يستخدمه DEBUG وINFO وWARN. public void log(String message) { System.out.println("[" + name() + "] " + message); } }

values() و valueOf()

يُولّد المترجم بصمت دالتَين ثابتتَين على كل تعداد:

  • values() — تُعيد مصفوفة جديدة تحتوي جميع الثوابت بترتيب الإعلان. استخدمها للتكرار.
  • valueOf(String name) — تجد ثابتًا باسمه الدقيق؛ تُطلق IllegalArgumentException إن لم يتطابق الاسم.
// التكرار على جميع الثوابت for (Carrier c : Carrier.values()) { System.out.printf("%s: %.2f%n", c, c.shippingCost(1.0)); } // التحليل من إدخال المستخدم أو ملفات الضبط String input = "EXPRESS"; Carrier chosen = Carrier.valueOf(input); // يُعيد Carrier.EXPRESS
valueOf حسّاس لحالة الأحرف. Carrier.valueOf("express") تُطلق IllegalArgumentException في وقت التشغيل. دائمًا حوّل المدخل أولًا: Carrier.valueOf(input.toUpperCase())، أو لفّ الاستدعاء في try-catch.

يرث كل تعداد أيضًا name() (تُعيد اسم الثابت المُعلَن كـ String) وordinal() (تُعيد موضعه بدءًا من الصفر). اعتمد على name() للعرض والتسلسل؛ وكن حذرًا مع ordinal() لأن إدراج ثابت في المنتصف يُغيّر جميع الأرقام التالية.

EnumSet — مجموعات البتات الفعّالة لثوابت التعداد

EnumSet<E extends Enum<E>> هو تنفيذ لواجهة Set محسَّن خصيصًا لثوابت التعداد. داخليًا يستخدم قناعًا بِتيًا من نوع long واحد (أو اثنين للتعدادات التي تتجاوز 64 ثابتًا)، ما يجعل كل عملية — add وcontains وremove — تحقيقًا بِتيًا بزمن O(1) بدلًا من بحث في جدول التشتيت.

import java.util.EnumSet; public enum Permission { READ, WRITE, DELETE, ADMIN } // --- إنشاء المجموعات --- EnumSet<Permission> readOnly = EnumSet.of(Permission.READ); EnumSet<Permission> allPerms = EnumSet.allOf(Permission.class); EnumSet<Permission> noPerms = EnumSet.noneOf(Permission.class); EnumSet<Permission> nonAdmin = EnumSet.complementOf(EnumSet.of(Permission.ADMIN)); // --- فحص الصلاحيات --- boolean canDelete = readOnly.contains(Permission.DELETE); // false boolean canRead = allPerms.contains(Permission.READ); // true // --- العمليات الجماعية مطابقة لـ Set --- noPerms.add(Permission.READ); noPerms.addAll(EnumSet.of(Permission.WRITE, Permission.DELETE)); System.out.println(noPerms); // [READ, WRITE, DELETE]
فضّل EnumSet على HashSet متى كانت عناصر مجموعتك ثوابت تعداد. إنّه أسرع وأقل استهلاكًا للذاكرة، وخرج toString() يكون دائمًا بترتيب الإعلان مما يُسهّل تتبّع الأخطاء.

EnumMap — خريطة مرتّبة وسريعة مفاتيحها ثوابت تعداد

EnumMap<K extends Enum<K>, V> هو رفيق EnumSet. يخزّن القيم في مصفوفة داخلية مُفهرَسة بـ ordinal() لكل ثابت، مما يمنح وصولًا بزمن O(1) مع عبء يكاد يكون صفرًا ويكفل دائمًا التكرار بترتيب الإعلان.

import java.util.EnumMap; import java.util.Map; public enum Day { MON, TUE, WED, THU, FRI, SAT, SUN } EnumMap<Day, String> schedule = new EnumMap<>(Day.class); schedule.put(Day.MON, "Team standup at 9am"); schedule.put(Day.WED, "Design review at 2pm"); schedule.put(Day.FRI, "Sprint retro at 4pm"); // التكرار يكون بترتيب MON → SUN، وليس ترتيب الإدراج. for (Map.Entry<Day, String> entry : schedule.entrySet()) { System.out.println(entry.getKey() + ": " + entry.getValue()); } // البحث String fridayTask = schedule.getOrDefault(Day.FRI, "No meetings"); System.out.println(fridayTask); // Sprint retro at 4pm

نمط شائع في بيئات العمل الحقيقية هو الجمع بين EnumMap وأجسام الدوال الخاصة بالثوابت. خزّن استراتيجية (دالة lambda أو مرجع دالة) لكل ثابت، ثم اعمل التوزيع عبر الخريطة بدلًا من switch:

import java.util.EnumMap; import java.util.function.DoubleUnaryOperator; enum TaxBand { BASIC, HIGHER, ADDITIONAL } EnumMap<TaxBand, DoubleUnaryOperator> taxCalc = new EnumMap<>(TaxBand.class); taxCalc.put(TaxBand.BASIC, income -> income * 0.20); taxCalc.put(TaxBand.HIGHER, income -> income * 0.40); taxCalc.put(TaxBand.ADDITIONAL, income -> income * 0.45); double tax = taxCalc.get(TaxBand.HIGHER).applyAsDouble(50_000); System.out.println("Tax owed: " + tax); // 20000.0

الخلاصة

التعدادات المتقدمة أكثر من مجرد ثوابت مسمّاة — إنّها نمط كائني الطابع خفيف الوزن. بالجمع بين أجسام الدوال الخاصة بالثوابت مع EnumSet وEnumMap، يمكنك نمذجة قواعد المجال بوضوح، وتجنّب عبارات switch الهشّة المبعثرة في قاعدة الكود، والاستفادة من خصائص الأداء في المجموعات المحسَّنة للتعدادات. في الدرس القادم ستتعرّف على Records، ميزة Java الحديثة الأخرى التي تُكمل التعدادات.