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

السجلات: التخصيص والقيود

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

السجلات: التخصيص والقيود

تناولت الدرسان السابقان بناء الجملة الخاص بالسجلات والمكوّنات المُولَّدة تلقائيًا. تعلّمت أن record Point(int x, int y) يمنحك مُنشئًا، وواصلات وصول، وequals، وhashCode، وtoString مجانًا. يتعمّق هذا الدرس أكثر: ما الذي يمكنك إضافته بنفسك للسجل، وأين يضع JVM الحدود الصارمة؟

إضافة توابع النسخة

السجل هو صفٌّ كامل المواصفات. يمكنك إضافة أي عدد من توابع النسخة — وهي طرق عرض للقراءة فقط أو حسابات مبنية على قيم المكوّنات.

public record Money(long cents, String currency) { // خاصية محسوبة — لا حاجة لحقل إضافي public double inMajorUnit() { return cents / 100.0; } // تابع استعلام public boolean isZero() { return cents == 0; } // عملية حسابية تُعيد كائن Money جديدًا (السجلات غير قابلة للتعديل) public Money add(Money other) { if (!currency.equals(other.currency)) { throw new IllegalArgumentException("Currency mismatch"); } return new Money(cents + other.cents, currency); } }

لاحظ أن add لا تُعدّل this — بل تُعيد نسخة جديدة. هذا هو النمط الأسلوبي الصحيح لأنواع القيم غير القابلة للتعديل.

المصانع الساكنة (Static Factory Methods)

المُنشئات غير مرنة: تستدعيها بقيم خام وهذا كل شيء. المصانع الساكنة هي نمط Java الكلاسيكي الذي يتيح لك مُنشئات مسمّاة، والتحقق من الصحة، والتخزين المؤقت، والنية الواضحة.

public record Money(long cents, String currency) { // مُنشئ مُدمَج — التحقق قبل تعيين الحقول public Money { if (cents < 0) throw new IllegalArgumentException("Negative amount"); if (currency == null || currency.isBlank()) throw new IllegalArgumentException("Currency required"); currency = currency.toUpperCase(); // توحيد الصياغة داخل المُنشئ المُدمَج } // مصنع ساكن: بناء من نص عشري public static Money of(String amount, String currency) { // "9.99" -> 999 سنت long c = Math.round(Double.parseDouble(amount) * 100); return new Money(c, currency); } // ثابت الصفر المسمّى عبر مصنع public static Money zero(String currency) { return new Money(0, currency); } }
فضّل المصانع الساكنة على المُنشئات الإضافية. تدعم السجلات مُنشئًا واحدًا فقط (أو صيغته المُدمَجة). تُتيح المصانع الساكنة التعبير عن النية في الاسم — Money.of("9.99", "USD") أوضح بكثير من new Money(999L, "USD").

الحقول الساكنة وثوابت الأدوات المساعدة

يمكن للسجلات امتلاك حقول static. القيد على الحقول الإضافية يقتصر على حقول النسخة فقط، لأن الحقل الإضافي للنسخة سيُخلّ بضمان أن قيمة السجل تُوصَف بالكامل عبر مكوّناته.

public record Coordinates(double lat, double lon) { // ثابت ساكن — مسموح به private static final double EARTH_RADIUS_KM = 6371.0; // أداة مساعدة ساكنة على النوع public static Coordinates origin() { return new Coordinates(0.0, 0.0); } public double distanceTo(Coordinates other) { // معادلة هافرسين (مبسّطة) double dLat = Math.toRadians(other.lat - lat); double dLon = Math.toRadians(other.lon - lon); double a = Math.sin(dLat / 2) * Math.sin(dLat / 2) + Math.cos(Math.toRadians(lat)) * Math.cos(Math.toRadians(other.lat)) * Math.sin(dLon / 2) * Math.sin(dLon / 2); return EARTH_RADIUS_KM * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); } }

تطبيق الواجهات

يمكن للسجلات تطبيق الواجهات — وهذه نقطة توسّع جوهرية. غالبًا ما تجمعها مع Comparable، أو واجهات توصيف مخصّصة، أو تسلسلات هرمية من الواجهات المُختومة.

public record Money(long cents, String currency) implements Comparable<Money> { @Override public int compareTo(Money other) { if (!currency.equals(other.currency)) throw new IllegalArgumentException("Cannot compare different currencies"); return Long.compare(cents, other.cents); } } // الاستخدام List<Money> prices = List.of( new Money(1500, "USD"), new Money(200, "USD"), new Money(900, "USD") ); List<Money> sorted = prices.stream().sorted().toList(); // النتيجة: [Money[cents=200, ...], Money[cents=900, ...], Money[cents=1500, ...]]

ما الذي لا تستطيع السجلات فعله

يأتي ضمان عدم القابلية للتعديل مع قيود صارمة:

