مدخل إلى جافا الأصناف المتداخلة (Nested Classes) في جافا


رضوى العربي

تُعدّ الأصناف (classes) اللَبِنة الأساسية عالية المستوى (high-level) بالبرنامج، حيث تُستخدَم لتمثيل الأفكار والكيانات المُعقدة ضِمْن البرنامج وما يَرتبِط بها من بيانات (data) وسلوكيات (behavior). يَدفَع ذلك البعض إلى وَضْع الأصناف بمكانة خاصة، فيُحاولون تَجنُّب كتابة الأصناف الصغيرة جدًا والموجودة فقط لتجميع مجموعة من البيانات معًا، ويَرَونَها تافهة مع أنها قد تَكُون مفيدة أحيانًا أو حتى ضرورية في بعض الأحيان الآخرى. لحسن الحظ، تَرفَع الجافا هذا الحرج؛ حيث تَسمَح بكتابة تعريف صَنْف (class definition) ضِمْن تعريف صَنْف آخر، وبذلك لَمْ تَعُدْ تلك الأصناف الصغيرة جدًا موجودة بمُفردها، وإنما أَصبحَت جُزءًا من أصناف أكبر ذات هيبة. إلى جانب ذلك، هنالك العديد من الأسباب الآخرى التي قد تَدفَعك لتَضْمِين تعريف صنف (class definition) داخل صنف آخر.

الصَنْف المُتداخِل (nested class) هو ببساطة أيّ صنف يَقَع تعريفه (definition) داخل تعريف صنف آخر. وفي الواقع، يُمكِن حتى كتابة تعريف صَنْف داخل تابع (method) والذي بدوره يَقَع ضِمْن صنف. الأصناف المُتداخِلة إِما أن تَكُون مُسمَاة (named) أو مجهولة الاسم (anonymous). سنعود لاحقًا إلى الأصناف مجهولة الاسم (anonymous classes) أما بالنسبة للأصناف المُتداخِلة المُسمَاة (named nested class)، فيُمكِنها أن تَكُون ساكنة (static) أو غَيْر ساكنة (non-static) وذلك كأي شيء مُعرَّف ضِمْن صَنْف. بالمثل من الأصناف، يُمكِن للواجهات (interfaces) أيضًا أن تَقَع ضِمْن تعريفات الأصناف (class definitions) وقد تَكُون ساكنة (static) أو غَيْر ساكنة (non-static)، كما قد تَحتوِي تعريفات الواجهات (interface definitions) على أصناف مُتداخِلة ساكنة (static nested classes) أو واجهات آخرى، ولكننا لن نَتَعرَّض لذلك.

الأصناف المتداخلة (nested) الساكنة

تُعرَّف الأصناف المُتداخِلة (nested class) الساكنة كأي تعريف لصَنْف عادي باستثناء أمرين: أولهما وقوع التعريف ضِمْن صنف آخر، والآخر اِستخدَام المُبدِّل static ضِمْن التّصْريح. يُعدّ أي صنف مُتداخِل ساكن جزءًا من البنية الساكنة (static) للصَنْف الحاضن له والذي قد يَستخدِمه لإنشاء كائنات بالطريقة العادية. في المقابل، يُمكِن اِستخدَامه أيضًا خارج صَنْفه الحاضن، ولكن ينبغي أن يُشير اسمه في تلك الحالة إلى كَوْنه عضوًا بصَنْفه الحاضن، أي ينبغي أن يُستخدَم الاسم الكامل للصنف والذي يَتَكوَّن من اسم صَنْفه الحاضن متبوعًا بنقطة ثم باسمه. كأيّ مُكوِّن ساكن آخر ضِمْن صَنْف، تُعدّ الأصناف المُتداخِلة (nested class) الساكنة جزءًا من الصَنْف ذاته بنفس الطريقة التي يُعدّ بها أي مُتْغيِّر عضو (member variable) ساكن جزءًا من الصَنْف ذاته.

