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

التعدادات مع الحقول والدوال

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

التعدادات مع الحقول والدوال

التعداد البسيط الذي تكون ثوابته مجرّد أسماء مفيد، لكنّه لا يكشف إلا عن القليل. في Java، كل enum هو في حقيقته كلاس كامل — وهذا يعني أن كل ثابت يمكنه حمل بياناته الخاصة وتعريض سلوكه الخاص. يوضح هذا الدرس كيف تمنح ثوابت التعداد منشئات وحقولًا ودوالًا، ويشرح لماذا يكون ذلك أفضل في الغالب من مصفوفة موازية أو كلاس مساعد مليء بخرائط ثابتة.

لماذا نربط البيانات بالثوابت؟

تخيّل أنّك تُنمذج كواكب المجموعة الشمسية. يمكنك تخزين كتلة كل كوكب ونصف قطره في HashMap منفصل، لكن ذلك يعني أن كل كود يحتاج كتلة الكوكب سيضطر للبحث عنها بالمفتاح وخطر الحصول على null. التصميم الأفضل هو أن يحمل الثابت نفسه البيانات — فتصبح البيانات والثابت كتلة واحدة لا تنفصل ودائمًا متزامنة.

منشئات التعداد وحقوله

أضِف منشئًا إلى enum تمامًا كما تفعل مع كلاس عادي — لكن انتبه لقاعدتين: المنشئ خاص دائمًا (تُطبّق Java ذلك)، وعلى كل ثابت تزويد الوسائط المطلوبة بين قوسين مباشرةً بعد اسمه.