  • لا حقول نسخة إضافية. يجب أن تُعلَن كل حالة كمكوّن في الترويسة. هذا مقصود — هوية السجل هي مكوّناته.
  • لا يمكن للسجلات توسيع صفّ. تمتد ضمنيًا من java.lang.Record ولا يمكنها توسيع أي شيء آخر، لأن JVM يجب أن يكون قادرًا على ضمان عقد المكوّنات.
  • لا يمكن أن تكون السجلات مجرّدة. السجل المجرّد سيُقوّض ضمان القيمة غير القابلة للتعديل والمختومة.
  • لا يمكن إزالة واصلات الوصول للمكوّنات. يمكنك تجاوزها (مثلًا لإضافة نسخ دفاعية لأنواع المكوّنات القابلة للتعديل)، لكن لا يمكنك إخفاؤها أو حذفها.
  • لا يمكنك جعل السجل قابلًا للتعديل. حقول المكوّنات دائمًا private final. أي محاولة لإعادة تعيينها داخل الصفّ تُفضي إلى خطأ في وقت الترجمة.
المكوّنات القابلة للتعديل فخٌّ. إن احتوى السجل على كائن قابل للتعديل — مثل record Snapshot(List<String> items) — فالمرجع نهائي لكن القائمة نفسها ليست كذلك. لا يزال بإمكان المستدعي تعديلها عبر snapshot.items().add("oops"). الحل: أنشئ نسخة دفاعية في المُنشئ المُدمَج: items = List.copyOf(items);
السجلات مقابل JavaBeans. سؤال مقابلات شائع: لماذا لا تتّبع السجلات اصطلاح واصلات getX()؟ تستخدم السجلات الصيغة البسيطة x() لأنها تُمثّل حاملات بيانات، لا كائنات قابلة للتعديل. الأطر البرمجية التي تتطلّب واصلات بنمط JavaBean (Jackson القديم وبعض موفّري JPA) قد تحتاج إلى ضبط صريح أو مُسلسِل مخصّص.

تجاوز التوابع المُولَّدة

يمكن تجاوز جميع التوابع المُولَّدة الأربعة — المُنشئ الأساسي، وequals، وhashCode، وtoString:

public record Temperature(double value, String unit) { // تجاوز toString لصيغة أكثر ودّية @Override public String toString() { return String.format("%.1f %s", value, unit); } // تجاوز equals للسماح بالمقارنة بين وحدات مختلفة (مبسّط) @Override public boolean equals(Object obj) { if (this == obj) return true; if (!(obj instanceof Temperature other)) return false; double thisC = toCelsius(); double otherC = other.toCelsius(); return Math.abs(thisC - otherC) < 0.001; } @Override public int hashCode() { return Double.hashCode(Math.round(toCelsius() * 1000) / 1000.0); } private double toCelsius() { return switch (unit) { case "C" -> value; case "F" -> (value - 32) * 5.0 / 9.0; case "K" -> value - 273.15; default -> throw new IllegalArgumentException("Unknown unit: " + unit); }; } }

الخلاصة

السجلات موجزة لكنها ليست جامدة. يمكنك إثراؤها بتوابع النسخة، والمصانع الساكنة، والثوابت الساكنة، وتطبيقات الواجهات. القيود الصارمة — لا حقول نسخة إضافية، لا وراثة من صفوف، لا قابلية للتعديل — هي مزايا لا عيوب: تضمن أن قيمة السجل تُوصَف دائمًا بشكل كامل وشفّاف عبر المكوّنات التي أعلنتها في ترويسته. حين تحتاج إلى مرونة أكبر مما تتيحه السجلات، فالصفّ العادي غير القابل للتعديل (مع حقول private final وبدون setters) هو الأداة الصحيحة.