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

الأساليب الافتراضية (Default Methods)

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

الأساليب الافتراضية (Default Methods)

قدّمت Java 8 إحدى أهم الإضافات العملية على الواجهات: الأساليب الافتراضية. الأسلوب الافتراضي هو أسلوب يُعلَن مباشرة داخل الواجهة ويحمل جسمًا برمجيًا ملموسًا. أي صنف ينفّذ الواجهة يرث هذا التنفيذ الافتراضي تلقائيًا — لكنه يظلّ حرًا في تجاوزه.

المشكلة التي تحلّها الأساليب الافتراضية

قبل Java 8، لم تستطع الواجهات احتواء سوى توقيعات الأساليب المجرّدة. أوجد هذا مشكلة مؤلمة في إصدارات المكتبات: بمجرّد أن تنشر مكتبة واجهةً ويُنفّذها كود حقيقي، لم يكن بالإمكان إضافة أسلوب جديد إلى تلك الواجهة دون كسر كل منفّذ موجود. كل صنف قائم سيفشل فجأة في الترجمة لأنه يفتقر إلى الأسلوب الجديد.

واجه إطار مجموعات Java هذه المشكلة بالضبط حين وصلت تعابير lambda والتدفقات في Java 8. احتاجت التسلسلية الهرمية لـCollection إلى أساليب جديدة مثل forEach وstream وremoveIf. بدون الأساليب الافتراضية، كانت إضافة أيٍّ منها إلى Iterable أو Collection ستكسر ملايين البرامج حول العالم.

الفكرة الجوهرية: الأساليب الافتراضية هي أداة توافق مع الإصدارات السابقة. تتيح لواجهة ما التطوّر بمرور الوقت بشحن تنفيذ افتراضي مناسب، حتى لا يضطر المنفّذون الحاليون إلى التغيير.

الصياغة

استخدم الكلمة المفتاحية default قبل نوع الإرجاع داخل جسم الواجهة:

public interface Greeter { // مجرّد — يجب على المنفّذين توفير هذا String getName(); // افتراضي — يوجد جسم ملموس هنا في الواجهة مباشرة default String greet() { return "Hello, " + getName() + "!"; } }

أي صنف ينفّذ Greeter يجب أن يوفّر getName()، لكنه يحصل على greet() مجانًا:

public class EnglishGreeter implements Greeter { private final String name; public EnglishGreeter(String name) { this.name = name; } @Override public String getName() { return name; } // greet() موروث من الواجهة — لا حاجة لتكراره } public class Main { public static void main(String[] args) { Greeter g = new EnglishGreeter("Alice"); System.out.println(g.greet()); // Hello, Alice! } }

تجاوز الأسلوب الافتراضي

يستطيع المنفّذ دائمًا اختيار تجاوز الأسلوب الافتراضي بمنطقه الخاص، تمامًا كتجاوز أي أسلوب آخر:

public class FormalGreeter implements Greeter { private final String title; private final String name; public FormalGreeter(String title, String name) { this.title = title; this.name = name; } @Override public String getName() { return title + " " + name; } @Override public String greet() { // يستبدل الافتراضي من الواجهة كليًا return "Good day, " + getName() + ". I trust you are well."; } }

استدعاء الأسلوب الافتراضي من الواجهة داخل التجاوز

إن أردت توسيع السلوك الافتراضي بدلًا من استبداله، استخدم InterfaceName.super.methodName():

public class VerboseGreeter implements Greeter { private final String name; public VerboseGreeter(String name) { this.name = name; } @Override public String getName() { return name; } @Override public String greet() { // ابدأ بالافتراضي من الواجهة، ثم أضف نصًا إضافيًا return Greeter.super.greet() + " Welcome aboard!"; } } // الناتج: Hello, Bob! Welcome aboard!

مثال واقعي: تطوير واجهة إضافات

تخيّل أنك تشحن مكتبة بهذه الواجهة في الإصدار 1.0:

// v1.0 — الأصل public interface DataProcessor { void process(String data); }

عشرات المستخدمين ينفّذونها. في الإصدار 1.1 تريد أن يدعم كل معالج عملية معالجة مجمّعة، لكنك لا تريد كسر الكود الموجود. الأسلوب الافتراضي هو الحل:

// v1.1 — توسيع متوافق مع الإصدارات السابقة public interface DataProcessor { void process(String data); // لا يزال مجرّدًا default void processBatch(java.util.List<String> items) { for (String item : items) { process(item); // يستدعي الأسلوب المجرّد الذي يوفّره المنفّذ } } }

يُعيد جميع المنفّذين الحاليين الترجمة دون تغييرات. يحصلون على processBatch تلقائيًا — فهي تستدعي process مرة لكل عنصر باستخدام أي منطق يمتلكه الصنف الفرعي. يستطيع المنفّذون ذوو الأداء العالي تجاوزها بنهج مُحسَّن.

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

مشكلة الماسة وقواعد الحلّ

بما أن صنفًا واحدًا يستطيع تنفيذ واجهات متعددة (يُغطّى في الدرس التالي)، قد توفّر واجهتان معًا أسلوبًا افتراضيًا بنفس التوقيع. لا تختار Java واحدًا بصمت — يجبرك المترجم على حلّ التعارض:

public interface A { default String hello() { return "Hello from A"; } } public interface B { default String hello() { return "Hello from B"; } } // خطأ في الترجمة: الصنف C يرث تنفيذات افتراضية متعارضة لـ hello() public class C implements A, B { // يجب عليك التجاوز لحلّ التعارض @Override public String hello() { return A.super.hello(); // اختر صراحةً الافتراضي من A } }
قاعدة التعارض: إذا وفّرت واجهتان افتراضيًا لنفس الأسلوب، يجب على الصنف المنفّذ تجاوز ذلك الأسلوب — يرفض المترجم الترجمة حتى يُحسم الغموض. هذا مقصود: Java لا تخمّن.

ما الأساليب الافتراضية ليست كذلك

  • هي ليست بديلًا عن الصنف المجرّد. لا تستطيع الاحتفاظ بحالة نسخة (لا حقول).
  • هي ليست مخصّصة لإضافة منطق أعمال إلى الواجهة. غرضها الأساسي هو تطوير الواجهة البرمجية ومساعدات الراحة.
  • هي ليست نفس أساليب static في الواجهات (تلك تنتمي لنوع الواجهة نفسه لا للنسخ — يُغطّى في الدرس التالي).

الخلاصة

تمنح الأساليب الافتراضية الواجهاتِ جسمًا ملموسًا لأسلوب أو أكثر. وجودها يخدم أساسًا تمكين الواجهات القائمة من اكتساب قدرات جديدة دون كسر كل صنف ينفّذها. يملك الصنف المنفّذ دائمًا الكلمة الأخيرة: يرث الافتراضي، أو يتجاوزه، أو يستدعيه عبر InterfaceName.super.method() مضيفًا منطقه الخاص. حين تتعارض واجهتان على نفس اسم الأسلوب الافتراضي، يطالب المترجم بحلٍّ صريح.