public enum Planet { MERCURY(3.303e+23, 2.4397e6), VENUS (4.869e+24, 6.0518e6), EARTH (5.976e+24, 6.37814e6), MARS (6.421e+23, 3.3972e6); private final double mass; // بالكيلوغرام private final double radius; // بالمتر Planet(double mass, double radius) { this.mass = mass; this.radius = radius; } public double mass() { return mass; } public double radius() { return radius; } static final double G = 6.67300E-11; public double surfaceGravity() { return G * mass / (radius * radius); } public double surfaceWeight(double otherMass) { return otherMass * surfaceGravity(); } }

يمكنك الآن كتابة كود معبّر بدون أي بنى بيانات خارجية:

double earthWeight = 75.0; // كيلوغرام double mass = earthWeight / Planet.EARTH.surfaceGravity(); for (Planet p : Planet.values()) { System.out.printf("وزنك على %s هو %6.2f%n", p, p.surfaceWeight(mass)); }
منشئات التعداد خاصة ضمنيًا. كتابة private Planet(...) مقبولة لكنّها زائدة. لا يمكنك جعل منشئ التعداد public أو protected — تحظر اللغة ذلك لأن المُصرِّف وحده هو من يُنشئ نُسَخ التعداد.

التعدادات بديلًا لثوابت int

استخدام شائع جدًا هو رموز حالة HTTP أو رموز أخطاء قواعد البيانات التي تحتاج قيمة رقمية ورسالة مقروءة معًا:

public enum HttpStatus { OK(200, "OK"), CREATED(201, "Created"), BAD_REQUEST(400, "Bad Request"), UNAUTHORIZED(401, "Unauthorized"), NOT_FOUND(404, "Not Found"), INTERNAL_SERVER_ERROR(500, "Internal Server Error"); private final int code; private final String reason; HttpStatus(int code, String reason) { this.code = code; this.reason = reason; } public int code() { return code; } public String reason() { return reason; } @Override public String toString() { return code + " " + reason; } public boolean isSuccess() { return code >= 200 && code < 300; } public boolean isError() { return code >= 400; } }

يصبح الاستخدام مقروءًا وآمن النوع:

HttpStatus status = HttpStatus.NOT_FOUND; System.out.println(status); // 404 Not Found System.out.println(status.isError()); // true if (status == HttpStatus.NOT_FOUND) { System.out.println("المورد مفقود: " + status.reason()); }
تجاوز toString() بشكل هادف. الدالة الافتراضية toString() تُعيد اسم الثابت (مثل "NOT_FOUND"). تجاوزها لإعادة تمثيل ذي معنى — كـ "404 Not Found" — يجعل السجلات ورسائل الخطأ أوضح بكثير دون أي تعقيد في مواضع الاستخدام.

دوال البحث العكسي — إيجاد ثابت من قيمة حقله

حاجة شائعة هي البحث العكسي: بالنظر إلى رقم رمز حالة قادم من استجابة شبكية، تريد الحصول على ثابت HttpStatus المقابل. الأسلوب المعتاد هو دالة مصنع ثابتة على التعداد نفسه:

public static HttpStatus fromCode(int code) { for (HttpStatus s : values()) { if (s.code == code) return s; } throw new IllegalArgumentException("رمز HTTP غير معروف: " + code); }

للمسارات الحساسة للأداء التي تحتوي على ثوابت كثيرة، ابنِ Map في مُهيِّئ ثابت:

import java.util.Map; import java.util.stream.Collectors; import java.util.Arrays; // داخل جسم التعداد: private static final Map<Integer, HttpStatus> BY_CODE = Arrays.stream(values()) .collect(Collectors.toUnmodifiableMap(HttpStatus::code, s -> s)); public static HttpStatus fromCode(int code) { HttpStatus s = BY_CODE.get(code); if (s == null) throw new IllegalArgumentException("رمز غير معروف: " + code); return s; }
الحقول الثابتة في التعداد تتهيأ بعد الثوابت. حقل ثابت يُشير إلى values() يجب أن يظهر بعد قائمة الثوابت في الكود المصدري، وإلا ستُطلق JVM خطأ ExceptionInInitializerError عند بدء التشغيل. ثوابت التعداد دائمًا تتهيأ أولًا.

الدوال المجرّدة — سلوك مختلف لكل ثابت

أحيانًا يحتاج كل ثابت إلى تنفيذ مختلف لنفس العملية. يمكنك الإعلان عن دالة مجرّدة على التعداد وتزويد جسم لكل ثابت بأسلوب يشبه الكلاس المجهول:

public enum Operation { PLUS("+") { @Override public double apply(double x, double y) { return x + y; } }, MINUS("-") { @Override public double apply(double x, double y) { return x - y; } }, TIMES("*") { @Override public double apply(double x, double y) { return x * y; } }, DIVIDE("/") { @Override public double apply(double x, double y) { return x / y; } }; private final String symbol; Operation(String symbol) { this.symbol = symbol; } public abstract double apply(double x, double y); @Override public String toString() { return symbol; } }
for (Operation op : Operation.values()) { System.out.printf("%.1f %s %.1f = %.1f%n", 6.0, op, 2.0, op.apply(6.0, 2.0)); } // 6.0 + 2.0 = 8.0 // 6.0 - 2.0 = 4.0 // 6.0 * 2.0 = 12.0 // 6.0 / 2.0 = 3.0

هذا النمط قوي لأن المُصرِّف يُجبرك على تنفيذ apply لكل ثابت. أضفت عملية جديدة؟ يُخبرك المُصرِّف فورًا إذا نسيت تنفيذ الدالة.

تنفيذ الواجهات

يمكن للتعداد تنفيذ واجهة أو أكثر، وهو مفيد بشكل خاص حين تريد التعامل مع ثوابت التعداد والكائنات العادية بشكل متعدد الأشكال:

public interface Describable { String describe(); } public enum Priority implements Describable { LOW(1), MEDIUM(5), HIGH(10); private final int weight; Priority(int weight) { this.weight = weight; } public int weight() { return weight; } @Override public String describe() { return name() + " (weight=" + weight + ")"; } }
التعدادات لا تستطيع توسيع كلاسات (فهي تُوسِّع ضمنيًا java.lang.Enum)، لكنّها تستطيع تنفيذ أيّ عدد من الواجهات. استخدم هذا حين تريد لمجموعة من الثوابت الاندماج في تصميم متعدد الأشكال قائم دون إدخال هرمية كلاسات منفصلة.

الخلاصة

تعدادات Java هي كلاسات. كل ثابت هو نسخة وحيدة من ذلك الكلاس، تُنشأ مرة واحدة بواسطة JVM. منح الثوابت حقولًا ومنشئًا خاصًا يُجمع البيانات مع الثابت بحيث لا يمكن أن يتفكّك ارتباطها. الدوال على التعداد — بما فيها المجرّدة ذات أجسام خاصة بكل ثابت — تُتيح لكل ثابت تغليف سلوكه الخاص. ينتج عن ذلك كود آمن النوع، مقروء، وسهل التوسيع: إضافة ثابت جديد تُجبرك على تزويد جميع البيانات والسلوك المطلوب، والمُصرِّف يتحقق من عملك.