الواجهات والأصناف المجرّدة

الواجهات التعريفية والمختومة

15 دقيقة الدرس 9 من 14

الواجهات التعريفية والمختومة

ليست كل الواجهات موجودة لوصف السلوك من خلال عقود الأساليب. تحلّ فئتان متخصّصتان — الواجهات التعريفية (Marker Interfaces) والواجهات المختومة (Sealed Interfaces) — مشكلتين مختلفتين جوهريًا: الأولى تلصق بيانات وصفية بالفئة، والثانية تقيّد الفئات المسموح لها بتنفيذ الواجهة. يظهر كلا النمطين في كود Java الحقيقي، لذا فهم متى تستخدم كلًا منهما وسبب ذلك أمر ضروري.

الواجهات التعريفية

الواجهة التعريفية لا تحتوي على أيّ أساليب. غرضها الوحيد هو وسم الفئة حتى تتمكن الشيفرة الأخرى — عادةً إطار العمل أو JVM نفسه — من اكتشاف هذا الوسم في وقت التشغيل باستخدام instanceof أو الانعكاس (Reflection).

// المثال الكلاسيكي من JDK public interface Serializable { // لا شيء هنا — الوسم هو العقد بأكمله }

عندما تستقبل آلية التسلسل في JVM كائنًا ما، تتحقق من obj instanceof Serializable قبل المتابعة. إذا لم تكن الفئة موسومة، يُطلق NotSerializableException. لا توجد توقيعات أساليب؛ كل منطق التسلسل الفعلي يقع داخل JVM وفي ObjectOutputStream.

من الواجهات التعريفية الأخرى المعروفة في JDK: Cloneable التي تُفعّل آلية النسخ المحمية Object.clone()، وRandomAccess التي تُشير إلى أن القائمة List تدعم الوصول المُفهرَس O(1) مما يتيح للخوارزميات اختيار مسار أسرع.

كتابة واجهة تعريفية خاصة بك

يمكنك تعريف واجهة تعريفية لإرفاق معنى دلالي بالفئات في تطبيقك الخاص:

// تُعلّم أمرًا بأنه آمن لإعادة التشغيل بعد فشل النظام public interface Idempotent { } public class PlaceOrderCommand implements Idempotent { private final String orderId; public PlaceOrderCommand(String orderId) { this.orderId = orderId; } public String getOrderId() { return orderId; } } public class CommandBus { public void execute(Object command) { if (command instanceof Idempotent) { System.out.println("آمن لإعادة المحاولة: " + command.getClass().getSimpleName()); } // ... إيفاد الأمر } }
البديل الحديث — التوصيفات (Annotations). منذ Java 5، يمكن للتوصيفات مثل @Idempotent حمل نفس البيانات الوصفية بأقل تعقيد، بل يمكنها أيضًا حمل سمات (مثل @Idempotent(retries = 3)). في الكود الجديد، يُفضَّل استخدام التوصيفات بدلًا من الواجهات التعريفية إلا إذا كنت بحاجة فعلية لفحص نظام الأنواع عبر instanceof أو قيد جنيري مثل <T extends Idempotent>.

الواجهات المختومة (Java 17)

تُدرج الواجهة المختومة صراحةً كل فئة (أو واجهة) مسموح لها بتنفيذها. أيّ فئة غير مدرجة في القائمة تُسبّب خطأ في وقت الترجمة. هذا يمنح مؤلف المكتبة تحكمًا دقيقًا في التسلسل الهرمي للأنواع.

// فقط Triangle وCircle وRectangle يمكنها تنفيذ Shape public sealed interface Shape permits Triangle, Circle, Rectangle { } public final class Triangle implements Shape { public final double base, height; public Triangle(double base, double height) { this.base = base; this.height = height; } } public final class Circle implements Shape { public final double radius; public Circle(double radius) { this.radius = radius; } } public final class Rectangle implements Shape { public final double width, height; public Rectangle(double width, double height) { this.width = width; this.height = height; } }

يجب الإعلان عن كل فئة مسموح لها بإحدى طرق ثلاث:

  • final — تُغلق التسلسل الهرمي نهائيًا؛ لا مزيد من الاشتقاق.
  • sealed — الفئة المسموح لها بدورها تُقيّد أنواعها الفرعية.
  • non-sealed — تُعيد فتح التسلسل الهرمي؛ يمكن لأي شخص توسيع هذه الفئة أو تنفيذها.

لماذا الختم مهم: مطابقة الأنماط الشاملة

العائد الحقيقي من الواجهات المختومة يكمن في تعابير switch المُقدَّمة في Java 17 فأحدث. لأن المُترجِم يعرف كل منفّذ محتمل، يمكنه التحقق من أن تعبير switch الخاص بك شامل — لا حاجة لفرع default:

public static double area(Shape shape) { return switch (shape) { case Triangle t -> 0.5 * t.base * t.height; case Circle c -> Math.PI * c.radius * c.radius; case Rectangle r -> r.width * r.height; // لا حاجة لـ default — المُترجِم يعرف أنه لا توجد أشكال أخرى }; }

إذا أضفت لاحقًا Pentagon إلى قائمة permits، يصبح كل switch على Shape يفتقر لحالة Pentagon خطأ في وقت الترجمة — المُترجِم يرشدك إلى كل موقع استدعاء يحتاج تحديثًا.

الواجهات المختومة تتكامل بشكل طبيعي مع السجلات (Records). السجلات ضمنيًا final، لذا تفي بشرط الختم بشكل مثالي:
public sealed interface Shape permits Triangle, Circle, Rectangle { } public record Triangle(double base, double height) implements Shape { } public record Circle(double radius) implements Shape { } public record Rectangle(double width, double height) implements Shape { }
هذا المزيج هو الأسلوب الاصطلاحي في Java 17 لنمذجة أنواع البيانات الجبرية (مشابهة لأنواع الاتحاد أو التعدادات التي تحمل بيانات).

الواجهات المختومة مقابل التعدادات (Enums)

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

يجب أن تكون جميع الأنواع المسموح بها في نفس الحزمة (أو الوحدة). يفرض المُترجِم هذا القيد حتى تكون المجموعة الكاملة من الأنواع الفرعية دائمًا مرئية في مكان واحد. توزيع التسلسلات الهرمية المختومة على حزم متعددة يُكسر الضمانة ولن يُترجَم.

الخلاصة

تُلصق الواجهات التعريفية وسمًا بالفئة يمكن للشيفرة في وقت التشغيل اكتشافه عبر instanceof؛ يستخدم JDK هذا النمط في Serializable وCloneable وRandomAccess. في الكود الجديد، التوصيفات غالبًا بديل أنظف إلا إذا احتجت قيدًا في نظام الأنواع. أما الواجهات المختومة المُضافة في Java 17، فتُقيّد من يمكنه تنفيذ الواجهة، محوّلةً المُترجِم إلى حارس للتسلسل الهرمي لأنواعك ومُتيحةً مطابقة الأنماط الشاملة في تعابير switch — أداة قوية لنمذجة المجموعات المغلقة من الأنواع المترابطة.