لنَفْترِض مثلًا وجود صَنْف، اسمه WireFrameModel يُمثِل مجموعة من الخطوط بفضاء ثلاثي الأبعاد. يَحتوِي ذلك الصنف على صَنْف مُتداخِل ساكن Line يُمثِل خطًا واحدًا. يُمكِننا الآن كتابة تعريف الصنف WireFrameModel والصنف المُتداخِل Line كالتالي:

public class WireFrameModel {

   . . . // أعضاء آخرى ضمن الصنف

   static public class Line {
       // ‫يمثل خطًا من النقطة (x1,y1,z1) إلى النقطة (x2,y2,z2) بفضاء 
       // ثلاثي الأبعاد
      double x1, y1, z1;
      double x2, y2, z2;
   } // ‫نهاية الصنف Line

   . . . // أعضاء آخرى ضمن الصنف

} // ‫نهاية الصنف WireFrameModel

لاحِظ أن الاسم الكامل للصَنْف المُتداخِل هو WireFrameModel.Line ويُمكِنك اِستخدَامه للتّصْريح عن مُتْغيِّر مثلًا. تستطيع تَحْديدًا إنشاء كائن من الصَنْف Line باِستخدَام البَانِي new Line()‎ من داخل الصَنْف WireFrameModel، بينما قد تَستخدِم التعبير new WireFrameModel.Line()‎ لإنشائه من خارج الصَنْف.

يستطيع أي صَنْف مُتداخِل (nested) ساكن الوصول إلى أي عُضو ساكن (static members) مُعرَّف بالصنف الحاضن له حتى وإن كان عضوًا خاصًا (private). بالمثل، يستطيع الصَنْف الحاضن لصَنْف مُتداخِل (nested) ساكن الوصول إلى أعضاء ذلك الصنف المُتداخِل (nested) حتى وإن كانت خاصة (private). يُمثِل ذلك دافعًا قويًا لاِستخدَام الأصناف المُتداخِلة (nested)؛ نظرًا لأنها تَسمَح لصَنْف معين بالوصول إلى الأعضاء الخاصة (private) المُعرَّفة بصَنْف آخر دون الحاجة إلى إِتاحة تلك الأعضاء بصورة أعم لجميع الأصناف الآخرى. وأخيرًا، يُمكِن لأي صَنْف مُتداخِل (nested) أن يَكُون خاصًا (private) وعندها يُمكِن اِستخدَامه من داخل الصَنْف الحاضن له فقط.

على الرغم من أن تعريف الصَنْف Line يَقَع ضِمْن الصَنْف WireFrameModel، ستُخزَّن النسخة المُصرَّفة (compiled) من الصَنْف Line بملف مُنفصل، اسمه هو WireFrameModel$Line.class، أي أنه عند تصريف تعريف الصَنْف (class definition) -بالأعلى-، سيُنشِئ المُصرِّف ملفًا منفصلًا لكل صَنْف.

الأصناف الداخلية (inner)

يُطلَق على الأصناف المُتداخِلة (nested) غَيْر الساكنة (non-static) اسم "الأصناف الداخلية (inner classes)". من الناحية العملية، هي لا تَختلِف كثيرًا عن الأصناف المُتداخِلة الساكنة (static)، مع أنها تُعدّ جزءًا من كائنات الأصَنْاف الحاضنة لها لا الأصناف ذاتها.

