تُعدّ الأصناف (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) من الصَنْف.
أما إذا أردت اِستخدَامه من خارج صَنْفه الحاضن له، فلابُدّ من الإشارة إليه باِستخدَام اسمه الكامل والذي يُكْتَب بالصياغة
لنَفْترِض أننا نريد كتابة صنف يُمثِل مباراة بوكر، بحيث يَتَضمَّن صنفًا داخليًا يُمثِل لاعبي المباراة. يُمكِننا كتابة تعريف الصنف 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
العادي- كائنًا جديدًا ينتمي لصَنْف مجهول الاسم يُشبه الصَنْف أو الواجهة
إذا كان لدينا الواجهة 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.
أفضل التعليقات
لا توجد أية تعليقات بعد
انضم إلى النقاش
يمكنك أن تنشر الآن وتسجل لاحقًا. إذا كان لديك حساب، فسجل الدخول الآن لتنشر باسم حسابك.