مكوّنات السجلات وأساليبها
في الدرس السابق تعرّفت على كيفية الإعلان عن سجل وعلى السبب الذي يجعله يُولّد كثيرًا من الكود النمطي نيابةً عنك. الآن نغوص مستوىً أعمق: كيف يعمل هذا الكود المُولَّد بالضبط، وكيف يمكنك تخصيص البنّاءات، وكيف تُضيف التحقق من الصحة وسلوكًا إضافيًا دون أن تفقد الإيجاز الذي يجعل السجلات قيّمة؟
ما الذي يُولّده السجل تلقائيًا؟
كل مكوّن في السجل — المتغيرات المُعلَنة في الترويسة — يصبح حقلًا خاصًا نهائيًا. ولكل مكوّن يُولّد المُصرِّف أيضًا أسلوب وصول عام يحمل اسم المكوّن تمامًا (بلا بادئة get). مثال:
record Point(double x, double y) {}
تحصل مجانًا على:
public double x() — يُعيد حقل x.
public double y() — يُعيد حقل y.
- بنّاء قانوني
Point(double x, double y) يُعيّن كلا الحقلين.
- أسلوب
equals() وأسلوب hashCode() وأسلوب toString() مبنيّة على المكوّنات.
تسمية أساليب الوصول تختلف عن JavaBeans. أساليب الجلب التقليدية تُسمّى getName(). أساليب وصول السجل تُسمّى name(). لو استدعيت point.getX() على سجل ستحصل على خطأ في التصريف — الأسلوب الصحيح هو point.x().
البنّاء القانوني
البنّاء القانوني هو الذي يأخذ كل مكوّن معاملًا بترتيب الإعلان. يُولَّد بالكامل افتراضيًا. يمكنك الإعلان عنه صراحةً لإضافة منطق — غالبًا للتحقق من الصحة:
record Range(int min, int max) {
Range(int min, int max) { // بنّاء قانوني صريح
if (min > max) {
throw new IllegalArgumentException(
"min (" + min + ") يجب ألا يتجاوز max (" + max + ")");
}
this.min = min;
this.max = max;
}
}
الآن كل طريقة لإنشاء Range تمر عبر هذا البنّاء، فالشرط الثابت يُطبَّق دائمًا:
Range صحيح = new Range(1, 10); // مقبول
Range خاطئ = new Range(10, 1); // يطرح IllegalArgumentException
البنّاء المُدمَج — تحقق أنظف
كتابة البنّاء القانوني صراحةً مُطوَّلة، لأنك تضطر لتكرار قائمة المعاملات وتعيين كل حقل. توفّر سجلات Java صيغة أقصر تُسمّى البنّاء المُدمَج: تحذف قائمة المعاملات كليًا، ويُعيّن المُصرِّف المعاملات (المُعدَّلة ربما) إلى الحقول تلقائيًا في النهاية.
record Range(int min, int max) {
Range { // بنّاء مُدمَج — بلا قائمة معاملات
if (min > max) {
throw new IllegalArgumentException(
"min (" + min + ") يجب ألا يتجاوز max (" + max + ")");
}
// التعيين this.min = min; this.max = max; يحدث تلقائيًا
}
}
داخل البنّاء المُدمَج يمكنك أيضًا إعادة تعيين متغيرات المعاملات قبل تخزينها:
record NormalizedEmail(String address) {
NormalizedEmail {
if (address == null || address.isBlank()) {
throw new IllegalArgumentException("البريد الإلكتروني لا يجوز أن يكون فارغًا");
}
address = address.strip().toLowerCase(); // إعادة تعيين المعامل، ليس this.address
}
}
فضّل البنّاء المُدمَج للتحقق والتطبيع. إنه أقصر وأقل عُرضةً للخطأ (لا خطر نسيان تعيين)، ويعبّر عن النية بوضوح. استخدم البنّاء القانوني الصريح فقط حين تحتاج تحكمًا دقيقًا بما يُخزَّن.
إضافة أساليب النسخة
يمكن للسجلات احتواء أي عدد من أساليب النسخة العادية. لا تستطيع إضافة حالة قابلة للتغيير (لا حقول إضافية غير ساكنة)، لكنها تستطيع احتساب قيم مشتقة من المكوّنات:
record Circle(double radius) {
Circle {
if (radius <= 0) throw new IllegalArgumentException("يجب أن يكون نصف القطر موجبًا");
}
double area() {
return Math.PI * radius * radius;
}
double circumference() {
return 2 * Math.PI * radius;
}
boolean contains(double x, double y) {
return (x * x + y * y) <= (radius * radius);
}
}
استخدامه يبدو طبيعيًا:
Circle c = new Circle(5.0);
System.out.println(c.radius()); // 5.0 — أسلوب الوصول المُولَّد
System.out.println(c.area()); // 78.539...
System.out.println(c.contains(3, 4)); // true (3² + 4² = 25 = 5²)
الأعضاء الساكنة
يمكن للسجلات امتلاك حقول وأساليب ساكنة أيضًا، وهو الأسلوب المعهود لتوفير بنّاءات ذات أسماء دلالية أو ثوابت:
record Money(long cents, String currency) {
static final Money ZERO_USD = new Money(0, "USD");
static Money ofDollars(double dollars) {
return new Money(Math.round(dollars * 100), "USD");
}
Money add(Money other) {
if (!currency.equals(other.currency)) {
throw new IllegalArgumentException("لا يمكن جمع عملتين مختلفتين");
}
return new Money(cents + other.cents, currency);
}
@Override
public String toString() {
return String.format("%s %.2f", currency, cents / 100.0);
}
}
Money price = Money.ofDollars(9.99);
Money shipping = Money.ofDollars(4.95);
Money total = price.add(shipping);
System.out.println(total); // USD 14.94
لا يمكنك تجاوز أساليب الوصول المُولَّدة لتغيير نوع إرجاعها. يمكنك تجاوز أسلوب الوصول لإضافة منطق (مثلًا نسخة دفاعية)، لكن نوع المعامل ونوع الإرجاع يجب أن يتطابقا تمامًا مع نوع المكوّن وإلا لن يُصرَّف الكود.
تجاوز equals وhashCode وtoString
يتحقق equals() المُولَّد من جميع المكوّنات بـ Objects.equals()، ويستخدم hashCode() كل المكوّنات أيضًا. هذا صحيح لمعظم السجلات. تجاوزهما فقط حين تحتاج دلالات مخصصة — مثل المساواة غير الحساسة لحالة الأحرف لسجل مبني على نص:
record CaseInsensitiveKey(String value) {
@Override
public boolean equals(Object o) {
return o instanceof CaseInsensitiveKey(String v)
&& value.equalsIgnoreCase(v);
}
@Override
public int hashCode() {
return value.toLowerCase().hashCode();
}
}
الخلاصة
تُولّد السجلات أساليب وصول مكتوبة بنوع صارم (تحمل اسم المكوّن بلا بادئة get)، وبنّاءً قانونيًا، وأساليب equals وhashCode وtoString قائمة على القيمة. استخدم البنّاء المُدمَج للتحقق من الصحة أو تطبيع المدخلات بصورة موجزة. أضف أساليب النسخة للسلوك المشتق وأساليب ساكنة للإنشاء المريح. تتيح لك هذه الأدوات بناء أنواع قيم معبّرة وآمنة بكود بالغ الإيجاز.