لا تُعدّ الأعضاء غَيْر الساكنة (non-static) المُعرَّفة بأي صنف جزءًا فعليًا من الصنف ذاته على الرغم من أن شيفرتها مُتضمَّنة بتعريف ذلك الصنف (class definition)، وهو ما يَنطبِق على الأصناف الداخلية (inner classes). تُحدِّد تلك الأعضاء ما ستَحتوِيه كائنات ذلك الصنف عند إنشائها، وهو ما يَنطبِق أيضًا على الأصناف الداخلية على الأقل منطقيًا أي كما لو كان كل كائن من الصنف الحاضن يَتَضمَّن نسخة خاصة من الصَنْف المُتداخِل (nested) -لا تَأخُذ ذلك بالمعنى الحرفي-. بإمكان تلك النُسخة الوصول لجميع توابع النُسخ (instance methods) ومُتْغيَّرات النُسخ (instance variables) المُعرَّفة بالكائن حتى وإن كانت خاصة (private). مثلًا، إذا كان لدينا كائنين من صَنْف يَتَضمَّن صنفًا داخليًا (inner class)، فإن نُسختي الصَنْف الداخلي بهذين الكائنين مختلفتان؛ لأنهما يُشيران إلى مُتْغيَّرات نُسخ وتوابع نُسخ تَقَع ضِمْن كائنات مختلفة. كيف تُقرِّر ما إذا كان ينبغي لصنف مُتداخِل (nested) معين أن يَكُون ساكنًا أو غَيْر ساكن؟ الأمر بسيط للغاية: إذا اِستخدَم الصنف المُتداخِل مُتْغيِّر نسخة (instance variable) أو تابع نسخة (instance method) من الصَنْف الحاضن، فلابُدّ أن يَكُون غَيْر ساكن (non-static)، أما إن لم يَستخدِم أيًا منها، فيُمكِنه أن يَكُون ساكنًا (static).

يُستخدَم الصنف الداخلي (inner class) في غالبية الأحوال ضِمْن الصَنْف الحاضن له فقط. وفي تلك الحالة، لا يَختلِف اِستخدَامه كثيرًا عن اِستخدَام أيّ صنف آخر؛ فيُمكِنك أن تُصرِّح عن مُتْغيِّر أو أن تُنشِئ كائنًًا باِستخدَام الاسم البسيط للصَنْف الداخلي، ولكن فقط ضِمْن الأجزاء غَيْر الساكنة (non-static) من الصَنْف.

أما إذا أردت اِستخدَامه من خارج صَنْفه الحاضن له، فلابُدّ من الإشارة إليه باِستخدَام اسمه الكامل والذي يُكْتَب بالصياغة .، حيث أن هو مُتْغيِّر يُشير إلى كائن مُتضمِّن لصَنْف داخلي (inner class). لاحِظ أنه لكي تَتَمكَّن من إنشاء كائن ينتمي لصَنْف داخلي، لابُدّ أولًا أن تُنشِئ كائنًا من صَنْفه الحاضن.

لنَفْترِض أننا نريد كتابة صنف يُمثِل مباراة بوكر، بحيث يَتَضمَّن صنفًا داخليًا يُمثِل لاعبي المباراة. يُمكِننا كتابة تعريف الصنف PokerGame كالتالي:

public class PokerGame {  // يمثل مباراة بوكر

    class Player {  // يمثل أحد لاعبي المباراة
       .
       .
       .
    } // ‫نهاية الصنف Player

    private Deck deck;      // مجموعة ورق اللعب
    private int pot;        // قيمة الرهان

    .
    .
    .

} // ‫نهاية الصنف PokerGame

إذا كان game مُتْغيِّرًا من النوع PokerGame، فإنه سيَتَضمَّن نسخة خاصة به من الصَنْف Player على نحو منطقي. كأي صَنْف عادي آخر، تستطيع اِستخدَام التعبير new Player()‎ لإنشاء كائن جديد من الصَنْف Player بشَّرْط أن يَقَع ذلك داخل تابع نسخة (instance method) بكائن الصَنْف PokerGame. في المقابل، تستطيع اِستخدَام تعبير مثل game.new Player()‎ لإنشاء كائن من الصَنْف Player من خارج الصَنْف PokerGame، ولكنه في الواقع أمر نادر الحدوث. يستطيع أي كائن من الصَنْف Player الوصول إلى مُتْغيِّرات النُسخ deck و pot المُعرَّفة بكائن الصَنْف PokerGame. من الجهة الأخرى، يَمتلك أي كائن من الصَنْف PokerGame نسخة خاصة به من كُلًا من deck و pot و Players. فمثلًا، يَستخدِم لاعبو مباراة بوكر معينة المُتْغيِّرين deck و pot الخاصين بتلك المباراة تَحْديدًا، بينما يَستخدِم لاعبو مباراة بوكر آخرى المُتْغيِّرين deck و pot الخاصين بتلك المباراة الآخرى. ذلك بالتحديد هو تأثير كَوْن الصَنْف Player غير ساكن، وهو في الواقع الطريقة الطبيعية التي ينبغي للاعبين التصرُّف على أساسها. يُمثِل كل كائن من الصَنْف Player لاعبًا ضِمْن مباراة بوكر مُحدَّدة، أما إذا كان الصَنْف Player صَنْْفًا مُستقلًا أو صنفًا مُتداخِلًا (nested) ساكنًا، فسيُمثِل عندها الفكرة العامة للاعب البوكر بصورة مُستقلة عن أي مباراة بوكر مُحدَّدة.

