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

مقدّمة إلى Records

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

مقدّمة إلى Records

كتب كلّ مطوّر جافا في حياته كلاسًا لا غرض منه سوى حمل عدد من القيم وتمريرها — إحداثيتا نقطة، استجابة من واجهة برمجية، نتيجة بحث. كتابة هذا الكلاس بالطريقة التقليدية تعني ملء لوحة المفاتيح بمنشئ، وحقل لكل عنصر، وgetters، وequals()، وhashCode()، وtoString(). هذا نحو 30–50 سطرًا لما هو في جوهره مجرد صف مسمّى. أصبحت السجلات (records) ميزةً دائمة في لغة جافا منذ الإصدار 16، وجاءت لتحلّ هذه المشكلة تحديدًا.

المشكلة التي تحلّها السجلات

لنفكّر في كلاس بسيط يمثّل نقطة ثنائية الأبعاد:

// الطريقة التقليدية — 30+ سطرًا، كلّها شكليات ميكانيكية public final class Point { private final int x; private final int y; public Point(int x, int y) { this.x = x; this.y = y; } public int getX() { return x; } public int getY() { return y; } @Override public boolean equals(Object o) { if (this == o) return true; if (!(o instanceof Point p)) return false; return x == p.x && y == p.y; } @Override public int hashCode() { return Objects.hash(x, y); } @Override public String toString() { return "Point[x=" + x + ", y=" + y + "]"; } }

في كل مرة يتغيّر فيها التصميم تضطرّ لتعديل عدة توابع. القصد الأصلي — "نقطة لها x وy" — يضيع وسط ضجيج التنفيذ.

الكلمة المفتاحية record

يُعلن السجل عن اسم الكلاس ومكوّناته في سطر واحد:

public record Point(int x, int y) { }

هذا السطر الواحد يمنحك كل ما منحه الإصدار ذو الثلاثين سطرًا:

  • حقلان private final، هما x وy.
  • منشئ أساسي (canonical constructor)Point(int x, int y) — يُسنِد كلا الحقلين.
  • توابع وصول x() وy() (لاحظ: بدون بادئة get — وهذا مقصود).
  • equals() قائم على القيمة يقارن جميع المكوّنات.
  • hashCode() متسق مشتق من جميع المكوّنات.
  • toString() مقروء يطبع Point[x=3, y=7].
السجلات تصف البيانات لا السلوك. التحوّل الذهني الأساسي هو أن السجل حامل بيانات شفّاف: واجهته البرمجية هي مكوّناته فحسب. لهذا السبب يولّد المُصرِّف كل شيء من قائمة المكوّنات وحدها.

استخدام السجل

Point origin = new Point(0, 0); Point p = new Point(3, 7); System.out.println(p); // Point[x=3, y=7] System.out.println(p.x()); // 3 System.out.println(p.y()); // 7 System.out.println(origin.equals(new Point(0, 0))); // true System.out.println(p.equals(origin)); // false

لاحظ أن equals قائم على القيمة: كائنان منفصلان من نوع Point بنفس الإحداثيات يُعدّان متساويَين، تمامًا كما تتوقّع من كلاس بيانات.

السجلات ضمنيًا نهائية (final)

الكلاس المُعرَّف كسجل هو final بشكل افتراضي — لا يمكنك التوسّع منه. يمكنك تطبيق واجهات (interfaces)، لكن لا يمكنك إنشاء كلاس فرعي من سجل. هذا ليس قيدًا بل خيارًا تصميميًا مدروسًا يُبقي معنى السجل واضحًا: هويّته تتحدّد كليًا بمكوّناته، والسماح للكلاسات الفرعية بإضافة حالة خفية يكسر هذا الضمان.

// هذا لن يُصرَّف: // public class ColorPoint extends Point { } // خطأ: لا يمكن التوسع من سجل // هذا مقبول تمامًا — تطبيق واجهة: public interface Locatable { int x(); int y(); } public record Point(int x, int y) implements Locatable { }

السجلات غير قابلة للتغيير بحكم التصميم

كل حقل مكوّن هو private final. لا يوجد setters مولَّد. بمجرد إنشاء السجل لا يمكن تغيير حالته. هذه الثبوتية (immutability) إحدى أهم الخصائص التي يجلبها السجل — تجعله آمنًا للمشاركة عبر خيوط التنفيذ (threads) وسهل التفكير فيه.

استخدم السجلات بحرية كمعاملات للتوابع وقيم إرجاع. لأنها غير قابلة للتغيير وتمتلك equals() وhashCode() موثوقَين، تعمل بشكل صحيح كمفاتيح في Map وفي Set وفي المجموعات — دون الحاجة إلى نسخ دفاعية.

مثال ثانٍ: ملخّص استجابة HTTP

public record HttpResponse(int statusCode, String body) { } HttpResponse ok = new HttpResponse(200, "Hello"); HttpResponse err = new HttpResponse(404, "Not found"); System.out.println(ok); // HttpResponse[statusCode=200, body=Hello] System.out.println(ok.statusCode()); // 200 // مساواة قائمة على القيمة var a = new HttpResponse(200, "Hello"); var b = new HttpResponse(200, "Hello"); System.out.println(a.equals(b)); // true

ما السجلات ليست عليه

  • ليست JavaBeans — لا توابع وصول بأسلوب getX()، ولا منشئ بلا معاملات، ولا setters.
  • ليست مجرد اختصار لأي كلاس — استخدم السجلات فقط حين يكون الكلاس حقًا هو بياناته (نتيجة، أمر، إحداثية، نطاق). إن احتجت قابلية التغيير أو الوراثة، استخدم كلاسًا عاديًا.
  • ليست أنواع قيمة (كـ structs في C) — لا تزال كائنات مرجعية مخصَّصة على الكومة (heap) في الـ JVM الحالية.
لا تلجأ إلى السجلات حين تحتاج تغييرًا متحكَّمًا فيه. إن كان تصميمك يستوجب تغيير حقل بعد الإنشاء، فالسجل ليس الأداة المناسبة. الكلاس العادي مع حقول خاصة وsetters صريحة هو الخيار الصحيح هنا.

الخلاصة

السجلات هي إجابة جافا على ضريبة الشكليات في كلاسات البيانات غير قابلة التغيير. الكلمة المفتاحية record تستبدل عشرات الأسطر من الكود الميكانيكي بتعريف واحد يُعبّر بوضوح عمّا يكون عليه النوع. يولّد المُصرِّف المنشئ الأساسي والتوابع الوصولية وequals وhashCode وtoString نيابةً عنك — والنوع الناتج نهائي وثابت وصحيح بحكم البناء. في الدرس القادم ستتعلّم كيف تُضيف توابعك الخاصة إلى سجل وتُخصّص المنشئ المولَّد حين تحتاج التحقق من المدخلات.