الأصناف الداخلية مجهولة الاسم (anonymous)

قد تُعرِّف صنفًا داخليًا (inner class)، ثم تَجِد نفسك تَستخدِمه بسطر وحيد بالبرنامج، فهل ينبغي حقًا في تلك الحالة تعريف مثل ذلك الصنف؟ ربما، ولكن قد تُفضِّل أيضًا أن تَستخدِم صنفًا داخليًا مجهول الاسم (anonymous) في مثل هذه الحالات. يُكْتَب أي صَنْف داخلي مجهول الاسم بالصيغة التالية:

new  <superclass-or-interface> ( <parameter-list> ) {
    <methods-and-variables>
}

يُعرِّف الباني -بالأعلى- صَنْفًا جديدًا بدون أن يُعطيه أي اسم، ويُنشِئ كائنًا يَنتمِي إلى ذلك الَصَنْف عند تَشْغِيل البرنامج. في العموم، يُنشِئ هذا الشكل من العَامِل new -والذي تستطيع اِستخدَامه أينما استطعت اِستخدَام العامل new العادي- كائنًا جديدًا ينتمي لصَنْف مجهول الاسم يُشبه الصَنْف أو الواجهة ، والتي تَعمَل كأساس لصنف ذلك الكائن مع إضافة المُتْغيرِّات والتوابع المُعرَّفة، أي أنه يُنشِئ كائنًا فريدًا. الأهم من ذلك هو أنه يُنشئِه فقط بذلك الجزء من البرنامج حيث تحتاج إليه. لا يُشترَط أن يَكُون ذلك الأساس صنفًا، فيُمكِن أن يَكُون واجهة (interface) أيضًا، ولكن في تلك الحالة -أي اِستخدَام واجهة كأساس-، يَكُون الصَنْف مجهول الاسم مُلزَمًا بتّنْفيذ (implement) الواجهة، أي بتعريف (define) جميع التوابع المُصرَّح (declare) عنها ضِمْن تلك الواجهة كما أن قائمة المُعامِلات لابُدّ أن تَكُون فارغة. في المقابل، إذا اِستخدَمت صنفًا كأساس، فبإمكانك تمرير مُعامِلات (parameters) بحيث يَستقبِلها أحد البواني (constructor) المُعرَّفة بالصَنْف الأعلى .

إذا كان لدينا الواجهة Drawable المُعرَّفة كالتالي:

public interface Drawable {
    public void draw(GraphicsContext g);
}

لنَفْترِض الآن أننا نحتاج إلى كائن من النوع Drawable لكي يَرسِم مربعًا أحمر اللون طوله يُساوِي ١٠٠ بكسل. قد تختار أن تُعرِّف صنفًا جديدًا يُنفِّذ الواجهة Drawable، ثم تُنشِئ كائنًا منه. في المقابل، قد تَستخدِم صنفًا مجهول الاسم (anonymous class) لإنشاء الكائن بتَعْليمَة واحدة فقط، كالتالي:

Drawable redSquare = new Drawable() {
       public void draw(GraphicsContext g) {
          g.setFill(Color.RED);
          g.fillRect(10,10,100,100);
       }
   };

يُشير redSquare -بالأعلى- إلى كائن يُنفِّذ الواجهة Drawable، ويَرسِم مربعًا أحمر اللون عند استدعاء تابعه draw()‎. لاحِظ أن الفاصلة المنقوطة بنهاية التَعْليمَة ليست جزءًا من تعريف الصنف (class definition)، وإنما هي تلك التي تُوجد بنهاية أي تَعْليمَة تّصْريح (declaration).

يَشيع تمرير الأصناف مجهولة الاسم (anonymous class) كمُعامِلات فعليّة (actual parameters) للتوابع. فمثلًا، يَستقبِل التابع التالي كائنًا من النوع Drawable، ثم يَرسمه بسياقين رُسوميين (graphics contexts) مختلفين:

void drawTwice( GraphicsContext g1, GraphicsContext g2, Drawable figure ) {
    figure.draw(g1);
    figure.draw(g2);
}

عندما تَستدعِي ذلك التابع، يُمكِنك أن تُمرِّر صنفًا داخليًا (inner class) مجهول الاسم (anonymous) كقيمة للمُعامِل (parameter) الثالث، كالتالي:

drawTwice( firstG, secondG, new Drawable() {
          void draw(GraphicsContext g) {
             g.fillOval(10,10,100,100);
          }
     } );

يُنشِئ المُصرِّف ملف صنف ‎.class منفصل لكل صنف مُتداخِل (nested) مجهول الاسم ضِمْن الصَنْف. إذا كان اسم الصنف الأساسي هو MainClass، فإن أسماء ملفات الأصناف المتداخلة مجهولة الاسم تكون كالتالي: MainClass$1.class و MainClass$2.class و MainClass$3.class وهكذا.

تُمثِل الواجهة Drawable المُعرَّفة بالأعلى واجهة نوع دالة (functional interface)، وبالتالي نستطيع في هذه الحالة اِستخدَام تعبيرات لامدا (lambda expressions) بدلًا من الأصناف مجهولة الاسم. يُمكِن إعادة كتابة المثال الأخير كالتالي:

drawTwice( firstG, secondG, g -> g.fillOval(10,10,100,100) );

كما يُمكِن إعادة تعريف المُتْغيِّر redSquare كالتالي:

Drawable redSquare = g -> {
          g.setFill(Color.RED);
          g.fillRect(10,10,100,100);
       };

لا يُنشِئ المُصرِّف أي ملفات جديدة لتعبيرات لامدا (lambda expressions) وهو ما يُحسَب لها، ولكن لاحِظ أنها تُستخدَم فقط مع واجهات نوع الدالة (functional interfaces). في المقابل، تستطيع اِستخدَام الأصناف مجهولة الاسم (anonymous classes) مع أي واجهة أو صنف. قبل الإصدار ٨ من الجافا، كانت الأصناف مجهولة الاسم تُستخدَم لمعالجة الأحداث (events) ببرامج واجهات المُستخدِم الرُسومية (GUI)، ولكن مع الإصدار ٨ ومنصة جافا إف إكس (JavaFX)، أصبحت تعبيرات لامدا مُستخدَمة بكثرة ضِمْن هذا السياق بدلًا من الأصناف مجهولة الاسم.

الأصناف المحلية (local) وتعبيرات لامدا

تستطيع أن تُعرِّف صنفًا ضِمْن تعريف برنامج فرعي (subroutine definition). تُعرَف تلك النوعية من الأصناف باسم "الأصناف المحلية (local classes)". يَقْتصِر اِستخدَام أي صنف محلي على البرنامج الفرعي (subroutine) المُعرَّف بداخله، ولكن يُمكِن لكائنات ذلك الصنف أن تُستخدَم خارج البرنامج الفرعي، فمثلًا، قد يُعاد كائن ينتمي لصنف محلي من برنامج فرعي أو قد يُمرَّر كمُعامِل (parameter) لبرنامج فرعي آخر. هذا ممكن لأننا نستطيع عمومًا إِسْناد كائن ينتمي لصنف معين B إلى مُتْغيِّر من النوع A طالما كان B صنفًا فرعيًا من A في حالة كان A عبارة عن صنف، أما إذا كان واجهة فطالما كان B يُنفِّذ A. لنَفْترِض مثلًا أن لدينا برنامج فرعي (subroutine) يَستقبِل مُعامِلًا من النوع Drawable -الواجهة المُعرَّفة بالأعلى-، فنستطيع ببساطة أن نُمرِّر إليه أي كائن طالما كان يُنفِّذ (implements) تلك الواجهة، وعليه، قد ينتمي الكائن لصنف محلي (local class) يُنفِّذ تلك الواجهة.

بمثال سابق بهذا القسم، مَرَّرنا كائنًا من النوع Drawable إلى التابع drawTwice()‎، والذي يَستقبِل مُعاملًا (parameter) من النوع Drawable. في ذلك المثال، كان الصنف المُمرَّر صنفًا داخليًا مجهول الاسم (anonymous inner class). لاحِظ أن الأصناف المحلية عادة ما تَكُون مجهولة الاسم (anonymous) والعكس، لكنه مع ذلك ليس أمرًا ضروريًا. فمثلًا، قد تَستخدِم صنفًا مجهول الاسم لتعريف القيمة المبدئية لمُتْغيِّر عام (global variable). في تلك الحالة، لا يَقع الصَنْف ضِمْن أي برنامج فرعي (subroutine)، وبالتالي لا يُعدّ محليًا (local).

يُمكِن لأي صَنْف محلي (local class) أن يُشير إلى أي مُتْغيِّر محلي (local variables) ضِمْن برنامجه الفرعي أو إلى أي من مُعامِلاته (parameters) المُمرَّرة، ولكن وفقًا لعدة شروط: لابُدّ أن يُصرَّح عن ذلك المُتْغيِّر المحلي (local variable) أو المُعامِل (parameter) باستخدام المُبدِّل final أو أن يَكُون على الأقل "نهائيًا على نحو فعال". يُعدّ المُعامِل (parameter) "نهائيًا على نحو فعال" إذا لم تَتغيّر قيمته داخل البرنامج الفرعي (subroutine) بما في ذلك أصنافه المحلية (local class) التي أشارت إلى ذلك المُعامِل (parameter) بينما يُعدّ المُتْغيِّر المحلي (local variable) "نهائيًا على نحو فعال" إذا لم تَتغيّر قيمته أبدًا بعد التهيئة المبدئية (initialize). في المقابل، تستطيع الأصناف المحلية الإشارة إلى أي مُتْغيِّر عام (global variables) ودون أي شروط.

تَنطبِق نفس شروط اِستخدَام المُتْغيِّرات المحلية (local variables) على تعبيرات لامدا (lambda expressions)، والتي هي كثيرة الشبه بالأصناف مجهولة الاسم (anonymous classes). تَستعرِض الشيفرة التالية برنامجًا فرعيًا يَستخدِم كُلًا من واجهة برمجة التطبيقات stream والواجهة Runnable -ناقشناها بالقسم ٤.٥-؛ لطباعة الأعداد من ١ إلى ١٠ بترتيب غير مُحدَّد يَعتمِد على مجرى مُتوازي (parallel stream):

static void print1to10() {
    ArrayList<Runnable> printers = new ArrayList<>();
    for (int i = 1; i <= 10; i++) {
        int x = i;
        printers.add( () -> System.out.println(x) );
    }
    printers.parallelStream().forEach( r -> r.run() );
}

لمّا كان المُتْغيِّر المحلي x "نهائيًا على نحو فعال"، تَمكَّنا من اِستخدَامه ضِمْن تعبير لامدا (lambda expression). في المقابل، لا يُمكِن اِستخدَام المُتْغيِّر i ضِمْن تعبير لامدا؛ لأنه غَيْر نهائي حيث تَتغيّر قيمته عند تّنْفيذ التعبير i++‎.

ترجمة -بتصرّف- للقسم Section 8: Nested Classes من فصل Chapter 5: Programming in the Large II: Objects and Classes من كتاب Introduction to Programming Using Java.



1 شخص أعجب بهذا


تفاعل الأعضاء


لا توجد أيّة تعليقات بعد



يجب أن تكون عضوًا لدينا لتتمكّن من التعليق

انشاء حساب جديد

يستغرق التسجيل بضع ثوان فقط


سجّل حسابًا جديدًا

تسجيل الدخول

تملك حسابا مسجّلا بالفعل؟


سجّل دخولك الآن