المحتوى عن 'مدخل إلى جافا'.



مزيد من الخيارات

  • ابحث بالكلمات المفتاحية

    أضف وسومًا وافصل بينها بفواصل ","
  • ابحث باسم الكاتب

نوع المُحتوى


التصنيفات

  • الإدارة والقيادة
  • التخطيط وسير العمل
  • التمويل
  • فريق العمل
  • دراسة حالات
  • التعامل مع العملاء
  • التعهيد الخارجي
  • السلوك التنظيمي في المؤسسات
  • عالم الأعمال
  • التجارة والتجارة الإلكترونية
  • نصائح وإرشادات
  • مقالات ريادة أعمال عامة

التصنيفات

  • PHP
    • Laravel
    • ووردبريس
  • جافاسكريبت
    • Node.js
    • React
    • AngularJS
    • Vue.js
    • jQuery
    • Cordova
  • HTML
    • HTML5
  • CSS
  • SQL
  • لغة C#‎
  • لغة C++‎
  • لغة C
  • بايثون
    • Flask
    • Django
  • لغة روبي
    • Sass
    • إطار عمل Bootstrap
    • إطار العمل Ruby on Rails
  • لغة Go
  • لغة جافا
  • لغة Kotlin
  • برمجة أندرويد
  • لغة Swift
  • لغة R
  • لغة TypeScript
  • ‎.NET
    • ASP.NET
  • الذكاء الاصطناعي
  • صناعة الألعاب
    • Unity3D
    • منصة Xamarin
  • سير العمل
    • Git
  • سهولة الوصول
  • مقالات برمجة عامة

التصنيفات

  • تجربة المستخدم
  • الرسوميات
    • إنكسكيب
    • أدوبي إليستريتور
    • كوريل درو
  • التصميم الجرافيكي
    • أدوبي فوتوشوب
    • أدوبي إن ديزاين
    • جيمب
  • التصميم ثلاثي الأبعاد
    • 3Ds Max
    • Blender
  • نصائح وإرشادات
  • مقالات تصميم عامة

التصنيفات

  • خوادم
    • الويب HTTP
    • قواعد البيانات
    • البريد الإلكتروني
    • DNS
    • Samba
  • الحوسبة السّحابية
    • Docker
  • إدارة الإعدادات والنّشر
    • Chef
    • Puppet
    • Ansible
  • لينكس
    • ريدهات (Red Hat)
  • خواديم ويندوز
  • FreeBSD
  • حماية
    • الجدران النارية
    • VPN
    • SSH
  • شبكات
    • سيسكو (Cisco)
  • مقالات DevOps عامة

التصنيفات

  • التسويق بالأداء
    • أدوات تحليل الزوار
  • تهيئة محركات البحث SEO
  • الشبكات الاجتماعية
  • التسويق بالبريد الالكتروني
  • التسويق الضمني
  • التسويق بالرسائل النصية القصيرة
  • استسراع النمو
  • المبيعات
  • تجارب ونصائح
  • مبادئ علم التسويق

التصنيفات

  • إدارة مالية
  • الإنتاجية
  • تجارب
  • مشاريع جانبية
  • التعامل مع العملاء
  • الحفاظ على الصحة
  • التسويق الذاتي
  • مقالات عمل حر عامة

التصنيفات

  • الإنتاجية وسير العمل
    • مايكروسوفت أوفيس
    • ليبر أوفيس
    • جوجل درايف
    • شيربوينت
    • Evernote
    • Trello
  • تطبيقات الويب
    • ووردبريس
    • ماجنتو
  • أندرويد
  • iOS
  • macOS
  • ويندوز
  • الترجمة بمساعدة الحاسوب
    • omegaT
    • memoQ
  • أساسيات استعمال الحاسوب
  • مقالات عامة

التصنيفات

  • شهادات سيسكو
    • CCNA
  • شهادات مايكروسوفت
  • شهادات Amazon Web Services
  • شهادات ريدهات
    • RHCSA
  • شهادات CompTIA
  • مقالات عامة

أسئلة وأجوبة

  • الأقسام
    • أسئلة ريادة الأعمال
    • أسئلة العمل الحر
    • أسئلة التسويق والمبيعات
    • أسئلة البرمجة
    • أسئلة التصميم
    • أسئلة DevOps
    • أسئلة البرامج والتطبيقات
    • أسئلة الشهادات المتخصصة

التصنيفات

  • ريادة الأعمال
  • العمل الحر
  • التسويق والمبيعات
  • البرمجة
  • التصميم
  • DevOps

تمّ العثور على 38 نتائج

  1. تُعدّ الأصناف (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.
  2. تَسمَح بعض اللغات البرمجية كائنية التوجه (object-oriented programming)، مثل C++‎، للصَنْف بأن يَتمدَّد (extend) من أكثر من مُجرّد صَنْف أعلى (superclass) واحد، وهو ما يُعرَف باسم الوراثة المُتعدّدة (multiple inheritance). بالرسم التالي مثلًا، يَتمدَّد الصنف E من صنفين أعليين (superclasses) مباشرةً، هما الصنفين A و B، بينما يَتمدَّد الصنف F من ثلاثة أصناف أعلين (superclasses) مباشرةً: أراد مُصمِّمي الجافا أن يجعلوا اللغة بسيطة على نحو معقول، ولمّا وجدوا أن مزايا الوراثة المُتعدّدة (multiple inheritance) لا تَستحِقّ ما يُقابِلها من تعقيد مُتزايد، فإنهم لم يُدعِّموها باللغة. ومع هذا، تُوفِّر الجافا ما يُعرَف باسم الواجهات (interfaces) والتي يُمكِن اِستخدَامها لتحقيق الكثير من أهداف الوراثة المُتعدّدة. لقد تَعرَّضنا -بالقسم ٤.٥- لواجهات نوع الدالة (functional interfaces) وعلاقتها بتعبيرات لامدا (lambda expressions)، ورأينا أنها تُخصِّص تابعًا (method) وحيدًا. في المقابل، يُمكِن للواجهات (interfaces) أن تَكُون أكثر تعقيدًا بمراحل كما أن لها استخدامات آخرى كثيرة. من غَيْر المُحتمَل أن تحتاج إلى كتابة واجهات (interfaces) خاصة بك حاليًا؛ فهي ضرورية فقط للبرامج المُعقَّدة نسبيًا، ولكن هنالك عدة واجهات (interfaces) مُستخدَمة بحزم جافا القياسية (Java's standard packages) بطرائق مُهِمّة وتحتاج إلى تَعلُّم طريقة اِستخدَامها. تعريف الواجهات (interfaces) وتنفيذها (implementation) لقد تَعرَّضنا لمصطلح "الواجهة (interface)" ضِمْن أكثر من سياق، سواء فيما يَتَعلَّق بالصناديق السوداء (black boxes) في العموم أو فيما يَتَعلَّق بالبرامج الفرعية (subroutines) على وجه الخصوص. تَتكوَّن واجهة أي برنامج فرعي (subroutine interface) من اسمه، ونوعه المُعاد (return type)، وعدد مُعامِلاته (parameters) وأنواعها. تُمثِل تلك المعلومات كل ما أنت بحاجة إلى مَعرِفته لكي تَتَمكَّن من استدعاء البرنامج الفرعي. بالإضافة إلى ذلك، يَمتلك أي برنامج فرعي جزءًا تّنْفيذيًا (implementation)، هو كتلة الشيفرة المُعرِّفة له (defines) والتي تُنفَّذ عند استدعاءه. بلغة الجافا، كلمة interface هي كلمة محجوزة تَحمِل معنًى تقنيًا إضافيًا. وفقًا لهذا المعنى، تَتكوَّن الواجهة من مجموعة من واجهات توابع النُسخ (instance method interfaces) بدون أجزائها التّنفيذية (implementations). يستطيع أي صنف أن يُنفِّذ (implement) واجهة معينة بتوفير الأجزاء التّنْفيذية (implementation) لجميع التوابع المُخصَّصة ضِمْن تلك الواجهة. اُنظر المثال التالي لواجهة (interface) بسيطة جدًا بلغة الجافا: public interface Strokeable { public void stroke(GraphicsContext g); } تبدو الشيفرة بالأعلى مشابهة لتعريف صنف (class definition) باستثناء حَذْف الجزء التّنْفيذي (implementation) للتابع stroke()‎. إذا أراد صنف معين أن يُنفِّذ تلك الواجهة Strokeable، فلابُدّ له من أن يُوفِّر جزءًا تّنْفيذيًا للتابع stroke()‎ كما قد يَتَضمَّن أي توابع أو متغيرات آخرى. اُنظر الشيفرة التالية على سبيل المثال: public class Line implements Strokeable { public void stroke(GraphicsContext g) { . . . // ارسم خطًا } . . . // توابع ومتغيرات وبواني آخرى } لكي يُنفِّذ صنف واجهةً (interface) معينةً، ينبغي عليه أن يَفعَل أكثر من مُجرّد توفير الأجزاء التّنْفيذية (implementation) لجميع التوابع ضِمْن تلك الواجهة، فعليه تحديدًا أن يُعلن صراحةً عن تّنفيذه (implements) لتلك الواجهة باستخدام الكلمة المحجوزة implements كالمثال بالأعلى. لابُدّ لأي صنف حقيقي (concrete class) يَرغَب بتّنْفيذ الواجهة Strokeable من أن يُعرِّف تابع نسخة اسمه stroke()‎، لذا سيَتضمَّن أي كائن (object) مُنشَئ من هذا الصنف التابع stroke()‎. يُعدّ الكائن مُنفِّذًا (implement) لواجهة معينة إذا كان ينتمي لصنف يُنفِّذ (implements) تلك الواجهة، فمثلًا، يُنفِّذ أي كائن من النوع Line الواجهة Strokeable. في حين يستطيع الصنف أن يَتمدَّد (extend) من صنف واحد فقط، فإنه في المقابل يستطيع أن يُنفِّذ (implements) أي عدد من الواجهات (interfaces). وفي الواقع، يُمكِن للصنف أن يَتمدَّد (extend) من صنف آخر، وأن يُنفِّذ واجهة واحدة أو أكثر بنفس ذات الوقت، لذلك نستطيع كتابة التالي مثلًا: class FilledCircle extends Circle implements Strokeable, Fillable { . . . } على الرغم من أن الواجهات (interfaces) ليست أصنافًا (classes)، فإنها تُشبهها إلى حد كبير. في الواقع، أي واجهة (interface) هي أَشْبه ما تَكُون بصنف مُجرّد (abstract class) لا يُستخدَم لإنشاء كائنات، وإنما كقاعدة لإنشاء أصناف فرعية (subclasses). تُعدّ البرامج الفرعية (subroutines) ضِمْن أي واجهة توابعًا مجردةً (abstract methods)، والتي لابُدّ لأيّ صنف حقيقي (concrete class) يَرغَب بتّنْفيذ تلك الواجهة من أن يُنفِّذها (implement). تستطيع الموازنة بين الواجهة Strokeable والصَنْف المُجرّد (abstract class) التالي: public abstract class AbstractStrokeable { public abstract void stroke(GraphicsContext g); } يَكمُن الفرق بينهما في أن الصنف الذي يَتمدَّد (extend) من الصنف AbstractStrokeable لا يُمكِنه أن يَتَمدَّد من أي صنف آخر. أما الصنف الذي يُنفِّذ الواجهة Strokeable يستطيع أن يَتَمدَّد من أي صنف آخر كما يستطيع أن يُنفِّذ (implement) أي واجهات (interfaces) آخرى. بالإضافة إلى ذلك، يُمكِن لأي صنف مُجرّد (abstract class) أن يَتَضمَّن توابعًا غير مُجرّدة (non-abstract) وآخرى مُجرّدة (abstract). في المقابل، تستطيع أي واجهة (interface) أن تَتَضمَّن توابعًا مُجرّدة فقط، لذا فهي أَشْبه ما تَكُون بصنف مُجرّد نقي (pure). ينبغي أن تُصرِّح عن التوابع ضِمْن أي واجهة (interface) على أساس كَوْنها -أي التوابع- عامة public ومُجردّة abstract. ولمّا كان هذا هو الخيار الوحيد المُتاح، فإن تَخْصِيص هذين المُبدِّلين (modifiers) ضِمْن التّصْريح (declaration) ليس ضروريًا. إلى جانب التّصريح (method declarations) عن التوابع، يُمكِن لأي واجهة (interface) أن تُصرِّح عن وجود مُتْغيِّرات (variable declarations)، وينبغي عندها أن تُصرِّح عنها على أساس كَوْنها عامة public، وساكنة static، ونهائية final، ولذا فإنها تَصيِر عامة وساكنة ونهائية بأي صنف يُنفِّذ (implements) تلك الواجهة. ولمّا كان هذا هو الخيار الوحيد المُتاح للتّصْريح عنها، فإن تَخْصِيص تلك المُبدِّلات (modifiers) ضِمْن التّصْريح (declaration) ليس ضروريًا. اُنظر المثال التالي: public interface ConversionFactors { int INCHES_PER_FOOT = 12; int FEET_PER_YARD = 3; int YARDS_PER_MILE = 1760; } هذه هي الطريقة المناسبة لتعريف (define) ثوابت مُسماة (named constants) يُمكِن اِستخدَامها بعدة أصناف. يُمكِن لأي صنف يُنفِّذ (implements) الواجهة ConversionFactors أن يَستخدِم الثوابت المُعرَّفة بتلك الواجهة (interface) كما لو كانت مُعرَّفة بالصنف. لاحِظ أن أي مُتْغيِّر مُعرَّف ضِمْن واجهة (interface) هو بالنهاية ثابت (constant) وليس مُتْغيِّرًا على الإطلاق. وفي العموم، لا يُمكِن لأي واجهة (interface) أن تُضيف مُتْغيِّرات نُسخ (instance variables) إلى الأصناف التي تُنفِّذها (implement). يُمكِن لأي واجهة (interface) أن تَتَمدَّد (extend) من واجهة واحدة أو أكثر. على سبيل المثال، إذا كان لدينا الواجهة Strokeable المُعطاة بالأعلى، بالإضافة إلى الواجهة Fillable والتي تُعرِّف التابع fill(g)‎، نستطيع عندها تعريف الواجهة التالية: public interface Drawable extends Strokeable, Fillable { // المزيد من التوابع أو الثوابت } ينبغي لأي صَنْف حقيقي (concrete class) يُنفِّذ الواجهة Drawable من أن يُوفِّر الأجزاء التّنْفيذية (implementations) لكُلًا من التابع stroke()‎ من الواجهة Strokeable، والتابع draw()‎ من الواجهة Fillable، بالإضافة إلى أي توابع مُجرّدة (abstract methods) آخرى قد تُخصِّصها الواجهة Drawable مباشرة. عادة ما تُعرَّف (define) الواجهة (interface) ضِمْن ملف ‎.java الخاص بها، والذي لابُدّ أن يَكُون له نفس اسم الواجهة. فمثلًا، تُعرَّف الواجهة Strokeable بملف اسمه Strokeable.java. وبالمثل من الأصناف (classes)، يُمكِن للواجهة (interface) أن تقع ضِمْن حزمة (package)، كما يُمكِنها أن تَستورِد (import) أشياءً من حزم آخرى. التوابع الافتراضية (default methods) بداية من الإصدار الثامن من الجافا، تستطيع الواجهات (interfaces) أن تَتَضمَّن ما يعرف باسم "التوابع الافتراضية (default methods)"، والتي تَملُك جزءًا تّنْفيذيًا (implementation) بعكس التوابع المُجرّدة (abstract methods) المُعتادة. تُورَث التوابع الافتراضية من الواجهات (interfaces) إلى أصنافها المُنفِّذة (implement) بنفس الطريقة التي تُورَث بها التوابع العادية من الأصناف إلى أصنافها الفرعية. لذا عندما يُنفِّذ (implement) صنف معين واجهةً تَحتوِي على توابع افتراضية، فإنه لا يَكُون مضطرًا لأن يُوفِّر جزءًا تّنْفيذيًا (implementation) لأي تابع افتراضي (default method) ضِمْن الواجهة، مع أن بإمكانه القيام بذلك إذا كان لديه تّنْفيذًا (implementation) مُختلفًا. تَدفَع التوابع الافتراضية (default methods) لغة الجافا خطوة للأمام بطريق دَعْم الوراثة المُتعدّدة (multiple inheritance)، ولكنها مع ذلك ليست وراثة مُتعدّدة بحق؛ لأن الواجهات لا تستطيع تعريف مُتْغيِّرات نُسخ (instance variables). تستطيع التوابع الافتراضية (default methods) استدعاء التوابع المجردة (abstract methods) المُعرَّفة بنفس الواجهة، لكنها لا تستطيع الإشارة إلى أي مُتْغيِّر نسخة (instance variable). ملحوظة: تستطيع واجهات نوع الدالة (functional interfaces) أيضًا أن تَحتوِي على توابع افتراضية (default methods) بالإضافة إلى التابع المُجرّد (abstract method) الوحيد الذي بإمكانها تَخْصِيصه. ينبغي أن تُصرِّح عن التوابع الافتراضية (default methods) على أساس كَوْنها عامة public، ولمّا كان ذلك هو الخيار الوحيد المُتاح، فإن تَخْصِيص المبدل public ضِمْن التّصْريح (declaration) ليس ضروريًا. في المقابل، لابُدّ من كتابة المُبدِّل default بشكل صريح أثناء التّصْريح عن أي تابع افتراضي (default method). اُنظر المثال التالي: public interface Readable { // تمثل مصدر إدخال public char readChar(); // اقرأ المحرف التالي المُدْخَل default public String readLine() { //اقرأ حتى نهاية السطر StringBuilder line = new StringBuilder(); char ch = readChar(); while (ch != '\n') { line.append(ch); ch = readChar(); } return line.toString(); } } لابُدّ لأي صنف حقيقي (concrete class) يُنفِّذ الواجهة (interface) -بالأعلى- من أن يُوفِّر تّنْفيذًا (implementation) للتابع readChar()‎. في المقابل، سيَرِث ذلك الصنف تعريف readLine()‎ من الواجهة، ولكنه قد يُوفِّر تعريفًا (definition) جديدًا إذا كان ذلك ضروريًا. عندما يَتَضمَّن صنف معين تّنفيذًا (implementation) لتابع افتراضي (default method)، فإن ذلك التّنْفيذ الجديد يُعيد تعريف (overrides) التابع الافتراضي الموجود بالواجهة (interface). بالمثال السابق، يَستدعِي التابع الافتراضي readLine()‎ التابع المُجرّد readChar()‎، والذي يَتوفَّر تعريفه (definition) فقط من خلال الأصناف المُنفِّذة للواجهة، ولهذا تُعدّ الإشارة إلى readChar()‎ مُتعدِّدة الأشكال (polymorphic). كُتب التّنْفيذ الافتراضي للتابع readLine()‎ بحيث يَكُون ملائمًا لأي صنف يُنفِّذ الواجهة Readable. اُنظر الشيفرة التالية والتي تَتَضمَّن صنفًا يُنفِّذ الواجهة Readable كما يَحتوِي على البرنامج main()‎ لاختبار الصَنْف: public class Stars implements Readable { public char readChar() { if (Math.random() > 0.02) return '*'; else return '\n'; } public static void main(String[] args) { Stars stars = new Stars(); for (int i = 0 ; i < 10; i++ ) { // اِستدعي التابع الافتراضي String line = stars.readLine(); System.out.println( line ); } } } تُوفِّر التوابع الافتراضية (default methods) إمكانية شبيهة لما يُعرَف باسم "المخلوط (mixin)" المُدعَّم ببعض اللغات البرمجية الآخرى، والتي تَعنِي المقدرة على خَلْط وظائف مصادر آخرى إلى داخل الصنف. لمّا كان بإمكان أي صنف أن يُنفِّذ أي عدد من الواجهات (interfaces)، فإنه يستطيع خلط وظائف عدة مصادر آخرى مختلفة. الواجهات كأنواع كما هو الحال مع الأصناف المُجرّدة (abstract classes)، لا يُمكِنك إنشاء كائن فعليّ من واجهة (interface)، ولكن تستطيع التّصْريح (declare) عن مُتْغيِّر نوعه عبارة عن واجهة. لنَفْترِض مثلًا أن لدينا الواجهة Strokeable المُعرَّفة بالأعلى، ويُنفِّذها كُلًا من الصنفين Line و Circle، يُمكِنك عندها كتابة التالي: // صرح عن متغير من النوع‫ Strokeable والذي يمكنه الإشارة إلى أي // كائن ينفذ تلك الواجهة Strokeable figure; figure = new Line(); // ‫يشير إلى كائن من الصنف Line figure.stroke(g); // ‫اِستدعي التابع stroke() من الصنف Line figure = new Circle(); // ‫يشير الآن إلى كائن من الصنف Circle figure.stroke(g); // ‫اِستدعي التابع stroke() من الصنف Circle يُمكِن لأي مُتْغيِّر من النوع Strokeable أن يُشير إلى أي كائن طالما كان صَنْفه يُنفِّذ الواجهة Strokeable. لمّا كان figure مُتْغيِّرًا من النوع Strokeable، ولأن أي كائن من النوع Strokeable يَحتوِي على التابع stroke()‎، فحتمًا سيَحتوِي الكائن الذي يُشير إليه المُتْغيِّر figure على التابع stroke()‎، ولهذا فإن التَعْليمَة figure.stroke(g)‎ صالحة تمامًا. تُستخدَم الأنواع (types) في العموم إما للتّصْريح (declare) عن مُتْغيِّر، أو لتَخْصِيص نوع معامل برنامج فرعي (routine)، أو لتَخْصِيص النوع المُعاد (return type) من دالة (function). النوع في العموم إما أن يَكُون صنفًا أو واجهة (interface) أو أحد الأنواع البسيطة (primitive) الثمانية المَبنية مُسْبَقًا (built-in). ليس هنالك من أيّ احتمال آخر، ربما باستثناء بعض الحالات الخاصة كأنواع التعداد (enum) والتي هي بمثابة نوع خاص من الأصناف. من بين كل تلك الأنواع، الأصناف هي الوحيدة التي يُمكِن اِستخدَامها لإنشاء كائنات (objects). يُمكِنك أيضًا اِستخدَام الواجهات (interface) لتَخْصِيص النوع الأساسي (base type) لمصفوفة. فمثلًا، تستطيع أن تُصرِّح عن مُتْغيِّر أو أن تُنشِئ مصفوفة باستخدام نوع المصفوفة Strokeable[]‎، وفي تلك الحالة، يُمكِن لعناصر تلك المصفوفة الإشارة إلى أي كائن طالما كان يُنفِّذ الواجهة Strokeable. اُنظر الشيفرة التالية: Strokeable[] listOfFigures; listOfFigures = new Strokeable[10]; listOfFigures[0] = new Line(); listOfFigures[1] = new Circle(); listOfFigures[2] = new Line(); . . . تَملُك جميع عناصر تلك المصفوفة التابع stroke()‎، مما يَعنِي إمكانية كتابة تعبيرات مثل listOfFigures.stroke(g)‎. ترجمة -بتصرّف- للقسم Section 7: Interfaces من فصل Chapter 5: Programming in the Large II: Objects and Classes من كتاب Introduction to Programming Using Java.
  3. تُعدّ الأفكار الأساسية التي تَرتَكِز عليها البرمجة كائنية التوجه (object-oriented programming) واضحة وبسيطة على نحو معقول، ومع ذلك فإنها ستَأخُذ منك بعض الوقت لكي تَعتَاد عليها تمامًا. بالإضافة إلى ذلك، هنالك الكثير من التفاصيل الدقيقة وراء تلك الأفكار الأساسية والتي قد تَكُون مزعجة في بعض الأحيان. سنُحاوِل أن نُغطِي بعضًا منها بالجزء المُتبقِي من هذا الفصل، وتَذَكَّر أنه ليس ضروريًا أن تَتَمكَّن من إِتقان كل تلك التفاصيل خاصة عند قرائتها لأول مرة. سنَفْحَص بهذا القسم تحديدًا المُتْغيِّرين this و super المُعرَّفين تلقائيًا بأي تابع نسخة (instance method) أو بَانِي كائن (constructor). المتغير الخاص this ما الذي يعنيه اِستخدَام مُعرِّف بسيط مثل amount أو process()‎ للإشارة إلى مُتْغيِّر أو تابع؟ تعتمد الإجابة بشكل أساسي على قواعد النطاق (scope rules)، والتي تُوضِح الأماكن التي يُمكِنها الوصول إلى أيّ مُتْغيِّر أو تابع قد صَرَّح عنه البرنامج وكيفية القيام بذلك. فمثلًا، قد يُشير اسم بسيط لمُتْغيِّر بتعريف تابع (method definition) إلى مُتْغيِّر محليّ (local variable) أو مُعامِل (parameter) في حالة وجود أي منهما ضِمْن النطاق (scope)، أيّ إن كان التَّصْريح (declaration) عن أيّ منهما ما يزال فعالًا بمكان حُدوث تلك الإشارة بالشيفرة، فإذا لم يَكُن كذلك، فإن ذلك الاسم لابُدّ وأنه يُشير إلى مُتْغيِّر عضو (member variable) مُعرَّف بنفس الصنف الذي حدثت فيه تلك الإشارة. بالمثل، أي اسم بسيط لتابع لابُدّ وأنه يُشير إلى تابع (method) مُعرَّف بنفس الصنف. يَملُك أي عضو ساكن (static member) بصنف اسمًا بسيطًا يُمكِن اِستخدَامه داخل تعريف الصَنْف (class definition) فقط. في المقابل، يُستخدَم الاسم الكامل للعضو . للإشارة إليه من خارج الصَنْف. فمثلًا، Math.PI هو مُتْغيِّر عضو ساكن (static member variable) اسمه البسيط هو PI داخل الصنف Math. لاحِظ أنه من المُمكن دومًا الإشارة إلى أي عضو ساكن باِستخدَام اسمه الكامل حتى من داخل الصنف المُعرَّف بداخله بل قد يَكُون ذلك ضروريًا في بعض الأحيان، مثلًا عندما يَكُون الاسم البسيط للمُتغير الساكن مخفيًا نتيجة وجود مُتْغيِّر محليّ (local variable) أو مُعامِل (parameter) بنفس الاسم. بالمثل، يَملُك أي عضو نسخة (instance member)، سواء كان مُتْغيِّر نسخة (instance variable) أو تابع نسخة (instance method)، اسمًا بسيطًا يُمكِن اِستخدَامه بتوابع النسخ (instance methods) -لا التوابع الساكنة (static methods)- بالصنف المُعرَّف بداخله. تَذَكَر أن أي مُتْغيِّر نسخة أو تابع نسخة هو جزء من الكائن ذاته وليس صَنْفه، فكل كائن يَمتلك نسخة خاصة به من ذلك المُتْغيِّر أو التابع. بالإضافة إلى الاسم البسيط، يَملُك أي عضو نسخة (instance member) اسمًا كاملًا، شطره الأول عبارة عن مَرجِع (reference) يُشير إلى الكائن المُتضمِّن لذاك العضو. مثلًا، إذا كان std مُتْغيِّر يُشير إلى كائن من النوع Student، فقد تُمثِل std.test1 الاسم الكامل لمُتْغيِّر نسخة (instance variable) اسمه test1 مُعرَّف ضِمْن الكائن. إذًا، عندما تَستخدِم الاسم البسيط لمُتْغيِّر نُسخة (instance variable) مثل test1 بداخل صنف معين، أين الكائن الذي يَتَضمَّن ذلك المُتْغيِّر؟ في الواقع، الأمر بسيط نوعًا ما. لنَفْترِض أنك أَشرت إلى الاسم test1 بتعريف تابع نسخة (instance method) معين، والذي هو بالتأكيد جزء من كائن من النوع Student. عندما يُنفَّذ ذلك التابع، فإن الاسم البسيط test1 يُشير عندها إلى المُتْغيِّر test1 المُعرَّف بذلك الكائن. في الواقع، هذا هو السبب وراء عدم إمكانية استخدام الأسماء البسيطة لأعضاء النسخ (instance members) ضِمْن أي تابع ساكن (static methods)؛ لأنه وبينما يُنفَّذ ذلك التابع الساكن، لا يَكُون هناك أي كائن، ومن ثم لا يَكُون هناك أي أعضاء نُسخ (instance members). يَضَعنا هذا أمام السؤال التالي: كيف نَستخدِم الاسم الكامل لعضو نسخة (instance members) داخل نفس الصَنْف المُعرَّف بداخله؟ نُريد ببساطة طريقة تُمكِّنا من الإشارة إلى "نفس ذات الكائن الحاضن للتابع". في الواقع، تُوفِّر الجافا المُتْغيِّر الخاص this لهذا الغرض. تستطيع اِستخدَام المُتْغيِّر this بتابع نسخة معين (instance method)؛ للإشارة إلى نفس ذات الكائن الحاضن لنفس ذات التابع. فمثلًا، إذا كان var هو مُتْغيِّر نسخة (instance variable) مُعرَّف بنفس ذات الكائن الحاضن لنفس ذات التابع، فإن this.var هو الاسم الكامل لذلك المُتْغيِّر. وبالمثل، إذا كان otherMethod()‎ هو تابع نسخة (instance method) بنفس ذات الكائن، فيُمكِن اِستخدَام this.otherMethod()‎ لاستدعاء ذلك التابع. عندما يُنفِّذ الحاسوب تابع نسخة (instance method) معين، فإنه تلقائيًا يَضَع المُتْغيِّر this بحيث يُشير إلى نفس ذات الكائن الحاضن للتابع. تَستخدِم بعض اللغات البرمجية كائنية التوجه (object oriented) الاسم self بدلًا من this. يُرَى عندها الكائن ككيان يَستقبِل الرسائل، ويَرُد عليها بتَّنْفيذ أمر معين. وفقًا لذلك الكيان، يُشير مُتْغيِّر نسخة (instance variable) مثل self.name إلى نسخة name الخاصة بالكيان، بينما يَكُون استدعاء تابع نسخة (instance method) مثل self.redraw()‎ بمثابة قوله "اِرسِل إلى نفسي رسالة redraw". يَشيع استخدام المُتْغيِّر الخاص this ببواني الكائنات (constructors)، كالتالي: public class Student { private String name; // اسم الطالب public Student(String name) { this.name = name; } . . // المزيد من المتغيرات والتوابع . } بباني الكائن (constructor) -بالأعلى-، أَصبَح مُتْغيِّر النسخة (instance variable)‏ name مَخفيًّا نتيجة اِستخدَام مُعامِل صُّوريّ (formal parameter) يَحمِل نفس الاسم name. مع ذلك، تستطيع اِستخدَام اسمه الكامل this.name للإشارة إليه. يُشير الاسم name على يمين تَعْليمَة الإِسْناد this.name = name إلى المُعامِل الصُّوريّ (formal parameter)، وتُسنَد قيمته إلى مُتْغيِّر النسخة this.name. يَشيِع اِستخدَام تلك الصياغة، وهي مقبولة في العموم؛ فليس هناك أي حاجة لاختيار أسماء جديدة للمُعامِلات الصُّوريّة المُستخدَمة فقط لتهيئة مُتْغيِّرات النُسخ (instance variables)، لذا اِستخدِم نفس الاسم لكليهما ببساطة. يُستخدَم المُتْغيِّر الخاص this لأغراض آخرى أيضًا. على سبيل المثال، بينما تَكْتُب تابع نسخة (instance method)، قد تحتاج إلى تمرير الكائن الحاضن للتابع إلى برنامج فرعي (subroutine) كمُعامِل فعليّ (actual parameter). في تلك الحالة، تستطيع ببساطة تمرير this. مثلًا، يُستخدَم System.out.println(this);‎ لطباعة تمثيل نصي (string representation) للكائن. بالإضافة إلى ذلك، قد تُسنِد this إلى مُتْغيِّر آخر ضِمْن تَعْليمَة إِسْناد (assignment statement) أو تُخزِّنه بمصفوفة. وفي العموم، تستطيع أن تَفعَل به أي شيء يُمكِنك فعله مع أي مُتْغيِّر آخر باستثناء تَعْدِيل قيمته (اِفْترِض أنه مُتْغيِّر نهائي مُعرَّف باِستخدَام المُبدِّل final). المتغير الخاص super تُعرِّف الجافا المُتْغيِّر الخاص super للاِستخدَام بتعريفات توابع النُسخ (instance methods) بالأصناف الفرعية (subclasses) تحديدًا. بالمثل من this، يُشير المُتْغيِّر الخاص super إلى نفس ذات الكائن الحاضن للتابع، ولكنه يَنسَى -إذا جاز التعبير- أن ذلك الكائن ينتمي إلى صَنْفه الفرعي (subclass)، ويَتَذكَّر فقط انتماءه إلى صَنْفه الأعلى (superclass). الفكرة ببساطة كالتالي: قد يَتَضمَّن الصَنْف الفرعي بعض الإضافات إلى الصَنْف الأعلى (superclass) أو حتى بعض التَعْديلات عليه، فكانت الحاجة إلى وجود طريقة للإشارة إلى التوابع (methods) والمُتْغيِّرات المُعرَّفة بالصَنْف الأعلى (superclass)، وهذا ما يُوفِّره المُتْغيِّر super فهو ببساطة لا يَعلَم أي شيء عن تلك الإضافات والتعديلات. لنَفْترِض أن لديك صَنْف فرعي يَحتوِي على تابع نسخة (instance method) اسمه doSomething()‎، ثُمَّ كَتبَت بأحد توابعه تَعْليمَة استدعاء البرنامج الفرعي التالية super.doSomething()‎. لمّا كان المُتْغيِّر super لا يَعلَم أي شيء عن التابع doSomething()‎ المُعرَّف بالصنف الفرعي، فهو فقط يَعلَم الأشياء المُعرَّفة بالصنف الأعلى (superclass)، فإنه سيُحاوِل تَّنْفيذ تابع اسمه doSomething()‎ من الصَنْف الأعلى (superclass)، فإذا لم يَجِدْه، أيّ في حالة كان التابع doSomething()‎ إضافةً وليس تعديلًا، فستَحصُل على خطأ في بناء الجملة (syntax error). وفَّرت الجافا المُتْغيِّر الخاص super لكي تَتَمكَّن من الوصول إلى أشياء كانت قد عُرِّفت بالصنف الأعلى (superclass)، ولكنها أصبحت مَخفيّة بواسطة بعض الأشياء المُعرَّفة بالأصناف الفرعية (subclasses). مثلًا، دائمًا ما سيُشير الاسم super.var إلى مُتْغيِّر النسخة (instance variable)‏ var المُعرَّف بالصنف الأعلى (superclass). يَكُون ذلك مفيدًا في بعض الحالات كالتالي: إذا اِحتوَى صنف معين على مُتْغيِّر نسخة (instance variable) له نفس اسم مُتْغيِّر نسخة بصنفه الأعلى (superclass)، فعندها سيَحتوِي أي كائن من الصَنْف الفرعي على مُتْغيِّرين لهما نفس الاسم، الأول مُعرَّف كجزء من الصنف نفسه، والآخر مُعرَّف كجزء من الصنف الأعلى، أيّ لا يَحلّ المُتْغيِّر بالصنف الفرعي (subclass) مَحلّ المُتْغيِّر بالصَنْف الأعلى حتى لو كان لهما نفس الاسم وإنما يُخفيه فقط، وعليه ما يزال المُتْغيِّر من الصنف الأعلى قابلًا للوصول باِستخدَام super. بالمثل، عندما يَتَضمَّن صنف فرعي (subclass) تابع نسخة (instance method) له نفس بصمة (signature) تابع مُعرَّف بالصنف الأعلى (superclass)، فإن التابع من الصنف الأعلى يُصبِح مَخفيًّا بنفس الطريقة، ويُقال عندها أن التابع من الصنف الفرعي أعاد تعريف (override) التابع من الصنف الأعلى. ومع ذلك، يُمكِن اِستخدَام المُتْغيِّر الخاص super للوصول إلى التابع من الصنف الأعلى (superclass). غالبًا ما يُستخدَم المُتْغيِّر super لتمديد (extend) سلوك تابع موروث بدلًا من أن يستبدله بالكامل، وذلك بكتابة تابع جديد يُعيد تعريف (override) التابع الأصلي. يَتَضمَّن التابع الجديد استدعاءً لتابع الصنف الأعلى (superclass) من خلال المُتْغيِّر super إلى جانب الشيفرة الجديدة المطلوب إضافتها بالأساس. لنَفْترِض مثلًا أن لدينا الصنف PairOfDice والذي يَتَضمَّن تابعًا اسمه roll()‎، والآن نريد إنشاء صنف فرعي GraphicalDice ليُمثِل حجري نَّرد مرسومين على الشاشة. ينبغي للتابع roll()‎ المُعرَّف بالصنف الفرعي GraphicalDice أن يُنفِّذ كل شيء يقوم به التابع roll()‎ المُعرَّف بالصنف الأعلى PairOfDice، وهو ما يُمكِننا التعبير عنه بالاستدعاء super.roll()‎، والذي سيَستدعِي نسخة التابع بالصنف الأعلى (superclass). بالإضافة إلى ذلك، ينبغي للتابع roll()‎ المُعرَّف بالصنف الفرعي GraphicalDice أن يُعيد رسم حجرى النَّرد لإظهار القيم الجديدة. يُمكِننا تعريف الصنف GraphicalDice كالتالي: public class GraphicalDice extends PairOfDice { public void roll() { super.roll(); // ‫استدعي roll من الصنف PairOfDice redraw(); // استدعي تابع لرسم حجري النرد } . . // ‫المزيد بما يتضمن تعريف redraw() . } يَسمَح ذلك عمومًا بتمديد (extend) سلوك التابع roll()‎ حتى لو لم تَكُن على علم بطريقة تَّنْفيذه (implementation) بالصنف الأعلى. super و this كبواني كائن لا تُورَث بواني الكائن (constructors) عمومًا، أي أنك إذا أنشأت صنفًا فرعيًا (subclass) من صنف موجود، فإن بواني الكائن المُعرَّفة بالصنف الأعلى (superclass) لا تُصبِح جزءًا من الصنف الفرعي. يُمكِنك إضافة بواني جديدة بالصنف الفرعي إذا أردت، وفي حالة عدم إضافتها، سيُنشِئ الحاسوب بَانِي كائن افتراضي (default constructor) بدون أي مُعامِلات (parameters). إذا كان الصنف الأعلى (superclass) يَحتوِي على باني كائن (constructor) يُنفِّذ الكثير من التهيئة الضرورية، فيبدو أنك ستَضطرّ إلى تَكْرار كل ذلك العمل بالصنف الفرعي. بالإضافة إلى ذلك، قد لا تَملُك شيفرة الصنف الأعلى (superclass)، ولا تَعلَم حتى طريقة تَّنْفيذها (implementation)، وهو ما قد يُمثِل مشكلة حقيقية، والتي قد تُصبِح مستحيلة إذا كان الباني المُعرَّف بالصنف الأعلى يَستخدِم مُتْغيِّرات أعضاء خاصة (private member variables)، ففي تلك الحالة، أنت لا تَملُك حتى صلاحية للوصول إليها من الصنف الفرعي. لابُدّ إذًا من وجود طريقة لإصلاح ذلك. في الواقع، قد يُستخدَم المُتْغيِّر الخاص super كأول تَعْليمَة بالباني (constructor) المُعرَّف بالصنف الفرعي؛ لكي يَستدعِي الباني (constructor) المُعرَّف بالصنف الأعلى (superclass). قد تَكُون طريقة كتابة ذلك مُضللة نوعًا ما؛ فهي تبدو كما لو كنت تَستدعِي super كبرنامج فرعي (subroutine) على الرغم من أنه ليس كذلك، وفي العموم لا يُمكِنك استدعاء البواني (constructors) بنفس طريقة استدعاء البرامج الفرعية. والآن، لنَفْترِض أن الصنف PairOfDice يَتَضمَّن باني كائن (constructor) يَستقبِل مُعامِلين (parameters) من النوع int، يُمكِننا إذًا أن نُعرِّف صنفًا فرعيًا (subclass) منه كالتالي: public class GraphicalDice extends PairOfDice { public GraphicalDice() { // باني كائن لهذا الصنف // استدعي الباني من الصنف الأساسي بقيم المعاملات 3 و 4 super(3,4); initializeGraphics(); // نفذ التهيئة الخاصة بالصنف الفرعي } . . // المزيد من البواني والمتغيرات والتوابع . } تَستدعِي التَعْليمَة super(3,4);‎ الباني (constructor) المُعرَّف بالصنف الأعلى. لابُدّ من كتابته بالسطر الأول من الباني المُعرَّف بالصنف الفرعي (subclass)، وفي حالة عدم استدعاء أي باني من الصنف الأعلى بصورة صريحة، يُستدعَى تلقائيًا بَانِي الصنف الأعلى الافتراضي (default constructor) أي ذلك الذي لا يَستقبِل أي مُعامِلات (parameters)، وإذا لم يَكُن موجودًا، سيُبلِّغ المُصرِّف (compiler) عن وجود خطأ في بناء الجملة (syntax error). يُستخدَم المُتْغيِّر الخاص this بنفس الطريقة تمامًا، ولكن لاستدعاء باني (constructor) آخر ضِمْن نفس الصنف. سيَبدُو عندها السطر الأول من الباني كما لو كان استدعاءً لبرنامج فرعي اسمه هو this، وسيُنفَّذ عندها مَتْن الباني المُستدعَى مما يُجنّبك تكرار نفس الشيفرة ضِمْن عدة بواني مختلفة. على سبيل المثال، يُستخدَم الصنف MosaicCanvas -من القسم ٤.٧- لتمثيل شبكة من المستطيلات الملونة، ويَتَضمَّن باني يَستقبِل أربعة مُعامِلات (parameters) كالتالي: public MosaicCanvas(int rows, int columns, int preferredBlockWidth, int preferredBlockHeight) يَستقبِل ذلك الباني مجموعة من الخيارات يُنفِّذ على أساسها الكثير من التهيئة (initialization). قد تحتاج إلى إضافة بواني (constructors) آخرى بعدد أقل من الخيارات لتَكُون أسهل في الاِستخدَام، ولكن ما يزال تَّنْفيذ التهيئة كاملة أمرًا ضروريًا. يَحتوِي الصنف على البواني التالية: public MosaicCanvas() { this(42,42); } public MosaicCanvas(int rows, int columns) { this(rows,columns,16,16); } يَستدعِى كل باني كائن (constructor) منهما باني كائن آخر بالإضافة إلى توفير قيم ثابتة للمُعامِلات الآخرى التي لم يَستقبِلها. على سبيل المثال، يَستدعِي التعبير this(42,42)‎ الباني الثاني، والذي بدوره يَستدعِي الباني الأساسي ذو المُعامِلات الأربعة. في الواقع، يُستدعَى الباني الأساسي بجميع الحالات وذلك لضمان تَّنْفيذ التهيئة (initialization) الضرورية كاملةً. ترجمة -بتصرّف- للقسم Section 6: this and super من فصل Chapter 5: Programming in the Large II: Objects and Classes من كتاب Introduction to Programming Using Java.
  4. تُستخدَم الأصناف (classes) لتمثيل مجموعة كائنات (objects) تَتَشارَك نفس البِنْية (structure) والسلوك (behavior). يُحدِّد الصَنْف بِنْية تلك الكائنات عن طريق تَخْصِيصه للمُتْغيِّرات الموجودة ضِمْن أي نسخة (instance) من الصَنْف، كما يُحدِّد سلوكها بتَعْريفه لتوابع نُسخ (instance methods) تُعبر عن سلوك (behavior) تلك الكائنات. هذه الفكرة قوية للغاية، ولكنها مع ذلك مُمكِنة بغالبية اللغات البرمجية التقليدية، فما هي إذًا الفكرة الجديدة التي تُقدِّمها البرمجة كائنية التوجه (object-oriented programming)، وتُفرِّقها عن البرمجة التقليدية؟ في الواقع، تُدعِّم البرمجة كائنية التوجه كُلًا من الوراثة (inheritance) والتعددية الشكلية (polymorphism)، والتي تسمح للأصناف (classes) بتمثيل التشابهات بين مجموعة كائنات تَتَشارَك بعضًا من بِنْيتها وسُلوكها، وليس كلها. توسيع (extend) الأصناف الموجودة لابُدّ لأي مبرمج أن يفهم المقصود بكُلًا من المفاهيم التالية: الصَنْف الفرعي (subclass)، والوراثة (inheritance)، والتعددية الشكلية (polymorphism). قد تَستغرِق بعض الوقت حتى تستطيع تَوْظِيف مواضيع مثل الوراثة (inheritance) تَوْظِيفًا فعليًا بعيدًا عن مُجرَّد تَمْديد (extending) بعض الأصناف الموجودة، وهو ما سنُناقشه بالجزء الأول من هذا القسم. ببداية تَعامُل المبرمجين مع الكائنات، عادةً ما يَقْتصر اِستخدَامهم للأصناف الفرعية على موقف واحد فقط: "التَعْديل على صَنْف موجود بالفعل أو الإضافة إليه بحيث يَتلائَم مع ما يُريدونه". يُعدّ ذلك أكثر شيوعًا من تصميم مجموعة كاملة من الأصناف الأعلى والأصناف الفرعية (subclasses). تستطيع أن تُمدِّد (extend) صَنْف فرعي من صَنْف آخر باستخدام الصيغة التالية: public class <subclass-name> extends <existing-class-name> { . . // التعديلات والإضافات . } لنَفْترِض أنك تُريد كتابة برنامج للعبة الورق "بلاك جاك (Blackjack)" تَستخدِم فيه الأصناف Card و Hand و Deck من القسم ٥.٤. أولًا، تَختلِف اليد (hand) بلعبة بلاك جاك نوعًا ما عن اليد في العموم؛ حيث تُحسَب قيمة اليد بلعبة بلاك جاك وِفقًا لقواعد اللعبة، والتي يُمكِن تَلخَّيصها كالتالي: تُحسَب قيمة اليد بجَمْع قيم ورق اللعب باليد، بحيث تَكُون قيمة ورقة اللعب العددية (numeric card)، مثل "ثلاثة" أو "عشرة"، مُساوِية لنفس قيمتها العددية، أما ورقة اللعب المُصورة، مثل "الرجل" أو "الملكة" أو "الملك"، فقيمتها تُساوِي عشرة. وأخيرًا، قيمة ورقة "الآص" قد تُساوِي ١ أو ١١. إنها في العموم تُساوِي ١١ إلا لو أدى ذلك إلى قيمة كلية أكبر من ٢١، مما يَعنِي أن ورقة "الآص" الثانية والثالثة والرابعة قيمها بالضرورة ستُساوِي ١. أحد الطرائق لمعالجة ما سبق هو توسيع (extend) الصنف Hand الموجود بإضافة تابع (method) يَحسِب قيمة اليد وفقًا للعبة بلاك جاك. اُنظر تعريف (definition) الصنف: public class BlackjackHand extends Hand { /** * Computes and returns the value of this hand in the game * of Blackjack. */ public int getBlackjackValue() { int val; // قيمة اليد boolean ace; int cards; // عدد ورق اللعب باليد val = 0; ace = false; cards = getCardCount(); // تابع معرف بالصنف الأعلى for ( int i = 0; i < cards; i++ ) { // أضف قيمة ورقة اللعب الحالية Card card; // ورقة اللعب الحالية int cardVal; // قيمة بلاك جاك لورقة اللعب الحالية card = getCard(i); cardVal = card.getValue(); // قيمة ورقة اللعب if (cardVal > 10) { cardVal = 10; } if (cardVal == 1) { ace = true; // هناك على الأقل ورقة آص } val = val + cardVal; } if ( ace == true && val + 10 <= 21 ) val = val + 10; return val; } // نهاية‫ getBlackjackValue } // نهاية الصنف‫ BlackjackHand لمّا كان الصَنْف BlackjackHand هو صَنْف فرعي (subclass) من الصَنْف الأعلى Hand، فإن أيّ كائن من الصَنْف الفرعي BlackjackHand سيَتَضمَّن، إلى جانب تابع النسخة getBlackjackValue()‎ المُعرَّف ضِمْن صَنْفه الفرعي، جميع مُتْغيِّرات النسخ (instance variables) وتوابع النسخ (instance methods) المُعرَّفة بالصَنْف الأعلى Hand. فمثلًا، إذا كان bjh مُتْغيِّرًا من النوع BlackjackHand، يَصِِح اِستخدَام أي من الاستدعاءات التالية: bjh.getCardCount()‎ و bjh.removeCard(0)‎ و bjh.getBlackjackValue()‎. على الرغم من أن التابعين الأول والثاني مُعرَّفين بالصَنْف Hand، إلا أنهما قد وُرِثَا إلى الصَنْف BlackjackHand. تُورَث المُتْغيِّرات والتوابع من الصَنْف الأعلى Hand إلى الصَنْف الفرعي BlackjackHand، ولهذا تستطيع اِستخدَامها بتعريف الصَنْف الفرعي BlackjackHand كما لو كانت مُعرَّفة فعليًا بذلك الصَنْف، باستثناء تلكم المُعرَّفة باِستخدَام المُبدِّل private ضِمْن الصَنْف الأعلى؛ حيث يَمنَع ذلك المُبدِّل الوصول إليها حتى من الأصناف الفرعية (subclasses). مثلًا، بتعريف التابع getBlackjackValue()‎ -بالأعلى-، تَمَكَّنت التَعْليمَة cards = getCardCount();‎ من استدعاء تابع النسخة getCardCount()‎ المُعرَّف بالصَنْف Hand. يُساعد تَمْديد (extend) الأصناف على الاعتماد على أعمالك السابقة، وفي الواقع لقد كُتبَت الكثير من الأصناف القياسية (standard classes) خصيصًا لتَكُون قاعدةً وأساسًا لإنشاء أصناف فرعية. تُستخدَم مُبدِّلات الوصول (access modifiers) مثل public و private للتَحكُّم بالوصول إلى أعضاء (members) الصَنْف. عندما نَأخُذ الأصناف الفرعية (subclasses) بالحُسبان، يُصبِح من المناسب الحديث عن مُبدِّل وصول آخر تُوفِّره الجافا، هو protected. عند اِستخدَام ذلك المُبدِّل أثناء التَّصْريح عن تابع (method) أو مُتْغيِّر عضو (member variable) بصَنْف، يُصبِح اِستخدَام ذلك العضو مُمكنًا ضِمْن الأصناف الفرعية لذلك الصَنْف وليس بأي مكان آخر. هنالك استثناء: عندما تَستخدِم المُبدِّل protected للتّصريح عن عضو (member)، فإنه يَكُون قابلًا للوصول من أي صَنْف بنفس الحزمة (package). ذَكَرَنا من قَبْل أنه في حالة عدم تخصيص أي مُبدِّل وصول (access modifier) لعضو معين، فإنه يُصبِح قابلًا للوصول من جميع الأصناف الواقعة ضِمْن نفس الحزمة وليس بأي مكان آخر. يُعدّ المُبدَّل protected أكثر تحررًا من ذلك بقليل؛ فبالإضافة إلى الأصناف الواقعة ضِمْن نفس الحزمة، فإنه يجعل العضو أيضًا قابلًا للوصول من الأصناف الفرعية (subclasses) التي لا تقع ضِمْن نفس الحزمة. عندما تُصرِّح عن تابع أو مُتْغيِّر عضو باستخدام المُبدِّل protected، يُصبِح ذلك العضو جزءًا من تّنْفيذ (implementation) الصنف لا واجهته (interface) العامة، كما يُسمَح للأصناف الفرعية (subclasses) باِستخدَام ذلك الجزء من التّنْفيذ (implementation) أو تَعْدِيله. على سبيل المثال، يَتَضمَّن الصَنْف PairOfDice مُتْغيِّري النسخة die1 و die2 لتمثيل الأعداد الظاهرة على حجرى نَّرد. قد نُصرِّح عنهما باِستخدَام المُبدِّل private لكي يُصبِح تَعْديل قيمتهما من خارج الصنف مستحيلًا، ثُمَّ نُعرِّف توابع جَلْب (getter methods) للسماح بقراءة قيمتهما من خارج الصنف، لكن لو تَبيَّن لك إمكانية الحاجة لاِستخدَام الصنف PairOfDice لإنشاء أصناف فرعية (subclasses)، فقد يَكُون من الأفضل حينها السماح لتلك الأصناف الفرعية (subclasses) بتَعْديل قيم تلك الأعداد، أي تعديل قيم تلك المُتْغيِّرات. فمثلًا، يَرسم الصنف الفرعي GraphicalDice حجري النَّرد، وقد يَضطرّ إلى تعديل قيم تلك الأعداد بتوقيت غَيْر ذلك الذي يُرمَي فيه حجري النَّرد. بدلًا من اِستخدَام المُبدِّل public في تلك الحالة، قد نَستخدِم المُبدِّل protected للتَّصْريح عن كُلًا من die1 و die2، مما سيَسمَح للأصناف الفرعية فقط -دون العالم الخارجي- بتَعْديل قيمتهما. كبديل، قد تُفضِّل تعريف توابع ضَبْط (setter methods) لتلك المُتْغيِّرات ضِمْن الصنف الأعلى، بحيث يُصرَّح عنها باِستخدَام المُبدِّل protected، وذلك لكي تَتَمكَّن من التَأكُّد من أن القيمة المطلوب إِسْنادها للمُتْغيِّر تقع ضِمْن نطاق يتراوح بين ١ و ٦. الوراثة (inheritance) وسلالة أصناف (class hierarchy) يُمكِن لصَنْف معين أن يَرِث جزء أو كل بِنيته (structure)، وسلوكه (behavior) من صَنْف آخر، وهو ما يُعرَف باسم الوراثة (inheritance). يُقال عن الصنف الوَارِث أنه صنفًا فرعيًا (subclass) من الصنف المَورُوث. إذا كان الصنف B هو صنف فرعي من الصنف A، فإننا نستطيع أيضًا أن نقول أن الصنف A هو صنف أعلى (superclass) من الصنف B. قد يُضيف الصنف الفرعي إلى ما وَرِثه من بنية وسلوك، كما قد يُعدِّل أو يَستبدِل ما وَرِثه من سلوك. تَستخدِم بعض اللغات البرمجية الآخرى، مثل لغة C++‎، مصطلحات أخرى كالصنف المُشتقّ (derived class) والصنف الأساسي (base class) بدلًا من الصنف الفرعي (subclass) والصنف الأعلى (superclass). عادةً ما تُوضَح العلاقة بين الصنف الفرعي (subclass) والصنف الأعلى (superclass) برسم توضيحي، يقع فيه الصنف الفرعي (subclass) أسفل صنفه الأعلى (superclass) ويَكُون مُتصلًا به، تمامًا كما هو مُوضَح بيسار الصورة التالية: تُستخدَم الشيفرة التالية لإنشاء صَنْف اسمه B بحيث يكون صنفًا فرعيًا من صنف آخر اسمه A: class B extends A { . . // إضافات أو تعديلات على الموروث من الصنف‫ A . } يُمكِنك أن تُصرِّح عن عدة أصناف على أساس كَوْنها أصناف فرعية (subclasses) من نفس الصنف الأعلى (superclass)، وتُعدّ الأصناف الفرعية في تلك الحالة "أصناف أخوة (sibling classes)" تَتَشارَك بعضًا من بِنيتها (structure) وسلوكها (behavior)، تَحْديدًا تلك المُوروثة من الصَنْف الأعلى المشترك، فمثلًا، الأصناف B و C و D بيمين الصورة السابقة هي أصناف أخوة (sibling classes). يُمكِن للوراثة (inheritance) أن تتمدَّد أيضًا عَبْر أجيال من الأصناف، فمثلًا، الصنف E -بالصورة السابقة- هو صنف فرعي من الصنف D، والذي هو بدوره صنف فرعي من الصنف A، ويُعدّ الصنف E عندها صنفًا فرعيًا من الصنف A حتى وإن لَمْ يَكُن صنفًا فرعيًا مباشرًا. تُشكِّل مجموعة الأصناف تلك سُلالة أصناف (class hierarchy) صغيرة. مثال والآن، لنَفْترِض أننا نريد كتابة برنامج ينبغي له التَعامُل مع المركبات المتحركة كالسيارات، والشاحنات، والدراجات النارية (قد يَستخدِمه قسم المركبات المتحركة لأغراض تَعقُّب التسجيلات). يُمكِننا أن نُعرِّف صَنْفًا اسمه Vehicle لتمثيل جميع أنواع المركبات، ولأن السيارات والشاحنات والدراجات النارية هي أنواع من المركبات، يُمكِننا تمثيلها باستخدام أصناف فرعية (subclasses) من الصنف Vehicle، كما هو مُوضَح بسُلالة الأصناف (class hierarchy) التالية: قد يَتَضمَّن الصنف Vehicle مُتْغيِّرات نُسخ (instance variables) مثل registrationNumber و owner، بالإضافة إلى توابع نُسخ (instance methods) مثل transferOwnership()‎. ستَملُك جميع المركبات تلك المُتْغيِّرات والتوابع. نستطيع الآن تعريف الأصناف الفرعية (subclasses) الثلاثة Car و Truck و Motorcycle المُشتقّة من الصَنْف Vehicle بحيث يَحمِل كُلًا منها مُتْغيِّرات وتوابع تَخُصّ ذلك النوع المُحدَّد من المركبات، فمثلًا، قد يُعرَّف الصنف Car مُتْغيِّر نسخة numberOfDoors، بينما قد يُعرَّف الصنف Truck مُتْغيِّر نسخة numberOfAxles، وقد يُضيف الصنف Motorcycle مُتْغيِّرًا منطقيًا اسمه hasSidecar. يُمكِننا التَّصْريح عن تلك الأصناف كالتالي (ببرنامج حقيقي، ستُعرَّف تلك الأصناف عادةً بملفات مُنفصلة وعلى أساس كَوْنها عامة): class Vehicle { int registrationNumber; Person owner; // (بفرض تعريف الصنف Person) void transferOwnership(Person newOwner) { . . . } . . . } class Car extends Vehicle { int numberOfDoors; . . . } class Truck extends Vehicle { int numberOfAxles; . . . } class Motorcycle extends Vehicle { boolean hasSidecar; . . . } لنَفْترِض أن myCar هو مُتْغيِّر من النوع Car صُرِّح عنه وهُيئ باِستخدَام التَعْليمَة التالية: Car myCar = new Car(); الآن، نستطيع الإشارة إلى myCar.numberOfDoors؛ لأن numberOfDoors مُتْغيِّر نسخة بالصنف Car. بالإضافة إلى ذلك، لمّا كان الصنف Car مُشتقًّا من الصنف Vehicle، فإن أي كائن من الصنف Car يَملُك بنية وسلوك الكائنات من الصنف Vehicle، مما يعني إمكانية الإشارة إلى كُلًا من myCar.registrationNumber و myCar.owner و myCar.transferOwnership()‎. بالعالم الحقيقي، تُعدّ كُلًا من السيارات والشاحنات والدراجات النارية مركبات، وهو ما أَصبَح مُتحقِّقًا بالبرنامج أيضًا، فأيّ كائن من النوع Car أو Truck أو Motorcycle يُعدّ كذلك كائنًا من النوع Vehicle تلقائيًا. يَقُودنا ذلك إلى الحقيقة الهامة التالية: إذا أمكَّن لمُتْغيِّر حَمْل مَرجِع إلى كائن من الصَنْف A، فحتمًا بإِمكانه حَمْل مَرجِع إلى الكائنات المُنتمية لأيّ من أصناف A الفرعية. عمليًا، يَعنِي ذلك إمكانية إِسْناد كائن من الصنف Car إلى مُتْغيِّر من النوع Vehicle، كالتالي: Vehicle myVehicle = myCar; أو كالتالي: Vehicle myVehicle = new Car(); بَعْد إجراء أيّ من التَعْليمَتين -بالأعلى-، سيَحمِل المُتْغيِّر myVehicle مَرجِعًا (reference) إلى كائن من النوع Vehicle، والذي هو في حقيقته نُسخة (instance) من الصَنْف الفرعي Car. يُدرك الكائن حقيقة انتماءه للصنف Car وليس فقط للصنف Vehicle، كما تُخزَّن تلك المعلومة -أيّ صنف الكائن الفعليّ- كجزء من الكائن نفسه، وتستطيع حتى اختبار ما إذا كان كائن معين ينتمي إلى أحد الأصناف عن طريق العَامِل instanceof، كالتالي: if (myVehicle instanceof Car) ... تَفْحَص التَعْليمَة -بالأعلى- ما إذا كان الكائن المُشار إليه باستخدام المُتْغيِّر myVehicle هو بالأساس من النوع Car. في المقابل، لا تَصِح تَعْليمَة الإِسْناد (assignment) التالية: myCar = myVehicle; // خطأ لأن myVehicle قد يُشير إلى أنواع آخرى غَيْر النوع Car. يُشبه ذلك المشكلة التي تَعرَّضنا لها بالقسم الفرعي ٢.٥.٦: لن يَسمَح الحاسوب بإِسْناد قيمة من النوع int إلى مُتْغيِّر من النوع short؛ لأن ليس كل int هو short بالضرورة. بالمثل، فإنه لن يَسمَح بإِسْناد قيمة من النوع Vehicle إلى مُتْغيِّر من النوع Car؛ لأن ليس كل كائن من النوع Vehicle هو كائن من النوع Car بالضرورة. بإِمكانك تَجاوُز تلك المشكلة عن طريق إجراء عملية التَحْوِيل بين الأنواع (type-casting)، فمثلًا، إذا كنت تَعلَم -بطريقة ما- أن myVehicle يُشير فعليًا إلى كائن من النوع Car، تستطيع اِستخدَام ‎(Car)myVehicle لإجبار الحاسوب على التَعامُل مع myVehicle كما لو كان من النوع Car. لذا تستطيع كتابة التالي: myCar = (Car)myVehicle; تستطيع حتى الإشارة إلى ‎((Car)myVehicle).numberOfDoors. لاحِظ أن الأقواس ضرورية لأسباب تَتَعلَّق بالأولوية (precedence)؛ فالعَامِل . لديه أولوية أكبر من عملية إجراء التَحْوِيل بين الأنواع (type-cast) أيّ أن التعبير ‎(Car)myVehicle.numberOfDoors سيُقرأ كما لو كان مَكْتُوبًا على الصورة (Car)(myVehicle.numberOfDoors) وستُجرَى عندها محاولة للتَحْوِيل من النوع int إلى Vehicle وهو أمر مستحيل. سنَفْحَص الآن مثالًا لكيفية اِستخدَام ما سبق ضِمْن برنامج. لنَفْترِض أننا نُريد طباعة البيانات المُتعلّقة بالكائن المُشار إليه باِستخدَام المُتْغيِّر myVehicle، فمثلًا، نَطبَع numberOfDoors إذا كان الكائن من النوع Car. لا نستطيع ببساطة الإشارة إليه باستخدام myVehicle.numberOfDoors؛ لأن الصَنْف Vehicle لا يَتَضمَّن أي numberOfDoors، وإنما نستطيع كتابة التالي: System.out.println("Vehicle Data:"); System.out.println("Registration number: " + myVehicle.registrationNumber); if (myVehicle instanceof Car) { System.out.println("Type of vehicle: Car"); Car c; c = (Car)myVehicle; // تحويل للنوع للوصول إلى numberOfDoors System.out.println("Number of doors: " + c.numberOfDoors); } else if (myVehicle instanceof Truck) { System.out.println("Type of vehicle: Truck"); Truck t; t = (Truck)myVehicle; // تحويل للنوع للوصول إلى numberOfAxles System.out.println("Number of axles: " + t.numberOfAxles); } else if (myVehicle instanceof Motorcycle) { System.out.println("Type of vehicle: Motorcycle"); Motorcycle m; m = (Motorcycle)myVehicle; // تحويل للنوع للوصول إلى hasSidecar System.out.println("Has a sidecar: " + m.hasSidecar); } فيما هو مُتعلِّق بالأنواع الكائنية (object type)، عندما يُجرِي الحاسوب عملية التَحْويل بين الأنواع (type cast)، فإنه يَفْحص أولًا ما إذا كانت تلك العملية صالحة أم لا، فمثلًا، إذا كان myVehicle يُشير إلى كائن من النوع Truck، فلا تَصِح العملية ‎(Car)myVehicle، وعليه، سيُبلَّغ عن اعتراض (exception) من النوع ClassCastException، ولهذا تَستخدِم الشيفرة -بالأعلى- العَامِل instanceof لاختبار نوع المُتْغيِّر قَبْل إجراء عملية التحويل بين الأنواع (type cast)؛ لتتجنَّب حُدوث الاعتراض ClassCastException. لاحِظ أن هذا الفَحْص يَحدُث وقت التنفيذ (run time) وليس وقت التصريف (compile time)؛ لأن النوع الفعليّ للكائن الذي يُشير إليه المُتْغيِّر myVehicle لا يَكُون معروفًا وقت تصريف البرنامج. التعددية الشكلية (polymorphism) لنَفْحص برنامجًا يَتعامَل مع أشكال تُرسَم على الشاشة، والتي قد تَكُون مستطيلة (rectangles)، أو بيضاوية (ovals)، أو مستطيلة بأركان دائرية (roundrect)، ومُلوَّنة جميعًا بألوان مختلفة. سنَستخدِم الأصناف الثلاثة Rectangle و Oval و RoundRect لتمثيل الأشكال المختلفة بحيث تَرِث تلك الأصناف من صَنْف أعلى (superclass) مُشترك Shape، وذلك لتمثيل السمات المشتركة بين جميع الأشكال. فمثلًا، قد يَتَضمَّن الصنف Shape مُتْغيِّرات نُسخ (instance variables) لتمثيل كُلًا من لون الشكل، وموضعه، وحجمه، كما قد يَتَضمَّن توابع نُسخ (instance methods) لتَعْديل قيم تلك السمات. مثلًا، لتَعْديل لون أحد الأشكال، فإننا سنُعدِّل قيمة مُتْغيِّر نُسخة ثم نُعيد رسم الشكل بلونه الجديد: class Shape { Color color; // لابد من استيرادها من حزمة‫ javafx.scene.paint void setColor(Color newColor) { // تابع لتغيير لون الشكل color = newColor; // غير قيمة متغير النسخة redraw(); // أعد رسم الشكل ليظهر باللون الجديد } void redraw() { // تابع لرسم الشكل ? ? ? // ما الذي ينبغي كتابته هنا؟ } . . . // المزيد من متغيرات النسخ والتوابع } // نهاية الصنف‫ Shape يُرسَم كل نوع من تلك الأشكال بطريقة مختلفة، ولهذا ستُواجهنا مشكلة بخصوص التابع redraw()‎. نستطيع عمومًا استدعاء التابع setColor()‎ لأي نوع من الأشكال، وبالتالي سيَحتَاج الحاسوب إلى تَّنْفيذ الاستدعاء redraw()‎، فكيف له إذًا أن يَعرِف الشكل الذي ينبغي عليه رَسْمه؟ نظريًا، يَطلُب الحاسوب من الشكل نفسه أن يقوم بعملية الرسم، فبالنهاية، يُفْترَض أن يَكُون كل كائن من النوع Shape على علم بما عليه القيام به لكي يَرسِم نفسه. عمليًا، يَعنِي ذلك أن كل صنف من الأصناف الثلاثة ينبغي له أن يُعرِّف نسخته الخاصة من التابع redraw()‎، كالتالي: class Rectangle extends Shape { void redraw() { . . . // تعليمات رسم مستطيل } . . . // المزيد من توابع ومتغيرات النسخ } class Oval extends Shape { void redraw() { . . . // تعليمات رسم شكل بيضاوي } . . . // المزيد من توابع ومتغيرات النسخ } class RoundRect extends Shape { void redraw() { . . . // تعليمات رسم مستطيل دائري الأركان } . . . // المزيد من توابع ومتغيرات النسخ } لنَفْترِض أن someShape هو مُتْغيِّر من النوع Shape، أيّ أنه يستطيع الإشارة إلى أيّ كائن ينتمي لأيّ من الأنواع Rectangle و Oval و RoundRect. أثناء تَّنْفيذ البرنامج، قد تَتغيَّر قيمة المُتْغيِّر someShape، مما يَعنِي أنه قد يُشير إلى كائنات من أنواع مختلفة بأوقات مختلفة! لذا، عندما تُنفَّذ التَعْليمَة التالية: someShape.redraw(); فإن نسخة التابع redraw المُستدعَاة فعليًا تَكُون تلك المُتناسبة مع النوع الفعليّ للكائن الذي يُشير إليه المُتْغيِّر someShape. بالنظر إلى نص الشيفرة فقط، قد لا تَتَمكَّن حتى من مَعرِفة الشكل الذي ستَرسِمه تلك التَعْليمَة حيث يعتمد ذلك بالأساس على القيمة التي تَصادَف أن احتواها someShape أثناء تَّنْفيذ البرنامج. علاوة على ذلك، لنَفْترِض أن تلك التَعْليمَة مُضمَّنة بحَلْقة تَكْرار (loop) وتُنفَّذ عدة مرات. إذا كانت قيمة someShape تتغيَّر بينما تُنفَّذ حَلْقة التَكْرار، فقد تَستدعِي نفس تلك التَعْليمَة someShape.redraw();‎ -أثناء تَّنْفيذها عدة مرات- توابعًا (methods) مختلفة، وتَرسِم أنواعًا مختلفة من الأشكال. يُقال عندها أن التابع redraw()‎ هو مُتعدِّد الأشكال (polymorphic). في العموم، يُعدّ تابع معين مُتعدد الأشكال (polymorphic) إذا كان ما يُنفِّذه ذلك التابع مُعتمدًا على النوع الفعليّ (actual type) للكائن المُطبقّ عليه التابع أثناء وقت التَّنْفيذ (run time)، وتُعدّ التعددية الشكلية (polymorphism) واحدة من أهم السمات المميزة للبرمجة كائنية التوجه (object-oriented programming). تَتضِح الصورة أكثر إذا كان لديك مصفوفة أشكال (array of shapes). لنَفْترِض أن shapelist هو مُتْغيِّر من النوع Shape[]‎ يُشير إلى مصفوفة قد أُنشأت وهُيأت عناصرها بمجموعة كائنات، والتي قد تَكُون من النوع Rectangle أو Oval أو RoundRect. نستطيع رسم جميع الأشكال بالمصفوفة بكتابة التالي: for (int i = 0; i < shapelist.length; i++ ) { Shape shape = shapelist[i]; shape.redraw(); } بينما تُنفَّذ الحلقة (loop) -بالأعلى-، فإن التَعْليمَة shape.redraw()‎ قد تَرسِم مستطيلًا أو شكلًا بيضاويًا أو مستطيلًا دائري الأركان اعتمادًا على نوع الكائن الذي يُشير إليه عنصر المصفوفة برقم المَوضِع i. ربما تَتضِح الفكرة أكثر إذا بدّلنا المصطلحات قليلًا كالتالي: بالبرمجة كائنية التوجه (object-oriented programming)، تُعدّ تَعْليمَة استدعاء أي تابع بمثابة إرسال رسالة إلى كائن، بحيث يَرُد ذلك الكائن على تلك الرسالة بتَّنْفيذ التابع المناسب. مثلًا، التَعْليمَة someShape.redraw();‎ هي بمثابة رسالة إلى الكائن المُشار إليه باِستخدَام someShape، ولمّا كان هذا الكائن يَعرِف بالضرورة النوع الذي ينتمي إليه، فإنه يَعرِف الكيفية التي ينبغي أن يَرُد بها على تلك الرسالة. وفقًا لذلك، يُنفِّذ الحاسوب الاستدعاء someShape.redraw();‎ بنفس الطريقة دائمًا، أيّ يُرسِل رسالة يَعتمِد الرد عليها على المُستقبِل. تَكُون الكائنات (objects) عندها بمثابة كيانات نَشِطة تُرسِل الرسائل وتَستقبِلها، كما تَكُون التعددية الشكلية (polymorphism) -وفقًا لهذا التصور- جزءًا طبيعًيا، بل وضروريًا، فتَعنِي فقط أنه من الممكن لكائنات مختلفة الرد على نفس الرسالة بطرائق مختلفة. تَسمَح لك التعددية الشكلية (polymorphism) بكتابة شيفرة تُنفِّذ أشياء قد لا تَتَصوَّرها حتى أثناء الكتابة، وهو في الواقع أحد أكثر الأشياء الرائعة المُتعلِّقة بها. لنَفْترِض مثلًا أنه قد تَقرَّر إضافة مستطيلات مشطوفة الأركان (beveled rectangles) إلى أنواع الأشكال التي يُمكِن للبرنامج التَعامُل معها. لتَّنْفيذ (implement) المستطيلات مشطوفة الأركان، يُمكِننا أن نُضيف صنفًا فرعيًا (subclass) جديدًا، وليَكُن BeveledRect، مُشتقًّا من الصنف Shape، ثم نُعرِّف به نسخته من التابع redraw()‎. تلقائيًا، ستَجِدْ أن الشيفرة التي كنا قد كتبناها سابقًا، أيّ someShape.redraw()‎، قد أَصبَح بإِمكانها فجأة رسم المستطيلات مشطوفة الأركان على الرغم من أن صَنْف تلك المستطيلات لم يَكُن موجودًا من الأساس أثناء كتابة تلك التَعْليمَة. تُرسَل الرسالة redraw إلى الكائن someShape أثناء تَّنْفيذ التَعْليمَة someShape.redraw();‎. لنَفْحَص مرة آخرى التابع المسئول عن تَعْديل لون الشكل والمُعرَّف بالصنف Shape: void setColor(Color newColor) { color = newColor; // غير قيمة متغير النسخة redraw(); // أعد رسم الشكل ليظهر باللون الجديد } بالأعلى، تُرسَل الرسالة redraw ولكن إلى أي كائن؟ حسنًا، يُعدّ التابع setColor هو الآخر بمثابة رسالة كانت قد أُرْسِلَت إلى كائن معين. لذا، فإن الرسالة redraw تُرسَل إلى نفس ذلك الكائن الذي كان قد اِستقبَل الرسالة setColor. إذا كان هذا الكائن مستطيلًا، فإنه يَحتوِي على تابع redraw()‎ لرسم المستطيلات وذاك التابع هو ما سيُنفَّذ. أما إذا كان هذا الكائن شكلًا بيضاويًا، فسيُنفَّذ التابع redraw()‎ المُعرَّف بالصنف Oval. يَعنِي ذلك أنه ليس من الضروري لتَعْليمَة redraw();‎ بالتابع setColor()‎ أن تَستدعِي التابع redraw()‎ المُعرَّف بالصنف Shape، وإنما قد يُنفَّذ أي تابع redraw()‎ طالما كان مُعرَّفًا بصنف فرعي مشتق من Shape. هذه هي فقط حالة آخرى من التعددية الشكلية (polymorphism). الأصناف المجردة (abstract classes) وقتما يَضطرّ كائن من الصنف Rectangle أو Oval أو RoundRect إلى رَسْم نفسه، سيُنفَّذ التابع redraw()‎ المُعرَّف بالصنف المناسب، وهو ما يَقُودنا للسؤال التالي: ما دور التابع redraw()‎ المُعرَّف بالصنف Shape؟ وكيف لنا أن نُعرِّفه؟ قد تتفاجئ من الإجابة، ففي الحقيقة ينبغي أن نَترُكه فارغًا لأن الصنف Shape لا يُمثِل سوى فكرة مُجردة (abstract) لشكل، وبالتأكيد ليس هناك طريقة لرسم شيء كذلك، وإنما يُمكِن فقط رَسْم الأشكال الحقيقية (concrete) كالمستطيلات والأشكال البيضاوية. إذًا، لماذا ينبغي أن نُعرِّف التابع redraw()‎ بالصنف Shape من الأساس؟ حسنًا، نحن مُضطرون إلى ذلك وإلا لن يَصِح استدعائه بالتابع setColor()‎ المُعرَّف بالصنف Shape كما لن يَصِح كتابة شيئًا مثل someShape.redraw();‎؛ حيث سيَعترِض المُصرِّف لكَوْن someShape مُتْغيِّرًا من النوع Shape في حين لا يَحتوِي ذلك الصَنْف على التابع redraw()‎. وعليه، لن يُستدعَى التابع redraw()‎ المُعرَّف بالصنف Shape نهائيًا، وربما -إذا فكرت بالأمر- ستَجِدْ أنه ليس هناك أي سبب قد يَضطرّنا حتى إلى إنشاء كائن فعليّ من الصنف Shape. تستطيع بالتأكيد إنشاء مُتْغيِّرات من النوع Shape، ولكنها دومًا ستُشير إلى كائنات (objects) من الأصناف الفرعية (subclasses) للصَنْف Shape. في مثل تلك الحالات، يُعدّ الصنف Shape صنفًا مُجرَّدًا (abstract class). لا تُستخدَم الأصناف المُجرّدة (abstract class) لإنشاء كائنات، وإنما فقط تُمثِل قاعدة وأساسًا لإنشاء أصناف فرعية أيّ أنها موجودة فقط للتعبير عن بعض السمات والخاصيات المشتركة بجميع أصنافها الفرعية (subclasses). أما الصَنْف غَيْر المُجرّد فيُعدّ صنفًا حقيقيًا (concrete class). في العموم، تستطيع إنشاء كائنات تنتمي لأصناف حقيقية (concrete class) وليس لأصناف مُجرّدة (abstract class)، كما يستطيع مُتْغيِّر من نوع هو عبارة عن صنف مُجرّد (abstract class) أن يُشير فقط إلى كائنات تنتمي لأحد الأصناف الفرعية الحقيقية (concrete subclasses) المُشتقّة من ذلك الصنف المُجرّد (abstract class). بالمثل، يُعدّ التابع redraw()‎ المُعرَّف بالصنف Shape تابعًا مجردًا (abstract method)؛ فهو لا يُستدعَى نهائيًا وإنما تُستدعَى توابع redraw()‎ المُعرَّفة بالأصناف الفرعية للصنف Shape لرَسْم الأشكال بصورة فعليّة. اِضطرّرنا مع ذلك لتعريف التابع redraw()‎ بالصَنْف Shape للتأكيد على فِهم جميع الأشكال للرسالة redraw، أي أنه موجود فقط لتَخْصِيص الواجهة (interface) المُشتركة لنُسخ التابع redraw()‎ الفعليّة والحقيقية (concrete) والمُعرَّفة بالأصناف الفرعية، وبالتالي ليس هناك ما يَستدعِي احتواء التابع المُجرَّد redraw()‎ بالصنف Shape على أيّ شيفرة. كما ذَكَرَنا سابقًا، يُعدّ كُلًا من الصنف Shape وتابعه redraw()‎ بمثابة أفكارًا مجردةً (abstract) على نحو دلالي فقط حتى الآن، ولكن تستطيع إِعلام الحاسوب بهذه الحقيقة على نحو صياغي (syntactically) أيضًا عن طريق إضافة المُبدِّل abstract إلى تعريف (definition) كُلًا منهما. بخلاف أي تابع عادي، لا يُكتَب الجزء التنفيذي (implementation) للتوابع المُجرّدة (abstract method) وإنما تُستخدَم فاصلة منقوطة (semicolon). في المقابل، لابُدّ من كتابة الجزء التنفيذي (implementation) للتابع المُجرّد (abstract method) ضِمْن أي صَنْف فرعي حقيقي (concrete subclass) مُشتقّ من الصنف المُجرّد (abstract class). تَستعرِض الشيفرة التالية طريقة تعريف الصَنْف Shape كصنف مُجرّد (abstract class): public abstract class Shape { Color color; // لون الشكل void setColor(Color newColor) { // تابع لتغيير لون الشكل color = newColor; // غير قيمة متغير النسخة redraw(); // أعد رسم الشكل ليظهر باللون الجديد } abstract void redraw(); // تابع مجرد ينبغي أن يُعرَّف بالأصناف الفرعية الحقيقية . . . // المزيد من التوابع والنسخ } // نهاية الصنف‫ Shape بمُجرّد اِستخدَامك للمُبدِّل abstract أثناء التَّصْريح عن الصَنْف، لا تستطيع إنشاء كائنات فعليّة من النوع Shape، وسيُبلِّغ الحاسوب عن حُدوث خطأ في بناء الجملة (syntax error) إذا حاولت القيام بذلك. ربما يُعدّ الصَنْف Vehicle -الذي ناقشناه بالأعلى- صنفًا مجردًا (abstract class) أيضًا، فليس هناك أي طريقة لامتلاك مركبة كتلك، وإنما لابُدّ للمركبة الفعليّة أن تَكُون سيارة أو شاحنة أو دراجة نارية أو حتى أي نوع حقيقي (concrete) آخر. ذَكَرنا بالقسم الفرعي ٥.٣.٢ أن أيّ صنف لا يُصرَّح عن كَوْنه صنف فرعي (subclass) من أيّ صنف آخر -أيّ بدون الجزء extends- فإنه تلقائيًا يُصبِح صنفًا فرعيًا من الصنف القياسي Object كالتالي: public class myClass { . . . يُعدّ التَّصْريح بالأعلى مُكافئًا للتالي تمامًا: public class myClass extends Object { . . . يُمكِننا القول إذًا أن الصَنْف Object يقع بقمة سُلالة أصناف (class hierarchy) ضخمة تَتَضمَّن جميع الأصناف الآخرى. من الناحية الدلالية، يُعدّ الصنف Object صنفًا مجردًا (abstract class) بل ربما أكثر صنف مجرد على الإطلاق، ومع ذلك، لم يُصرَّح عنه على هذا الأساس من الناحية الصياغية، أيّ تستطيع إنشاء كائنات من النوع Object ولكن لن تَجِدْ الكثير لتفعله بها. ولأن أيّ صَنْف هو بالضرورة صنف فرعي (subclass) من الصَنْف Object، فيُمكِن لمُتْغيِّر من النوع Object أن يُشير إلى أي كائن مهما كان نوعه. بالمثل، تستطيع مصفوفة من النوع Object[]‎ أن تَحمِل كائنات من أي نوع. تَستخدِم الشيفرة المصدرية بالملف ShapeDraw.java الصَنْف المُجرّد Shape، بالإضافة إلى مصفوفة من النوع Shape[]‎ لحَمْل قائمة من الأشكال. قد تَرغَب بإلقاء نظرة على ذلك الملف مع أنك لن تَتَمكَّن من فهمه بالكامل حاليًا؛ فتعريفات الأصناف الفرعية بالملف مختلفة نوعًا ما عن تلك التي رأيتها بالأعلى. مثلًا، يَستقبِل التابع draw()‎ مُعامِلًا (parameter) من النوع GraphicsContext؛ لأن الرَسْم بالجافا يَتطلَّب سياقًا رُسوميًا. سنَتَعرَّض لأمثلة شبيهة بالفصول اللاحقة بعدما تَتَعرَف على برمجة واجهات المُستخدِم الرُسومية (GUI). يُمكِنك مع ذلك فَحْص تعريف الصنف Shape وأصنافه الفرعية (subclasses) بالإضافة إلى طريقة اِستخدَام المصفوفة لحَمْل قائمة الأشكال. تَستعرِض الصورة التالية لقطة للشاشة أثناء تَشْغِيل البرنامج: بعد تَشْغِيل البرنامج ShapeDraw، تستطيع أن تُضيف شكلًا جديدًا إلى الصورة عن طريق الضغط على أيّ من الأزرار الموجودة بالأسفل. يُمكِنك أيضًا اختيار لون ذلك الشكل من خلال قائمة الألوان الموجودة أسفل مساحة الرسم (drawing area). سيَظهَر الشكل الجديد بالركن الأيسر العلوي من مساحة الرسم (drawing area)، وبمُجرّد ظُهوره، تستطيع سَحْبه باستخدَام الفأرة. سيُحافِظ الشكل دومًا على ترتيبه بالنسبة لبقية الأشكال الموجودة على الشاشة حتى أثناء عملية السَحْب، ومع ذلك، تستطيع اختيار شكل معين ليُصبِح أمام جميع الأشكال الأخرى عن طريق الضغط باستمرار على المفتاح "shift" بينما تَضغَط على ذلك الشكل. في الحقيقة، يُستخدَم الصنف الفعليّ للشكل أثناء إضافته إلى الشاشة فقط. بعد ذلك، يُعالَج بالكامل على أساس كَوْنه شكلًا مجردًا (abstract). على سبيل المثال، يَتعامَل البرنامج (routine) المسئول عن عملية السَحْب مع مُتْغيِّرات من النوع Shape، ولا يُشير نهائيًا إلى أي من الأصناف الفرعية (subclasses). عندما يحتاج إلى رَسْم شكل، فإنه فقط يَستدعِي التابع draw أي أنه غَيْر مضطرّ لمَعرِفة طريقة رَسْم الشكل أو حتى مَعرِفة نوعه الفعليّ، وإنما تُوكَل عملية الرسم إلى الكائن ذاته. إذا أردت أن تُضيف نوعًا جديدًا من الأشكال إلى البرنامج، كل ما عليك القيام به هو الآتي: أولًا، ستُعرِّف صنفًا فرعيًا (subclass) جديدًا مُشتقًّا من Shape، ثم ستُضيف زرًا جديدًا وتُبرمجه بحيث يُضيف الشكل إلى الشاشة. ترجمة -بتصرّف- للقسم Section 5: Inheritance, Polymorphism, and Abstract Classes من فصل Chapter 5: Programming in the Large II: Objects and Classes من كتاب Introduction to Programming Using Java.
  5. سنتناول خلال هذا القسم عدة أمثلة للتصميم كائني التوجه (object-oriented). اخترنا أن تَكُون تلك الأمثلة بسيطة بما فيه الكفاية لتَسمَح لنا بتصميم أصناف قابلة لإعادة الاِستخدَام على نحو معقول. تحديدًا، سنُصمّم لعبة ورق تَستخدِم مجموعة ورق اللعب القياسية (deck) والمعروفة باسم "مجموعة ورق لعب البوكر". تصميم الأصناف يمكننا توصيف لعبة الورق كالتالي: "كأيّ لعبة ورق عادية، سيَحصُل كل لاعب على أكثر من ورقة لعب. بدايةً ستُخلَط (shuffle) مجموعة ورق اللعب (deck). بعد ذلك، ستُسحَب ورقة واحدة بكل مرة من مجموعة ورق اللعب (deck)، ثُمَّ تُوزَّع إلى يد (hand) أحد اللاعبين. قد يُستبعَد بعض ورق اللعب (cards) من يد أحد اللاعبين، وقد يُضاف ورق جديد. يَعتمِد فوز لاعب معين بالمباراة أو خسارته لها على كُلًا من قيم (values) (آص، ٢، ٣، ..، ملك)، ورموز (suits) ("بستوني spades"، "ديناري diamonds"، "قلب hearts"، "سباتي clubs") ورق اللعب الذي تَسلَّمه". إذا بحثنا عن الأسماء المذكورة بالتوصيف (specification) السابق، فسنَجِدْ التالي: لعبة، لاعب، يد (hand)، ورقة لعب (card)، مجموعة ورق اللعب (deck)، قيمة (value)، رمز (suit). يُعدّ كُلًا من الاسمين "قيمة" و "رمز" مُجرَّد قيم بسيطة، ويُمكِن تمثيلها كمُتْغيِّرات نُسخ (instance variables) بالصنف المُمثِل لورقة اللعب Card. يُمكِننا تمثيل الأسماء الخمسة الآخرى بواسطة أصناف (classes)، ولكننا سنَقْتصِر فقط على تلك الأسماء الأكثر قابلية لإعادة الاِستخدَام: ورقة لعب (card)، يد (hand)، مجموعة ورق اللعب (deck). أما إذا بحثنا عن الأفعال المذكورة بالتوصيف، فسنَجِدْ التالي: "خلط (shuffle) مجموعة ورق اللعب" و "توزيع ورقة لعب من مجموعة ورق اللعب". سنَستخدِم تابعي النُسخ (instance methods)‏ shuffle()‎ و dealCard()‎ لتمثيل كُلًا منهما ضِمْن الصَنْف Deck. بالإضافة إلى ذلك، هنالك أيضًا: "إضافة ورقة لعب إلى يد اللاعب" و "استبعاد ورقة لعب من يد اللاعب". سنَستخدِم تابعي النُسخ (instance methods)‏ addCard()‎ و removeCard()‎ لتمثيل كُلًا منهما ضِمْن الصنف Hand. أخيرًا، ورق اللعب هو كيان سلبي نوعًا ما، ولكن ينبغي على الأقل أن نَتَمكَّن من تَحْديد كُلًا من قيمته (value) ورمزه (suit). سنكتشف المزيد من توابع النُسخ الضرورية لاحقًا. أولًا، سنبدأ بتصميم الصنف Deck تفصيليًا. عندما نُنشِئ مجموعة ورق لعب (deck) لأول مرة، فإنها ستَحتوِي على ٥٢ ورقة لعب (card) مُرتَّبة معياريًا. لإنشاء مجموعة ورق لعب (deck) جديدة، سيَحتاج الصَنْف Deck إلى بَانِي (constructor) بدون أي مُعامِلات (parameters)؛ لأن أي مجموعة ورق لعب جديدة دائمًا ما تَكُون هي نفسها. سيَتَضمَّن الصَنْف Deck تابع النُسخة (instance method)‏ shuffle()‎ المسئول عن ترتيب مجموعة ورق اللعب عشوائيًا، كما سيَتَضمَّن تابع النُسخة dealCard()‎ المسئول عن جَلْب ورقة اللعب (card) التالية من مجموعة ورق اللعب (deck). هذا التابع هو عبارة عن دالة (function) تُعيد قيمة من النوع Card؛ لأن المُستدعِي يحتاج إلى مَعرِفة ورقة اللعب (card) المُوزَّعة. في المقابل، لن يَستقبِل ذلك التابع أي مُعامِلات (parameters)؛ فليس هناك أيّ معلومات ينبغي تمريرها إلى مجموعة ورق اللعب (deck) عند سَحْب ورقة لعب (card)، فأنت فقط تُوزِّع ورقة اللعب التالية أيًا كانت. لكن ماذا سيَحدُث عند استدعاء التابع dealCard()‎ بينما لم يَعُدْ هناك أي ورق لعب إضافي بمجموعة ورق اللعب؟ في العموم، تُعدّ محاولة توزيع ورقة لعب من مجموعة ورق لعب فارغة بمثابة خطأ، لذا يُمكِن للصَنْف عندها أن يُبلِّغ عن اعتراض (exception). ولكن كيف سيَتَمكَّن البرنامج من مَعرِفة ما إذا كانت مجموعة ورق اللعب فارغة أم لا؟ قد يحتفظ البرنامج بعدد ورق اللعب المُستخدَم إلى الآن، ولكن ينبغي لمجموعة ورق اللعب نفسها أن تَكُون على دراية بتلك المعلومة، ولهذا سنُضيف تابع النسخة cardsLeft()‎ ليُعيد عدد ورق اللعب المُتبقِّي بمجموعة ورق اللعب، مما سيُمكِّن البرنامج أيضًا من سؤال مجموعة ورق اللعب عن العدد المُتبقِّي. اُنظر التوصيف الكامل لجميع البرامج الفرعية (subroutines) بالصنف Deck: /** * Constructor. Create an unshuffled deck of cards. */ public Deck() /** * Put all the used cards back into the deck, * and shuffle it into a random order. */ public void shuffle() /** * As cards are dealt from the deck, the number of * cards left decreases. This function returns the * number of cards that are still left in the deck. */ public int cardsLeft() /** * Deals one card from the deck and returns it. * @throws IllegalStateException if no more cards are left. */ public Card dealCard() يَتَضمَّن التوصيف كل ما تحتاج إلى مَعرِفته لكي تَتَمكَّن من اِستخدَام الصَنْف Deck، ولكنه لا يُخبرنا عن كيفية تّنْفيذ الصنف، فبالنهاية هذا ليس تمرينًا على كتابة الشيفرة (coding) وإنما على التصميم (design). يمكنك أيضًا الإطلاع على الشيفرة المصدرية للصنف بالملف Deck.java إذا شئت. ستلاحظ أن الصنف يحتوي على متغير نسخة (instance variable) عبارة عن مصفوفة من النوع Cards. ربما لن تفهم بعض النقاط بالتنفيذ (implementation) الداخلي للصنف، ولكن ما يزال بإمكانك استخدامه بالبرامج الخاصة بك كصندوق أسود (black box). بالمثل، يُمكِننا تحليل الصَنْف Hand. عندما نُنشِئ يد (hand) لأول مرة، فإنها لا تَحتوِي على أي ورق لعب (card). سيَتَضمَّن الصَنْف Hand تابع النُسخة (instance method)‏ addCard()‎ المسئول عن إضافة ورقة لعب إلى اليد. يَستقبِل ذلك التابع مُعامِلًا (parameter) من النوع Card لتَخْصِيص ورقة اللعب (card) المضافة. سيَتَضمَّن الصنف أيضًا تابع النُسخة removeCard()‎ المسئول عن استبعاد ورقة لعب من اليد. بالمثل، يَستقبِل ذلك التابع مُعامِلًا (parameter) من النوع Card لتَخْْصِيص ورقة اللعب (card) المُستبعدَة. والآن، لنطرح السؤال التالي: عند استبعاد ورقة لعب معينة من يد، هل ينبغي أن نُخصِّص ورقة اللعب ذاتها كأن نقول "استبعد ورقة الآص البستوني" أم عَبْر مَوضِعها باليد كأن نقول "استبعد ورقة اللعب الثالثة باليد"؟ يُمكِننا في الواقع السماح بالخيارين، فكما تَعلَم، يُمكِن لأيّ صنف أن يَتَضمَّن تابعين (methods) بنفس الاسم بشَّرْط أن يَكُون لديهما أعداد مختلفة أو أنواع مختلفة من المُعامِلات. ولهذا، سيَتَضمَّن الصَنْف نسختين من تابع النسخة removeCard()‎. يَستقبِل الأول مُعامِلًا (parameter) من النوع Card؛ لتَخْصِيص ورقة اللعب ذاتها المطلوب استبعادها، بينما يَستقبِل الآخر مُعامِلا (parameter) من النوع int؛ لتَخْصِيص مَوضِع ورقة اللعب المطلوب استبعادها. قد تَحتوِي اليد على عدد مُتْغيِّر من ورق اللعب، لذا سنُضيف تابع النُسخة getCardCount()‎ ليُعيد عدد ورق اللعب الموجود باليد. وأخيرًا، يُمكِننا أيضًا أن نضيف توابع نسخ (instance methods) آخرى لترتيب ورق اللعب باليد؛ حيث يُفضِّل كثير من اللاعبين ترتيب ورق اللعب بأيديهم بحيث يَتجَاوَر ورق اللعب من نفس القيمة. اُنظر التوصيف الكامل للصَنْف Hand القابل لإعادة الاستخدام: /** * Constructor. Create a Hand object that is initially empty. */ public Hand() /** * Discard all cards from the hand, making the hand empty. */ public void clear() /** * Add the card c to the hand. c should be non-null. * @throws NullPointerException if c is null. */ public void addCard(Card c) /** * If the specified card is in the hand, it is removed. */ public void removeCard(Card c) /** * Remove the card in the specified position from the * hand. Cards are numbered counting from zero. * @throws IllegalArgumentException if the specified * position does not exist in the hand. */ public void removeCard(int position) /** * Return the number of cards in the hand. */ public int getCardCount() /** * Get the card from the hand in given position, where * positions are numbered starting from 0. * @throws IllegalArgumentException if the specified * position does not exist in the hand. */ public Card getCard(int position) /** * Sorts the cards in the hand so that cards of the same * suit are grouped together, and within a suit the cards * are sorted by value. */ public void sortBySuit() /** * Sorts the cards in the hand so that cards are sorted into * order of increasing value. Cards with the same value * are sorted by suit. Note that aces are considered * to have the lowest value. */ public void sortByValue() يمكنك الإطلاع على الشيفرة المصدرية للصَنْف بالملف Hand.java. لاحِظ أنك لن تَتَمكَّن من فهم بعض الأشياء ضِمْن تَّنْفيذ (implementation) الصَنْف، ولكنها لن تمنعك من اِستخدَام الصَنْف بمشروعاتك. الصنف Card سنَفْحَص تصميم الصَنْف Card وتَّنْفيذه (implementation) تفصيليًا. يَحتوِي الصنف على بَانِي (constructor) يَستقبِل كُلًا من قيمة (value) ورقة اللعبة المُنشئة ورمزها (suit). نستطيع تمثيل الرموز الأربعة باستخدام الأعداد الصحيحة ٠ و ١ و ٢ و ٣، ولكن لصعوبة تَذكُّر الرمز الذي يُمثله كل عدد منها، عَرَّفنا أربعة ثوابت مُسماة (named constants) بالصَنْف Card لتمثيل الاحتمالات الأربعة، فمثلًا، يُمثِل Card.SPADES الرمز "بستوني spades". صَرَّحنا عن تلك الثوابت لتَكُون من النوع int، وباِستخدَام المُبدِّلات public final static. كان من المُمكن أن نَستخدِم أنواع التعداد (enumerated type)، ولكن سنكتفي بالاعتماد على الثوابت من النوع العددي الصحيح. أما قيم ورق اللعب المُحتملة فهي الأعداد من ١ وصولًا إلى ١٣، بحيث تُمثِل الأعداد ١ و ١١ و ١٢ و ١٣ كُلًا من "الآص" و "الرجل أو الشبّ" و "الملكة أو البنت" و "الملك أو الشايب" على الترتيب. بالمثل، عَرَّفنا بعض الثوابت المُسماة (named constants) لتمثيل كُلًا من ورقة الآص، وورق اللعب المُصور. ستَجِدْ أيضًا أننا قد أضفنا ورقة "الجوكر". بمُجرَّد معرفة كُلًا من قيمة (value) ورقة اللعب، ورمزها (suit)، نستطيع استدعاء البَانِي (constructor) لإنشاء كائن من النوع Card، كالتالي: card1 = new Card( Card.ACE, Card.SPADES ); // أنشئ ورقة آص بستوني card2 = new Card( 10, Card.DIAMONDS ); // أنشئ ورقة عشرة ديناري // ‫يُسمَح بذلك طالما كانت s و v تعبيرات من النوع العددي الصحيح card3 = new Card( v, s ); يحتاج أي كائن من الصَنْف Card إلى مُتْغيِّرات نُسخ (instance variables) لتمثيل كُلًا من قيمة ورقة اللعب ورمزها، لذلك أضفنا مُتْغيِّرات النسخ suit و value للصَنْف. بدايةً، صَرَّحنا عنها باِستخدَام المُبدِّل private حتى يَستحِيل تَعْديلها من خارج الصَنْف، وفي المقابل، أضفنا تابعي الجَلْب getSuit()‎ و getValue()‎ لكي تَتَمكَّن الشيفرة خارج الصَنْف من قراءة قيمة ورقة اللعب ورمزها. يَعنِي ذلك أن قيم تلك المُتْغيِّرات لن تتغير نهائيًا بَعْد تهيئتها المبدئية بالباني (constructor)، ولهذا كان من المنطقي التَّصْريح عنها باستخدام المُبدِّل final. يُمكِنك التَّصْريح عن أيّ مُتْغيِّر نُسخة (instance variable) عمومًا باِستخدَام المُبدِّل final، بشَّرْط إِسْناد قيمة إليه إما بتَعْليمَة التَّصْريح (declaration) أو بكل البواني (constructor) المُعرَّفة بذلك الصَنْف. تعدّ كائنات ذلك الصَنْف كائنات ثابتة أو غَيْر قابلة للتعديل (immutable)؛ لأن جميع مُتْغيِّرات النسخ المُعرَّفة ضِمْنها قد صُرِّح عنها باِستخدَام المُبدِّل final. أضفنا أيضًا بعض التوابع (methods) الآخرى إلى الصَنْف لطباعة كائناته (objects) بصيغة مقروءة، فمثلًا، بدلًا من طباعة العدد ٢ المُستخدَم لتمثيل الرمز "ديناري diamonds" ضِمْن الصَنْف، يُفضَّل طباعة الكلمة "Diamonds". ولأن عملية طباعة رمز ورقة اللعب شيئًا يُرجَح أن نحتاج إليه بالكثير من البرامج الآخرى، كان من المعقول إضافته للصَنْف، ولهذا أضفنا توابع النُسخ getValueAsString()‎ و getSuitAsString()‎ بحيث تُعيد التمثيل النصي (string representations) لكُلًا من قيمة ورقة اللعب ورمزها على الترتيب. بالإضافة إلى ذلك، عَرَّفنا تابع النسخة toString()‎ ليُعيد سِلسِلة نصية تَتَضمَّن كُلًا من قيمة ورقة اللعب ورمزها، مثل "ملكة القلوب". يُستدعَى هذا التابع (method) تلقائيًا لتَحْوِيل كائن من النوع Card إلى النوع String أينما اِستخدَمناه ضِمْن سياق يحتاج إلى سِلسِلة نصية، مثلًا عند ضمه (concatenate) إلى سِلسِلة نصية باِستخدَام العَامِل +. أي أن التَعْليمَة التالية: System.out.println( "Your card is the " + card ); تُكافئ تمامًا التَعْليمَة: System.out.println( "Your card is the " + card.toString() ); إذا كانت ورقة اللعب هي "ملكة القلوب"، فستَطبَع التَعْليمَتان السابقتان نفس السِلسِلة النصية "Your card is the Queen of Hearts". اُنظر شيفرة الصَنْف Card بالكامل، كما أنها موجودة بالملف Card.java. يُعدّ هذا الصنف عامًا بما فيه الكفاية بما يُتيِح إعادة اِستخدَامه، أيّ أن العمل الذي بذلناه أثناء كُلًا من التصميم (designing)، وكتابة الشيفرة، والاختبار (testing) سيُؤتي ثماره على المدى الطويل. public class Card { public final static int SPADES = 0; public final static int HEARTS = 1; public final static int DIAMONDS = 2; public final static int CLUBS = 3; public final static int JOKER = 4; public final static int ACE = 1; public final static int JACK = 11; public final static int QUEEN = 12; public final static int KING = 13; /** * This card's suit, one of the constants SPADES, HEARTS, DIAMONDS, * CLUBS, or JOKER. The suit cannot be changed after the card is * constructed. */ private final int suit; /** * The card's value. For a normal card, this is one of the values * 1 through 13, with 1 representing ACE. For a JOKER, the value * can be anything. The value cannot be changed after the card * is constructed. */ private final int value; /** * Creates a Joker, with 1 as the associated value. (Note that * "new Card()" is equivalent to "new Card(1,Card.JOKER)".) */ public Card() { suit = JOKER; value = 1; } /** * Creates a card with a specified suit and value. * @param theValue the value of the new card. For a regular card (non-joker), * the value must be in the range 1 through 13, with 1 representing an Ace. * You can use the constants Card.ACE, Card.JACK, Card.QUEEN, and Card.KING. * For a Joker, the value can be anything. * @param theSuit the suit of the new card. This must be one of the values * Card.SPADES, Card.HEARTS, Card.DIAMONDS, Card.CLUBS, or Card.JOKER. * @throws IllegalArgumentException if the parameter values are not in the * permissible ranges */ public Card(int theValue, int theSuit) { if (theSuit != SPADES && theSuit != HEARTS && theSuit != DIAMONDS && theSuit != CLUBS && theSuit != JOKER) throw new IllegalArgumentException("Illegal playing card suit"); if (theSuit != JOKER && (theValue < 1 || theValue > 13)) throw new IllegalArgumentException("Illegal playing card value"); value = theValue; suit = theSuit; } /** * Returns the suit of this card. * @returns the suit, which is one of the constants Card.SPADES, * Card.HEARTS, Card.DIAMONDS, Card.CLUBS, or Card.JOKER */ public int getSuit() { return suit; } /** * Returns the value of this card. * @return the value, which is one of the numbers 1 through 13, inclusive for * a regular card, and which can be any value for a Joker. */ public int getValue() { return value; } /** * Returns a String representation of the card's suit. * @return one of the strings "Spades", "Hearts", "Diamonds", "Clubs" * or "Joker". */ public String getSuitAsString() { switch ( suit ) { case SPADES: return "Spades"; case HEARTS: return "Hearts"; case DIAMONDS: return "Diamonds"; case CLUBS: return "Clubs"; default: return "Joker"; } } /** * Returns a String representation of the card's value. * @return for a regular card, one of the strings "Ace", "2", * "3", ..., "10", "Jack", "Queen", or "King". For a Joker, the * string is always numerical. */ public String getValueAsString() { if (suit == JOKER) return "" + value; else { switch ( value ) { case 1: return "Ace"; case 2: return "2"; case 3: return "3"; case 4: return "4"; case 5: return "5"; case 6: return "6"; case 7: return "7"; case 8: return "8"; case 9: return "9"; case 10: return "10"; case 11: return "Jack"; case 12: return "Queen"; default: return "King"; } } } /** * Returns a string representation of this card, including both * its suit and its value (except that for a Joker with value 1, * the return value is just "Joker"). Sample return values * are: "Queen of Hearts", "10 of Diamonds", "Ace of Spades", * "Joker", "Joker #2" */ public String toString() { if (suit == JOKER) { if (value == 1) return "Joker"; else return "Joker #" + value; } else return getValueAsString() + " of " + getSuitAsString(); } } // end class Card لعبة ورق بسيطة أخيرًا، تَستعرِض الشيفرة التالية برنامجًا كاملًا يَستخدِم الصَنْفين Card و Deck. يَسمَح البرنامج للمُستخدِم بلعب لعبة ورق بسيطة اسمها هو HighLow. تُخلَط مجموعة ورق اللعب (deck)، وتُسحَب ورقة لعب واحدة، ليراها المُستخدِم. بَعْد ذلك، عليه أن يَتوقَّع ما إذا كانت ورقة اللعب التالية ستَكُون أكبر أو أقل من ورقة اللعب الحالية. إذا كان تَوقُّع المُستخدِم صحيحًا، تَحلّ ورقة اللعب التالية مَحلّ ورقة اللعب الحالية، ثم يَتوقَّع المُستخدِم مرة آخرى، ويستمر الأمر إلى أن يَتوقَّع المُستخدِم تَوقُّعًا خاطئًا. مجموع النقاط التي تَحصَّل عليها المُستخدِم تُساوِي عدد التوقُّعات الصحيحة. يَتَضمَّن البرنامج تابعًا ساكنًا (static method). تُوكَل مُهِمّة لعب مباراة واحدة فقط من لعبة الورق HighLow لذلك التابع. يَسمَح البرنامج main()‎ للمُستخدِم بلعب عدة مباريات، وبالنهاية، يُخبره بمتوسط النقاط التي تَحصَّل عليها خلال جميع المباريات. لن نَمُرّ عَبْر مراحل تطوير الخوارزمية (algorithm) المُستخدَمة بالبرنامج، لكن ينبغي عليك أن تقرأها بحرص، وأن تتأكَّد من فِهم طريقة عملها. يُعيد البرنامج الفرعي (subroutine) المَسئول عن لعب مباراة واحدة من لعبة الورق HighLow نقاط المُستخدِم بالمباراة كقيمة مُعادة (return value) إلى البرنامج main حيث تُستخدَم. اُنظر البرنامج: import textio.TextIO; public class HighLow { public static void main(String[] args) { System.out.println("This program lets you play the simple card game,"); System.out.println("HighLow. A card is dealt from a deck of cards."); System.out.println("You have to predict whether the next card will be"); System.out.println("higher or lower. Your score in the game is the"); System.out.println("number of correct predictions you make before"); System.out.println("you guess wrong."); System.out.println(); int gamesPlayed = 0; // عدد المباريات التي لعبها المستخدم int sumOfScores = 0; // مجموع نقاط المستخدم double averageScore; // متوسط نقاط المستخدم // يحمل رد المستخدم على سؤاله عما إذا كان يريد لعب مباراة إضافية boolean playAgain; do { int scoreThisGame; // نقاط المستخدم بمباراة واحدة scoreThisGame = play(); // العب مباراة مع المستخدم sumOfScores += scoreThisGame; gamesPlayed++; System.out.print("Play again? "); playAgain = TextIO.getlnBoolean(); } while (playAgain); averageScore = ((double)sumOfScores) / gamesPlayed; System.out.println(); System.out.println("You played " + gamesPlayed + " games."); System.out.printf("Your average score was %1.3f.\n", averageScore); } // ‫نهاية main() /** * Lets the user play one game of HighLow, and returns the * user's score in that game. The score is the number of * correct guesses that the user makes. */ private static int play() { // أنشئ كائن مجموعة ورق لعب جديدة وخزن مرجعه بالمتغير Deck deck = new Deck(); Card currentCard; // ورقة اللعب الحالية التي يراها المستخدم // ورقة اللعب التالية التي يتوقع المستخدم ما // إذا كانت أكبر أو أقل من ورقة اللعب الحالية Card nextCard; int correctGuesses ; // عدد توقعات المستخدم الصحيحة char guess; // ‫تخمين المستخدم ويحمل القيمة L أو H deck.shuffle(); // رتب مجموعة ورق اللعب عشوائيًا correctGuesses = 0; currentCard = deck.dealCard(); System.out.println("The first card is the " + currentCard); while (true) { // تنتهي الحلقة عند التخمين الخاطئ /* اقرأ تخمين المستخدم */ System.out.print("Will the next card be higher (H) or lower (L)? "); do { guess = TextIO.getlnChar(); guess = Character.toUpperCase(guess); if (guess != 'H' && guess != 'L') System.out.print("Please respond with H or L: "); } while (guess != 'H' && guess != 'L'); /* اسحب ورقة اللعب التالية وأظهرها للمستخدم */ nextCard = deck.dealCard(); System.out.println("The next card is " + nextCard); /* افحص تخمين المستخدم */ if (nextCard.getValue() == currentCard.getValue()) { System.out.println("The value is the same as the previous card."); System.out.println("You lose on ties. Sorry!"); break; // أنهي المباراة } else if (nextCard.getValue() > currentCard.getValue()) { if (guess == 'H') { System.out.println("Your prediction was correct."); correctGuesses++; } else { System.out.println("Your prediction was incorrect."); break; // أنهي المباراة } } else { // إذا كانت ورقة اللعب التالية أقل if (guess == 'L') { System.out.println("Your prediction was correct."); correctGuesses++; } else { System.out.println("Your prediction was incorrect."); break; // أنهى المباراة } } /* تهيئة ضرورية للتكرار التالي ضمن الحلقة */ currentCard = nextCard; System.out.println(); System.out.println("The card is " + currentCard); } // نهاية الحلقة System.out.println(); System.out.println("The game is over."); System.out.println("You made " + correctGuesses + " correct predictions."); System.out.println(); return correctGuesses; } // ‫نهاية play() } // ‫نهاية الصنف HighLow ترجمة -بتصرّف- للقسم Section 4: Programming Example: Card, Hand, Deck من فصل Chapter 5: Programming in the Large II: Objects and Classes من كتاب Introduction to Programming Using Java.
  6. يُمكِن تطبيق المفاهيم كائنية التوجه (object-oriented) على عملية تصميم البرامج وكتابتها في العموم بأكثر من طريقة، منها التحليل والتصميم كائني التوجه (object-oriented analysis and design). تُطبِق تلك الطريقة الأساليب كائنية التوجه (object-oriented) على أُولَى مراحل تطوير البرمجيات، والمسئولة عن تصميم البرنامج ككل. تَتَلخَّص تلك الطريقة بتَحْديد مجموعة الكيانات المُرتَبِطة بموضوع المشكلة (problem domain)، والتي يُمكِن تمثيلها ككائنات (objects). على مستوى آخر، تُشجِع البرمجة كائنية التوجه (object-oriented programming) المبرمجين على إنشاء "أدوات برمجية مُعمَّمة" قابلة للاِستخدَام بالعديد من المشروعات البرمجية المختلفة. سنَستخدِم "الأدوات البرمجية المُعمَّمة" بصورة أكبر حين نبدأ باِستخدَام الأصناف القياسية (standard classes) بالجافا. سنبدأ هذا القسم بفَحْص بعضًا من تلك الأصناف المَبنية مُسْبَقًا (built-in)، والمُستخدَمة لإنشاء أنواع معينة من الكائنات (objects)، وسنعود مُجدّدًا إلى الصورة العامة بنهاية القسم. بعض الأصناف المبنية مسبقًا بالبرمجة كائنية التوجه (object-oriented programming)، يَكُون التركيز عادةً على تصميم أصناف جديدة، وكتابة تَّنْفيذها (implementation)، ولكن يَنبغي أيضًا ألا نُهمِل ذلك العدد الكبير من الأصناف (classes) التي وفرها لنا مُصمّمي الجافا. يُمكِننا أن نَستخدِم بعضًا من تلك الأصناف لإنتاج أصناف جديدة، بينما قد نَستخدِم البعض الآخر مُباشرة لإِنشاء كائنات. لكي تَتَمكَّن فعليًا من لغة الجافا، ينبغي أن تَكُون على دراية بعدد كبير من تلك الأصناف المَبنية مُسْبَقًا (built-in classes)، وهو ما يَحتاج إلى الكثير من الوقت والخبرة لتطويره. سنَفْحَص خلال هذا القسم بعضًا من تلك الأصناف. يُمكِننا أن نُنشِئ سِلسِلة نصية (string) من نصوص أصغر باِستخدَام العَامِل +، ولكن لا يَكُون ذلك دائمًا هو الحل الأفضل. فمثلًا، إذا كان str مُتْغيِّرًا من النوع String و ch محرفًا (character)، فإن تّنْفيذ الأمر str = str + ch;‎ يَتَضمَّن إنشاء كائن جديد كليًا من النوع String، مُكوَّن من نُسخة من str مع قيمة ch مُلحقَّة بآخره. يَستغرِق نَسخ السِلسِلة النصية بعض الوقت كما يَتَطلَّب قدرًا كبيرًا من المعالجة. في المقابل، يَسمَح الصنف StringBuilder بإنشاء سِلسِلة نصية طويلة من نصوص أصغر بكفاءة. يُمكِننا أن نُنشِئ كائنًا ينتمي إلى الصنف StringBuilder كالتالي: StringBuilder builder = new StringBuilder(); تُصرِّح تلك التَعْليمَة عن المُتْغيِّر builder، وتُهيئه مبدئيًا، بحيث يُشير إلى كائن من الصَنْف StringBuilder. بالقسم الفرعي ٤.٨.١، ناقشنا دمج عمليتي التّصريح (declaration) والتهيئة المبدئية (initialization) ضِمْن تَعْليمَة واحدة فيما يَتعَلَّق بالأنواع الأساسية (primitive types)، ولكن الأمر هو نفسه بالنسبة للكائنات. تمامًا كأي كائن من الصَنْف String، تَتَكوَّن كائنات الصَنْف StringBuilder من مُتتالية من المحارف. ولكن بإمكانك إلحاق محارف جديدة بدون إنشاء نُسخ جديدة من نفس البيانات التي تَتَضمَّنها تلك الكائنات بالفعل. إذا كانت x عبارة عن قيمة من أيّ نوع، وكان builder هو المُتْغيِّر المُعرَّف بالأعلى، فإن الأمر builder.append(x)‎ سيُضيف تمثيل x النصي (string representation) إلى نهاية البيانات الموجودة بالمُتْغيِّر builder، وهو ما يُعدّ أكثر كفاءة عمومًا من الاستمرار بنَسْخ البيانات مع كل مرة نُلحِق خلالها شيئًا جديدًا. تستطيع إنشاء سِلسِلة نصية طويلة باِستخدَام كائن من الصَنْف StringBuilder، وعبر مُتتالية من أوامر append()‎، وعندما تَكتمِل السِلسِلة النصية، ستُعيد الدالة builder.toString()‎ نُسخة من السِلسِلة النصية كقيمة عادية من النوع String. يَتوفَّر الصَنْف StringBuilder ضِمْن الحزمة (package) القياسية java.lang، ولذلك يُمكِن اِستخدَام الاسم البسيط للصَنْف بدون استيراده (import). تَحتوِي حزمة java.util على عدد من الأصناف المفيدة، فمثلًا، تَتَوفَّر أصناف للتَعامُل مع تجميعات (collections) من الكائنات، والتي سنتناولها تفصيليًا بالفصل العاشر. تَعرَّضنا للصَنْف java.util.Scanner المُعرَّف ضِمْن نفس الحزمة بالقسم الفرعي ٢.٤.٦. يَتوفَّر أيضًا الصَنْف java.util.Date، والذي يُستخدَم لأغراض تمثيل الوقت، فمثلًا، عندما تُنشِئ كائنًا (object) من النوع Date بدون مُعامِلات (parameters)، تُمثِل النتيجة كُلًا من التاريخ والوقت الحالي. يُمكِن عَرْض تلك المعلومة كالتالي: System.out.println( new Date() ); لمّا كان الصَنْف Date مُعرَّفًا بحزمة java.util، كان لابُدّ من إِتاحته أولًا بالبرنامج عن طريق استيراده، وذلك بكتابة أي من التَعْليمَتين import java.util.Date;‎ أو import java.util.*;‎ ببداية البرنامج، وبَعْدها ستَتَمكَّن من اِستخدَام الصَنْف. (ناقشنا الحزم والاستيراد بالقسم الفرعي ٤.٦.٣). لنَفْحَص أيضًا الصَنْف java.util.Random. تُعدّ كائنات ذلك الصَنْف مصدرًا للأعداد العشوائية، فيُمكِن لكائن من النوع Random أن يُنتج أعدادًا عشوائية سواء كانت صحيحة (integers) أو حقيقية (real). وفي الحقيقة، تَستخدِم الدالة القياسية Math.random()‎ كائنًا من ذلك الصَنْف وراء الكواليس؛ لإنتاج أعدادها العشوائية. اُنظر الشيفرة التالية لإنشاء كائن من الصَنْف Random: Random randGen = new Random(); بفَرْض أن N هو عدد صحيح موجب، سيُنتج randGen.nextInt(N)‎ عددًا صحيحًا عشوائيًا ضِمْن نطاق يتراوح من صفر وحتى N-1. يُمكِننا استخدام ذلك بتجربة رمِي حجري النَّرد لتَسهيل المُهِمّة؛ فبدلًا من كتابة die1 = (int)(6*Math.random())+1;‎، يُمكِننا كتابة die1 = randGen.nextInt(6)+1;‎. قد لا تتفق على أن تلك الطريقة أسهل نوعًا ما خاصة مع اضطرارنا لاستيراد (import) الصَنْف java.util.Random، وإنشاء كائن منه. يُمكِننا أيضًا اِستخدَام كائن من النوع Random لإنتاج ما يُعرَف باسم "التوزيع الاحتمالي الغاوسي (gaussian distribution)". تُستخدَم الكثير من أصناف جافا القياسية ببرمجة واجهات المُستخدِم الرسومية (GUI)، وسنَمُرّ عبر الكثير منها بالفصل السادس، ولكن سنَمُرّ هنا سريعًا على الصَنْف Color من حِزمة javafx.scene.paint حتى نَستخدِمه بالمثال التالي. يُمثِل أي كائن من الصَنْف Color لونًا يُستخدَم أثناء الرسم، ولقد تَعرَّضنا بالفعل لعدة ألوان ثابتة (constants) مثل Color.RED بالقسم ٣.٩. في الواقع، تلك الثوابت ما هي إلا مُتْغيِّرات أعضاء ساكنة (static) ونهائية (final) مُعرَّفة بالصَنْف Color، وقيمها عبارة عن كائنات من النوع Color. بالإضافة إلى تلك الألوان المُعرَّفة مُسْبَقًا، يُوفِّر الصَنْف Color مجموعة من البَوانِي (constructors) تَسمَح لك بإنشاء كائنات جديدة من الصَنْف لتَمثيل أيّ لون آخر. يَستقبِل إحداها ٣ مُعامِلات (parameters) من النوع double ويُمكِنك استدعائه باِستخدَام new Color(r,g,b)‎. تُمثِل تلك المُعامِلات كُلًا من اللون الأحمر والأخضر والأزرق بنظام الألوان RGB، ويَنبغي أن تَقع قيمها ضِمْن نطاق يتراوح من ٠ وحتى ١. تَعنِي القيمة ٠ للمُعامِل r أن اللون الفعليّ لا يَتَضمَّن اللون الأحمر نهائيًا، بينما تَعنِي القيمة ١ أنه يَتَضمَّن أكبر قدر مُمكن من اللون الأحمر. يَتوفَّر باني آخر يُستدعَى على الصورة new Color(r,g,b,t)‎، والذي يَستقبِل مُعامِلًا إضافيًا من النوع double، ويَنبغي أن تَقع قيمته ضِمْن نطاق يتراوح من ٠ وحتى ١. يُحدِّد ذلك المعامل درجة شفافية اللون (transparency)، بحيث تُمثِل القيم الأكبر من المُعامِل t لونًا أقل شفافية، فمثلًا، عندما تَرسم بلون شفاف جزئيًا، تَظهَر الخلفية عبر اللون إلى حد معين. .تَتَضمَّن الكائنات من الصَنْف Color عددًا قليلًا من توابع النسخ (instance methods). على سبيل المثال، تَتَوفَّر دوال (functions) مثل getRed()‎ لجَلْب قيمة اللون الأحمر بنظام RGB، وبالمثل للون الأخضر والأزرق. مع ذلك، لا يُوفِّر الصَنْف أي توابع ضَبْط (setter methods) لتَعْديل قيم تلك الألوان، حيث تُعدّ الكائنات من الصَنْف Color كائنات ثابتة أو غَيْر قابلة للتعديل (immutable)، بمعنى أن جميع مُتْغيِّرات النُسخ (instance variables) المُعرَّفة بداخلها هي نهائية -مُعرَّفة باِستخدَام المُبدِّل final-، وبالتالي لا يُمكِن تَعْديلها بَعْد إنشاء الكائن. لاحِظ أن السَلاسِل النصية من النوع String هي مثال آخر على الكائنات غَيْر القابلة للتعديل (immutable). النقطة الرئيسية مما سبق هو التأكيد على أن أصناف جافا القياسية تُوفِّر حلولًا للكثير من المشاكل التي قد تواجهها، لذا إذا تَعرَّضت لمُهِمّة (task) شائعة نوعًا ما، فلرُبما من الأفضل أن تَبحَث قليلًا بمَرجِع الجافا، لتَرى ما إذا كان هناك صَنْف يؤدي الغرض الذي تحتاج إليه. الصنف Object تُعدّ القدرة على إنشاء أصناف فرعية (subclasses) مُشتقَّة من صَنْف واحدة من أهم سمات البرمجة كائنية التوجه (object-oriented programming). يَرِث (inherit) الصَنْف الفرعي جميع خاصيات (properties) الصَنْف الأصلي وسلوكياته (behaviors)، ولكن بإِمكانه تَعْديلها وكذلك الإضافة إليها. ستَتَعلَّم طريقة إنشاء الأصناف الفرعية (subclasses) في القسم ٥.٥. في الواقع، أيّ صَنْف بلغة الجافا -باستثناء صَنْف وحيد- هو بالنهاية صَنْف فرعي من صَنْف آخر، فحتى لو أنشأت صَنْفًا، ولم تُصرِّح عن كَوْنه صَنْفًا فرعيًا من صَنْف آخر، فإنه تلقائيًا يُصبِح صنفًا فرعيًا من صَنْف خاص اسمه Object مُعرَّف بحزمة java.lang، وهو الصَنْف الاستثنائي الوحيد الذي لا يُعدّ صَنْفًا فرعيًا من أي صَنْف آخر. يُعرِّف الصنف Object مجموعة من توابع النُسخ (instance methods)، والتي تَرثها (inherit) جميع الأصناف الآخرى، لذا يُمكِن لأيّ كائن (object) مهما كان أن يَستخدِم تلك التوابع. سنَذكُر واحدة منها فقط بهذا القسم، وسنَتَعرَّض لمجموعة آخرى منها لاحقًا. يُعيد تابع النسخة toString()‎ المُعرَّف بالصنف Object قيمة من النوع String، والتي يُفْترَض أن تَكُون بمثابة تمثيلًا نصيًا (string representation) للكائن. في أي مرة نَطبَع فيها كائنًا أو نَضُمّه (concatenate) إلى سِلسِلة نصية، أو بصورة أعم نَستخدِمه بسياق يَتطلَّب سِلسِلة نصية، يُستدعَى هذا التابع ضمنيًا ليُحوِّل ذلك الكائن تلقائيًا إلى النوع String. تُعيد نسخة toString المُعرَّفة بالصنف Object اسم الصنف الذي ينتمي إليه الكائن مع ترميز التجزئة (hash code) الخاص به، وهو ما قد لا يَكُون مفيدًا. لذا عندما تُنشِئ صَنْفًا، تستطيع إعادة تعريف التابع toString()‎ بحيث تَحلّ النسخة الجديدة مَحلّ النسخة الموروثة، فمثلًا، يُمكِننا إضافة التابع (method) التالي لأيّ من أصناف PairOfDice المُعرَّفة بالقسم السابق: /** * يعيد تمثيلًا نصيًا لحجري نرد */ public String toString() { if (die1 == die2) return "double " + die1; else return die1 + " and " + die2; } إذا كان dice مُتْغيِّرًا يُشير إلى كائن من الصَنْف PairOfDice، فسيُعيد dice.toString()‎ سَلاسِل نصية مثل "‎3 and 6" و "‎5 and 1" و "double 2‎" بِحَسْب الأعداد الظاهرة على حجري النَّرد. يُستدَعى هذا التابع تلقائيًا لتَحْوِيل المُتْغيِّر dice إلى النوع String بالتَعْليمَة التالية: System.out.println( "The dice came up " + dice ); قد يَكُون خَرْج التعليمة -بالأعلى- "The dice came up 5 and 1" أو "The dice came up double 2". سنَتَعرَّض لمثال آخر للتابع toString()‎ بالقسم التالي. كتابة صنف واستخدامه سنَكْتُب الآن برنامج تَحرِيكة (animation)، بالاعتماد على نفس المنصة المُستخدَمة بالقسم الفرعي ٣.٩.٣، سيَكُون بمثابة مثالًا على تصميم صَنْف (class) جديد واِستخدَامه. ستُظهِر التَحرِيكة عددًا من الأقراص شبه الشفافة، والمُلوَّنة عشوائيًا، والمُتموضِعة بأماكن عشوائية عَبْر النافذة. عند تشغيل التحريكة، سيَزدَاد حجم تلك الأقراص، وعندما يَصِل حجم أي منها إلى حد معين، فإنه سيَختفِي ليَحلّ مَحلّه قرص آخر جديد بمكان عشوائي. قد يَحدُث ذلك -أيّ الاختفاء والظهور- بصورة عشوائية أيضًا بعيدًا عن حجم القرص. تَستعرِض الصورة التالية لقطة للشاشة أثناء تَشْغِيل البرنامج: سيَكُون كل قرص ضِمْن التَحرِيكة عبارة عن كائن (object)، ولأن أيّ قرص يَملُك عدة خاصيات (properties)، مثل اللون والموقع والحجم، فسنَستخدِم مُتْغيِّرات نُسخ (instance variables) لتمثيل كُلًا منها ضِمْن الكائن. أما بالنسبة لتوابع النُسخ (instance methods)، فيَنبغِي لنا التفكير أولًا بالكيفية التي سنَستخدِم بها القرص عَبْر البرنامج. سنَحتاج عمومًا إلى رسم القرص، لذا سنُضيف تابع النسخة draw(g)‎، حيث g هو كائن السِّياق الرُسومي (graphics context) المُستخدَم للرسم. يُمكِن للصَنْف أيضًا أن يَتَضمَّن بَانِي كائن (constructors) واحد أو أكثر لتهيئة الكائنات مبدئيًا. لا تَكُون البيانات المُفْترَض تمريرها للباني كمُعامِلات (parameters) واضحة دومًا. في هذا المثال، سنكتفي بتمرير كُلًا من موقع الدائرة وحجمها كمُعامِلات، أما لونها فسيَصنَعه الباني باِستخدَام قيم عشوائية للألوان الثلاثة بنظام RGB. اُنظر تعريف الصَنْف كاملًا: import javafx.scene.paint.Color; import javafx.scene.canvas.GraphicsContext; public class CircleInfo { public int radius; // نصف قطر الدائرة public int x,y; // موقع مركز الدائرة public Color color; // لون الدائرة /** * Create a CircleInfo with a given location and radius and with a * randomly selected, semi-transparent color. * @param centerX The x coordinate of the center. * @param centerY The y coordinate of the center. * @param rad The radius of the circle. */ public CircleInfo( int centerX, int centerY, int rad ) { x = centerX; y = centerY; radius = rad; double red = Math.random(); double green = Math.random(); double blue = Math.random(); color = new Color( red,green,blue, 0.4 ); } /** * Draw the disk in graphics context g, with a black outline. */ public void draw( GraphicsContext g ) { g.setFill( color ); g.fillOval( x - radius, y - radius, 2*radius, 2*radius ); g.setStroke( Color.BLACK ); g.strokeOval( x - radius, y - radius, 2*radius, 2*radius ); } } لاحِظ أننا قد صَرَّحنا عن مُتْغيِّرات النُسخ (instance variables) على أساس كَوْنها عامة (public)؛ لتبسيط الأمور، لكن يُفضَّل عمومًا كتابة ضوابط (setters) وجوالب (setters) لكُلًا منها. سنُعرِّف البرنامج main داخل الصنف GrowingCircleAnimation. ولأن البرنامج سيَستخدِم ١٠٠ قرص، كُلًا منها عبارة عن كائن من الصنف CircleInfo، فإنه سيَحتاج إلى مصفوفة من الكائنات لتَخْزِينها. لذا يُعرِّف البرنامج مُتْغيِّر مصفوفة (array variable) كمُتْغيِّر نُسخة (instance variable) ضِمْن الصنف، كالتالي: private CircleInfo[] circleData; // يحمل بيانات 100 قرص لاحِظ أن المُتْغيِّر circleData ليس ساكنًا (static). تَعتمِد برمجة واجهات المُستخدِم الرسومية (GUI) في العموم على الكائنات (objects) بدلًا من المُتْغيِّرات والتوابع الساكنة. يُمكِننا عمومًا تَخيُّل وجود عدة كائنات من الصنف GrowingCircleAnimation تَعمَل بصورة مُتزامنة. لذا ينبغي لكُلًا منها أن يمتلك مصفوفته الخاصة من الأقراص. بصيغة آخرى، كل تحريكة (animation) عبارة عن كائن، وكل كائن يَمتلك نسخته الخاصة من مُتْغيِّر النسخة circleData. إذا كان circleData ساكنًا، فسيَكُون هناك مصفوفة واحدة فقط، وستبدو جميع التَحرِيكات مُتطابقة تمامًا. الآن، لابُدّ أن نُنشِئ المصفوفة ونَملؤها بالبيانات. في البرنامج التالي، قُمنا بذلك حتى قَبْل رسم أول إطار (frame) ضِمْن التَحرِيكة. أولًا، اِستخدَمنا التعبير new CircleInfo[100]‎ لإنشاء المصفوفة، ثُمَّ أنشأنا مائة كائن من النوع CircleInfo لمَلئ المصفوفة. لاحِظ أن الكائنات الجديدة تَكُون عشوائية فيما يَتعلَّق بحجمها ومَوضِعها. بفَرْض أن width و height هي أبعاد مساحة الرسم (drawing area)، اُنظر الشيفرة التالية: circleData = new CircleInfo[100]; // أنشئ المصفوفة for (int i = 0; i < circleData.length; i++) { // أنشئ الكائنات circleData[i] = new CircleInfo( (int)(width*Math.random()), (int)(height*Math.random()), (int)(100*Math.random()) ); } يَزداد نصف قطر القرص بكل إطار (frame)، ثم يُعاد رسمه كالتالي: circleData[i].radius++; circleData[i].draw(g); قد تبدو التَعْليمَات -بالأعلى- مُعقدة نوعًا ما، لذا دَعْنَا نَفْحَصها عن قرب. أولًا، يُمثِل circleData‎ أحد عناصر المصفوفة circleData، أيّ أنه مُتْغيِّر من النوع CircleInfo. يُشير ذلك المُتْغيِّر إلى كائن من النوع CircleInfo، والذي لابُدّ أن يَحتوِي على مُتْغيِّر النُسخة (instance variable) العام radius، ويَكُون اسمه الكامل هو circleData.radius. لمّا كان مُتْغيِّر النسخة ذاك من النوع int، نستطيع تطبيق عامل الزيادة ++ عليه لزيادة قيمته بمقدار الواحد، أيّ أن تأثير التَعْليمَة circleData.radius++‎ هو زيادة نصف قطر الدائرة بمقدار الواحد. يُعدّ السطر الثاني من الشيفرة مشابهًا للأول، باستثناء أن circleData.draw يُمثِل تابع نُسخة (instance method) بالكائن. تَستدعِي التَعْليمَة circleData.draw(g)‎ تابع النُسخة ذاك، وتُمرِّر له المُعامِل g، والذي يُمثِل كائن السِّياق الرسومي المُستخدَم للرسم. يُمكِنك الإطلاع على الشيفرة المصدرية للبرنامج بالملف GrowingCircleAnimation.java إذا كنت مهتمًا. ولأن البرنامج يَستخدِم الصَنْف CircleInfo، ستحتاج أيضًا إلى نسخة الملف CircleInfo.java لتَصْرِيف البرنامج وتَشْغِيله. التحليل والتصميم كائني التوجه يَبنِي كل مبرمج عادةً مَخزونًا من التقنيات والخبرات، تَتَمثَل بهيئة قطع من الشيفرة مَكْتُوبة ومُجربَة، بحيث يَتمكَّن من إعادة اِستخدَامها ضِمْن أي برامج جديدة. تُنسَخ الشيفرة القديمة ببساطة إلى البرنامج الجديد، ثم تُعدَّل بما يَتلائَم مع البرنامج الجديد. يَعتمِد عندها كامل المشروع على قدرة المبرمج على سَحْب جزء الشيفرة ذاك، الذي كان قد كَتبَه ضِمْن مشروع سابق، والذي بدا أنه قابل للتَخْصِيص بحيث يَتَناسب مع المشروع الجديد. ولكن في الواقع، تُعانِي تلك الطريقة من كَوْن عملية التَعْدِيل تلك عُرضة للخطأ، كما أنها مُستهلِكة للوقت. تُحبِّذ أيضًا الشركات الكبيرة توفير الموارد بتَجنُّب إعادة اختراع العجلة مع كل مشروع جديد، ولكن تُصبِح مُهِمّة تَعقُّب كل تلك العجلات القديمة أكثر صعوبة مع الحجم الهائل من المشروعات بتلك الشركات. من الجهة الآخرى، تُعدّ الأصناف قطعًا برمجية قابلة لإعادة الاِستخدَام بدون الحاجة لإجراء أي تَعْديلات عليها بشَّرْط أن تَكُون قد صُمّمت تصميمًا جيدًا. مثلًا، لا تُعدّ الأصناف المصنوعة خصيصًا لإنجار مُهِمّة مُحدَّدة جدًا ضِمْن برنامج مُحدَّد مُصمّمة تصميمًا جيدًا، وإنما لابُدّ أن يَكُون الصنف مصنوعًا بحيث يُمثِل نوعًا واحدًا محددًا من الأشياء أو مفهومًا واحدًا مترابطًا. لمّا كانت تلك المفاهيم والأشياء عامة كفاية لأن تَتكرَّر ضِمْن العديد من المشاكل، تزداد إمكانية الحاجة لاِستخدَام تلك الأصناف ضِمْن عدة مشروعات وبدون إجراء أي تَعْديلات. ولأننا نَتعامَل مع لغة كائنية التوجه (object-oriented)، يُمكِننا أيضًا إنشاء أصناف فرعية (subclasses) من صَنْف موجود، وهو ما يُزيد من قابلية الأصناف لإعادة الاستخدَام. فمثلًا، إذا احتاج صَنْف معين لمزيد من التَخْصيص، يُمكِننا ببساطة إنشاء صَنْف فرعي (subclass) منه، وإجراء أي إضافات أو تَعْديلات على الصَنْف الفرعي بدون تَعْديل الصَنْف الأصلي. نستطيع القيام بذلك حتى لو لم يَكُن لدينا صلاحية وصول لشيفرة الصَنْف، ولا نعلم أي شيء عن التفاصيل التَّنْفيذية (implementation) الداخلية الخاصة به. في الواقع، يُعدّ الصنف PairOfDice -من القسم السابق- مثالًا على قطعة برمجية مُعمَّمة، حيث يُمثِل الصنف مفهومًا واحدًا متماسكًا هو "حجري نَّرد". تَحمِل مُتْغيِّرات النُسخ (instance variables) بيانات مُتعلِّقة بحالة حجري النَّرد أي العدد الظاهر بكل حجر، كما يُمثِل تابع النُسخة (instance method) سلوك حجري النَّرد أي القدرة على رَميهما. على الرغم من إمكانية تحسِّين ذلك الصَنْف، فإنه قابلًا لإعادة الاستخدام بالكثير من المشروعات البرمجية المختلفة. في المقابل، لا يُعدّ الصنف Student -من القسم السابق- قابلًا لإعادة الاستخدام؛ حيث يُبدو وكأنه قد صُمّم خِصيصًا لتمثيل مجموعة طلبة ضِمْن دورة تدريبية من نوع خاص، تَعتمِد درجاتها على ثلاثة اختبارات فقط، فإذا كان هناك حاجة لمزيد من الاختبارات أو الأوراق، فسيُصبِح الصنف عديم الفائدة. علاوة على ذلك، ستَحدُث مشكلة في حالة وجود شخصين لهما نفس الاسم، لذا عادةً ما يُستخدَم رقم هوية عددي بدلًا من الاسم. ولكن للإنصاف، فإن إنشاء صَنْف مُتعدد الأغراض لتمثيل الطلبة هو أمر أصعب كثيرًا من مُجرّد إنشاء صَنْف مُتعدد الأغراض لتمثيل حجري نَّرد. تَتَكوَّن عملية تطوير أيّ مشروع برمجي ضخم من مجموعة من المراحل، بدايةً من مرحلة توصيف (specification) المشكلة المطلوب حلّها، ثُمَّ تحليلها (analysis)، وتصميم (design) البرنامج اللازم لحلّها، ثُمَّ تأتي مرحلة كتابة الشيفرة (coding)، والتي يَتحوِّل خلالها التصميم إلى لغة برمجية فعليّة، وبعد ذلك تأتي مرحلة الاختبار (testing)، وتَنْقيح الأخطاء (debugging). يَتبَع ذلك فترة طويلة من الصيانة (maintenance)، والتي تَتَضمَّن إصلاح أي مشاكل جديدة عُثر عليها بالبرنامج، وكذلك تَعْديله بحيث يَتكيَّف مع أي تَغْيير بمُتطلبات البرنامج. تُسمَى مُتتالية المراحل تلك باسم "دورة حياة تطوير البرمجيات (software life cycle)". نادرًا ما تَتتابَع تلك المراحل على نحو مُتتالي تمامًا، فمثلًا، قد يَتَّضِح أن التوصيف مُتعارض أو غَيْر مُكتمل أثناء مرحلة التحليل، أو قد يَتطلَّب العُثور على مشكلة أثناء مرحلة الاختبار (testing) عودة سريعة إلى مرحلة كتابة الشيفرة (coding) على الأقل، أو حتى تصميمًا جديدًا إذا كانت المشكلة كبيرة بما فيه الكفاية. وأخيرًا، عادةً ما تَتَضمَّن مرحلة الصيانة (maintenance) إعادة بعض الأعمال من المراحل السابقة. نجاح أيّ مشروع برمجي ضخم عادةً ما يَكُون مَشْرُوطًا بتبَنّي أسلوب منهجي دقيق خلال جميع مراحل التطوير. يُعرَف ذلك الأسلوب المنهجي، والذي يَستخدِم مبادئ التصميم الجيد، باسم "هندسة البرمجيات (software engineering)". يُحاوِل مهندسي البرمجيات عمومًا بناء برامج مستوفية لتوصيفاتها (specifications)، وبحيث تَكُون مكتوبة بطريقة تُسهِل من تَعْدِيلها إن اقْتَضَت الضرورة. هنالك الكثير من الأساليب التي يُمكِن تطبيقها لتصميم البرامج بطريقة منهجية، وتَشتمِل غالبيتها على رسم صناديق صغيرة تُمثِل مُكوِّنات البرنامج مع أسهم مُعنوَنة لتمثيل العلاقات بين تلك الصناديق. لقد ناقشنا كائنية التوجه (object orientation) فيما يَتعلَّق بمرحلة كتابة الشيفرة (coding)، لكن تَتوفَّر أيضًا أساليب كائنية التوجه لمرحلتي التحليل والتصميم. يَكُون السؤال بهذه المرحلة من مراحل تطوير البرمجيات: كيف نَتَمكَّن من اكتشاف أو اختراع البنية الكلية للبرنامج؟ اتبع النصيحة التالية، والتي تُعدّ بمثابة أسلوبًا بسيطًا كائني التوجه لمرحلتي التحليل والتصميم: سَجِّل توصيف المشكلة، ثُمَّ ارسم خطًا أسفل جميع الأسماء الموجودة بذلك التوصيف. ينبغي أن تَكُون تلك الأسماء مُرشَّحة كأصناف (classes) أو ككائنات (objects) ضِمْن تصميم البرنامج. بالمثل، ارسم خطًا أسفل جميع الأفعال (verbs) الموجودة بالتوصيف. ينبغي أن تَكُون تلك الأفعال مُرشَّحة كتوابع (methods). هذه هي نقطة البداية فقط، فقد يَكشِف مزيد من التحليل (analysis) عن الحاجة لإضافة أصناف أو توابع آخرى، كما قد يَكشِف عن إمكانية اِستخدَام أصناف فرعية (subclasses) تَتشارَك الخاصيات والسلوكيات المُتشابهة بينها. "حلّل المشكلة لاكتشاف المفاهيم المُتضمَّنة، ثم عرِّف أصنافًا لتمثيل تلك المفاهيم. ينبغي أن يُحاكِي التصميم المشكلة، أو بتعبير آخر، ينبغي أن تَعكِس بنية البرنامج بنية المشكلة نفسها بصورة طبيعية." قد يَكُون ذلك تبسيطًا مُخِلًّا، ولكنه واضح وفعال عمومًا. ترجمة -بتصرّف- للقسم Section 3: Programming with Objects من فصل Chapter 5: Programming in the Large II: Objects and Classes من كتاب Introduction to Programming Using Java.
  7. تختلف الأنواع الكائنية (object types) بلغة الجافا عن الأنواع البسيطة (primitive type)، فمثلًا، لا تُنشِئ تَعْليمَة التّصْريح (declare) عن مُتْغيِّر، نوعه عبارة عن صَنْف (class)، كائنًا (object) من ذلك الصنف، بل ينبغي أن تقوم بذلك صراحةً. وفقًا للحاسوب، تَتَكوَّن عملية إنشاء كائن من محاولة العثور على مساحة غَيْر مُستخدَمة بقسم الكومة (heap) من الذاكرة وبحيث تَكُون كبيرة بما فيه الكفاية لحَمْل الكائن المطلوب إنشائه، ومن ثَمَّ مَلْئ مُتْغيِّرات نُسخ (instance variables) ذلك الكائن. كمبرمج، أنت لا تهتم عادةً بالمكان الذي يُخزَّن فيه الكائن بالذاكرة، ولكن ستَرغَب غالبًا بالتَحكُّم بالقيم المبدئية المُخزَّنة بمُتْغيِّرات نُسخ ذلك الكائن، كما قد تحتاج في بعض الأحيان إلى إجراء تهيئة (initialization) أكثر تعقيدًا مع كل كائن جديد يُنشَئ. تهيئة متغيرات النسخ (initialization) تستطيع عمومًا إِسْناد قيمة مبدئية إلى أيّ مُتْغيِّر نسخة (instance variable) أثناء التَّصْريح (declaration) عنه، وذلك كأيّ مُتْغيِّر عادي آخر. على سبيل المثال، لنَفْترِض مثلًا أن لدينا الصَنْف PairOfDice المُعرَّف بالأسفل. سيُمثِل أيّ كائن من هذا الصَنْف حجري نَّرد، وسيَحتوِي على مُتْغيِّري نسخة لتمثيل الأعداد الظاهرة على حجري النَّرد، بالإضافة إلى تابع نُسخة (instance method) مسئول عن مُهِمّة رمِي حجرى النَّرد. public class PairOfDice { public int die1 = 3; // العدد الظاهر على الحجر الأول public int die2 = 4; // العدد الظاهر على الحجر الثاني public void roll() { // حاكي تجربة رمي حجري النرد die1 = (int)(Math.random()*6) + 1; die2 = (int)(Math.random()*6) + 1; } } // ‫نهاية الصنف PairOfDice وفقًا لتعريف الصَنْف بالأعلى، سيُهيَئ (initialize) مُتْغيِّري النُسخة die1 و die2 إلى القيم ٣ و ٤ على الترتيب أينما بَنينا كائنًا من الصنف PairOfDice. ينبغي أن تفهم كيفية حُدوث ذلك: لمّا كان من المُمكن إِنشاء عدة كائنات من الصَنْف PairOfDice، فإنه، وبكل مرة يُنشَئ فيها واحد منها، فإن الكائن المُنشَىء سيَحصُل على مُتْغيِّري نُسخة (instance variables) خاصين به، ثم ستُنفَّذ تَعْليمتَي الإِسْناد die1 = 3 و die2 = 4 لمَلئ قيم مُتْغيِّراته. اُنظر النسخة التالية من الصَنْف PairOfDice لمزيد من الإيضاح: public class PairOfDice { public int die1 = (int)(Math.random()*6) + 1; public int die2 = (int)(Math.random()*6) + 1; public void roll() { die1 = (int)(Math.random()*6) + 1; die2 = (int)(Math.random()*6) + 1; } } // ‫نهاية الصنف PairOfDice وفقًا للتعريف بالأعلى، فإنه، وبكل مرة يُنشَئ فيها كائن (object) من الصَنْف PairOfDice، ستُهيَئ مُتْغيِّرات النُسخ إلى قيم عشوائية كما لو كنا نَرمِي حجري نَّرد على طاولة اللعب. لمّا كانت تلك التهيئة (initialization) تُنفَّذ لكل كائن على حدى، فستُحسَب تلك القيم لكل حجري نَّرد، وسيَحصُل كل حجري نَّرد مختلفين على قيم مبدئية مختلفة. سيَكُون الوضع مختلفًا بالتأكيد في حالة تهيئة المُتْغيِّرات الأعضاء الساكنة (static member variables)؛ وذلك لوجود نُسخة وحيدة فقط من أيّ مُتْغيِّر ساكن (static)، والتي تُهيَئ (initialization) مرة واحدة فقط عند تحميل الصَنْف (class) لأول مرة. تُهيَئ مُتْغيِّرات النُسخ (instance variable) بقيم مبدئية افتراضية تلقائيًا إذا لَمْ يُوفِّر لها المبرمج قيمة مبدئية. فمثلًا، تُهيَئ مُتْغيِّرات النسخ من الأنواع العددية مثل int و double تلقائيًا إلى الصفر إذا لم يُوفِّر لها المبرمج قيم آخرى، أما المُتْغيِّرات من النوع boolean فتُهيئ إلى القيمة false، بينما المُتْغيِّرات من النوع char تُهيَئ للمحرف المقابل لقيمة ترميز اليونيكود (Unicode code) رقم صفر ‎\u0000. وأخيرًا، بالنسبة لمُتْغيِّرات النُسخ كائنية النوع (object type)، فإن قيمها المبدئية الافتراضية هي القيمة الفارغة null. على سبيل المثال، لمّا كانت السَلاسِل النصية من النوع String عبارة عن كائنات (objects)، فإن القيمة المبدئية الافتراضية للمُتْغيِّرات من النوع String تُساوِي null. بواني الكائنات (constructors) يُستخدَم العَامِل new لإنشاء الكائنات (objects)، فمثلًا، يُمكِننا كتابة الشيفرة التالية لإنشاء كائن من الصَنْف PairOfDice: PairOfDice dice; // ‫صرح عن متغير من النوع PairOfDice // أنشئ كائنا جديدا من الصنف واسنده مرجعه الى المتغير dice = new PairOfDice(); يُخصِّص التعبير new PairOfDice()‎ مساحة بالذاكرة للكائن، ويُهيِئ مُتْغيِّرات النسخ (instance variables) الخاصة به، وأخيرًا، يُعيد مَرجِعًا (reference) إليه كقيمة للتعبير، والتي تُخزِّنها تَعْليمَة الإِسْناد (assignment statement) بالمُتْغيِّر dice، أي سيُشير dice إلى الكائن الجديد المُنشَئ للتو بعد تَّنْفيذ تَعْليمَة الإِسْناد. يبدو الجزء PairOfDice()‎ وكأنه عملية استدعاء لبرنامج فرعي (subroutine call). ليس هذا في الواقع مجرد مصادفة؛ فهو بالفعل يُمثِل عملية استدعاء، ولكن لنوع خاص من البرامج الفرعية تُعرَف باسم "البَانِي أو بَانِي الكائن (constructor)". قد يُربكك ذلك خاصة وأن تعريف الصَنْف PairOfDice لا يَحتوِي على أي برنامج فرعي بنفس تلك البصمة. في الحقيقة، لابُدّ لأيّ صَنْف من أن يَحتوِي على بَانِي كائن (constructor) واحد على الأقل، لذا يُوفِّر النظام بَانِي كائن افتراضي (default constructor) لأي صَنْف لم يُعرِّف له المبرمج بَانِي كائن (constructor). يَقْتصِر دور البواني الافتراضية على تَخْصِيص مساحة بالذاكرة للكائن، وتهيئة مُتْغيِّرات النُسخ (instance variables)، أما إذا أردت تَّنْفيذ أشياء آخرى عند إنشاء كائن من صَنْف معين، فستحتاج إلى تعريف باني كائن (constructor) واحد أو ربما أكثر ضِمْن تعريف ذلك الصَنْف. تُشبه تعريفات البَوانِي (constructors) في العموم تعريف أيّ برنامج فرعي (subroutine) آخر مع ثلاثة استثناءات. أولًا، لا يُمكِن تَخْصيص النوع المُعاد (return type) من البَانِي بما في ذلك المُبدِّل void. ثانيًا، ينبغي أن يَكُون اسم الباني هو نفسه اسم الصَنْف المُعرَّف بداخله. أخيرًا، تَقْتصِر المُبدِّلات (modifiers) التي يُمكِن اِستخدَامها بتعريف أيّ باني على مُبدِّلات الوصول (access modifiers)، أي public و private و protected، ولا يُمكِن اِستخدَام المُبدِّل static بالتحديد أثناء التَّصْريح عنها. في المقابل، يُمكِنك كتابة مَتْن البَانِي ككُتلَة من التَعْليمَات (statements) كأيّ مَتْن برنامج فرعي (subroutine body) تقليدي آخر؛ فلا يوجد أي قيود على التَعْليمَات التي يُمكِن اِستخدَامها ضِمْن المَتْن، كما يُمكِن للبَانِي أن يَستقبِل قائمة من المُعامِلات الصُّوريّة (formal parameters)، بل إن قدرته على اِستقبَال تلك المُعامِلات هي السبب الرئيسي من اِستخدَامه أساسًا، حيث تُوفِّر تلك المُعامِلات البيانات الضرورية لإِنشاء الكائن (object)، فمثلًا، يُمكِن لأحد بواني الصَنْف PairOfDice أن يُوفِّر قيم الأعداد المبدئية لحجري النَّرد. تَعرَض الشيفرة التالية تعريف الصَنْف في تلك الحالة: public class PairOfDice { public int die1; // العدد الظاهر بالحجر الأول public int die2; // العدد الظاهر بالحجر الثاني public PairOfDice(int val1, int val2) { // يُنشئ الباني حجري نرد ويُهيئهما مبدئيًا بالقيم الممررة die1 = val1; die2 = val2; } public void roll() { // حاكي تجربة الرمي die1 = (int)(Math.random()*6) + 1; die2 = (int)(Math.random()*6) + 1; } } // ‫نهاية الصنف PairOfDice صَرَّحت الشيفرة بالأعلى عن بَانِي كائن (constructor) على الصورة public PairOfDice(int val1, int val2) ...‎. ستُلاحِظ أننا لَمْ نُخصِّص النُوع المُعاد (return type) من البَانِي، كما أننا أعطيناه نفس اسم الصَنْف، وهذه هي الكيفية التي تُمكِّن مُصرِّف الجافا (Java compiler) من تَمييز بَوانِي الكائن. نُلاحِظ أيضًا أن البَانِي يَستقبِل مُعامِلين (parameters)، ينبغي لقيمهما أن تُمرَّر عند استدعاء البَانِي. على سبيل المثال، سيُنشِئ التعبير new PairOfDice(3,4)‎ كائنًا (object) من الصَنْف PairOfDice، ويُهيِئ مُتْغيِّرات نُسخه die1 و die2 مبدئيًا إلى القيم ٣ و ٤ على الترتيب. أخيرًا، ينبغي أن تُستخدَم القيمة المُعادة (return value) من البَانِي بطريقة ما، كالتالي: // ‫صرح عن متغير من الصنف PairOfDice PairOfDice dice; // ‫سيشير dice إلى كائن جديد من الصنف PairOfDice مُهيَأ مبدئيًا بالقيم 1 و 1 dice = new PairOfDice(1,1); الآن، وبعد أن أَضفنا بَانِي كائن إلى الصَنْف PairOfDice، لَمْ يَعُدْ بمقدورنا اِستخدَام التعبير new PairOfDice()‎ لإنشاء كائن؛ حيث تَحتوِي النسخة الجديدة من الصَنْف PairOfDice -بالأعلى- على بَانِي وحيد، والذي يَتَطلَّب مُعامِلين فعليين (actual parameter)، بينما نحن نحاول استدعاء بَانِي بدون أيّ مُعامِلات، ولأن النظام لا يُوفِّر البَانِي الافتراضي (default constructor) سوى للأصناف التي لا يَتَضمَّن تعريفها (class definition) أي بَانِي على الإطلاق، فسنحتاج إلى إضافة بَانِي آخر لا يَستقبِل أي مُعامِلات للصنف، وهو في الواقع أمر بسيط. تستطيع عمومًا إضافة أيّ عدد من البَوانِي (constructors) طالما كانت بَصْمتهم (signatures) مختلفة، أيّ طالما كان لديهم أعداد مختلفة أو أنواع مختلفة من المُعامِلات الصُّوريّة (formal parameters). بالشيفرة التالية، أَضفنا بَانِي بدون أي مُعامِلات إلى الصَنْف PairOfDice سيُهيِئ حجري النَّرد بقيم مبدئية عشوائية، كالتالي: public class PairOfDice { public int die1; // العدد الظاهر بالحجر الأول public int die2; // العدد الظاهر بالحجر الثاني public PairOfDice() { // يرمي الباني حجري النرد ليحصل على قيم عشوائية مبدئية roll(); } public PairOfDice(int val1, int val2) { // يُنشئ الباني حجري نرد ويُهيئهما مبدئيًا بالقيم الممررة die1 = val1; die2 = val2; } public void roll() { // حاكي تجربة الرمي die1 = (int)(Math.random()*6) + 1; die2 = (int)(Math.random()*6) + 1; } } // end class PairOfDice الآن، تستطيع إنشاء كائن من الصَنْف PairOfDice بطريقتين، إِما باِستخدَام التعبير new PairOfDice()‎ أو باِستخدَام التعبير new PairOfDice(x,y)‎، حيث x و y هي تعبيرات من النوع العَدَدَي الصحيح. تستطيع أيضًا اِستخدَام نفس الصَنْف -بالأعلى- بأي برنامج آخر يَتَعامَل مع حجري نَّرد، وبذلك لن تَضَطرّ إلى اِستخدَام التعبير الغامض نوعًا ما ‎(int)(Math.random()*6)+1 مرة آخرى؛ لأنه مُضمَّن بالفعل داخل الصَنْف PairOfDice، أيّ ستحتاج للتَعامُل مع مسألة رمِي حجري النَّرد مرة واحدة ضِمْن الصَنْف، ثم لا حاجة للقلق بشأنها مُجددًا. بالمثال التالي، يَستخدِم البرنامج main الصَنْف PairOfDice؛ لعدّ عدد مرات رمِي زوجين من حجري النَّرد حتى يَتساوَى حاصل مجموع كِلا الزوجين، وهو ما يُوضِح إِمكانية إنشاء عدة نُسخ (instances) من نفس الصَنْف: public class RollTwoPairs { public static void main(String[] args) { PairOfDice firstDice; // يشير إلى الزوج الأول من حجري النرد firstDice = new PairOfDice(); PairOfDice secondDice; // يشير إلى الزوج الثاني من حجري النرد secondDice = new PairOfDice(); int countRolls; // عدد مرات الرمي int total1; // حاصل مجموع الزوج الأول int total2; // حاصل مجموع الزوج الثاني countRolls = 0; do { // إرم زوجي حجري النرد حتى يتساوى حاصل مجموع كلا منهما firstDice.roll(); // إرم الزوج الأول total1 = firstDice.die1 + firstDice.die2; // Get total. System.out.println("First pair comes up " + total1); secondDice.roll(); // إرم الزوج الثاني total2 = secondDice.die1 + secondDice.die2; // Get total. System.out.println("Second pair comes up " + total2); countRolls++; // أزد عدد الرميات System.out.println(); // Blank line. } while (total1 != total2); System.out.println("It took " + countRolls + " rolls until the totals were the same."); } // ‫نهاية main() } // ‫نهاية الصنف RollTwoPairs البَوانِي في العموم عبارة عن برامج فرعية (subroutines)، ولكن من نوع خاص. هي بلا شك ليست توابع نُسخ (instance methods)؛ لأنها لا تنتمي إلى الكائنات (objects). لكونها مسئولة عن إِنشاء الكائنات، أي لابُدّ من وُجودها قَبْل وُجود أيّ كائن من الصَنْف، فإنها قد تَكُون أكثر شبهًا بالبرامج الفرعية الأعضاء الساكنة (static) مع أنه لا يُسمَح باِستخدَام المُبدِّل static أثناء تعريفها. تقنيًا، البَوانِي ليست أعضاء (members) ضِمْن الصَنْف على الإطلاق، ولا يُشار إليها على أساس كَوْنها توابع (methods) بالصَنْف. بخلاف البرامج الفرعية (subroutines) الآخرى، تستطيع استدعاء البَوانِي فقط من خلال العَامِل new، ويُكْتَب على الصيغة التالية: new <class-name> ( <parameter-list> ) قد تَكُون قائمة المُعامِلات فارغة. تُعدّ الشيفرة بالأعلى تعبيرًا (expression)؛ لأنها بالنهاية تَحسِب قيمة وتُعيدها، حيث تُعيد مَرجِعًا (reference) إلى الكائن المنُشَئ، والذي يُفترَض عادةً تَخْزِينه بمُتْغيِّر. تستطيع أيضًا اِستدعاء البَوانِي (constructor call) بطرائق شتى آخرى، مثلًا، كمُعامِل (parameter) ضِمْن تَعْليمَة اِستدعاء برنامج فرعي (subroutine call)، أو كجُزء من تعبير (expression) أكثر تعقيدًا. لاحِظ أنه في حالة عَدْم الاحتفاظ بالمَرجِع (reference) المُعاد داخل مُتْغيِّر، فلن تَتَمكَّن من الإشارة إلى الكائن المُنشَئ مرة آخرى. يُعدّ استدعاء البَانِي (constructor call) عمومًا أكثر تعقيدًا من أيّ استدعاء عادي لبرنامج فرعي (subroutine) أو لدالة (function)، لذا من المهم أن تَفهَم الخطوات التي يُنفِّذها الحاسوب أثناء استدعاء البَوانِي: يَعثُر الحاسوب على كُتلَة غَيْر مُستخدَمة بقسم الكَوْمة (heap) من الذاكرة، بشَّرْط أن تَكُون كبيرة بما فيه الكفاية لتَحمِل كائن من النوع المُخصَّص. يُهيِئ الحاسوب مُتْغيِّرات النُسخ (instance variables) للكائن، فإذا كان التَّصْريح عنها يَتَضمَّن قيمة مبدئية، فإنه يَحسِب تلك القيمة ويُخزِّنها بمُتْغيِّر النُسخة. أما إن لَمْ تَكُن مُضمَّنة، فإنه يَستخدِم القيمة المبدئية الافتراضية. تُحسَب قيم المُعامِلات الفعليّة (actual parameters) بالبَانِي -إن وُجدت-، ثم تُسنَد إلى المُعامِلات الصُّوريّة (formal parameters). تُنفَّذ التَعْليمَات الموجودة بمَتْن البَانِي (constructor body) إن وُجدت. يُعاد مَرجِع (reference) إلى الكائن المُنشَئ كقيمة لتعبير استدعاء البَانِي (constructor call) تَكُون النتيجة النهائية هي حُصولك على مَرجِع (reference) يُشير إلى الكائن المُنشَئ حديثًا. كمثال آخر، سنُضيف بَانِي كائن (constructor) إلى الصَنْف Student المُستخدَم بالقسم الأول، كذلك أَضفنا مُتْغيِّر نُسخة (instance variable) خاص اسمه هو name. اُنظر الشيفرة: public class Student { private String name; // اسم الطالب public double test1, test2, test3; // درجات الطالب public Student(String theName) { // يستقبل باني الكائن اسم الطالب if ( theName == null ) throw new IllegalArgumentException("name can't be null"); name = theName; } public String getName() { // تابع جلب لقراءة قيمة متغير النسخة return name; } public double getAverage() { // احسب متوسط درجات الطالب return (test1 + test2 + test3) / 3; } } // ‫نهاية الصنف Student عَرَّفنا بَانِي كائن (constructor) يَستقبِل مُعامِلًا (parameter) من النوع String ليُمثِل اسم الطالب. يَحتوِي أي كائن من النوع Student على بيانات طالب معين، ويُمكِننا عمومًا إِنشاء كائنات من ذلك الصَنْف باِستخدَام التَعْليمَات التالية مثلًا: std = new Student("John Smith"); std1 = new Student("Mary Jones"); بالنسخة الأصلية من ذلك الصَنْف، كان المُبرمج يَضطرّ لإِسْناد قيمة مُتْغيِّر النُسخة name بعد إنشاء الكائن، مما يَعنِي عدم وجود ضمانة لأن يَتَذكَّر المُبرمج ضَبْط قيمة ذلك المُتْغيِّر بصورة سليمة. في المقابل، بالنسخة الجديدة من الصَنْف، لا يُمكِن إنشاء كائن من الصَنْف Student إلا عن طريق استدعاء البَانِي (constructor)، والذي يَضبُط قيمة مُتْغيِّر النُسخة name تلقائيًا، كما أنه يَضمَن ألا تَكُون تلك القيمة فارغة. يُسهِل ذلك عمومًا من عَمَل المبرمج، ويُجنِّبه عددًا كبيرًا من الأخطاء البرمجية (bugs). لاحِظ أننا قد اِستخدَمنا المُبدِّل private ضِمْن تَعْليمَة التَّصْريح عن مُتْغيِّر النُسخة name، وهو ما يُوفِّر نوعًا آخر من الضمانة؛ فبذلك لن يَتَمكَّن أي مكان بالشيفرة خارج الصنف Student من الوصول مباشرة إلى مُتْغيِّر النُسخة name، فمثلًا، تُضبَط قيمته بشكل غَيْر مباشر عند استدعاء البَانِي (constructor). في حين يَتَضمَّن الصَنْف دالة جَلْب (getter function) هي getName()‎، والتي يُمكِن من خلالها مَعرِفة قيمة مُتْغيِّر النُسخة name لطالب معين من مكان خارج الصَنْف، فإن الصَنْف لا يَحتوِي على أيّ تابع ضَبْط (setter method) أو على أي طريقة آخرى لتَعْدِيل قيمة المُتْغيِّر name، أيّ بمُجرَّد إنشاء كائن من ذلك الصَنْف، فإنه سيَظلّ مُحتفِظًا بنفس قيمة المُتْغيِّر name المَبدئية طوال فترة وجوده بالبرنامج. في تلك الحالة، رُبما من الأفضل التَّصْريح عن مُتْغيِّر النُسخة name باِستخدَام المُبدِّل final. يُمكِنك التَّصْريح عن أيّ مُتْغيِّر نُسخة (instance variable) عمومًا باِستخدَام المُبدل final بشَّرْط إِسْناد قيمة إليه إما بتَعْليمَة التَّصْريح (declaration) أو بكل البواني (constructor) المُعرَّفة بذلك الصَنْف. في العموم، لا يُمكِن إِسْناد قيمة إلى مُتْغيِّر نُسخة (instance variable) قد صُرِّح عنه باِستخدَام المُبدِّل final، ولكن يُستَثنَى من ذلك مَتْن البواني (constructor). حسنًا، ماذا سيَحدُث عندما تَستخدِم أعضاء ساكنة (static) وآخرى غَيْر ساكنة (non-static) ضِمْن نفس الصَنْف؟ يَعرِف أيّ كائن عمومًا الصَنْف الذي يَنتمِي إليه، ويُمكِنه الإشارة إلى أعضاء صَنْفه الساكنة (static members). لذا يُمكِن لأيّ تابع نُسخة (instance method) بالصَنْف أن يُشير إلى المُتْغيِّرات الأعضاء (member variables) الساكنة أو أن يَستدعِي البرامج الفرعية الأعضاء (member subroutines) الساكنة. لكن، لاحِظ أن هناك دائمًا نُسخة وحيدة فقط من أيّ عضو ساكن، والتي تَنتمِي للصَنْف ذاته، مما يَعنِي أن جميع كائنات صَنْف معين تَتَشارك نُسخة وحيدة من أيّ عضو ساكن مُعرَّف بالصَنْف. كمثال، اُنظر النسخة التالية من الصَنْف Student، والتي أُضيف إليها مُتْغيِّر نُسخة ID لكل طالب، بالإضافة إلى عضو ساكن (static member) هو nextUniqueID. على الرغم من وجود مُتْغيِّر ID بكل كائن من النوع Student، فهناك مُتْغيِّر nextUniqueID واحد فقط: public class Student { private String name; // اسم الطالب public double test1, test2, test3; // درجات الطالب private int ID; // رقم معرف لهوية الطالب private static int nextUniqueID = 0; // للاحتفاظ برقم الهوية المتاح التالي Student(String theName) { // باني كائن يستقبل اسم الطالب ويسند رقم هوية فريد إليه name = theName; nextUniqueID++; ID = nextUniqueID; } public String getName() { // ‫تابع جلب لقراءة قيمة متغير النسخة name return name; } public int getID() { // تابع جلب لقراءة قيمة رقم الهوية return ID; } public double getAverage() { // احسب متوسط درجات الطالب return (test1 + test2 + test3) / 3; } } // ‫نهاية الصنف Student لمّا كان المُتْغيِّر nextUniqueID ساكنًا (static)، فإنه يُهيَئ مبدئيًا باِستخدَام التعبير nextUniqueID = 0 مرة واحدة فقط عند تحميل الصَنْف لأول مرة. بعد ذلك، عندما نُحاوِل إنشاء كائن من الصَنْف Student، سيُنفِّذ الباني (constructor) التَعْليمَة nextUniqueID++;‎، والتي تُزِيد دائمًا قيمة نفس المُتْغيِّر العضو الساكن (static member variable) بمقدار الواحد، فتُصبِح قيمة المُتْغيِّر nextUniqueID مُساوِية للعدد ١ بَعْد إنشاء أول كائن من الصَنْف Student، ثم تُصبِح مُساوِية للعدد ٢ بَعْد إنشاء الكائن الثاني، وتُصبِح مُساوِية للعدد ٣ بَعْد الكائن الثالث، ويستمر المُتْغيِّر بالزيادة مع كل كائن جديد يُنشَىء من ذلك الصَنْف. في المقابل، لمّا كان المُتْغيِّر ID عبارة عن مُتْغيِّر نُسخة (instance variable)، فإن كل كائن لديه نُسخته الخاصة من ذلك المُتْغيِّر، والتي يُخْزِّن فيها البَانِي القيمة الجديدة من المُتْغيِّر nextUniqueID بَعْد إِنشائه للكائن. صُمّم الصَنْف Student عمومًا بحيث يَحصُل كل طالب على قيمة مختلفة لنُسخته من المُتْغيِّر ID تلقائيًا. ولأننا قد صَرَّحنا عن المُتْغيِّر ID بكَوْنه خاصًا (private)، فيَستحِيل أيضًا التَلاعُب بقيمة ذلك المُتْغيِّر بأيّ طريقة بَعْد إنشاء الكائن، وهو ما يَضمَن تَخْصِيص رقم هوية فريد ودائم لكل كائن من الصَنْف Student، وهو أمر رائع إذا فكرت بالأمر! لو أعدت التفكير بالأمر، ستَجِدْ أن تلك الضمانة ليست مُطلقة تمامًا، وإنما هي مُقْتصِرة فقط على البرامج التي تَستخدِم خيطًا (thread) واحدًا، أما البرامج مُتعددة الخيوط (multi-thread)، أي التي يُمكِن خلالها تَّنْفيذ عدة أشياء بنفس ذات الوقت، فإن الأشياء تُصبِح غريبة قليلًا، فقد يُنشِئ خيطان كائنين من الصَنْف Student بنفس ذات الوقت، ولهذا قد يَحصُل كِلا الكائنين في تلك الحالة على نفس رقم الهوية. سنعود إلى هذا الموضوع بالقسم الفرعي ١٢.١.٣، حيث ستَتَعلَّم كيفية حلّ تلك المشكلة. كانس المهملات (garbage collection) حتى الآن، كان حديثنا خلال هذا القسم مُقْتصِرًا على إنشاء الكائنات، فماذا عن هَدْمِها؟ تُحذَف الكائنات بلغة الجافا أتوماتيكيًا. يَقع أيّ كائن عمومًا بقسم الكَوْمة (heap) من الذاكرة، ويُمكِن الوصول إلى كائن معين فقط عَبْر المُتْغيِّرات التي تَحمِل مَراجِع (references) تُشير إليه. والآن، ماذا سيَحدُث لو لم يَعُدْ هناك أيّ مُتْغيِّرات تُشير إلى كائن معين؟ اُنظر مثلًا للشيفرة التالية (لاحِظ أنها لأغراض تعليمية فقط؛ فليس من المُحتمل على الإطلاق كتابة تَعْليمَتين على هذا النحو ببرنامج فعليّ): Student std = new Student("John Smith"); std = null; بالسطر الأول، اُنشِئ كائن من الصَنْف Student، ثم خُزِّن مَرجِع (reference) يُشير إليه بالمُتْغيِّر std، ثم بالسطر التالي، تَغيَّرت قيمة std، ولم يَعُدْ مَرجِع الكائن المُنشَئ للتو موجودًا، بل لم يَعُدْ هناك أي مَراجِع (references) تُشير إليه بأي مُتْغيِّر آخر، وبالتالي، لن يَتَمكَّن البرنامج من اِستخدَام ذلك الكائن مُجددًا، ولهذا يَنبغي استعادة مساحة الذاكرة المُخصَّصة للكائن لكي تُصبِح مُتاحة للاِستخدَام لأغراض آخرى. تَستخدِم الجافا إجراءً (procedure) يُعرَف باسم "كَنْس المُهملات (garbage collection)"؛ لاستعادة أجزاء الذاكرة التي كانت قد خُصِّصت للكائنات (objects)، والتي لَمْ يَعُدْ بإِمكان البرنامج الوصول إليها. يَتولَّى النظام -لا المبرمج- مسئولية تَعقُّب تلك الكائنات التي أَصبحت ضِمْن "المُهملات (garbage)". بالمثال السابق، تستطيع بسهولة أن تَرى أن الكائن قد أَصبح ضِمْن المُهملات، ولكن تَكُون الأمور في العادة أكثر تعقيدًا خاصة بَعْد اِستخدَام الكائن لفترة، فيُحتمَل عندها وجود عدة مَراجِع (references) تُشير إليه مُخزَّنة بعدة مُتْغيِّرات. لاحِظ أن الكائن لا يُعدّ جزءًا من المُهملات إلا بعد أن تُصبِح جميع مَراجِعه غَيْر مُتوفرة. تَقَع مسئولية حَذْف المُهملات على عاتق المبرمج بالكثير من اللغات البرمجية الأخرى. ولمّا كان تَعقُّب اِستخدَام الذاكرة يَنطوِي على الكثير من التعقيدات، فإنه عادةً ما يَتسبَّب بحُدوث الكثير من الأخطاء البرمجية (bugs) الخطيرة. أحد تلك الأخطاء هو خطأ المُؤشر المُعلّق (dangling pointer)، والذي يَحدُث عندما يَحذِف المبرمج عن طريق الخطأ كائنًا من الذاكرة ما يزال البرنامج يَملُك مَرجِعًا (references) إليه، مما يَتسبَّب بحُدوث مشاكل إذا حَاول البرنامج الوصول إلى كائن لم يَعُدْ موجودًا. نوع آخر من الأخطاء هو تَسريب الذاكرة (memory leak)، والذي يَحدُث عندما لا يهتم المبرمج بحَذْف الكائنات التي لم تَعُدْ مُستخدَمة، مما يُؤدي إلى مَلئ الذاكرة بكائنات غَيْر قابلة للوصول، ومِن ثَمَّّ قد يُعاني البرنامج من مشكلة نفاد الذاكرة مع أنها فقط مُهدرة في الواقع. يَستحِيل حُدوث مثل تلك الأخطاء بالجافا؛ لأنها تَعتمِد على إجراء كَنْس المُهملات (garbage collection)، وهو عمومًا فكرة قديمة اِستخدَمتها بعض اللغات البرمجية بدايةً من ستينيات القرن الماضي. لماذا إذًا لا تَستخدِمها جميع اللغات؟ لأنها في الماضي كانت بطيئة جدًا، ولكنها أَصبحت عملية بفضل العديد من الدراسات العلمية التي تناولت موضوع كَنْس المهملات عن كثب، بالإضافة إلى السرعة الفائقة للحواسيب الحديثة. ترجمة -بتصرّف- للقسم Section 2: Constructors and Object Initialization من فصل Chapter 5: Programming in the Large II: Objects and Classes من كتاب Introduction to Programming Using Java.
  8. تُعدّ البرمجة كائنية التوجه (object-oriented programming - OOP) بمثابة محاولة لتَمْكِين البرامج من نمذجة طريقة تفكيرنا بالعالم الخارجي وتَعامُلنا معه. بالأنماط الأقدم من البرمجة، كان المبرمج مُضطرًّا لتَحْديد المُهِمّة الحاسوبية التي ينبغي له تّنْفيذها لكي يَحلّ المشكلة الفعليّة التي تُواجهه. ووفقًا لهذا التصور، فإن البرمجة تَتكوَّن من محاولة إيجاد متتالية التَعْليمَات المسئولة عن إنجاز تلك المُهِمّة. في المقابل، تُحاوِل البرمجة كائنية التوجه (object-oriented programming) العُثور على عدة كائنات (objects) تُمثِل كيانات (entities) حقيقية أو مُجرّدة مرتبطة بموضوع المشكلة (problem domain). تَملُك تلك الكيانات سلوكيات معينة (behavior)، كما تَتَضمَّن مجموعة من البيانات، وتستطيع أن تَتَفاعَل مع بعضها البعض. ووفقًا لهذا التصور، فإن البرمجة تَتكوَّن من عملية تصميم مجموعة الكائنات (objects) التي يُمكِنها محاكاة المشكلة المطلوب حَلّها. يجعل ذلك تصميم البرامج أكثر طبيعية، وأسهل في الكتابة والفهم عمومًا. يُمكِننا القول أن البرمجة كائنية التوجه (OOP) هي مجرد تَغيير بوجهات النظر؛ فبحَسَب مفاهيم البرمجة القياسية، قد نُفكِر بالكائن (object) على أساس أنه مُجرّد مجموعة من المُتْغيِّرات والبرامج الفرعية (subroutines) المسئولة عن معالجة تلك المُتْغيِّرات ضُمِّنت معًا بطريقة ما. بل حتى يُمكِننا اِستخدَام الأساليب كائنية التوجه (object-oriented) بأي لغة برمجية، ولكن بالطبع هنالك فرق كبير بين لغة تَسمَح بالبرمجة كائنية التوجه، وآخرى تُدعِّمها بشكل نَشِط. تُدعِّم اللغات البرمجية كائنية التوجه، مثل الجافا، عدة مِيزَات تَجعلها مختلفة عن أي لغة قياسية آخرى، ولكي نَستغِل تلك السمات والميزات بأكبر قدر مُمكن، ينبغي أن نُعيد توجيه تفكيرنا على نحو سليم. ملحوظة: عادةً ما يُستخدَم مصطلح "التوابع (methods)" للإشارة إلى البرامج الفرعية (subroutines) ضِمْن سياق البرمجة كائنية التوجه (object-oriented)، ولهذا سيَستخدِم هذا الفصل مصطلح "التوابع (method)" بدلًا من مصطلح "البرامج الفرعية (subroutine)". الكائنات (objects) والأصناف (classes) والنسخ (instances) لقد تحدثنا عن الأصناف (classes) خلال الفصول السابقة، واَتضح لنا أن الصَنْف يُمكِنه أن يَحتوِي على مُتْغيِّرات وتوابع (methods) يُشار إليها باسم البرامج الفرعية. سنَنتقل الآن إلى الحديث عما يُعرَف باسم الكائنات (objects)، والتي هي وثيقة الصلة بالأصناف. بدايةً، تَعَرَّضنا للكائنات (objects) مرة وحيدة من قَبْل خلال القسم ٣.٩، ولم نَلحظ خلالها فارقًا جوهريًا؛ فلقد اِستبعدنا فقط كلمة static من تعريف إحدى البرامج الفرعية (subroutine definition). في الواقع، يَتَكوَّن أي كائن من مجموعة من المُتْغيِّرات والتوابع، فكيف يختلف إذًا عن الأصناف (classes)؟ للإجابة على مثل هذا السؤال، ستحتاج إلى طريقة تفكير مختلفة نسبيًا وذلك حتى تُدرِك أهمية الكائنات وتَتَمكَّن من اِستخدَامها بطريقة فعالة. ذَكَرَنا أن الأصناف تُستخدَم لوصف الكائنات (objects)، أو بتعبير أدق، أن الأجزاء غَيْر الساكنة (non-static) من الأصناف تُستخدَم لوصف الكائنات. قد لا يَكُون المقصود من ذلك واضحًا بما فيه الكفاية، لذا دَعْنَا نُصيغه بطريقة آخرى والتي هي أكثر شيوعًا نوعًا ما. نستطيع أن نقول أن الكائنات تنتمي إلى أصناف (classes)، ولكن ليس بنفس الطريقة التي ينتمي بها مُتْغيِّر عضو معين (member variable) لصنف. فلربما من الأدق أن نقول أن الأصناف تُستخدَم لإنشاء كائنات، فالصَنْف (class) هو أَشْبه ما يَكُون بمصنع أو مُخطط أوَّليّ (blueprint) لإنشاء الكائنات بحيث تُحدِّد الأجزاء غَيْر الساكنة (non-static) منه مجموعة المُتْغيِّرات والتوابع التي يُفْترَض أن تَتَضمَّنها كائنات ذلك الصنف. لذا فإن أحد الاختلافات الجوهرية بين الأصناف والكائنات هو حقيقة أن الكائنات تُنشَئ وتُهدَم أثناء تَشْغِيل البرنامج، بل يُمكِن اِستخدَام صَنْف معين لإنشاء أكثر من كائن (object) بحيث يَكُون لها جميعًا نفس البنية. لنَفْترِض أن لدينا الصَنْف التالي، والذي يَحتوِي على عدة مُتْغيِّرات أعضاء ساكنة (static member variables). يُمكِننا أن نَستخدِمه لتَخْزِين معلومات عن مُستخدِم البرنامج مثلًا: class UserData { static String name; static int age; } عندما يُحمِّل الحاسوب ذلك الصَنْف، فإنه سيُكرِّس جزءًا من الذاكرة له بحيث يَتَضمَّن ذلك الجزء مساحة لقيم المُتْغيِّرين name و age، أيّ سيَكُون هناك نسخة وحيدة فقط من كلا المُتْغيِّرين UserData.name و UserData.age ضِمْن البرنامج عمومًا. تُبيِّن الصورة التالية تَصوُّرًا للصَنْف داخل الذاكرة: تُعدّ المُتْغيِّرات الأعضاء الساكنة (static) جزءًا من تمثيل الصنف داخل الذاكرة، ولهذا تُستخدَم الأسماء الكاملة المُتضمِّنة لاسم الصَنْف ذاته مثل UserData.name و UserData.age للإشارة إليها. في المثال السابق، قررنا اِستخدَام الصنف UserData لتمثيل مُستخدِم البرنامج، ولمّا كان لدينا مساحة بالذاكرة لتَخْزِين البيانات المُتعلِّقة بمُستخدِم واحد فقط، فبطبيعة الحال سيَكُون البرنامج قادرًا على حَمْل بيانات مُستخدِم وحيد. لاحِظ أن الصنف UserData والمُتْغيِّرات الساكنة (static) المُعرَّفة بداخله تَظلّ موجودة طوال فترة تَشْغِيل البرنامج، وهذا هو المقصود بكَوْنها ساكنة بالأساس. لنَفْحَص الآن صَنْفًا مُشابهًا، ولكنه يَتَضمَّن بعضًا من المُتْغيِّرات غَيْر الساكنة (non-static): class PlayerData { static int playerCount; String name; int age; } أُضيف إلى الصنف PlayerData مُتْغيِّر ساكن هو playerCount أيّ أن اسمه الكامل هو PlayerData.playerCount، كما أنه مُخزَّن كجزء من تمثيل الصَنْف بالذاكرة، وتَتَوَّفر منه نُسخة واحدة فقط تَظلّ موجودة طوال فترة تَّنْفيذ البرنامج. بالإضافة إلى ذلك، يَتَضمَّن تعريف الصَنْف (class definition) مُتْغيِّرين غَيْر ساكنين (non-static)، ولأن المُتْغيِّرات غَيْر الساكنة لا تُعدّ جزءًا من الصنف ذاته، فإنه لا يُمكِن الإشارة إلى هذين المُتْغيِّرين بالأسماء PlayerData.name أو PlayerData.age. نستطيع الآن اِستخدَام الصَنْف PlayerData لإنشاء كائنات بأيّ قدر نُريده، بحيث يَمتلك كل كائن منها مُتْغيِّراته الخاصة به والتي تَحمِل الأسماء name و age، أيّ يَحصُل كل كائن على نسخته الخاصة من أجزاء الصَنْف غَيْر الساكنة (non-static)، وهذا هو ما يَعنِيه القول بأن أجزاء الصَنْف غَيْر الساكنة تُعدّ بمثابة مُخططًا أوَّليًّا أو قالبًا (template) للكائنات. تُبيِّن الصورة التالية تصورًا لذاكرة الحاسوب بَعْد إِنشاء عدة كائنات: كما ترى بالأعلى، يُعدّ المُتْغيِّر الساكن playerCount جزءًا من الصنف وتَتَوفَّر منه نُسخة وحيدة، بينما تَتَوفَّر نُسخة خاصة من المُتْغيِّرين غَيْر الساكنين name و age بكل كائن (object) مُنشَئ من ذلك الصنف. يُعدّ أيّ كائن (object) نُسخة (instance) من صَنْفه، ويَكُون على دراية بالصَنْف المُستخدَم لإِنشائه. تُبيِّن الصورة أن الصَنْف PlayerData يَحتوِي على ما يُعرَف باسم "البَانِي أو بَانِي الكائن (constructor)". البَانِي ببساطة هو برنامج فرعي (subroutine) يُستخدَم لإِنشاء الكائنات سنتحدث عنه لاحقًا. ولأننا نستطيع إِنشاء كائنات (objects) جديدة من الصَنْف PlayerData لتَمْثيل لاعبين جُدد بقَدْر ما نَشَاء، فقد يُصبِح لدينا الآن مجموعة من اللاعبين. على سبيل المثال، قد يَستخدِم برنامج معين الصَنْف PlayerData لتَخْزين بيانات مجموعة لاعبين ضِمْن لعبة بحيث يَملُك كُلًا منهم name و age خاص به، وعندما يَنضَم لاعب جديد إلى اللعبة، يَستطيع البرنامج أن يُنشِئ كائنًا جديدًا من الصَنْف PlayerData لتمثيل ذلك اللاعب، أما إذا غادر أحد اللاعبين، يَستطيع البرنامج هَدْم الكائن المُمثِل لذلك اللاعب، أي سيَستخدِم ذلك البرنامج شبكة من الكائنات لنَمذجة أحداث اللعبة بصورة ديناميكية (dynamic)، وهو ما لا يُمكِنك القيام به باِستخدَام المُتْغيِّرات الساكنة (static). يُعدّ أيّ كائن (object) نُسخة (instance) من الصَنْف المُستخدَم لإِنشائه، ونقول أحيانًا بأنه يَنتمي إلى ذلك الصَنْف. يُطلَق اسم مُتْغيِّرات النُسخ (instance variables) على المُتْغيِّرات التي يَتَضمَّنها الكائن بينما يُطلَق اسم توابع النُسخ (instance methods) على التوابع (methods) -أو البرامج الفرعية- ضِمْن الكائن. فمثلًا، إذا اِستخدَمنا الصَنْف PlayerData المُعرَّف بالأعلى لإِنشاء كائن، فإن ذلك الكائن يُعدّ نُسخة (instance) من الصنف PlayerData كما يُعدّ كُلًا من name و age مُتْغيِّرات نُسخ (instance variable) ضِمْن الكائن. لا يَتَضمَّن المثال بالأعلى أيّ توابع (methods)، ولكنها تَعَمَل عمومًا بصورة مشابهة للمُتْغيِّرات أيّ تُعدّ التوابع الساكنة (static) جزءًا من الصَنْف ذاته بينما تُعدّ التوابع غَيْر الساكنة (non-static) أو توابع النُسخ (instance methods) جزءًا من الكائنات (objects) المُنشئة من ذلك الصَنْف. لكن، لا تَأخُذ ذلك بمعناه الحرفي؛ فلا يَحتوِي كل كائن على نُسخته الخاصة من شيفرة توابع النُسخ (instance method) المُعرَّفة ضِمْن الصَنْف بشكل فعليّ وإنما يُعدّ ذلك صحيحًا على نحو منطقي فقط، وعمومًا سنستمر بالإشارة إلى احتواء كائن معين على توابع نُسخ (instance method) صَنْفه. ينبغي عمومًا أن تُميز بين الشيفرة المصدرية (source code) للصَنْف والصَنْف ذاته بالذاكرة. تُحدِّد الشيفرة المصدرية كُلًا من هيئة الصَنْف وهيئة الكائنات المُنشئة منه: تُحدِّد التعريفات الساكنة (static definitions) بالشيفرة العناصر التي ستُصبِح جُزءًا من الصَنْف ذاته بذاكرة الحاسوب بينما تُحدِّد التعريفات غَيْر الساكنة (non-static definitions) بالشيفرة العناصر التي ستُصبِح جُزءًا من كل كائن يُنشَئ من ذلك الصنف. بالمناسبة، يُطلَق اسم مُتْغيِّرات الأصناف (class variables) وتوابع الأصناف (class methods) على المُتْغيِّرات الأعضاء (member variables) الساكنة والبرامج الفرعية الأعضاء (member subroutines) الساكنة المُعرَّفة ضِمْن الصنف على الترتيب؛ وذلك لكَوْنها تنتمي للصَنْف ذاته وليس لنُسخ (instances) ذلك الصَنْف. يَتَّضِح لنا الآن الكيفية التي تَختلِف بها الأجزاء الساكنة من صنف معين عن الأجزاء غَيْر الساكنة، حيث يَخدِم كُلًا منهما غرضًا مُختلفًا تمامًا. وفي العموم، ستَجِدْ أن كثيرًا من الأصناف إِما أنها تَحتوِي على أعضاء ساكنة (static) فقط أو على أعضاء غَيْر ساكنة (non-static) فقط، ومع ذلك سنَرى أمثلة قليلة لأصناف تَحتوِي على مَزيج منهما. أساسيات الكائنات اِقْتصَر حديثنا إلى الآن على الكائنات (objects) في العموم دون ذِكْر أيّ تفاصيل عن طريقة اِستخدَامها بصورة فعليّة ضِمْن برنامج، لذا سنَفْحَص مثالًا مُحدَّدًا لنوضح ذلك. لنَفْترِض أن لدينا الصَنْف Student المُعرَّف بالأسفل، والذي يُمكِننا اِستخدَامه لتَخْزِين بعض بيانات الطلبة المُلتحِقة بدورة تدريبية: public class Student { public String name; // اسم الطالب public double test1, test2, test3; // الدرجة بثلاثة اختبارات public double getAverage() { // احسب متوسط درجة الاختبارات return (test1 + test2 + test3) / 3; } } // ‫نهاية الصنف Student لم يُستخدَم المُبدِّل static أثناء التَّصْريح عن أيّ من أعضاء الصَنْف Student، لذا فالصَنْف موجود فقط بغَرْض إِنشاء الكائنات (objects). يُحدِّد تعريف الصَنْف Student -بالأعلى- أن كائناته أو نُسخه (instance) ستَحتوِي على مُتْغيِّرات النُسخ (instance variables)‏ name و test1 و test2 و test3 كما ستَحتوِي على تابع نسخة (instance method) هو getAverage()‎. سيَحتوِي كل كائن -يُمثِل طالبًا- في العموم على اسم طالب معين ودرجاته، وعندما يُستدعَى التابع getAverage()‎ لكائن معين، ستُحسَب قيمة المتوسط باِستخدَام درجات الاختبار ضِمْن ذلك الكائن تَحْديدًا أيّ سيَحصُل كل كائن على قيمة متوسط مختلفة، وهذا هو في الواقع ما يَعنِيه انتماء توابع النُسخ (instance method) للكائنات المُفردة لا الأصناف نفسها. الأصناف بلغة الجافا هي أنواع تمامًا كالأنواع المَبنية مُسْبَقًا (built-in) مثل int و boolean، لذا تستطيع اِستخدَام اسم صَنْف معين لتَحْدِيد نوع مُتْغيِّر ضِمْن تَعْليمَة تَّصْريح (declaration statement) أو لتَخْصِيص نوع مُعامِل صُّوريّ (formal parameter) أو لتَحْدِيد نوع القيمة المُعادة (return type) من دالة (function). يُمكِن مثلًا تعريف مُتْغيِّر اسمه std من النوع Student باِستخدَام التَعْليمَة التالية: Student std; انتبه للنقطة الهامة التالية، لا يُؤدي تَّصْرِيحك (declare) عن مُتْغيِّر نوعه عبارة عن صنف إلى إِنشاء الكائن ذاته! تَرتبط هذه النقطة بالحقيقتين التاليتين: لا يُمكِن لأيّ مُتْغيِّر أن يَحمِل كائنًا. يستطيع المُتْغيِّر أن يَحمِل مَرجِعًا (reference) يُشير إلى كائن. يُفضَّل أن تُفكِر بالكائنات (objects) كما لو أنها تَعيش مُستقلة بذاكرة الحاسوب، تحديدًا بقسم الكَوْمة (heap) من الذاكرة. لا تَحمِل المُتْغيِّرات كائنية النوع (object type) قيم الكائنات نفسها وإنما تَحمِل المعلومات الضرورية للعُثور على تلك الكائنات بالذاكرة، تَحْديدًا تَحمِل عناوين مَواضِعها (location address) بالذاكرة، ويُقال عندها أن المُتْغيِّر يَحمِل مَرجِعًا (reference) أو مُؤشرًا (pointer) إلى الكائن. عندما تَستخدِم مُتْغيِّر كائني النوع (object type)، يَستخدِم الحاسوب المَرجِع (reference) المُخْزَّن بالمُتْغيِّر للعُثور على قيمة الكائن الفعليّة. يُنشِئ العَامِل (operator)‏ new كائنًا جديدًا (object) حيث يَستدعِى برنامجًا فرعيًا -ذكرناه سابقًا- يقع ضِمْن الصَنْف يُطلَق عليه اسم البَانِي (constructor)، ثم يُعيد مَرجِعًا (reference) يُشير إلى الكائن المُنشَىء. لنَفْترِض مثلًا أن لدينا مُتْغيِّر std من النوع Student المُصرَّح عنه بالأعلى، يُمكِننا الآن كتابة تَعْليمَة الإِسْناد التالية (assignment statement): std = new Student(); تُنشِئ تَعْليمَة الإِسْناد -بالأعلى- كائنًًا (object) جديدًا بقسم الكَوْمة (heap) من الذاكرة، والذي يُعدّ نُسخة (instance) من الصَنْف Student، ثم تُخزِّن التَعْليمَة مَرجِعًا (reference) إلى ذلك الكائن بالمُتْغيِّر std أيّ أن قيمة المُتْغيِّر عبارة عن مَرجِع (reference) أو مُؤشر (pointer) إلى الكائن الواقع بمكان ما. لا يَصِح في العموم أن تقول "قيمة المُتْغيِّر std هي الكائن ذاته" على الرغم من صعوبة تَجنُّب ذلك أحيانًا، ولكن لا تَقل أبدًا أن "الكائن مُخزَّن بالمُتْغيِّر std" فهو أمر غَيْر صحيح على الإطلاق. يُفْترَض عمومًا أن تقول "يُشير المُتْغيِّر std إلى الكائن"، وهو ما سنُحاول الالتزام به قَدْر الإِمكان. لاحِظ أنه إذا ذَكَرَنا مثلًا أن "std هو كائن"، فالمقصود هو "std هو مُتْغيِّر يُشيِر إلى كائن". لنَفْترِض أن المُتْغيِّر std يُشير إلى كائن (object) عبارة عن نُسخة (instance) من الصَنْف Student أيّ يَحتوِي ذلك الكائن بطبيعة الحال على مُتْغيِّرات النُسخ name و test1 و test2 و test3. يُمكِننا الإشارة إلى تلك المُتْغيِّرات باِستخدَام std.name و std.test1 و std.test2 و std.test3 على الترتيب. يَتَّبِع ذلك نفس نَمْط التسمية المُعتاد بأنه عندما يَكُون B جزءًا من A، فإن اسم B الكامل هو A.B. اُنظر المثال التالي: System.out.println("Hello, " + std.name + ". Your test grades are:"); System.out.println(std.test1); System.out.println(std.test2); System.out.println(std.test3); ستَطبَع الشيفرة بالأعلى الاسم وقيم الدرجات بالكائن المُشار إليه باِستخدَام المُتْغيِّر std. بنفس الأسلوب، يَحتوِي ذلك الكائن على تابع نسخة هو getAverage()‎، والذي يُمكِننا استدعائه باِستخدَام std.getAverage()‎. يُمكِنك كتابة الشيفرة التالية لطباعة مُتوسط درجات الطالب: System.out.println( "Your average is " + std.getAverage() ); تستطيع عمومًا اِستخدَام std.name أينما أَمَكَن اِستخدَام مُتْغيِّر من النوع String. فمثلًا، قد تَستخدِمه ضِمْن تعبير (expression) أو قد تُسنِد قيمة إليه أو قد تَستخدِمه حتى لاستدعاء أحد البرامج الفرعية (subroutines) المُعرَّفة بالصَنْف String. فمثلًا، يُمثِل التعبير std.name.length()‎ عدد المحارف باسم الطالب. قد لا تُشير مُتْغيِّرات الكائنات (object variables) أيّ تلك التي نوعها هو عبارة عن صَنْف -مثل المُتْغيِّر std- إلى أيّ كائن نهائيًا، وفي تلك الحالة، يُقال أنها تَحمِل مُؤشرًا فارغًا (null pointer) أو مَرجِعًا فارغًا (null reference). يُكْتَب المُؤشر الفارغ (null pointer) باِستخدَام كلمة null، ولهذا تستطيع اِستخدَام الشيفرة التالية لتَخْزِين مَرجِع فارغ بالمُتْغيِّر std: std = null; في المثال بالأعلى، لا يَحتوِي المُتْغيِّر على مُؤشر لأيّ شيء آخر وإنما تُعدّ null هي قيمته الفعليّة، لذا لا يَصِح أن تقول: "يُشير المُتْغيِّر إلى القيمة الفارغة (null)"، فالمُتْغيِّر نفسه هو قيمة فارغة. تستطيع اختبار ما إذا كانت قيمة المُتْغيِّر std فارغة باِستخدَام التالي: if (std == null) . . . عندما تَكُون قيمة مُتْغيِّر كائن (object variable) فارغة أيّ تَحتوِي على null، يَعنِي ذلك أن المُتْغيِّر لا يُشير إلى أيّ كائن، وبالتالي لا يَكُون هناك أيّ مُتْغيِّرات نُسخ (instance variables) أو توابع نُسخ (instance methods) يُمكِن الإشارة إليها، ويَكُون من الخطأ محاولة القيام بذلك عبر ذاك المُتْغيِّر. فمثلًا، إذا كانت قيمة المُتْغيِّر std هي null، وحَاوَل برنامج معين الإشارة إلى std.test1، سيُؤدِي ذلك إلى محاولة اِستخدَام مؤشر فارغ (null pointer) بطريقة خاطئة، ولهذا سيُبلَّغ عن اِعتراض مُؤشِر فارغ (null pointer exception) من النوع NullPointerException أثناء تّنْفيذ البرنامج. لنَفْحَص مُتتالية التَعْليمَات التالية: Student std, std1, // ‫صرح عن أربعة متغيرات من النوع Student std2, std3; // ‫أنشئ كائن جديد من الصنف Student وخزن مرجعه بالمتغير std std = new Student(); // ‫أنشئ كائن آخر من الصنف Student وخزن مرجعه بالمتغير std1 std1 = new Student(); // ‫انسخ مرجع std1 إلى المتغير std2 std2 = std1; // ‫خزن مؤشرا فارغا بالمتغير std3 std3 = null; // اضبط قيم متغيرات الأعضاء std.name = "John Smith"; std1.name = "Mary Jones"; بعدما يُنفِّذ الحاسوب التَعْليمَات بالأعلى، ستُصبِح ذاكرة الحاسوب كالتالي: يَتَبين لنا التالي بَعْد فَحْص الصورة بالأعلى: أولًا، يَحتوِي أيّ مُتْغيِّر على مَرجِع (reference) إلى كائن ظَهَرَت قيمته ضِمْن الصورة بهيئة سهم يُشير إلى الكائن (object). ثانيًا، يُمكِننا أن نَرَى أن السَلاسِل النصية من النوع String هي كائنات. ثالثًا، لا يُشير المُتْغيِّر std3 إلى أيّ مكان؛ لأنه فارغ ويَحتوِي على القيمة null. وأخيرًا، تُشير الأسهم من std1 و std2 إلى نفس ذات الكائن وهو ما يُبرِز النقطة المهمة التالية: عندما نُسنِد مُتْغيِّر كائن (object variable) إلى آخر، يُنسَخ المَرجِع (reference) فقط لا الكائن (object) المُشار إليه بواسطة ذلك المَرجِع. لنَأخُذ مثلًا التَعْليمَة std2 = std1;‎ كمثال، لمّا كانت تَعْليمَة الإِسْناد (assignment statement) في العموم تَنسَخ فقط القيمة المُخْزَّنة بمُعاملها الأيمن std1 إلى مُعاملها الأيسر std2، ولأن القيمة في تلك الحالة هي مُجرَّد مُؤشِر (pointer) إلى كائن وليست الكائن (object) ذاته، فإن تلك التَعْليمَة لم تُنشِئ كائنًا جديدًا، وإنما ضَبَطَت std2 بحيث يُشير إلى نفس الكائن الذي يُشير إليه std1. يَترتَب على ذلك عدة نتائج قد تَكُون مفاجئة بالنسبة لك. مثلًا، تُعدّ كُلًا من std1.name و std2.name أسماءً مختلفة لنفس مُتْغيِّر النُسخة (instance variable) المُعرَّف ضِمْن الكائن الذي يُشير إليه كِلا المُتْغيِّرين std1 و std2، ولهذا عندما تُسنِد السِلسِلة النصية "Mary Jones" مثلًا إلى std1.name، ستُصبِح قيمة std2.name هي أيضًا "Mary Jones". إذا التبس عليك الأمر، حَاوِل فقط أن تَستحضر الحقيقة التالية: "المُتْغيِّر ليس هو الكائن (object)، فالأول يَحمِل فقط مُؤشرًا (pointer) إلى الثاني." يُستخدَم العَامِلان == و ‎!=‎ لاختبار تَساوِي كائنين أو عدم تَساوِيهما على الترتيب، ولكن لاحِظ أن المَعنَى الدلالي (semantics) وراء تلك الاختبارات مُختلف عما أنت مُعتاد عليه. فمثلًا، يَفْحَص الاختبار if (std1 == std2)‎ ما إذا كانت القيم المُخْزَّنة بكُلًا من std1 و std2 مُتساوية، ولأن تلك القيم هي مُجرَّد مَراجِع (references) تُشير إلى كائنات وليست الكائنات ذاتها، فإن ذلك الاختبار يَفْحَص ما إذا كان std1 و std2 يُشيران إلى نفس ذات الكائن أيّ ما إذا كانا يُشيران إلى نفس المَوضِع بالذاكرة. لا يُعدّ ذلك مشكلة إذا كان هذا هو ما تُريد اختباره، لكن أحيانًا يَكُون المقصود هو مَعرِفة ما إذا كانت مُتْغيِّرات النُسخ (instance variables) تَحمِل نفس القيم بغَضْ النظر عن انتمائها لنفس ذات الكائن. في تلك الحالات، ستحتاج إلى إجراء الاختبار التالي: std1.test1 == std2.test1 && std1.test2 == std2.test2 && std1.test3 == std2.test3 && std1.name.equals(std2.name) ذَكَرَنا مُسْبَقًا أن السَلاسِل النصية من النوع String هي بالأساس كائنات، كما أَظَهَرنا السِلسِلتين النصيتين "Mary Jones" و "John Smith" بهيئة كائنات بالصورة التوضيحية بالأعلى ولكن بدون البنية الداخلية للنوع String. تُعدّ تلك السِلاسِل كائنات مميزة، وتُعامَل وفقًا لقواعد صيغة خاصة. كأيّ مُتْغيِّر كائن (object variable)، تَحمِل المُتْغيِّرات من النوع String مَرجِعًا (reference) إلى سِلسِلة نصية وليس السِلسِلة النصية ذاتها. بُناءً على ذلك، لا يَصِح عادةً اِستخدَام العَامِل == لاختبار تَساوِي السَلاسِل النصية. فمثلًا، إذا كان لدينا مُتْغيِّر من النوع String اسمه هو greeting ويُشير إلى السِلسِلة النصية "Hello"، فهل يَكُون الاختبار greeting == "Hello"‎ مُتحقِّقًا؟ في الواقع، يُشير كُلًا من المُتْغيِّر greeting والسِلسِلة النصية المُجرَّدة "Hello" إلى سِلسِلة نصية تَحتوِي على المحارف H-e-l-l-o. ومع ذلك، فإنهما قد يَكُونان كائنين مختلفين صَدَفَ فقط احتوائهما على نفس المحارف، ولأن ذلك التعبير يَختبِر ما إذا كان greeting و "Hello" يَحتوِيان على نفس المحارف، وكذلك ما إذا كانا واقْعين بنفس مَوضِع الذاكرة، فإن الاختبار greeting == "Hello"‎ لا يُعدّ مُتحقِّقًا. في المقابل، تَقْتصِر الدالة greeting.equals("Hello")‎ على اختبار ما إذا كان greeting و "Hello" يَحتوِيان على نفس المحارف وهو عادةً ما تَرغَب باختباره. أخيرًا، يُمكِن للمُتْغيِّرات من النوع String أن تَكُون فارغة أيّ تحتوي على القيمة null، وفي تلك الحالة، سيَكُون اِستخدَام العَامِل == مُناسبًا لاختبار ما إذا كان المُتْغيِّر فارغًا كالتالي greeting == null. ذَكَرَنا أكثر من مرة أن مُتْغيِّرات الكائنات لا تَحمِل الكائنات (objects) ذاتها، وإنما تَحمِل مَراجِع (references) تُشير إلى تلك الكائنات، وهو ما يَترتَب عليه بعض من النتائج الآخرى التي ينبغي أن تَكُون على دراية بها والتي ستَجِدها منطقية إذا استحضرت بذهنك الحقيقة التالية: "لا تُخْزَّن الكائنات (object) بالمُتْغيِّرات، وإنما بأماكن آخرى تُشير إليها المُتْغيِّرات". النتائج كالتالي: أولًا، لنَفْترِض أن لدينا مُتْغيِّر كائن (object variable) صُرِّح عنه باِستخدَام المُبدِّل final أي لا يُمكِن تَغْيير القيمة المُخْزَّنة بذلك المُتْغيِّر نهائيًا بَعْد تهيئته مبدئيًا (initialize)، ولأن تلك القيمة هي مَرجِع (reference) يُشير إلى كائن، فإن المُتْغيِّر سيَستمر بالإشارة إلى نفس الكائن طالما كان موجودًا. مع ذلك، يُمكِن تَعْديل البيانات المُخْزَّنة بالكائن لأن المُتْغيِّر هو ما صُرِّح عنه باِستخدَام المُبدِّل final وليس الكائن نفسه، لذا يُمكِن كتابة الآتي: final Student stu = new Student(); stu.name = "John Doe"; ثانيًا، لنَفْترِض أن obj هو مُتْغيِّر يُشير إلى كائن، ماذا سيَحدُث عند تمرير obj كمُعامِل فعليّ (actual parameter) إلى برنامج فرعي (subroutine)؟ ببساطة، ستُسنَد قيمة obj إلى مُعامِل صُّوريّ (formal parameter)، ثم سيُنفَّذ البرنامج الفرعي، ولأن البرنامج الفرعي ليس لديه سوى نُسخة من قيمة المُتْغيِّر obj، فإنه لن يَستطيع تَغْيير القيمة المُخْزَّنة بالمُتْغيِّر الأصلي. ومع ذلك، لمّا كانت تلك القيمة هي مَرجِع (reference) إلى كائن، سيَتَمكَّن البرنامج الفرعي من تَعْديل البيانات المُخْزَّنة بالكائن. أي أنه وبانتهاء البرنامج الفرعي، فحتمًا ما يزال المُتْغيِّر obj يُشير إلى نفس الكائن، ولكن البيانات المُخزَّنة بالكائن ربما تَكُون قد تَغيَّرت. لنَفْترِض أن لدينا مُتْغيِّر x من النوع int، اُنظر الشيفرة التالية: void dontChange(int z) { z = 42; } x = 17; dontChange(x); System.out.println(x); // output the value 17. لاحظ أن البرنامج الفرعي لم يَتَمكَّن من تَغْيير قيمة x، وهو ما يُكافئ التالي: z = x; z = 42; والآن، لنَفْترِض أن لدينا مُتْغيِّر stu من النوع Student، اُنظر الشيفرة التالية: void change(Student s) { s.name = "Fred"; } stu.name = "Jane"; change(stu); System.out.println(stu.name); // output the value "Fred". في حين لم تَتَغيَّر قيمة stu، تَغيَّرت قيمة stu.name، وهو ما يُكافئ كتابة: s = stu; s.name = "Fred"; الضوابط (setters) والجوالب (getters) ينبغي أن تُراعِي مسألة التَحكُّم بالوصول (access control) عند كتابة أصناف (classes) جديدة، فتَّصْريحك عن كَوْن العضو (member) عامًا (public) يَجعله قابلًا للوصول من أي مكان بما في ذلك الأصناف الآخرى، في حين أن تَّصْريحك عن كَوْنه خاصًا (private) يجعل اِستخدَامه مَقْصورًا على الصَنْف المُعرَّف بداخله فقط. يُفضَّل عمومًا التَّصْريح عن غالبية المُتْغيِّرات الأعضاء (member variables) -إن لم يَكُن كلها- على أساس كَوْنها خاصة (private)؛ حتى يَكُون بإمكانك التَحكُّم بما يُمكِن القيام به بتلك المُتْغيِّرات تَحكُّمًا كاملًا. بَعْد ذلك، إذا أردت أن تَسمَح لأصناف آخرى بالوُصول لقيمة أحد تلك المُتْغيِّرات المُصرَّح عنها بكَوْنها خاصة، فيُمكِنك ببساطة كتابة تابع وُصول عام (public accessor method) يُعيد قيمة ذلك المُتْغيِّر. على سبيل المثال، إذا كان لديك صَنْف يَحتوِي على مُتْغيِّر عضو (member variable) خاص اسمه title من النوع String، تستطيع كتابة التابع (method) التالي: public String getTitle() { return title; } يُعيد التابع -بالأعلى- قيمة المُتْغيِّر العضو title. اُصطلح على تسمية توابع الوصول (accessor methods) للمُتْغيِّرات وفقًا لنمط معين بحيث يَتكوَّن الاسم من كلمة "get" مَتبوعة باسم المُتْغيِّر بعد تَكْبير حُروفه الأولى (capitalizing)، ولهذا يَتكوَّن اسم تابع وصول (accessor method) المُتْغيِّر title -بالأعلى- من الكلمتين "get" و "Title" أيّ يُصبح الاسم getTitle()‎. ولهذا يُطلَق عادةً على توابع الوصول اسم توابع الجَلْب (getter methods)، لأنها تُوفِّر "تَّصْريح قراءة (read access)" للمُتْغيِّرات. انتبه أنه في حالة المُتْغيِّرات من النوع boolean، تُستخدَم عادةً كلمة "is" بدلًا من "get" أيّ أنه إذا كان لدينا مُتْغيِّر عضو من النوع boolean اسمه done، فإن اسم جَالِبه (getter) قد يَكُون isDone()‎. قد تحتاج أيضًا إلى تَوْفِير "تصريح كتابة (write access)" لمُتْغيِّر خاص (private)؛ حتى تَتَمكَّن الأصناف الآخرى من تَخْصيص قيم جديدة لذلك المُتْغيِّر. تُستخدَم توابع الضَبْط (setter method) -أو تسمى أحيانًا بتوابع التَعْدِيل (mutator method)- لهذا الغرض، وبالمثل من توابع الجَلْب (getter methods)، ينبغي أن يَتكوَّن اسم تابع ضَبْط (setter method) مُتْغيِّر معين من كلمة "set" مَتبوعة باسم ذلك المُتْغيِّر بعد تَكْبير حُروفه الأولى. بالإضافة إلى ذلك، لابُدّ أن يَستقبِل تابع الضَبْط مُعامِلًا (parameter) من نفس نوع المُتْغيِّر. يُمكِن كتابة تابع ضْبْط (setter method) المُتْغيِّر title كالتالي: public void setTitle( String newTitle ) { title = newTitle; } ستحتاج غالبًا إلى كتابة كُلًا من تابعي الجَلْب (getter method) والضَبْط (setter method) لمُتْغيِّر عضو خاص معين؛ حتى تَتَمكَّن الأصناف الآخرى من رؤية قيمة ذلك المُتْغيِّر والتَعْدِيل عليها كذلك. قد تتساءل، لما لا نُصرِّح عن المُتْغيِّر على أساس كَوْنه عامًا (public) من البداية؟ قد يَكُون ذلك منطقيًا إذا كانت الجوالب (getters) والضوابط (setters) مُقْتصرة على قراءة قيم المُتْغيِّرات وكتابتها، ولكنها في الحقيقة قادرة على القيام بأيّ شيء آخر. فمثلًا، يستطيع تابع جَلْب (getter method) مُتْغيِّر معين أن يَحتفظ بعَدَدَ مرات قرائته كالتالي: public String getTitle() { titleAccessCount++; // Increment member variable titleAccessCount. return title; } كما يستطيع تابع ضَبْط (setter method) مُتْغيِّر آخر أن يَفْحص القيمة المطلوب إِسْنادها إليه لتَحْديد ما إذا كانت صالحة أم لا كالتالي: public void setTitle( String newTitle ) { if ( newTitle == null ) // لا تسمح بسَلاسِل نصية فارغة title = "(Untitled)"; // استخدم قيمة افتراضية else title = newTitle; } يُفضَّل عمومًا كتابة تابعي ضَبْط (setter method) وجَلْب (getter method) المُتْغيِّر حتى لو وَجَدَت أن دورهما مُقْتصِر على قراءة قيمة المُتْغيِّر وكتابتها، فأنت لا تدري، ربما تُغيِّر رأيك مُستقبلًا بينما تُعيد تصميم ذلك الصنف أو تُحسنه. ولأن تابعي الضبط والجلب جزء من الواجهة العامة (public interface) للصنف بعكس المُتْغيِّرات الأعضاء (member variable) الخاصة، فستستطيع ببساطة أن تُعدِّل تَّنْفيذ (implementations) تلك التوابع -هذا إذا كنت قد اِستخدَمتها منذ البداية- بدون تَغْيير الواجهة العامة للصَنْف وبدون أن تُؤثِر على أيّ أصناف أُخرى كانت قد اِستخدَمت ذلك الصَنْف. أما إذا لَمْ تَكُن قد اِستخدَمتها وتُريد ذلك الآن، فستَضطرّ آسفًا إلى التَواصُل مع كل شخص صَدَف وأن اِستخدَم الصَنْف؛ لتُبلِّغه بأن عليه فقط أن يَتَعقَّب كل مَوضِع بشيفرته يَستخدِم فيه ذلك المُتْغيِّر ويُعدِّله بحيث يَستخدِم تابعي الضَبْط (setter method) والجَلْب (getter method) للمُتْغيِّر العضو بدلًا من اسمه. كملاحظة أخيرة: تَشترِط بعض الخصائص المُتقدمة بلغة الجافا تَسمية توابع الضَبْط (setter methods) والجَلْب (getter methods) وفقًا للنَمْط المذكور بالأعلى، لهذا ينبغي في العموم أن تَتَّبِعه بصرامة. انتبه أيضًا للتالي: في حين أننا قد نَاقشنا تابعي الضَبْط والجَلْب ضِمْن سياق المُتْغيِّرات الأعضاء، فإنه في الواقع يُمكِن تَعْرِيف كُلًا منهما حتى في حالة عدم وجود مُتْغيِّر. يُعرِّف تابعي الضَبْط والجَلْب بالأساس خاصية (property) ضِمْن الصَنْف، والتي قد يُناظرها مُتْغيِّر أو لا. بتعبير آخر، إذا كان لديك صَنْف يَحتوِي على تابع نسخة (instance method) مُعرَّف باِستخدَام المُبدِّلات public void، وبَصمته (signature) هي setValue(double)‎، فإن ذلك الصَنْف حتمًا يَمتلك "خاصية (property)" اسمها value من النوع double، وذلك بغض النظر عن وجود مُتْغيِّر عضو (member variable) يَحمِل الاسم value ضِمْن ذلك الصَنْف من عدمه. المصفوفات والكائنات ذَكَرنا بالقسم الفرعي ٣.٨.١ أن المصفوفات (arrays) هي بالأساس كائنات أو ربما كائنات مميزة -تمامًا كالسَلاسِل النصية من النوع String-، وتُكْتَب وفقًا لقواعد صيغة (syntax) خاصة. فمثلًا تَتَوفَّر نسخة خاصة من العَامِل new لإنشاء المصفوفات. كأيّ مُتْغيِّر كائن (object variable)، لا يَحمِل أيّ مُتْغيِّر مصفوفة (array variable) قيمة مصفوفة فعليّة وإنما يَحمِل مرجعًا (reference) يُشير إلى كائن مصفوفة (array object) والذي يَكُون مُْخْزَّنًا بجزء المَكْدَس (heap) من الذاكرة. يُمكِن أيضًا لمُتْغيِّر مصفوفة أن يَحمِل القيمة null في حالة عدم وجود مصفوفة فعليّة. لاحظ أن كل نوع مصفوفة (array type) كالنوع int[]‎ أو النوع String[]‎ يُقابله صَنْف (class). فلنَفْترِض أن list هو مُتْغيِّر من النوع int[]‎. إذا كان ذلك المُتْغيِّر يَحمِل القيمة null، فسيَكُون من الخطأ أن تحاول قراءة list.length أو قراءة أيّ من عناصر المصفوفة (array element)‏ list‎، وسيَتَسبَّب بحُدوث اعتراض (exception) من النوع NullPointerException. بفَرْض أن newlist هو مُتْغيِّر آخر من نفس النوع int[]‎، اُنظر التَعْليمَة التالية: newlist = list; يَقْتصِر دور تَعْليمَة الإِسْناد (assignment statement) بالأعلى على نَسْخ قيمة المَرجِع (reference) المُخْزَّنة بالمُتْغيِّر list فقط إلى newlist. إذا كان المُتْغيِّر list يَحمِل القيمة null، فسيَحمِل المُتْغيِّر newlist القيمة null أيضًا أما إذا كان المُتْغيِّر list يُشير إلى مصفوفة، فلن تَنسَخ تَعْليمَة الإِسْناد تلك المصفوفة وإنما ستَضبُط المُتْغيِّر newlist فقط بحيث يُشير إلى نفس المصفوفة التي يُشير إليها المُتْغيِّر list. اُنظر الشيفرة التالية كمثال: list = new int[3]; list[1] = 17; // ‫تشير newlist إلى نفس المصفوفة التي يشير إليها list newlist = list; newlist[1] = 42; System.out.println( list[1] ); لأن list[1]‎ و newlist[1]‎ هي مجرد أسماء مختلفة لنفس عنصر المصفوفة، فسيَكُون خَرْج الشيفرة بالأعلى هو 42 وليس 17. قد تَجِدْ كل تلك التفاصيل مُربكة بالبداية، ولكن بمُجرَّد أن تَستحضِر بذهنك أن "أي مصفوفة هي عبارة عن كائن (object) وأن أيّ مُتْغيِّر مصفوفة (array variables) هو فقط يَحمِل مؤشرًا (pointers) إلى مصفوفة"، سيُصبِح كل شيء بديهيًا. تَنطبِق تلك الحقيقة أيضًا على تمرير مصفوفة كمُعامِل (parameter) إلى برنامج فرعي (subroutine) حيث سيَستقبِل البرنامج في تلك الحالة نُسخة فقط من المُؤشر (pointer) وليس المصفوفة ذاتها، وبذلك سيَملُك مَرجِعًا (reference) إلى المصفوفة الأصلية، وعليه فإن أيّ تَعْدِيلات قد يُجرِيها على عناصر المصفوفة (array elements)، سيَمتدّ أثرها إلى المصفوفة الأصلية وستستمر إلى ما بَعْد انتهاء البرنامج الفرعي. بالإضافة إلى كَوْن المصفوفات كائنات (objects) بالأساس، فإنها قد تُستخدَم لحَمل كائنات أيضًا، ويَكُون نوع المصفوفة الأساسي (base type) في تلك الحالة عبارة عن صَنْف. في الواقع، لقد تَعرَّضنا لذلك بالفعل عندما تَعامَلنا مع المصفوفات من النوع String[]‎، ولكن لا يَقْتصِر الأمر على النوع String بل تستطيع اِستخدَام أيّ صَنْف آخر. فمثلًا، يُمكِننا إنشاء مصفوفة من النوع Student[]‎، والتي نوعها الأساسي هو الصنف Student المُعرَّف مُسْبَقًا بهذا القسم، وفي تلك الحالة، يَكُون كل عنصر بالمصفوفة عبارة عن مُتْغيِّر من النوع Student. يُمكِننا مثلًا كتابة الشيفرة التالية لتَخْزين بيانات 30 طالب داخل مصفوفة: Student[] classlist; // Declare a variable of type Student[]. classlist = new Student[30]; // The variable now points to an array. تَتَكوَّن المصفوفة -بالأعلى- من 30 عنصر هي classlist[0]‎ و classlist[1]‎ وحتى classlist[29]‎. عند إنشاء تلك المصفوفة، تُهيَئ عناصرها إلى قيمة مبدئية افتراضية تُساوِي القيمة الفارغة null في حالة الكائنات، لذلك يُصبِح لدينا 30 عنصر مصفوفة، كُلًا منها أينعم من النوع Student لكنه يَحتوِي على القيمة الفارغة (null) وليس على كائن فعليّ من النوع Student. ينبغي أن نُنشِئ تلك الكائنات بأنفسنا كالتالي: Student[] classlist; classlist = new Student[30]; for ( int i = 0; i < 30; i++ ) { classlist[i] = new Student(); } وعليه، يُشير كل عنصر مصفوفة classlist‎ إلى كائن من النوع Student أيّ يُمكِن اِستخدَام classlist‎ بأيّ طريقة يُمكِن بها اِستخدَام مُتْغيِّر من النوع Student. فمثلًا، يُمكِن اِستخدَام classlist[3].name للإشارة إلى اسم الطالب المُخْزَّن برقم المَوضِع الثالث. كمثال آخر، يُمكِن استدعاء classlist.getAverage()‎ لحساب متوسط درجات الطالب المُخْزَّن برقم المَوضِع i. ترجمة -بتصرّف- للقسم Section 1: Objects, Instance Methods, and Instance Variables من فصل Chapter 5: Programming in the Large II: Objects and Classes من كتاب Introduction to Programming Using Java.
  9. كما ذَكَرَنا سابقًا، الأسماء هي أحد أركان البرمجة الأساسية. تَتَضمَّن مسألة التَّصْريح (declaring) عن الأسماء واِستخدَامها الكثير من التفاصيل التي تَجنَّبنا غالبيتها إلى الآن، ولكن حان وقت الكشف عن كثير من تلك التفاصيل. حاول التركيز عمومًا على محتوى القسمين الفرعيين "التهيئة أثناء التصريح" و "الثوابت المسماة (named constants)"؛ لأننا سنُعيد الإشارة إليهما باستمرار. التهيئة (initialization) أثناء التصريح (declaration) عندما نُصرِّح عن مُتَغيِّر ما (variable declaration)، يُخصِّص الحاسوب مساحة من الذاكرة لذلك المُتَغيِّر، والتي ينبغي تَهيئتها (initialize) مبدئيًا، بحيث يَتَضمَّن ذلك المُتَغيِّر قيمة معينة قَبْل أي مُحاولة لاِستخدَامه ضِمْن تعبير (expression). عادةً ما ترى تَعْليمَة التَّصْريح (declaration) عن مُتَغيِّر محليّ معين (local variable) مَتبوعة بتَعْليمَة إِسْناد (assignment statement) لغرض تهيئة ذلك المُتَغيِّر مبدئيًا (initialization). على سبيل المثال: int count; // ‫صرح عن المتغير count count = 0; // ‫أسند القيمة 0 إلى المتغير count في الواقع، تستطيع تَضْمِين التهيئة المبدئية (initialization) للمُتَغيِّر بنفس تَعْليمَة التصريح (declaration statement)، أيّ يُمكِن اختصار التَعْليمَتين بالأعلى إلى التَعْليمَة التالية: int count = 0; // ‫صرح عن المتغير count وأسند إليه القيمة 0 سيُنفِّذ الحاسوب ذلك على خطوتين: سيُصرِّح أولًا عن مُتَغيِّر اسمه count، ثم سيُسنِد القيمة صفر إلى ذلك المُتَغيِّر المُنشَئ للتو. ليس ضروريًا أن تَكُون قيمة المُتَغيِّر المبدئية ثابتة (constant)، وإِنما قد تَتَكوَّن من أي تعبير (expression). يُمكِنك أيضًا تهيئة عدة مُتَغيِّرات بتَعْليمَة تَّصْريح واحدة كالتالي: char firstInitial = 'D', secondInitial = 'E'; int x, y = 1; // ‫صالح ولكن أسندت القيمة 1 إلى المتغير y فقط int N = 3, M = N+2; // ‫صالح، هُيأت القيمة N قبل استخدامها لمّا كان المُتَغيِّر المُتحكِّم بحَلْقة معينة (loop control variable) غير مَعنِي بأي شيء يَقَع خارج تلك الحَلْقة عمومًا، كان من المنطقي التَّصْريح عنه ضِمْن ذلك الجزء من البرنامج الذي يَستخدِمُه فعليًا. ولهذا يَشيَع اِستخدَام التَعْليمَة المُختصرة بالأعلى ضِمْن حَلْقات التَكْرار for؛ حيث تُمكِّنك من التَّصْريح عن المُتَغيِّر المُتحكِّم بالحَلْقة، وتَهيئته مبدئيًا بمكان واحد ضِمْن الحَلْقة ذاتها. على سبيل المثال: for ( int i = 0; i < 10; i++ ) { System.out.println(i); } الشيفرة بالأعلى هي بالأساس اختصار لما يَلي: { int i; for ( i = 0; i < 10; i++ ) { System.out.println(i); } } أَضفنا زوجًا إضافيًا من الأقواس حول الحَلْقة -بالأعلى- للتأكيد على أن المُتَغيِّر i قد أصبح مُتَغيِّرًا محليًا (local) لتَعْليمَة الحَلْقة for، أيّ أنه لَمْ يَعُدْ موجودًا بعد انتهاء الحَلْقة. يُمكِنك أيضًا إجراء التهيئة المبدئية (initialize) للمُتَغيِّرات الأعضاء (member variable) أثناء تَّصْريحك عنها (declare)، كما هو الحال مع المُتَغيِّرات المحليّة (local variable). فمثلًا: public class Bank { private static double interestRate = 0.05; private static int maxWithdrawal = 200; . . // المزيد من المتغيرات والبرامج الفرعية . } عندما يُحمِّل مُفسِّر الجافا (Java interpreter) صنفًا معينًا، فإنه يُنشِئ أي مُتَغيِّر عضو ساكن (static member variable) ضِمْن ذلك الصنف، ويُهيِئ قيمته المبدئية (initialization). لمّا كانت تَعْليمَات التَّصْريح (declaration statements) هي النوع الوحيد من التَعْليمَات التي يُمكِن كتابتها خارج أيّ برنامج فرعي (subroutine)، وعليه فإن تَعْليمَات الإِسْناد (assignment statements) غير ممكنة الكتابة خارجها، كان لزامًا تهيئة المُتَغيِّرات الأعضاء الساكنة أثناء التَّصْريح عنها -إذا أردنا القيام بالتهيئة-، فهو في تلك الحالة ليس مُجرَّد اختصار لتَعْليمَة تَّصْريح (declaration) مَتبوعة بتَعْليمَة إِسناد (assignment statement) مثلما هو الحال مع المُتَغيِّرات المحليّة. لاحِظ عدم إِمكانية القيام بالتالي: public class Bank { private static double interestRate; // غير صالح! لا يمكن استخدام تعليمة إسناد خارج برنامج فرعي interestRate = 0.05; . . . لذلك، غالبًا ما يَحتوِي التَّصْريح عن مُتَغيِّر عضو (member variables) على قيمته المبدئية. وكما ذَكَرَنا سلفًا بالقسم الفرعي ٤.٢.٤، فإنه في حالة عدم تَخْصِيص قيمة مبدئية لمُتَغيِّر عضو معين (member variable)، تُسنَد إليه قيمة مبدئية افتراضية. على سبيل المثال، اِستخدَام التَعْليمَة static int count للتَّصْريح عن المُتَغيِّر العضو count هو مُكافِئ تمامًا للتَعْليمَة static int count = 0;‎. نستطيع أيضًا تهيئة مُتَغيِّرات المصفوفة (array variables) مبدئيًا. ولمّا كانت المصفوفة عمومًا مُكوَّنة من عدة عناصر وليس مُجرَّد قيمة وحيدة، تُستخدَم قائمة من القيم، مَفصولة بفاصلة (comma)، ومُحاطة بزوج من الأقواس؛ لتهيئة (initialize) المُتَغيِّرات من ذلك النوع، كالتالي: int[] smallPrimes = { 2, 3, 5, 7, 11, 13, 17, 19, 23, 29 }; تُنشِئ التَعْليمَة بالأعلى مصفوفة أعداد صحيحة (array of int)، وتَملؤها بالقيم ضِمْن القائمة. يُحدِّد عدد العناصر بالقائمة طول المصفوفة المُعرَّفة، أيّ أن طولها (length) -في المثال بالأعلى- يُساوِي ١٠. تَقْتصِر صيغة (syntax) تهيئة المصفوفات -يُقصَد بذلك قائمة العناصر- على تَعْليمَات التَّصْريح (declaration statement)، أي بينما نُصرِّح عن مُتَغيِّر مصفوفة (array variable)، ولا يُمكِن اِستخدَامها ضِمْن تَعْليمَات الإِسْناد (assignment statements). تستطيع أيضًا إنشاء مصفوفة باِستخدَام العَامِل new، ثم تَستخدِمها لتهيئة مُتَغيِّر مصفوفة (array variable). لاحِظ أن تلك الصيغة صالحة ضِمْن تَعْليمَات الإِسْناد أيضًا على العَكْس من الصيغة السابقة. على سبيل المثال: String[] nameList = new String[100]; ستَحتوِي جميع عناصر المصفوفة على القيمة الافتراضية في المثال بالأعلى. التصريح عن المتغيرات باستخدام var منذ الإصدار العاشر، تُوفِّر الجافا صيغة (syntax) جديدة للتَّصْريح عن المُتَغيِّرات، باستخدَام كلمة var، بشَّرْط تَخْصيص قيمة مبدئية ضِمْن تَعْليمَة التَّصْريح. تمتلك المُتَغيِّرات المُصرَّح عنها بتلك الطريقة نوعًا محدَّدًا، كأي مُتَغيِّر عادي آخر، ولكن بدلًا من تَحْديد ذلك النوع تحديدًا صريحًا، يعتمد مُصرِّف الجافا (Java compiler) على نوع القيمة المبدئية المُخصَّصة لتَحْديد نوع ذلك المُتَغيِّر. تَقْتصِر تلك الصيغة، مع ذلك، على المُتَغيِّرات المحليّة (local variables)، أي تلك المُصرَّح عنها ضِمْن برنامج فرعي (انظر القسم الفرعي ٤.٢.٤). اُنظر تَعْليمَة التَّصْريح التالية: var interestRate = 0.05; تُستخدَم التَعْليمَة بالأعلى لتعريف (define) مُتَغيِّر محليّ اسمه interestRate بقيمة مبدئية تُساوِي ٠,٠٥، ولمّا كانت القيمة المبدئية المُخصَّصة من النوع double، يَكُون نوع ذلك المُتَغيِّر هو double. بالمثل، للتَّصْريح عن مُتَغيِّر محليّ اسمه nameList من النوع String[]‎، يُمكِنك كتابة التالي: var nameList = new String[100]; تستطيع أيضًا اِستخدَام كلمة var أثناء التَّصْريح عن المُتَغيِّر المُتحكِّم بالحَلْقة (loop control variable) ضِمْن الحَلْقة for، كالتالي: for ( var i = 0; i < 10; i++ ) { System.out.println(i); } قد لا يبدو ذلك مفيدًا في الوقت الحالي، ولكنك ستُدرِك أهميته عندما نَتَعرَّض لما يُعرَف باسم الأنواع المُعمَّمة أو الأنواع المُحدَّدة بمعاملات نوع (parameterized types)، والتي تُعدّ أكثر تعقيدًا. سنتناول تلك الأنواع بكُلًا من القسم ٧.٢، والفصل العاشر. الثوابت المسماة (named constants) في بعض الأحيان، لا يَكُون هناك حاجة لتَعْديل قيمة المُتَغيِّر بَعْد تهيئتها مبدئيًا (initialize). على سبيل المثال، هُيَئ المُتَغيِّر interestRate بالأعلى، بحيث تَكُون قيمته المبدئية مُساوِية للقيمة ٠,٠٥. دَعْنَا نفْترِض أنه لا حاجة لتَعْديل تلك القيمة طوال فترة تَّنْفيذ البرنامج. في مثل تلك الحالات، قد يبدو المُتَغيِّر عديم الفائدة، ولهذا قد تَتَساءل، لماذا قد يُعرِّف (defining) المبرمج ذلك المُتَغيِّر من الأساس؟ في الواقع، يَلجأ المُبرمجين عادةً إلى تعريف تلك المُتَغيِّرات؛ بهدف إعطاء قيمة عددية معينة (٠,٠٥ في هذا المثال) اسمًا له مَغزى، فبخلاف ذلك، ستَكُون القيمة ٠,٠٥ بلا أي مَعنَى؛ فمن الأسهل عمومًا فِهم تَعْليمَة مثل principal += principal*interestRate;‎ بالمُوازنة مع principal += principal*0.05. يُمكِن اِستخدَام المُبدِّل (modifier)‏ final أثناء التَّصْريح عن مُتَغيِّر معين (variable declaration)؛ للتَأكُّد من اِستحالة تَعْديل القيمة المُخزَّنة بذلك المُتَغيِّر بمجرد تَهيئتها (initialize). إذا صَرَّحت، مثلًا، عن المُتَغيِّر العضو interestRate باستخدام التَعْليمَة التالية: public final static double interestRate = 0.05; فسيَستحِيل تَعْديل قيمة ذلك المُتَغيِّر interestRate بأيّ مكان آخر داخل البرنامج، فإذا حَاوَلت، مثلًا، اِستخدَام تَعْليمَة إِسْناد لتَعْديل قيمته، فإن الحاسوب سيُبلِّغ عن وجود خطأ في بناء الجملة (syntax error) أثناء تَصْرِيف (compile) البرنامج. من المنطقي عمومًا اِستخدَام المُبدِّل final مع مُتَغيِّر عام (public) يُمثِل قيمة "مُعدل الفائدة"؛ فبينما يُريد بنك معين الإعلان عن قيمة "مُعدل الفائدة"، فإنه، وبكل تأكيد، لا يُريد أن يَسمَح لأشخاص عشوائية بتَعْديل قيمة "مُعدل الفائدة". تَبرز أهمية المُبدِّل final عند اِستخدَامه مع المُتَغيِّرات الأعضاء، ولكنه مع ذلك، قابل للاِستخدَام مع كلا من المُتَغيِّرات المحليّة (local variables)، والمُعامِلات الصُّوريّة (formal parameters). يُطلَق مصطلح الثوابت المُسمَاة (named constants) على المُتَغيِّرات الأعضاء الساكنة (static member variable) المُصرَّح عنها باِستخدَام المُبدِّل final؛ لأن قيمتها تَظلّ تابثة طوال فترة تَّنْفيذ البرنامج. اِستخدَامك للثوابت المُسمَاة (named constants) قادر على تَحسِّين شيفرة البرامج بشكل كبير، وجَعْلها مَقروءة؛ فأنت بالنهاية تُعطِي أسماء ذات مَغزى للمقادير المُهمة بالبرنامج، ويُوصَى عمومًا بأن تَكُون تلك الأسماء مُكوَّنة بالكامل من حروف كبيرة (upper case letters)، بحيث يَفصِل محرف الشرطة السُفلية (underscore) بين الكلمات إذا ما لَزِم الأمر. على سبيل المثال، يُمكِن تعريف "مُعدَل الفائدة" كالتالي: public final static double INTEREST_RATE = 0.05; يَشيِع اِستخدَام نَمط التسمية بالأعلى عمومًا بأصناف الجافا القياسية (standard classes)، والتي تُعرِّف الكثير من الثوابت المُسمَاة. فمثلًا، تَعرَّضنا بالفعل للمُتَغيِّر Math.PI من النوع double المُعرَّف ضِمْن الصنف Math باِستخدَام المُبدِّلات public final static. بالمثل، يَحتوِي الصَنْف Color على مجموعة من الثوابت المُسمَاة (named constants) مثل: Color.RED و Color.YELLOW، والتي هي مُتَغيِّرات من النوع Color، مُعرَّفة باِستخدَام نفس مجموعة المُبدِّلات public final static. تُعدّ ثوابت أنواع التعداد (enumerated type constants)، والتي تَعرَّضنا لها خلال القسم الفرعي ٢.٣.٣، مثالًا آخرًا للثوابت المُسمَاة. يُمكِننا تعريف تعداد من النوع Alignment كالتالي: enum Alignment { LEFT, RIGHT, CENTER } يُعرِّف ذلك التعداد كُلًا من الثوابت Alignment.LEFT و Alignment.RIGHT و Alignment.CENTER. تقنيًا، يُعدّ Alignment صنفًا (class)، وتُعدّ تلك الثوابت (constants) الثلاثة أعضاء ساكنة عامة ونهائية (public final static) ضِمْن ذلك الصَنْف. في الواقع، يُشبِه تعريف نوع التعداد (enumerated type) بالأعلى تعريف ثلاثة ثوابت من النوع int كالتالي: public static final int ALIGNMENT_LEFT = 0; public static final int ALIGNMENT_RIGHT = 1; public static final int ALIGNMENT_CENTER = 2; وفي الحقيقة، كانت تلك الطريقة هي الأسلوب المُتبَع قبل ظهور أنواع التعداد (enumerated types)، وما تزال في الواقع مُستخدَمة بكثير من الحالات. فبِتَوفُّر ثوابت الأعداد الصحيحة (integer constants) بالأعلى، تستطيع الآن ببساطة تعريف مُتَغيِّر من النوع int، وإِسْناد واحدة من تلك القيم الثلاثة ALIGNMENT_LEFT و ALIGNMENT_RIGHT و ALIGNMENT_CENTER له؛ بهدف تمثيل أنواع مختلفة من التعداد Alignment. مع ذلك هناك مشكلة، وهي عدم إِطلاع الحاسوب على نيتك باِستخدَام ذلك المُتَغيِّر لتمثيل قيمة من النوع Alignment، لذا فإنه لن يَعترِض إذا حاولت إِسْناد قيمة هي ليست ضِمْن تلك القيم الثلاثة الصالحة. في المقابل، ومع ظهور أنواع التعداد (enumerated type)، فإنه إذا كان لديك مُتَغيِّر من نوع التعداد Alignment، فيُمكِنك فقط إِسْناد إحدى تلك القيم الثابتة المُدرجة بتعريف التعداد إليه، وأي محاولة لإِسْناد قيم آخرى غَيْر صالحة ستُعدّ بمثابة خطأ ببناء الجملة (syntax error) سيَكْتَشفها الحاسوب أثناء تَصْرِيف (compile) البرنامج. يُوفِّر ذلك نوعًا من الأمان الإضافي، وهو أحد أهم مميزات أنواع التعداد (enumerated types). يُسهِل استخدام الثوابت المُسمَاة (named constants) من تَعْديل قيمة الثابت المُسمَى. بالطبع، لا يُقصَد بذلك تَعْديلها أثناء تَّنْفيذ البرنامج، وإنما بين تَّنْفيذ معين والذي يليه، أيّ تَعْديل القيمة داخل الشيفرة المصدرية ذاتها ثم إعادة تَصرِيف (recompile) البرنامج. لنُعيد التفكير بمثال "معدل الفائدة"، غالبًا ما ستَكُون قيمة "مُعدَل الفائدة" مُستخدَمة أكثر من مرة عبر شيفرة البرنامج. لنفْترِض أن البنك قد عَدَّل قيمة "معدل الفائدة"، وعليه ينبغي تَعْديلها بالبرنامج. إذا كانت القيمة ٠,٠٥ مُستخدَمة بشكل مُجرَّد ضِمْن الشيفرة، فسيَضطرّ المبرمج لتَعقُّب جميع الأماكن المُستخدِمة لتلك القيمة بحيث يُعدِّلها إلى القيمة الجديدة. يَصعُب القيام بذلك خصوصًا مع احتمالية اِستخدَام القيمة ٠,٠٥ داخل البرنامج لأهداف مختلفة غير "مُعدَل الفائدة"، بالإضافة إلى احتمالية اِستخدَام القيمة ٠,٠٢٥ مثلًا لتمثيل نصف قيمة "مُعدَل الفائدة". في المقابل، إذا كنا قد صَرَّحنا عن الثابت المُسمَى INTEREST_RATE ضِمْن البرنامج، واستخدَمناه بصورة مُتَّسقِة عبر شيفرة البرنامج بالكامل، فسنحتاج إلى تعديل سطر وحيد فقط هو سَطْر التهيئة المبدئية لذلك الثابت. لنأخذ مثالًا آخر من القسم السابق، سنُعيد خلاله كتابة نسخة جديدة من البرنامج RandomMosaicWalk. ستَستخدِم تلك النسخة عدة ثوابت مُسمَاة (named constants) بهدف تمثيل كُلًا من عدد الصفوف، وعدد الأعمدة، وحجم كل مربع صغير بنافذة الصَنْف mosaic. سيُصرَّح عن تلك الثوابت (constants) الثلاثة كمُتَغيِّرات أعضاء ساكنة نهائية (final static member) كالتالي: final static int ROWS = 20; // عدد صفوف النافذة final static int COLUMNS = 30; // عدد أعمدة النافذة final static int SQUARE_SIZE = 15; // حجم كل مربع بالنافذة عُدِّل أيضًا باقي البرنامج بحرِص بحيث يتلائم مع الثوابت المُسمَاة (named constants) المُعرَّفة، فمثلًا، لفَتْح نافذة Mosaic بالنسخة الجديدة من البرنامج، يُمكِن استخدام التَعْليمَة التالية: Mosaic.open(ROWS, COLUMNS, SQUARE_SIZE, SQUARE_SIZE); ليس من السهل دومًا العُثور على جميع تلك الأماكن التي يُفْترَض بها اِستخدَام ثابت مُسمَى معين. لذا، انتبه! ففي حالة عدم اِستخدَامك لثابت مُسمَى معين بصورة مُتَّسقِة عبر شيفرة البرنامج بالكامل، فأنت تقريبًا قد أَفسدت الهدف الأساسي منه. لذلك يُنصَح عادة بتجربة البرنامج عدة مرات، بحيث تَستخدِم قيمة مختلفة للثابت المُسمَى (named constant) في كل مرة؛ لاِختبار كَوْنه يَعَمَل بصورة سليمة بجميع الحالات. اُنظر النسخة الجديدة من البرنامج كاملة RandomMosaicWalk2 بالأسفل. لاحظ كيفية اِستخدَام الثابتين ROWS و COLUMNS داخل البرنامج randomMove()‎ عند تحريكه للتشويش (disturbance) من إحدى حواف النافذة mosaic إلى الحافة المضادة. لم تُضاف التعليقات (comments) لغرض تَوْفِير المساحة. public class RandomMosaicWalk2 { final static int ROWS = 20; // عدد الصفوف بالنافذة final static int COLUMNS = 30; // عدد الأعمدة بالنافذة final static int SQUARE_SIZE = 15; // حجم كل مربع بالنافذة static int currentRow; // رقم الصف المعرض للتشويش static int currentColumn; // رقم العمود المعرض للتشويش public static void main(String[] args) { Mosaic.open( ROWS, COLUMNS, SQUARE_SIZE, SQUARE_SIZE ); fillWithRandomColors(); currentRow = ROWS / 2; // ابدأ بمنتصف النافذة currentColumn = COLUMNS / 2; while (true) { changeToRandomColor(currentRow, currentColumn); randomMove(); Mosaic.delay(5); } } // نهاية‫ main static void fillWithRandomColors() { for (int row=0; row < ROWS; row++) { for (int column=0; column < COLUMNS; column++) { changeToRandomColor(row, column); } } } // نهاية‫ fillWithRandomColors static void changeToRandomColor(int rowNum, int colNum) { // اختر قيم عشوائية تتراوح بين 0 و 255 // لقيم الألوان الثلاثة (الأحمر، و الأزرق، والأخضر‫) // ‫بنظام الألوان RGB int red = (int)(256*Math.random()); int green = (int)(256*Math.random()); int blue = (int)(256*Math.random()); Mosaic.setColor(rowNum,colNum,red,green,blue); } // نهاية‫ changeToRandomColor static void randomMove() { // ‫اضبط القيمة عشوائيًا بحيث تتراوح من 0 وحتى 3 int directionNum; directionNum = (int)(4*Math.random()); switch (directionNum) { case 0: // تحرك للأعلى currentRow--; if (currentRow < 0) currentRow = ROWS - 1; break; case 1: // تحرك لليمين currentColumn++; if (currentColumn >= COLUMNS) currentColumn = 0; break; case 2: // تحرك للأسفل currentRow++; if (currentRow >= ROWS) currentRow = 0; break; case 3: // تحرك لليسار currentColumn--; if (currentColumn < 0) currentColumn = COLUMNS - 1; break; } } // ‫نهاية randomMove } // نهاية الصنف‫ RandomMosaicWalk2 التسمية وقواعد النطاق (scope rules) عندما نُصرِّح عن مُتَغيِّر ما (variable declaration)، يُخصِّص الحاسوب مساحة من الذاكرة لذلك المُتَغيِّر. بحيث تستطيع اِستخدَام اسم ذلك المُتَغيِّر ضِمْن جزء معين على الأقل من شيفرة البرنامج؛ بهدف الإشارة إلى تلك المساحة من الذاكرة أو إلى تلك البيانات المُخزَّنة بها. يُطلَق على ذلك الجزء من الشيفرة، والذي يَكُون فيه اسم المُتَغيِّر صالحًا للاِستخدَام، اسم نطاق المُتَغيِّر (scope of variable). بالمثل، يُمكِن الإشارة إلى نطاق كُلًا من أسماء البرامج الفرعية (subroutine) وأسماء المُعامِلات الصُّوريّة (formal parameter). لنبدأ بالأعضاء الساكنة من البرامج الفرعية، والتي تُعدّ قواعد نطاقها (scope rule) بسيطة نوعًا ما. يَمتد نطاق (scope) أي برنامج فرعي ساكن إلى كامل الشيفرة المصدرية للصَنْف (class) المُعرَّف بداخله، أيّ يُمكِن استدعاء ذلك البرنامج الفرعي من أيّ مكان داخل الصنف، بما في ذلك تلك الأماكن الواقعة قَبْل تعريف (definition) البرنامج الفرعي. بل أنه حتى من المُمكن لبرنامج فرعي معين استدعاء ذاته، وهو ما يُعرَف باسم التَعاود أو الاستدعاء الذاتي (recursion)، وهو أحد المواضيع المتقدمة نسبيًا، وسنتناوله عمومًا بالقسم ٩.١. وأخيرًا، إذا لم يَكُن البرنامج الفرعي خاصًا (private)، فتستطيع حتى الوصول إليه من خارج الصَنْف المُعرَّف بداخله بشَّرْط اِستخدَام اسمه الكامل. لننتقل الآن إلى الأعضاء الساكنة من المُتَغيِّرات، والتي تَملُك قواعد نطاق (scope rule) مشابهة بالإضافة إلى تعقيد واحد إضافي هو كالآتي. تستطيع عمومًا تعريف مُتَغيِّر محليّ (local variable) أو مُعامِل صُّوريّ (formal parameter) يَحمِل نفس اسم إحدى المُتَغيِّرات الأعضاء (member variable) ضِمْن الصَنْف، وفي تلك الحالة، يُعدّ المُتَغيِّر العضو مخفيًا ضِمْن نطاق المُتَغيِّر المحليّ أو المُعامِل الذي يَحمِل نفس الاسم. فعلى سبيل المثال، إذا كان لدينا الصَنْف Game كالتالي: public class Game { static int count; // متغير عضو static void playGame() { int count; // متغير محلي . . // ‫بعض التعليمات لتعريف playGame() . } . . // المزيد من المتغيرات والبرامج الفرعية . } // ‫نهاية الصنف Game يُشير الاسم count بالتَعْليمات المُؤلِّفة لمَتْن (body) البرنامج الفرعي playGame()‎ إلى المُتَغيِّر المحليّ (local variable). أما ببقية الصنف Game، فإنه سيُشيِر إلى المُتَغيِّر العضو (member variable)، بالطبع إذا لم يُخفَى باستخدام مُتَغيِّر محليّ آخر أو مُعامِلات تَحمِل نفس الاسم count. مع ذلك، ما يزال بإمكانك الإشارة إلى المُتَغيِّر العضو count بواسطة اسمه الكامل Game.count، والذي يُستخدَم، في العادة، خارج الصَنْف الذي عُرِّف به العضو، ولكن ليس هناك قاعدة تَمنع اِستخدَامه داخل الصنف. لهذا يُمكِن اِستخدَام الاسم الكامل Game.count داخل البرنامج الفرعي playGame()‎ للإشارة إلى المُتَغيِّر العضو بدلًا من المُتَغيِّر المحليّ. يُمكِننا الآن تلخيص قاعدة النطاق (scope rule) كالآتي: يَشمَل نطاق مُتَغيِّر عضو ساكن الصنف المُعرَّف بداخله بالكامل، وعندما يُصبِح الاسم البسيط (simple name) للمُتَغيِّر العضو مَخفيًا نتيجة تعريف مُتَغيِّر محليّ أو مُعامِل صُّوريّ يَحمِل نفس الاسم، يُصبِح من الضروري اِستخدَام الاسم الكامل على الصورة . للإشارة إلى المُتَغيِّر العضو. تُشبِه قواعد نطاق الأعضاء غير الساكنة (non-static) عمومًا تلك الخاصة بالأعضاء الساكنة، باستثناء أن الأولى لا يُمكِن اِستخدَامها بالبرامج الفرعية الساكنة (static subroutines)، كما سنرى لاحقًا. أخيرًا، يَتَكوَّن نطاق المُعامِل الصُّوريّ (formal parameter) لبرنامج فرعي معين من الكُتلَة (block) المُؤلِّفة لمَتْن البرنامج الفرعي (subroutine body). في المقابل، يَمتَد نطاق المُتَغيِّر المحليّ (local variable) بدايةً من تَعْليمَة التَّصْريح (declaration statement) المسئولة عن تعريف ذلك المُتَغيِّر وحتى نهاية الكُتلَة (block) التي حَدَثَ خلالها ذلك التَّصْريح. كما أشرنا بالأعلى، تستطيع التَّصْريح عن المُتَغيِّر المُتحكِّم بحَلْقة (loop control variable)‏ for ضمن التعليمة ذاتها على الصورة for (int i=0; i < 10; i++)‎، ويُعدّ نطاق مثل هذا التَّصْريح (declaration) حالة خاصة: فهو صالح فقط ضِمْن تَعْليمَة for، ولا يَمتَد إلى بقية الكُتلَة المتضمنة للتَعْليمَة. لا يُسمَح عمومًا بإعادة تعريف (redefine) اسم المُعامِل الصُّوريّ أو المُتَغيِّر المحليّ ضِمْن نطاقه (scope)، حتى إذا كان ذلك داخل كُتلَة مُتداخِلة (nested block)، كالتالي: void badSub(int y) { int x; while (y > 0) { int x; // ‫خطأ، لأن x مُعرَّفة بالفعل . . . } } في الواقع، تَسمَح بعض اللغات بذلك، بحيث يُخفِي التَّصْريح عن x ضِمْن حَلْقة while التَّصْريح الأصلى، ولكن لا تَسمَح الجافا بذلك، حيث يُصبِح اسم المُتَغيِّر متاحًا للاِستخدَام مرة آخرى فقط بعد انتهاء تَّنْفيذ الكُتلَة (block) المُصرَّح عن المُتَغيِّر ضِمْنها. اُنظر على سبيل المثال: void goodSub(int y) { while (y > 10) { int x; . . . // ينتهي نطاق‫ x هنا } while (y > 0) { int x; // ‫صالح، فتَّصريح x السابق انتهت صلاحيته . . . } } هل تَتَسبَّب أسماء المُتَغيِّرات المحليّة (local variable) بإخفاء أسماء البرامج الفرعية (subroutine names)؟ لا يُمكِن حُدوث ذلك لسبب قد يبدو مفاجئًا. ببساطة، لمّا كانت أسماء البرامج الفرعية دومًا مَتبوعة بزوج من الأقواس (parenthesis)، والتي يُسْتحسَن التفكير بها على أساس أنها جزء من اسم البرنامج الفرعي، كأن تقول البرنامج الفرعي main()‎ وليس البرنامج الفرعي main، فإن الحاسوب عمومًا يُمكِنه دائمًا مَعرِفة ما إذا كان اسم معين يُشير إلى مُتَغيِّر أم إلى برنامج فرعي، لذا ليس ضروريًا أن تكون أسماء المُتَغيِّرات والبرامج الفرعية مختلفة من الأساس، حيث يُمكِنك ببساطة تعريف مُتَغيِّر اسمه count وبرنامج فرعي بنفس الاسم count ضِمْن نفس الصَنْف. كذلك لمّا كان الحاسوب قادرًا على مَعرِفة ما إذا كان اسم معين يُشير إلى اسم صَنْف أم لا وِفقًا لقواعد الصيغة (syntax)، فإنه حتى يُمكِن إعادة اِستخدَام أسماء الأصناف (classes) بهدف تسمية كلا من المُتَغيِّرات والبرامج الفرعية. اسم الصنف هو بالنهاية نوع، لذا يُمكِن اِستخدَام ذلك الاسم للتََّصْريح عن المُتَغيِّرات والمُعامِلات الصُّوريّة (formal parameters)، بالإضافة إلى تَخْصيص نوع القيمة المُعادة (return type) من دالة (function) معينة. يَعنِي ذلك أنك تستطيع التََّصْريح عن الدالة التالية ضِمْن صَنْف اسمه Insanity: static Insanity Insanity( Insanity Insanity ) { ... } يَستخدِم التََّصْريح -بالأعلى- الاسم Insanity ٤ مرات، تُشير الأولى إلى النوع المُعاد (return type) من الدالة، بينما تُشير الثانية إلى اسم تلك الدالة، والثالثة إلى نوع مُعاملها الصُّوريّ (formal parameter)، والرابعة إلى اسم ذلك المُعامِل. لكن تَذَكَّر! لا يُعدّ كل ما هو مُتاح فكرة جيدة بالضرورة. ترجمة -بتصرّف- للقسم Section 8: The Truth About Declarations من فصل Chapter 4: Programming in the Large I: Subroutines من كتاب Introduction to Programming Using Java.
  10. قُدرتك على اِستيعاب طريقة عمل البرامج، وفهمها هو أمر جيد بلا شك، ولكنه يختلف تمامًا عن تَصميمها بحيث تُنفِّذ مُهِمّة معينة. لقد ناقشنا بالقسم ٣.٢ كيفية تَطْوير الخوارزميات بطريقة مَنهجية باستخدام أسلوب التصميم المُتدرج (stepwise refinement) والشيفرة الوهمية (pseudocode). التصميم المُتدرج هو عملية تَقَع ضِمْن استراتيجيات التصميم من أعلى لأسفل (top-down)، أيّ لابُدّ عمومًا من وجود نقطة معينة تَتَوقَّف عندها عن عملية التحسِّين التدريجي لخوارزمية الشيفرة الوهمية (pseudocode algorithm)، وتُحوِّل عندها تلك الخوارزمية مباشرة إلى شيفرة برنامج فعليّة. بدون البرامج الفرعية (subroutines)، ستُمثِل تلك النقطة مستوًى بدائيًا جدًا من العمليات، كتَعْليمَات الإِسْناد (assignment statements)، وعمليات الدَخْل والخَرْج، أما في حالة تَوفُّر البرامج الفرعية المسئولة عن إنجاز بعض المَهَامّ (tasks) المُحدَّدة، فسيَكُون بإمكانك التَوقُّف عن التحسِّين بمجرد تعبيرك عن الخوارزمية بنفس المصطلحات التي تَستخدِمها البرامج الفرعية. يَعنِي ذلك أننا قد أضفنا عنصر، هو بالأساس يَتَّبِع أسلوب التصميم من أسفل لأعلى (bottom-up)، إلى استراتيجيات التصميم من أعلى لأسفل (top-down approach) الخاصة بالتصميم المُتدرج. بفَرْض وجود مشكلة معينة، تستطيع الآن البدء بكتابة بعض البرامج الفرعية (subroutines) المسئولة عن إنجاز بعض المَهَامّ المُتَعلِّقة بموضوع البرنامج، بحيث تُصبِح تلك البرامج بمثابة صندوق من الأدوات الجاهزة (toolbox) يُمكِنك دَمْجها إلى الخوارزمية أثناء تَطْويرها، أو حتى قد تستطيع شراء صندوق أدوات برمجي جاهز (software toolbox)، كُتب بواسطة شخص آخر، بحيث يَتَضمَّن ذلك الصندوق مجموعة من البرامج الفرعية، التي ستَكُون بمثابة صندوقًا أسودًا (black boxes)، يُمكِنك تَوظِّيفه ضِمْن المشروع الخاص بك. بإمكانك حتى استخدام البرامج الفرعية مباشرة ضِمْن أساليب التصميم من أعلى لأسفل (top-down approach) الأكثر صرامة، بمعنى أنه وبينما تُحسِّن الخوارزمية بالطريقة المُعتادة، يُمكِنك أن تُعبر عن مُهِمّة فرعية (sub-task) معينة ضِمْن الخوارزمية بصورة استدعاء لبرنامج فرعي. يُصبِح تَطْوير ذلك البرنامج الفرعي عندها مشكلة مُستقلة تستطيع العمل عليها بشكل منفصل. لا يَتَعدَى اتباع تلك الطريقة أكثر من مُجرَّد تَقسِّيم للمشكلة الرئيسية إلى مشكلات صغيرة مُنفصلة، أيّ أنها ما تزال تَقَع ضِمْن استراتيجيات التصميم من أعلى لأسفل؛ لأن تحليلك للمشكلة هو الذي وَجَّهك لكتابة البرامج الفرعية. في المقابل، تَبدأ استراتيجيات التصميم من أسفل لأعلى (bottom-up approach) بكتابة البرامج الفرعية المُتَعلِّقة بموضوع البرنامج، أو الحصول عليها بطريقة ما، بحيث تَعَمَل تلك البرامج الفرعية بمثابة أساس أو قاعدة تُستخدَم لبناء حل للمشكلة الأساسية. الشروط المسبقة (precondition) والشروط اللاحقة (postcondition) تَعََمَل البرامج الفرعية (subroutines) عمومًا كلَبِنات أساسية (building blocks) ضِمْن البرنامج الأساسي، لذا لابُدّ أن تَكُون طريقة تَفْاعُلها مع ذلك البرنامج واضحة. تُحدِّد المواصفة الاصطلاحية (contract) لأيّ برنامج فرعي عمومًا طريقة التَفْاعُل تلك، وهو ما ناقشناه بالقسم ٤.١، ويُمكِن كتابتها باِستخدَام ما يُعرَف باسم الشروط المُسَبَّقة (precondition) واللاحقة (postcondition). لابُدّ أن تَكُون الشروط المُسَبَّقة (precondition) لأي برنامج فرعي (subroutine) مُتحقِّقة عند استدعائه. على سبيل المثال، إحدى الشروط المُسَبَّقة (precondition) للدالة Math.sqrt(x)‎ المبنية مُسْبَّقًا (built-in function) يَتمثَل في ضرورة أن تَكُون القيمة المُمرَّرة للمُعامِل (parameter)‏ x أكبر من أو تُساوِي الصفر؛ لأنه لا يُمكِن بطبيعة الحال حِسَاب الجذر التربيعي لعدد سالب. عمومًا، يُمثِل الشَّرْط المُسَبَّق (precondition) بمواصفة اصطلاحية معينة إلزامًا على مُستدعِي (caller) البرنامج الفرعي، أيّ أنه في حالة استدعائك لبرنامج فرعي معين دون تَوْفِية شَّرْطه المُسَبَّق، فما من سبب يَدْفَعك لتَوقع إِنجازه للمُهِمّة بشكل ملائم، فلرُبما يَنهار البرنامج أو يَكتفِي بإعادة نتائج غير صحيحة؛ لأنك ببساطة لم تَلتزِم بجانبك من الاتفاق، ولهذا لا تَلوّمن إلا نفسك. في المقابل، الشروط اللاحقة (postcondition) هي بمثابة الجانب الآخر من المواصفة الاصطلاحية (contract)، حيث تُمثِل إلزامًا على البرنامج الفرعي ذاته، أي بفَرْض اِستيفاء الشروط المُسَبَّقة (preconditions) لبرنامج فرعي معين، وعدم احتوائه على أية أخطاء برمجية (bugs)، فلابُدّ من تَحقُّق شروطه اللاحقة بعد الاستدعاء. على سبيل المثال، الشَّرْط اللاحق للدالة Math.sqrt(x)‎ يَتمثَل في ضرورة تَساوِي كُلًا من مربع القيمة المُعادة من تلك الدالة (function) وقيمة المُعامِل (parameter) المُمرَّرة عند استدعاء البرنامج الفرعي، وهو ما سيَكُون صحيحًا فقط في حالة استيفاء شَّرْطها المُسَبَّق بخصوص كَوْن قيمة المُعامِل أكبر من أو تُساوِي الصفر. مثال آخر هو البرنامج الفرعي System.out.print(x)‎ المَبنِي مُسْبَّقًا، والذي يَتَمثَل شَّرْطه اللاحق (postcondition) بطباعة قيمة مُعامِله (parameter) المُمرَّرة على الشاشة. عادة ما تَضَع الشروط المُسَبَّقة (preconditions) لأي برنامج فرعي قيودًا على قيم مُعاملاته، مثل البرنامج الفرعي Math.sqrt(x)‎، لكن لا يَقْتصِر دورها في الواقع على ذلك. قد تُشيِر تلك الشروط أيضًا إلى المُتَغيِّرات العامة (global variables) المُستخدَمة بالبرنامج الفرعي، أو قد تُحدِّد الحالة (state) التي ينبغي أن يَكُون عليها البرنامج عند استدعاء البرنامج الفرعي، وهو ما يَكُون مفيدًا إذا كانت عملية استدعاء ذلك البرنامج صالحة فقط بأوقات معينة. من الناحية الآخرى، يُحدِّد الشَّرْط اللاحق (postcondition) لأي برنامج فرعي مُهِمّته المُوكَلة إليه. فمثلًا، ينبغي أن يَتضمَّن الشَّرْط اللاحق لأي دالة (function) القيمة المُعادة منها. تُوصَف البرامج الفرعية أحيانًا باِستخدَام تعليقات (comments)، والتي ينبغي أن تُحدِّد شروط ذلك البرنامج المُسَبَّقة (preconditions) واللاحقة (postconditions). عندما تَستخدِم برنامجًا فرعيًا (subroutine) مكتوب مُسْبَّقًا، ستُخبرك تلك الشروط بكيفية اِستخدَام البرنامج بالإضافة إلى تِبيان الغرض منه. في المقابل، عندما تَكْتُب برنامجًا فرعيًا، ستَمنَحك تلك الشروط توصيفًا دقيقًا عما هو مُتوقَّع من ذلك البرنامج. سنَتَعرَّض خلال القسمين الفرعيين التاليين لمثال بسيط، سنَستخدِم فيه التعليقات لهذا الغرض، وستَكُون مَكْتوبة بصياغة تعليقات Javadoc مع عَنونة كُلًا من الشروط المُسَبَّقة واللاحقة. يرى كثير من علماء الحاسوب ضرورة إضافة وُسوم تَوْثيق جديدة ‎@precondition و ‎@postcondition إلى نظام Javadoc لعَنونة الشروط المُسَبَّقة واللاحقة بشكل صريح، ولكن لَمْ يَحدُث ذلك حتى الآن. مثال عن عملية التصميم سنُصمِّم الآن برنامجًا بالاعتماد على البرامج الفرعية بصورة أساسية، بمعنى أننا سنَستخدِم بعض البرامج الفرعية (subroutines) التي سُبق كتابتها كلَبِنة أساسية (building block)، كما أننا سنُصمِّم بعض البرامج الفرعية الجديدة التي قد نحتاجها لإكمال المشروع. فيما يَتعلَّق بالبرامج المَكْتوبة مُسْبَّقًا، سنَستخدِم واجهة برمجة تطبيقات (API) تَحتوِي على صنفين (classes) -كان الكاتب قد كَتَبَهما-، الصَنْف الأول هو Mosaic.java والذي بدوره يَعتمِد على الصنف الثاني MosaicCanvas.java. لاحظ ضرورة تَوفِّير كُلًا من الصنفين Mosaic و MosaicCanvas أثناء تَصْرِيف (compile) البرنامج وتَشْغِيله، مما يَعنِي وجود الملفين Mosaic.java و MosaicCanvas.java -أو ملفات الصَنْفين بَعْد التَصْرِيف- بنفس مجلد الصَنْف المُعرِّف للبرنامج. يَسمَح لك الصَنْف Mosaic بالتَعْامُل مع نافذة (window) مُكوَّنة من مجموعة من المستطيلات الصغيرة المُلوَّنة، والمُرَتَّبة بصورة صفوف وأعمدة، حيث يَتضمَّن أكثر من عضو برنامج فرعي ساكن (static member subroutines) يُمكِن اِستخدَامها لأغراض فَتْح النافذة، وغَلْقها، بالإضافة إلى التَلاعب بمُحتوياتها. يُوفِّر الصَنْف بطبيعة الحال صندوق أدوات (toolbox) أو واجهة برمجة تطبيقات (API) تَتضمَّن تلك المجموعة من البرامج (routines)، والتي نسْتَعْرِض بالأسفل بعضًا منها، مُوثَّقة باِستخدَام تعليقات Javadoc. تَذَكَّر أن تعليق Javadoc يَسبِق العنصر المَعنِي بذلك التعليق. /** * ‫افتح نافذة mosaic على الشاشة * ‫ينبغي أن يُستدعى هذا التابع قبل أي تابع آخر ضمن الصنف Mosaic * سينتهي البرنامج عندما يغلق المستخدم النافذة * * ‫الشرط المسبق: المعاملات rows و cols و h و w هي أعداد صحيحة موجبة * * ‫‫الشرط اللاحق: تفتح نافذة على الشاشة والتي يمكنها عرض صفوف وأعمدة * ‫من المستطيلات الملونة بحيث يكون عرض المستطيل يساوي w وطوله يساوي h * ‫كما أن عدد الصفوف هو قيمة المعامل الأول الممررة بينما عدد الأعمدة * هو قيمة المعامل الثاني. مبدئيًا، تكون جميع المستطيلات سوداء * * ‫ملحوظة: الصفوف مرقمة من 0 وحتى rows - 1 بينما الأعمدة مرقمة * ‫من 0 وحتى cols - 1 */ public static void open(int rows, int cols, int h, int w) /** * ضبط لون أحد مربعات النافذة * * ‫الشرط المسبق: لابد أن كون المعاملين row و col ضمن النطاق المسموح * ‫به لرقمي الصف والعمود، كما لابد أن تقع كلا من المعاملات r و g و b ‫بين العددين 0 و 255 * الشرط اللاحق: سيضبط لون المربع المخصص عن طريق رقمي الصف والعمود * الممررين إلى اللون المحدد عبر المعاملات الثلاثة‫ r و g و b * بحيث تعطي هذه المعاملات قيم اللون الأحمر والأخضر والأخضر * بنظام‫ RGB للمربع، قمثلًا يعطي المعامل r قيمة اللون الأحمر بحيث * تشير القيمة 0 إلى انعدام اللون الأحمر بينما تشير القيمة 255 إلى أكبر قدر ممكن من اللون */ public static void setColor(int row, int col, int r, int g, int b) /** * ‫جلب قيمة اللون الأحمر بنظام RGB لأحد المربعات * * ‫الشرط المسبق: لابد أن يقع كُلا من المعاملين row و col ضمن النطاق المسموح به * ‫الشرط اللاحق: إعادة قيمة اللون الأحمر بنظام RGB للمربع المخصص * ‫كعدد صحيح يقع بين العددين 0 و 255 */ public static int getRed(int row, int col) /** * ‫تعمل بنفس طريقة الدالة getRed */ public static int getGreen(int row, int col) /** * ‫تعمل بنفس طريقة الدالة getRed */ public static int getBlue(int row, int col) /** * لإبطاء تنفيذ البرنامج عبر الانتظار قليلًا * * الشرط المسبق: لابد أن يكون المعامل‫ milliseconds عددا موجبا * ‫الشرط اللاحق: سيتوقف البرنامج مؤقتًا لمدة تساوي الزمن الممرر * بوحدة المللي ثانية */ public static void delay(int milliseconds) تَذَكَّر أن البرامج الفرعية -بالأعلى- هي أعضاء (members) ضِمْن الصَنْف Mosaic، ولهذا ينبغي أن تَتضمَّن أسماء تلك البرامج (routine) اسم الصَنْف ذاته عند اِستدعائها بمكان يَقَع خارج الصنف Mosaic. على سبيل المثال، اِستخدِم الاسم Mosaic.isOpen()‎ بدلًا من الاسم isOpen. لا تُحدِّد تعليقات البرامج الفرعية -بالأعلى- ما سيَحدُث في حالة عدم استيفاء شروطها المُسَبَّقة (preconditions). على الرغم من أن البرامج الفرعية (subroutine)، في العموم، غير مُلزَمة فعليًا بما هو مَكْتوب ضِمْن مواصفاتها الاصطلاحية (contract)، فسيَكُون من الجيد مَعرِفة ما سيَحدُث في مثل تلك الحالات. على سبيل المثال، يُبلِّغ البرنامجين الفرعيين setColor()‎ أو getRed()‎ عن حُدوث اعتراض من النوع IllegalArgumentException في حالة عدم استيفاء شَّرْطهما المُسَبَّق: "ضرورة وقوع كلًا من row و col ضِمْن النِطاق المَسموح به لرَقمي الصف والعمود على الترتيب." تَسمَح مَعرِفتك لمثل تلك الحقيقة بكتابة برامج يُمكِنها التقاط (catch) ذلك الاعتراض (exception)، ومُعالجته، ولهذا سيَكُون من المفيد تَوْثيق (document) تلك المعلومة من خلال إضافة الوَسْم التوثيقي (doc tag)‏ ‎@throws‎ إلى تعليق Javadoc. تَتَبقَّى أسئلة آخرى عديدة تَتَعلَّق بكيفية تَصرُّف البرنامج الفرعي ضِمْن حالات معينة. على سبيل المثال، ماذا سيَحدُث إذا استدعينا البرنامج الفرعي Mosaic.open()‎ بينما هنالك نافذة مفتوحة بالفعل على الشاشة؟ في تلك الحالة، سيتجاهل البرنامج الفرعي عملية الاستدعاء الثانية. في الواقع، يَصعُب عادة إعداد تَوْثيق كامل على تلك الشاكلة، وأحيانًا ستحتاج إلى مُجرَّد تجربة حالة معينة لترى بنفسك كيفية تَصرُّفها، أو قد تَضطرّ أحيانًا للإطلاع على كامل الشيفرة المصدرية (source code) في حالة تَوفُّرها. تَتلخَّص فكرة البرنامج الذي سنقوم بتَطويره بكتابة تحريكة (animation) تَعتمِد على الصنف Mosaic. ببساطة، سنَملْئ نافذة بمربعات مُلوَّنة عشوائيًا، وبعدها، سنُغيِّر تلك الألوان عشوائيًا ضِمْن حَلْقة تَكْرار (loop) تستمر طالما كانت النافذة مفتوحة. قد تَعنِي عبارة "تَغْيِير الألوان عشوائيًا" الكثير من الأشياء المختلفة. المقصود هنا هو إِحداث ما يُشبِه "تشويش (disturbance)" يتحرك عبر النافذة، بحيث يُغيِّر ذلك التشويش من لون أيّ مربع يُواجهه. تُوضِح الصورة التالية ما قد تبدو عليه النافذة بلحظة معينة: مع اعتمادنا على البرامج الفرعية (routines) ضِمْن الصنف Mosaic، نستطيع كتابة التَصوُّر المَبدئي للبرنامج كالتالي: // ‫افتح نافذة Mosaic Open a Mosaic window // اِملئ النافذة بألوان عشوائية Fill window with random colors // تجول وغير لون المربعات عشوائيًا Move around, changing squares at random تبدو الخطوة "اِملئ النافذة بألوان عشوائية" كمُهِمّة (task) مُترابطة يُمكِن فصلها بصورة مستقلة، ولهذا سنَكتُب برنامجًا فرعيًا (subroutine) يَتولَى مسئولية تَّنْفيذ تلك المُهِمّة. في المقابل، يُمكِن إضافة مزيد من التفاصيل للخطوة الثالثة بحيث تُكتَب على عدة خطوات: اِبدأ من منتصف النافذة، ثُمَّ اِستمر بالتحرك إلى مربعات جديدة، وغَيّر لون تلك المربعات. ينبغي الاستمرار بتَّنْفيذ تلك الخطوات طالما كانت النافذة مفتوحة. يُمكِننا إعادة كتابة الخوارزمية (algorithm) لتُصبِح كالتالي: // ‫افتح نافذة Mosaic Open a Mosaic window // اِملئ النافذة بألوان عشوائية Fill window with random colors // اضبط المَوضع الحالي إلى المربع بمنتصف النافذة Set the current position to the middle square in the window // طالما كانت النافذة مفتوحة As long as the mosaic window is open: // غير لون المربع بالمَوضع الحالي عشوائيًا Randomly change color of the square at the current position // حرك الموضع الحالي لأعلى أو لأسفل أو لليسار أو لليمين عشوائيًا Move current position up, down, left, or right, at random سنَستخدِم مُتَغيِّرين من النوع الصحيح int هما currentRow و currentColumn؛ لتَمثيِل "المَوْضِع الحالي (current position)"، سيَحمِلان رقمي الصف (row number) والعمود (column number) للمربع الحالي الذي يُطبَق عليه التشويش (disturbance). لما كانت نافذة mosaic مُكوَّنة من ١٦ صف و ٢٠ عمود من المربعات، يُمكِننا تهيئة قيمة "المَوضِع الحالي" المَبدئية إلى منتصف النافذة عن طريق ضَبْط المُتَغيِّرين currentRow و currentColumn إلى القيمتين ٨ و ١٠ على الترتيب. سنَلجأ لاستخدام البرنامج الفرعي Mosaic.open()‎ لفَتْح النافذة، كما سنَكتُب برنامجين فرعيين (subroutines) إضافيين لإِنجاز مُهِمّتين ضِمْن حَلْقة التَكْرار while بحيث يَظلّ البرنامج main()‎ بسيطًا. نستطيع الآن تَحْوِيل الخوارزمية (algorithm) إلى الشيفرة التالية بلغة الجافا: Mosaic.open(16,20,25,25) fillWithRandomColors(); currentRow = 8; // الصف الواقع بمنتصف النافذة currentColumn = 10; // العمود الواقع بمنتصف النافذة while ( true ) { // ينتهي البرنامج عند غلق النافذة changeToRandomColor(currentRow, currentColumn); randomMove(); } سنُجرِي أيضًا تَعْديلًا إضافيًا، وهو إِبطاء التحريكة (animation) قليلًا، ولذلك سنُضِيف السطر Mosaic.delay(10);‎ إلى حَلْقة التَكْرار while. انتهينا الآن من إِعداد البرنامج main()‎، لكننا نَحتاج لكتابة البرامج الفرعية fillWithRandomColors()‎ و changeToRandomColor(int,int)‎ و randomMove()‎ لنُكمِل البرنامج. تُعدّ عملية كتابة كل برنامج منها بمَثابة مُهِمّة صغيرة مُنفصلة. سنبدأ بالبرنامج الفرعي fillWithRandomColors()‎ والمَسئول عن ضمان تَحقُّق الشَّرْط اللاحق (postcondition) التالي: "سيَتغيَّر لون كل مربع بالنافذة عشوائيًا." يُمكِننا كتابة خوارزمية (algorithm) لإِنجاز تلك المُهِمّة بأسلوب الشيفرة الوهمية (pseudocode) كالتالي: // لكل صف For each row: // لكل عمود For each column: // غير لون المربع بذلك الصف والعمود إلى لون عشوائي set the square in that row and column to a random color يُمكِن تَّنْفيذ (implement) السَطْرين "لكل صف"، و"لكل عمود" -بالأعلى- باِستخدَام حَلْقة التَكْرار for. أما بخصُوص السطر الأخير، فلقد قَررنا بالفعل كتابة البرنامج الفرعي changeToRandomColor(int,int)‎ المَسئول عن ضَبْط اللون. لاحِظ أن إِمكانية إعادة اِستخدَام برنامج فرعي معين بعدة مَواضِع يُعدّ أحد أهم مَكاسِب اِستخدَام البرامج الفرعية. يُمكِننا الآن كتابة البرنامج الفرعي fillWithRandomColors()‎ بلغة الجافا كالتالي: static void fillWithRandomColors() { int row, column; for ( row = 0; row < 16; row++ ) for ( column = 0; column < 20; column++ ) changeToRandomColor(row,column); } سننتقل الآن إلى البرنامج الفرعي التالي changeToRandomColor(int,int)‎. يُوفِّر الصَنْف Mosaic بالفعل التابع (method)‏ Mosaic.setColor()‎، والمُستخدَم لتَغْيِير لون المربع.لمّا كنا نُريد لونًا عشوائيًا، فسنحتاج إلى اختيار قيم عشوائية لكل من r و g و b، والتي ينبغي أن تَكُون أعدادًا صحيحة (integers) مُتراوحة بين العددين ٠ و ٢٥٥، وفقًا للشَّرْط المُسَبَّق (precondition) للبرنامج الفرعي Mosaic.setColor()‎، ولهذا سنَستخدِم المُعادلة (int)(256*Math.random()‎) لاختيار مثل تلك الأعداد. بالتالي، يُمكِننا كتابة البرنامج الفرعي المُستخدَم لتَغْيِير اللون عشوائيًا كالتالي: static void changeToRandomColor(int rowNum, int colNum) { int red = (int)(256*Math.random()); int green = (int)(256*Math.random()); int blue = (int)(256*Math.random()); Mosaic.setColor(rowNum,colNum,red,green,blue); } وأخيرًا، يُمكِننا الانتقال إلى البرنامج الفرعي randomMove()‎، والمَسئول عن تحريك التشويش (disturbance) عشوائيًا، لأعلى، أو لأسفل، أو يسارًا، أو يمينًا. سنَستخدِم عددًا عشوائيًا يَتراوح بين القيمتين ٠ و ٣؛ لإجراء الاختيار ما بين الاتجاهات الأربعة، فمثلًا، عندما تَكُون قيمة العدد مُساوِية للصفر، سيَتحرَك التشويش باتجاه معين، أما إذا كانت قيمته مُساوِية للواحد، فسيَتحرَك باتجاه آخر، وهكذا. لمّا كنا نَستخدِم المُتَغيِّرين currentRow و currentColumn لتحديد المَوضِع الحالي للتشويش، فإن تحريك ذلك المَوضِع لأعلى يَعنِي بالضرورة إِنقاص قيمة المُتَغيِّر currentRow بمقدار الواحد. يَترك ذلك استفهامًا عما يُفْترَض حُدوثه عندما يَصِل المُتَغيِّر currentRow إلى القيمة -١؛ خُصوصًا وأن ذلك سيَتَسبَّب بإخفاء التشويش خارج النافذة، وهو ما سينتهك الشَّرْط المُسَبَّق (precondition) لكثير من البرامج الفرعية ضِمْن الصنف Mosaic، ولهذا سنُحرِك التشويش إلى الحافة المُضادة من النافذة من خلال ضَبْط قيمة المُتَغيِّر currentRow إلى ١٥ (تَذَكَّر أن النافذة مُكوَّنة من ١٦ صف مُرقَّمَين من ٠ وحتى ١٥). بدلًا من القفز إلى الحافة الآخرى، نستطيع أيضًا تَجاهُل تلك الحالة. يُمكِننا معالجة تحريك التشويش (disturbance) للاتجاهات الثلاثة الأخرى بنفس الطريقة، بحيث نستعين بتَعْليمَة switch لتَحْديد الاتجاه ذاته. اُنظر شيفرة البرنامج الفرعي randomMove()‎: int directionNum; directionNum = (int)(4*Math.random()); switch (directionNum) { case 0: // تحرك لأعلى currentRow--; if (currentRow < 0) // إذا أصبح الموضع الحالي خارج النافذة currentRow = 15; // قم بتحريكه للحافة المضادة break; case 1: // تحرك لليمين currentColumn++; if (currentColumn >= 20) currentColumn = 0; break; case 2: // تحرك لأسفل currentRow++; if (currentRow >= 16) currentRow = 0; break; case 3: // تحرك لليسار currentColumn--; if (currentColumn < 0) currentColumn = 19; break; } البرنامج انتهينا الآن من كتابة جميع البرامج الفرعية، وتَبقَّى لنا تَجْميعها معًا بحيث نَحصُل على البرنامج كاملًا، بالإضافة إلى كتابة تعليقات Javadoc للصنف ذاته ولبرامجه الفرعية. لاحظ أننا قد عَرَّفنا المُتَغيِّرين currentRow و currentColumn كأعضاء ساكنة (static members) ضِمْن الصنف، وليس كمُتَغيِّرات محليّة (local)؛ وذلك لكَوْنهما مُستخدَمين ضِمْن أكثر من مُجرَّد برنامج فرعي (subroutines) واحد. تَتَوفَّر نسخة من الشيفرة المصدرية للبرنامج بالملف RandomMosaicWalk.java، وانتبه لكَوْنه يَعتمِد على كُلًا من الملفين Mosaic.java و MosaicCanvas.java. /** * يفتح هذا البرنامج نافذة مليئة بالمربعات الملونة عشوائيًا * بحيث يتحرك نوع من "التشويش" عشوائيًا عبر الشاشة ويغير من * لون أي مربع يواجهه بشكل عشوائي. يستمر البرنامج في العمل * طالما كانت النافذة مفتوحة */ public class RandomMosaicWalk { static int currentRow; // رقم الصف المعرض للتشويش static int currentColumn; // رقم العمود المعرض للتشويش /** * ‫يُنشيء برنامج main النافذة ويملؤها بألوان عشوائية * ثم يحرك التشويش بصورة عشوائية عبر النافذة طالما كانت مفتوحة */ public static void main(String[] args) { Mosaic.open(16,20,25,25); fillWithRandomColors(); currentRow = 8; // ابدأ بمنتصف النافذة currentColumn = 10; while (true) { changeToRandomColor(currentRow, currentColumn); randomMove(); Mosaic.delay(10); // احذف هذا السطر لزيادة سرعة التحريكة } } // نهاية main /** * يملأ النافذة بمربعات ملونة عشوائيا * * الشرط المسبق: لابد أن تكون النافذة مفتوحة * الشرط اللاحق: سيصبح كل مربع النافذة ملون بصورة عشوائية */ static void fillWithRandomColors() { int row, column; for ( row=0; row < 16; row++ ) { for ( column=0; column < 20; column++ ) { changeToRandomColor(row, column); } } } // نهاية fillWithRandomColors /** * يغير من لون مربع معين بالنافذة عشوائيًا * * الشرط المسبق: لابد أن يقع رقمي الصف والعمود الممررين ضمن * النطاق المسموح به لأرقام الصف والعمود * الشرط اللاحق: سيتغير لون المربع المخصص بواسطة رقمي الصف والعمود * * @param rowNum رقم الصف للمربع بحيث يبدأ عد الصفوف من الأعلى * @param colNum رقم الصف للمربع بحيث يبدأ عد الأعمدة من اليسار */ static void changeToRandomColor(int rowNum, int colNum) { // اختر قيم عشوائية تتراوح بين 0 و 255 // لقيم الألوان الثلاثة (الأحمر، والأزرق، والأخضر‫) ‫بنظام الألوان RGB int red = (int)(256*Math.random()); int green = (int)(256*Math.random()); int blue = (int)(256*Math.random()); Mosaic.setColor(rowNum,colNum,red,green,blue); } // ‫نهاية changeToRandomColor /** * يحرك التشويش عبر النافذة * * الشرط المسبق: لابد أن تكون المتغيرات العامة‫ currentRow و * ‫currentColumn ضمن النطاق المسموح به لرقمي الصف والعمود * الشرط اللاحق: يتغير رقمي الصف والعمود إلى أحد المواضع المجاورة * سواء للأعلى أو للأسفل أو لليسار أو لليمين */ static void randomMove() { int directionNum; directionNum = (int)(4*Math.random()); switch (directionNum) { case 0: // move up currentRow--; if (currentRow < 0) currentRow = 15; break; case 1: // move right currentColumn++; if (currentColumn >= 20) currentColumn = 0; break; case 2: // move down currentRow ++; if (currentRow >= 16) currentRow = 0; break; case 3: // move left currentColumn--; if (currentColumn < 0) currentColumn = 19; break; } } // ‫نهاية randomMove } // ‫نهاية الصنف RandomMosaicWalk ترجمة -بتصرّف- للقسم Section 7: More on Program Design من فصل Chapter 4: Programming in the Large I: Subroutines من كتاب Introduction to Programming Using Java.
  11. كل تَطوُّر يَحدُث بواجهات المُستخدِم، يُقابله تَعقيد أكبر ينبغي على المبرمج التَعامُل معه، ففي حين تستطيع كتابة واجهة برنامج طرفية (console user interface) بواسطة عدد قليل من البرامج الفرعية (subroutines) البسيطة التي تقرأ ما يَكْتُبه المُستخدِم، وتَطبَع خَرْج البرنامج إلى الطرفية، فإن تطوير واجهات المُستخدِم الرسومية (graphical user interface) العصرية هو أمر أكثر تعقيدًا بمراحل عدة، خاصة مع كل هذا الكم الهائل من النوافذ، والأزرار، وشرائط التمرير، والقوائم، وصناديق الإِدْخَال النصية، وغيره، والتي أينعم تَمنَح المُستخدِم تجربة سهلة ومريحة، ولكنها في ذات الوقت تُجبر المبرمج على التَعامُل مع كمية هائلة من الاحتمالات والتعقيدات المتزايدة، والتي تَكُون بهيئة عدد ضخم من البرامج الفرعية المُخصَّصة فقط لأغراض إدارة واجهة المُستخدِم (user interface) ناهيك عن البرامج الفرعية الآخرى المُخصَّصة لغَيْر ذاك الغرض. صناديق الأدوات (toolboxes) اعتاد مبرمجي حاسوب ماكنتوش الأصلي (Macintosh) على التعامل مع "صندوق أدوات ماكنتوش (Macintosh Toolbox)"، والذي يَتكوَّن من أكثر من ألف برنامج فرعي (subroutines) مختلف؛ حيث تَتَوفَّر برامج لتَّنْفيذ جميع العمليات التي يُتوقَّع من الحاسوب القيام بها، فمثلًا، تَتَوفَّر تلك البرامج المُتعلقة بواجهة المُستخدِم (user interface) مثل فَتْح النوافذ (windows) وإِغلاقها، ورَسْم كُلًا من الأشكال الهندسية (geometric) والنصوص على تلك النوافذ، وإضافة الأزرار إليها، والاستجابة إلى ضغطات الفأرة على تلك النوافذ، وإضافة القوائم (menus)، والاستجابة لما يَختاره المُستخدِم منها. إلى جانب برامج واجهة المُستخدِم، تَتَوفَّر أيضًا برامج لفَتْح الملفات، وقراءة البيانات منها، وكذلك للاتصال الشبكي، ولإرسال الخَرْج إلى الطابعة، ولمُعالجة الاتصال بين البرامج. في المُقابل، يُوفِّر مايكروسوفت ويندوز (Microsoft Windows) مجموعة آخرى من البرامج الفرعية (subroutines) للمبرمجين، والتي تَختلِف نوعًا ما عن تلك المُستخدَمة بماكنتوش (Macintosh). علاوة على ذلك، يُوفِّر لينكس (Linux) للمبرمجين أكثر من صندوق أدوات لبرمجة واجهات المُستخدِم الرسومية (GUI toolboxes) يُمكِنهم الاختيار بينها. أيّ مشروع برمجي هو بالنهاية خليط من كُلًا من الابتكار وإعادة الاِستخدَام. يبدأ المبرمج بمجموعة الأدوات البسيطة المَبْنِيَّة باللغة نفسها، كالمتغيرات، وتَعْليمَات الإِسْناد (assignment statements)، وتَعْليمَات التَفْرِيع if، وحَلْقات التَكْرار (loops). إلى جانب ذلك، فإنه قد يَستعِين بصناديق أدوات (toolboxes)، مَكْتوبة من قِبَل مُطوِّرين آخرين، والتي تَتضمَّن مجموعة من البرامج (routines) لتَّنْفيذ مَهَامّ معينة يَحتاجها المبرمج. إذا كانت تلك الأدوات مُصمَّمة تَصْمِيمًا جيدًا، فإنها تَكُون أَشْبه بصندوق أسود (black boxes)، لا يحتاج المبرمج أكثر من مُجرَّد استدعائها، دون مَعرِفة أية تفاصيل أو خطوات تُنفِّذها تلك الأداة لإنجاز المُهِمّة المُسنَدة إليها. كُل ما سبق يصُنَّف ضِمْن ذلك الجزء الخاص بإعادة الاِستخدَام، أما الجزء الآخر المُتَعلِّق بالابتكار، فيَتكوَّن من توظيف كل تلك الأدوات السابق ذِكرَها ضِمْن مشروع معين بهدف حل مشكلة معينة كمُعالجة النصوص، أو الاحتفاظ بالحسابات البنكية، أو مُعالجة الصور المُلتقَطة بواسطة مسبار فضاء، أو ألعاب الكمبيوتر، أو تَصفُّح الإنترنت، ..إلخ. يُطلَق على ذلك برمجة التطبيقات (applications programming). صندوق الأدوات البرمجي (software toolbox) هو أَشْبه ما يَكُون بصندوق أسود، لابُدّ أن يَكُون مَصحُوبًا بواجهة (interface)، والتي تَتَكوَّن من توصيف لجميع البرامج (routines) المُعرَّفة بداخله، أيّ ذِكْر مُعامِلات (parameters) كل برنامج، والغرض منه. تُكوِّن تلك التوصيفات ما يُعرَف باسم واجهة برمجة التطبيقات (Application Programming Interface)، وتُختصَر إلى API، فمثلًا، تَحتوِي واجهة برمجة تطبيقات ماكنتوش (Macintosh API) على تَوصِيف لجميع البرامج (routines) المُتاحة "بصندوق أدوات ماكنتوش (Macintosh Toolbox)". عادةً ما تُصدر الشركات المُصنعة للأجهزة العتادية (hardware device) -مثل بطاقات الشبكة (network cards) المسئولة عن توصيل الحاسوب بشبكة معينة- واجهة برمجة تطبيقات (API) خاصة بذلك الجهاز، تَحتوِي على قائمة بالبرامج (routines) التي يُمكِن للمبرمجين استدعائها؛ للاتصال مع الجهاز والتَحكُّم به. علاوة على ذلك، يُوفِّر العلماء المُساهمين بكتابة البرامج المسئولة عن حِسَاب بعض العمليات المُعقدة نوعًا ما -مثل حلّ المعادلات التفاضلية (differential equations)- واجهة برمجة تطبيقات (API)، تَسمَح للآخرين باستدعاء تلك البرامج (routines) دون الحاجة إلى فهم تفاصيل تلك العمليات. لغة الجافا مُدعَّمة بواجهة برمجة التطبيقات القياسية (standard API)، والتي تَعرَّضنا لأجزاء منها بالفعل، مثل البرنامج الفرعي الرياضي Math.sqrt()‎، والصَنْف String، وما يَحتوِيه من برامج (routines)، بالإضافة إلى برامج طباعة الخَرْج System.out.print()‎. تَتضمَّن أيضًا تلك الواجهة برامج (routines) لبرمجة واجهات المُستخدِم الرسومية (graphical user interfaces)، وللاتصالات الشبكية (network communication)، ولقراءة الملفات وكتابتها، وغيرها. يَظُنّ البعض أن تلك البرامج (routines) هي جزء من لغة الجافا ذاتها، ولكنها في الواقع مُجرَّد برامج فرعية (subroutines) قد كُتبت وأُتيحت للاِستخدَام ببرامج (programs) الجافا. ينبغي لأيّ واجهة برمجة تطبيقات جافا (Java API) العَمَل على جميع المنصات، وعندها يُعدّ برنامج (program) الجافا المَكْتوب وفقًا لتلك الواجهة مُستقلًا عن أيّ منصة (platform-independent)، أيّ أنه من المُمكِن تَشْغِيل نفس البرنامج (program) على منصات (platform) مُتعدِّدة، مثل ويندوز (Windows)، وماك (Mac OS)، ولينكس (Linux) وغيرها. ومع ذلك، لاحِظ أن الواجهة (interface) ذاتها هي التي تُعدّ مُستقلة عن المنصات (platform-independent)، أما تَّنْفيذ (implementation) تلك الواجهة فإنه قد يختلف من منصة لآخرى. فيما يتعلق بواجهة برمجة التطبيقات القياسية (standard API)، يَتضمَّن نظام الجافا (Java system) بأي حاسوب عمومًا تَّنْفيذًا (implementations) لجميع البرامج الموجودة بتلك الواجهة. عندما يَستدعِي برنامج جافا (Java program) واحدة من تلك البرامج القياسية (standard routines) ضِمْن الواجهة القياسية، فإن مُفسِّر الجافا (Java interpreter) -عند تَّنْفيذه للبرنامج (program)- يَسحَب تَّنْفيذ البرنامج المُستدعَى (routine implementation) المُتناسب مع المنصة (platform) الحالية، ثم يُنفِّذه. يَعنِي ذلك أنه بمُجرَّد تَعلُّمك لواجهة برمجة تطبيقات (API) واحدة، سيُصبِح بإمكانك استهداف مجموعة واسعة من المنصات، وهو ما يُعدّ ميزة قوية جدًا. حزم الجافا القياسية (standard packages) تُكتَب البرامج (routines) بواجهة برمجة التطبيقات القياسية (standard API) داخل أصناف (classes) كما هو الحال مع أي برنامج فرعي (subroutine). بالإضافة إلى ذلك، فإنه من المُمكِن تَجْميع الأصناف ضِمْن حزم (packages)، والتي تَعرَّضنا لها باختصار بالقسم الفرعي ٢.٦.٧؛ بهدف تنظيمها على نطاق أوسع (large-scale). يُمكِن أيضًا تَضْمِين الحزم (packages) داخل حزم آخرى لتحقيق مُستوى أعلى من التَجْميع. في الواقع، واجهة برمجة تطبيقات جافا القياسية (standard Java API) مُنفَّذة (implement) بالكامل ضِمْن عدة حزم، فمثلًا، تَحتوِي حزمة java على عدة حزم آخرى غَيْر مُتَعلِّقة بواجهة المُستخدِم الرسومية (non-GUI)، بالإضافة إلى احتوائها على أصناف AWT المُتَعَلِّقة بتلك الواجهات الرسومية. حزمة javax هي مثال آخر، وتَحتوِي على أصناف كثيرة، من ضِمْنها تلك الأصناف التي تَستخدِمها واجهة المُستخدِم الرسومية (GUI)‏ Swing. كذلك حزمة javafx والتي تَحتوِي على واجهة برمجة تطبيقات جافا إف إكس (JavaFX API) التي يَستخدِمها الكتاب لبرمجة واجهات المُستخدِم الرسومية (GUI)‏. قد تَحتوِي أي حزمة (package) على أصناف أو حزم آخرى، وتُسمَى تلك الأخيرة باسم الحزم الفرعية (sub-package). تَتضمَّن كُلًا من الحزمتين java و javafx على حزم فرعية (sub-packages). فمثلًا، تَحتوِي الحزمة الفرعية util على تشكيلة من الأصناف، بما في ذلك الصَنْف Scanner الذي ناقشناه بالقسم الفرعي ٢.٤.٦، وهي في الواقع مُتضمَّنة داخل حزمة java، ولذا يَكُون اسمها الكامل هو java.util. مثال آخر هو الحزمة الفرعية java.io، والتي تُسهِل من عمليات الخَرْج والدَخْل (input/output). كذلك الحزمة الفرعية java.net، والتي تتعامل مع الاتصالات الشبكية. أخيرًا الحزمة الفرعية الأكثر بساطة java.lang، والتي تَحتوِي على الأصناف الأساسية مثل String و Math و Integer و Double. اُنظر الصورة التالية والتي تَتضمَّن تمثيلًا رسوميًا (graphical representation) لمُستويات التَدَاخُل (nesting levels) بحزمة java، بما في ذلك حزمها الفرعية (sub-packages)، والأصناف الموجودة ضِمْن تلك الحزم الفرعية، بالإضافة إلى البرامج الفرعية (subroutines) الموجودة ضِمْن تلك الأصناف. لاحِظ أن هذا التمثيل الرسومي غَيْر كامل، فهو يَعرِض فقط عددًا قليلًا جدًا من العناصر الكثيرة الموجودة بكل عنصر. ] بالمثل، تَحتوِي حزمة javafx على الحزمة الفرعية javafx.scene، والتي تَحتوِي بدورها على كُلًا من الحزمتين الفرعيتين javafx.scene.control و javafx.scene.paint. تَحتوِي أولاهما على أصناف (classes) لتمثيل مُكوِّنات واجهة المُستخدِم الرسومية (GUI components)، كالأزرار (buttons)، وصناديق الإِدْخال (input boxes)، بينما تَحتوِي الآخرى على الصَنْف Color، وأصناف آخرى لأغراض مَلْئ الأشكال (filling) وتَحْدِيد حوافها (stroking). تَتضمَّن واجهة برمجة تطبيقات جافا القياسية (standard Java API) آلافًا من الأصناف مُجمَّعة ضِمْن مئات من الحزم. الكثير من تلك الأصناف هو، في الواقع، مُتخصِّص للغاية أو غَيْر مَعْروف، لذا لا داعي للإلمام بكامل واجهة برمجة تطبيقات جافا (Java API)، ولا حتى غالبيتها، فحتى خبراء المبرمجين ليسوا على دِرَايَة بكامل الواجهة. ستُواجه عشرات الأصناف (classes) أثناء دراستك لهذا الكتاب، وستَجِدْ أنهم كافيين تمامًا لكتابة تشكيلة واسعة من البرامج (programs). مع ذلك، يُمكِنك تَصفُّح توثيق واجهة برمجة التطبيقات (API) للإصدار ٨ من الجافا بالإضافة إلى توثيق واجهة برمجة تطبيقات جافا إف إكس (JavaFX) للإطلاع على ما هو مُتاح عمومًا. يُناقِش القسم الفرعي "الوحدات (modules)" بالأسفل بعضًا من التَغْيِيرات التي طرأت بالإصدار ٩ من الجافا، كما ستَجِدْ روابط تَوْثيق (documentation) الإصدار ١١ من الجافا. لكن لاحِظ أن تَوْثيق الإصدار ٨ يُعدّ أكثر سهولة في الاِستخدَام فيما يَتعلَّق بدراسة هذا الكتاب. استخدام الأصناف ضمن الحزم تَتضمَّن الحزمة javafx.scene.paint الصَنْف Color، لذا فإن الاسم الكامل للصنف هو javafx.scene.paint.Color. ذلك الصَنْف، وكأي صنف، هو بالنهاية نوع، أيّ أنك تستطيع اِستخدَامه للتَّصْريح (declare) عن كلًا من المُتَغيِّرات، والمُعامِلات (parameters)، وكذلك لتَخْصيص نوع القيمة المعادة (return type) من دالة (function). إذا أردت اِستخدَام ذلك الصَنْف ضِمْن أحد البرامج (program) التي تقوم بكتابتها، فإن أحد الطرائق للقيام بذلك هو استخدام الاسم الكامل للصَنْف كاسم للنوع. مثلًا، إذا كنت تريد التَّصْريح عن مُتَغيِّر اسمه rectColor من النوع Color، تستطيع كتابة التالي: javafx.scene.paint.Color rectColor; تُمثِل الشيفرة بالأعلى مُجرَّد تَّصْريح (declaration) عن مُتَغيِّر عادي على الصياغة ‎;‎. مع ذلك، فإن اِستخدَام الاسم الكامل للصَنْف هو حتمًا أمر مُتعب، وفي الواقع، نادرًا ما ستَجِدْ تلك الأسماء الكاملة مُستخدَمة بأيّ برنامج (program)؛ حيث تَسمَح الجافا بتَجَنُّب اِستخدَام الاسم الكامل للصَنْف، وفي المقابل، ستحتاج إلى اِستيراد (importing) ذلك الصَنْف أولًا. يُمكِنك استيراد الصَنْف بإضافة السَطْر التالي إلى بداية ملف الشيفرة المصدرية (source code): import javafx.scene.paint.Color; بذلك، تستطيع ببقية ذلك الملف كتابة الاسم البسيط (simple name) للصَنْف، أيّ Color، بدلًا من الاسم الكامل javafx.scene.paint.Color. لابُدّ أن يُكتَب سَطْر الاستيراد import ببداية الملف (بعد تَعْليمَة package في حالة وجودها)، وبحيث لا يَقَع سَطْر الاستيراد import ضِمْن أيّ صَنْف (class) مُعرَّف داخل الملف. يُسمِى البعض سَطْر الاستيراد import -بالأعلى- أحيانًا باسم التَعْليمَة (statement)، ولكن من الأنسب تَسميته بالمُوجِّه (directive)‏ import؛ لأنه ليس تَعْليمَة بالمعنى المُعتاد. والآن، سَمَح لك المُوجِّه import javafx.scene.paint.Color بكتابة التالي للتَّصْريح عن مُتَغيِّر: Color rectColor; يَقْتصِر دور المُوجِّه (directive)‏ import على السماح باِستخدَام الأسماء البسيطة للأصناف بدلًا من أسمائها الكاملة .، أي أنه لا يَستوِرد أي شيء فعليّ، فأنت ما زلت تستطيع الوصول إلى الصَنْف (class) بدون اِستخدَام ذلك المُوجِّه، فقط ستحتاج إلى تعيين اسم الصَنْف كاملًا. تَتَوفَّر طريقة مُختصرة لاستيراد (importing) جميع الأصناف الموجودة ضِمْن حزمة معينة. على سبيل المثال، اِستخدِم مُوجِّه import التالي بهدف استيراد جميع الأصناف (classes) الموجودة بحزمة java.util: import java.util.*; لاحِظ أنه في حين يَتطابَق محرف البدل (wildcard) * مع جميع الأصناف الموجودة ضِمْن حزمة معينة، فإنه لا يَتطابَق مع حزمها الفرعية (sub-packages)، أيّ أنك لا تستطيع استيراد جميع الحزم الفرعية (sub-packages) الموجودة ضِمْن حزمة javafx بمُجرَّد كتابة import javafx.*‎. لمّا كان اِستخدَام محرف البدل (wildcard) * بتَعْليمَة import يُتيح عددًا كبيرًا من أسماء الأصناف التي على الأرجح لن تُستخدَم، بل والتي ربما لا يَعَلم المبرمج عنها شيئًا، فإن بعض المبرمجين يُفضِّلون استيراد كل صَنْف سيَستخدِمونه فعليًا استيرادًا صريحًا وبصورة مُنفصلة. يَنصَح الكاتب باِستخدَام محرف البدل (wildcard) فقط مع الحزم (packages) الأكثر صلة بالتطبيق؛ بهدف استيراد جميع الأصناف (classes) الموجودة بها، أما في حالة اِستخدَام صَنْف واحد فقط أو اثنين من حزمة معينة، فلربما عندها من الأفضل اِستخدَام استيرادات (imports) فردية. على سبيل المثال، قد يَحتوِي برنامج يتعامل بصورة أساسية مع الشبكات على المُوجِّه import java.net.*;‎، بينما قد يَحتوِي برنامج آخر يقرأ الملفات ويكتبها على المُوجِّه import java.io.*;‎. لكن لاحِظ أنه في حالة بدأت باستيراد الكثير من الحزم بتلك الطريقة، فلابُدّ من الانتباه لأمر هام. قد تَحتوِي حزمتان مختلفتان على صَنْفين (classes) يَحمِل كلاهما نفس الاسم، فمثلًا تَحتوِي كُلًا من الحزمتين java.awt و java.util على صَنْف اسمه List. الآن، إذا استوردت كُلًا من java.awt.*‎ و java.util.*‎، فسيُصبِح الاسم البسيط للصَنْف List مُبهمًا، وبالتالي، إذا حاولت التَّصْريح (declare) عن مُتَغيِّر من النوع List، ستَحصُل على رسالة خطأ من المُصرِّف (compiler) بشأن وجود اسم صَنْف مُبْهَم. مع ذلك، ما زلت تستطيع اِستخدَام كِلا الصنفين بالبرنامج (program)، إما باِستخدَام الاسم الكامل للصَنْف مثل java.awt.List و java.util.List، أو باِستخدَام المُوجِّه import لاستيراد الأصناف المُفردة التي تحتاجها بدلًا من استيراد الحزم (packages) بالكامل. تُعدّ الحزمة java.lang أحد أهم الحزم الأساسية؛ لاحتوائها على بعض الأصناف الرئيسية، ولذلك تُستورَد جميع الأصناف الموجودة بتلك الحزمة أتوماتيكيًا بأيّ برنامج (program)، أيّ كما لو كان كل برنامج يبدأ بالتَعْليمَة import java.lang.*;‎. وهو في الواقع ما قد مَكَّنا سابقًا من اِستخدَام اسم الصَنْف String بدلًا من java.lang.String، واِستخدَام Math.sqrt()‎ بدلًا من java.lang.Math.sqrt()‎. ومع ذلك، يُمكِنك أيضًا اِستخدَام الصياغة الأطول من أسماء تلك الأصناف. تُستخدَم تَعْليمَة package لإنشاء حزم (packages) جديدة، والتي قد تَتضمَّن مجموعة من الأصناف (classes). كل ما عليك القيام به هو كتابة تلك التَعْليمَة ببداية ملفات الشيفرة المصدرية (source code) التي تَحتوِي على تعريف الأصناف المطلوب تَضْمِينها بتلك الحزمة. مثلًا، إذا أردت إنشاء حزمة اسمها utilities، اِستخدِم السطر التالي: package utilities; لابُدّ أن تُكْتَب تَعْليمَة package ببداية الملف، أيّ حتى قَبْل أيّ مُوجِّه import قد يَكُون موجودًا بذلك الملف. بالإضافة إلى ذلك، لابُدّ أن يوجد ذلك الملف بمجلد يَحمِل نفس اسم الحزمة، أي utilities بهذا المثال. وبالمثل، لابُدّ أن توجد ملفات الأصناف ضِمْن الحزم الفرعية (sub-package) بمجلدات فرعية. على سبيل المثال، ينبغي لملفات الأصناف الموجودة بحزمة اسمها utilities.net أن تكون موجودة بمجلد اسمه net الموجود بدوره بمجلد آخر اسمه utilities. يستطيع أي صَنْف داخل حزمة (package) معينة الوصول أتوماتيكيًا إلى جميع الأصناف (classes) الآخرى الموجودة بنفس الحزمة. بتعبير آخر، لا يحتاج صَنْف معين أن يَستورِد (import) الأصناف من نفس الحزمة (package) المُعرَّف بها. عادة ما يَلجأ المبرمجون لإنشاء حزم جديدة وذلك إِما بالمشروعات التي تُعرِّف عددًا كبيرًا من الأصناف؛ بهدف تنظيمها، أو عند إِنشائهم لصناديق أدوات (toolboxes)؛ لتوفير واجهة برمجة تطبيقات (APIs) لبعض الوظائف والميزات غير المُتوفِّرة بواجهة برمجة تطبيقات جافا القياسية (standard Java API). في الواقع، يَحظَى المبرمجون "من صَانِعي الأدوات" عادة بهَيْبة واحترام أكبر من مُبرمجي التطبيقات الذين يَستخدِمون تلك الأدوات. لاحظ أن غالبية الأصناف المَكْتوبة لهذا الكتاب غَيْر مُضمَّنة بأي حزمة، مع عدة استثناءات قليلة مثل الصَنْف TextIO بالحزمة textio. لأغراض هذا الكتاب، فإنك ستحتاج أن تدرك ماهية الحزم (packages)؛ حتى تَتَمكَّن من استيراد الصَنْف TextIO، وكذلك الأصناف الموجودة ضِمْن الحزم القياسية (standard packages). لاحِظ أن الحزم القياسية هي دائمًا مُتوفِّرة بجميع البرامج (programs) التي تَكتُبها، قد تَتَساءل، أين توجد تلك الأصناف القياسية (standard classes) بصورة فعليّة؟ يَعتمِد ذلك على إصدار الجافا المُستخدَم إلى حد كبير. مثلًا، بالإصدار ٨ من الجافا، فإنها تَكُون مُخزَّنة بملفات جافا أَرْشيفيّة (jar files/Java archive) تقع بالمجلد الفرعي lib الموجود بمجلد التثبيت الخاص ببيئة تَّنْفيذ الجافا (Java Runtime Environment). إن ملفات جافا الأَرْشيفيّة (jar file/Java archive) هي ببساطة ملفات بامتداد ‎.jar قد تَحتوِي على عدة أصناف. توجد غالبية الأصناف المُستخدَمة بالإصدار ٨ من الجافا بملف جافا أرشيفي (jar file) اسمه rt.jar. طرأت بعض التَغْيِيرات بالإصدار ٩ من الجافا، وهو ما سنُناقشه بالقسم الفرعي التالي. لاحِظ أن أيّ صَنْف بالنهاية لابُدّ وأن يقع ضِمْن حزمة (package)، حتى في حالة عدم تَحْدِيد الحزمة التي يُفْترَض أن يقع بها الصنف صراحةً، وفي تلك الحالات، يُوضَع الصَنْف تلقائيًا فيما يُعرَف باسم الحزمة الافتراضية (default package)، والتي ليس لها اسم. تقع تقريبًا كل الأمثلة التي ستراها بهذا الكتاب بالحزمة الافتراضية. الوحدات (modules) تُدعِّم الجافا منذ الإصدار ٩ ما يُعرَف باسم الوحدات (modules)، وهو ما تَسَبَّب بحُدوث بعض التَغْيِيرات على بنيتها ذات النطاقات الواسعة (large-scale structure). تُوفِّر الوحدات (modules) عمومًا مستوًى آخرًا من التَجْميع؛ حيث تَتضمَّن الوحدات مجموعة من الحزم، التي تَحتوِي على أصناف، والتي بدورها تَحتوِي على مُتَغيِّرات وتوابع (methods). ليس ضروريًا أن تقع الحزمة ضِمْن وحدة (module) حتى تُصبِح قابلة للاِستخدَام، ومع ذلك، فإن جميع الأصناف القياسية (standard classes) بكُلًا من جافا (Java) وجافا إف إكس (JavaFX) قد ضُمّنت داخل وحدات (modules). وَفَّرت الجافا الوحدات (modules) لعدة أسباب: أولًا، لتَحسِّين التَحكُّم بالوصول (access control)، وهو في الواقع أحد أهم الأسباب الرئيسية. قَبْل الوحدات، كان بإِمكانك اِستخدَام الأصناف العامة، أيّ تلك المُصرَّح عنها باِستخدَام المُبدِّل public، بأيّ مكان وداخل أيّ صَنْف ضِمْن أيّ حزمة، وبالمثل مُتَغيِّراته وتوابعه المُصرَّح عنها باِستخدَام المُبدِّل public. في المقابل، يَعنِي اِستخدَام المُبدِّل العام public مع صَنْف، مُعرَّف ضِمْن وحدة (module)، كَوْن ذلك الصَنْف عامًا public ضِمْن الوحدة (module) المُعرَّف بداخلها فقط. ومع ذلك، ما يزال بإمكان الوحدات تصدير (export) أي حزمة بشكل صريح، وبناءً عليه، ستُصبِح الأصناف العامة، أيّ تلك المُعرَّفة باِستخدَام المُبدِّل public، ضمن تلك الحزمة قابلة للوصول (accessible) مرة آخرى من أي مكان، بما في ذلك الوحدات (modules) الآخرى، وكذلك الأصناف التي ليست جزءًا من أي وحدة. يمكن حتى تَخْصيص الوحدات (modules) المطلوب تصدير الحزمة إليها، وهو ما يُوفِّر تَحكُّمًا بالوصول (access control) أكثر دقة. يَعنِي كل ما سبق أنه يُمكِننا الآن إنشاء حزم هي خاصة (private) بالأساس، أيّ أنها غَيْر مَرئية (invisible) من خارج الوحدة، ولكنها في نفس الوقت تُوفِّر خدمات (services) للحزم الآخرى داخل نفس الوحدة. يُمكِننا بذلك عدّ الوحدة نوعًا آخرًا من الصندوق الأسود (black box)، تَكُون فيه الحزم غَيْر المُصدَّرة (non-exported) جزءًا من تَّنْفيذه (implementation) الخفي. تُعدّ التركيبية (modularity) على هذا المُستوى الواسع من النطاق مهمة بحقّ، وبالأخص للتطبيقات ذات النطاقات الواسعة (large-scale applications). الدافع الآخر لتَوْفِير الوحدات (modules) هو الحجم الكلي لبيئة تَّنْفيذ الجافا القياسية (Java Runtime Environment)، وتُختصر إلى JRE. تَتضمَّن تلك البيئة جميع الأصناف القياسية (standard classes) على الرغم من أن التطبيقات (application) تَستخدِم عادةً جزءًا صغيرًا منها. تَسمَح التركيبية (Modularization) بإنشاء بيئات تَّنْفيذ جافا مُخصَّصة (custom JREs) أصغر؛ بحيث تحتوي فقط على الوحدات التي يحتاجها التطبيق. تَتضمَّن عُدة تطوير الجافا (Java Development Kit)، وتُختصر إلى JDK، الأمر jlink المُستخدَم لإنشاء بيئات تَّنْفيذ مُخصَّصة (custom runtimes)، والتي عادةً ما تَتضمَّن وحدات (modules) التطبيق ذاته بالإضافة إلى الوحدات القياسية (standard modules) المطلوبة لتَشْغِيل ذلك التطبيق (application). بعد ذلك، تُوزَّع (distribute) بيئات التَّنْفيذ تلك كتطبيق قائم بذاته (standalone)، والذي يُمكِن تَشْغِيله حتى بتلك الحواسيب التي لم تُثبَّت عليها عُدة تطوير الجافا (JDK). ولكن لاحِظ أنك ستحتاج إلى إنشاء إصدارات مختلفة من بيئة التَّنْفيذ المُخصَّصة (custom runtime) للمنصات المختلفة مثل الويندوز (Windows) والماك (Mac OS) ولينكس (Linux)، كما هو الحال مع عُدة تطوير الجافا (JDK) ذاتها. بالإضافة إلى ذلك، لن تُطبَق تحديثات الأمان (security updates) الصادرة لعُدة تطوير الجافا (JDK) أتوماتيكيًا على بيئات التَّنْفيذ المُخصَّصة (custom runtime)، ولذلك تقع مسئولية تحديثها على مُطوِّر التطبيق. عمومًا يُعدّ ذلك ميزة مفيدة جدًا للتطبيقات الكبيرة. منذ الإصدار ٩ من الجافا، تُخزَّن ملفات أصناف الوحدات القياسية (standard modules) المُصرَّفة داخل الملف modules، والموجود بالمجلد الفرعي lib الموجود بدوره بالمجلد الرئيسي لعُدة تطوير الجافا (JDK). صيغة ذلك الملف هي jimage، يُمكِنك التعامل معها باِستخدَام أداة سطر الأوامر jimage. في الواقع، عندما تُستخدَم أداة jlink لإنشاء بيئة تَّنْفيذ مُخصَّصة (custom runtime)، فإن جزءًا مما تقوم به هو إِنشاء ملف modules مُخصَّص يَحتوِي فقط على الوحدات (modules) المطلوبة لبيئة التَّنْفيذ (runtime) تلك. بفَحْص مجلد الإصدار ١٢ من عُدة تطوير الجافا (JDK) بحاسوب لينكس (Linux) الخاص بالكاتب، تَبيَّن أن الملف modules يَتضمَّن حوالي ٣٠١٩٩ صَنْف، ضِمْن ١٠٠٠ حزمة، ضِمْن ٧٠ وحدة، كما وصل حجمه إلى حوالي ١٣٠ ميجا بايت. يَحتوِي المجلد الرئيسي لعُدة تطوير الجافا (JDK) أيضًا على المجلد الفرعي jmods، والذي يَتضمَّن تلك الوحدات (modules) ولكن بصيغة آخرى، وعمومًا هو ليس مطلوبًا لتَصْرِيف البرامج وتَشْغِيلها، وغالبًا يَقْتصِر اِستخدَامه على الأداة jlink على حَدْ عِلم الكاتب. تَتضمَّن عُدة تطوير الجافا (JDK) مجموعة من الوحدات، من بينها الوحدتين java.base و java.desktop. تَحتوِي أولاهما على الحزم الأساسية مثل java.lang و java.util، بينما تَتضمَّن الثانية حزم (packages) خاصة بـ"أدوات تَحكُّم واجهة المُستخدَم الرسومية Swing ‏(Swing GUI toolkit)". في المقابل، تَتضمَّن منصة جافا إف إكس (JavaFX) كُلًا من الوحدات javafx.base و javafx.control و javafx.graphics بالإضافة إلى وحدات آخرى غير شائعة الاِستخدَام عمومًا. يُقسَّم تَوْثيق واجهة برمجة التطبيقات (API) للإصدارات التركيبية (modular) من الجافا إلى وحدات (modules)، مُقسَّمة بدورها إلى حزم (packages)، وأخيرًا إلى أصناف (classes)، وهو ما قد يَجعَل التَوْثيق (documentation) أصعب قليلًا في التَصفُّح بالموازنة مع الإصدارات الأقدم. مع ذلك تَتوفَّر مِيزَة بحث فعالة بالموقع الالكتروني للتَوْثيق. يُمكِنك تَصفُّح توثيق الإصدار ١١ من الجافا بالإضافة إلى توثيق الإصدار ١١ من جافا إف إكس (JavaFX). ليس ضروريًا أن يَقَع الصَنْف ضِمْن وحدة (module)، وفي تلك الحالة، فإنه ما يزال يستطيع اِستخدَام الحزم من الوحدات الاخرى، بشَّرْط أن تَكُون تلك الحزم قد صُدّرت (exported) بالوحدات المُعرَّفة داخلها. يستطيع المبرمج عمومًا اِستخدَام الأصناف الموجودة بعُدة تطوير الجافا (JDK) دون الحاجة للتفكير نهائيًا بالوحدات أو حتى مَعرِفة وجودها. يَنطبِق ذلك على برامج سطر الأوامر (command-line programs) بهذا الكتاب، أما برامج واجهة المُستخدِم الرسومية (GUI programs) التي تَستخدِم منصة جافا إف إكس (JavaFX)، فالأمور تختلف قليلًا منذ الإصدار ١١ من الجافا؛ وذلك لأن تلك المنصة قد حُذفَت من عُدة تطوير الجافا (JDK)، وأصبحت تُوزَّع بصورة مستقلة كمجموعة من الوحدات. لذلك، عند تَصْرِيف برنامج جافا إف إكس (JavaFX)، أو تَشْغِيله، ستحتاج، كما رأينا بالقسم ٢.٦، إلى تَخْصيص مسار الوحدة (module path)، والذي لابُدّ أن يَحتوِي على وحدات تلك المنصة، كما ستحتاج إلى تمرير قيمة للخيار ‎--add-modules. مُرِّرت ALL-MODULE-PATH كقيمة للخيار ‎--add-modules بالقسم ٢.٦؛ للسماح للبرنامج بالوصول إلى أي وحدات (modules) موجودة بمسار الوحدة (module path) المُمرَّر. بدلًا من ذلك، قد تُمرِّر قائمة بأسماء الوحدات المُستخدَمة فعليًا بالبرنامج فقط. تنبيه: لا يُغطِى هذا الكتاب موضوع الوحدات (modules) بصورة أكبر من مُجرَّد اِستخدَامها مع منصة جافا إف إكس (JavaFX)، بالإضافة إلى المعلومات الأساسية بهذا القسم. التوثيق بأداة Javadoc ينبغي عمومًا كتابة تَوْثيق (documentation) جيد لأيّ واجهة برمجة تطبيقات (API)؛ وذلك حتى يَتَمكَّن المبرمجين من اِستخدَامها بصورة فعالة. يَشيِع اِستخدَام نظام Javadoc لتَوْثيق غالبية واجهات برمجة تطبيقات جافا (Java APIs)، فمثلًا، يُستخدَم هذا النظام لتجهيز تَوْثيق حزم الجافا القياسية (standard packages)، كما يَنشُر غالبية المبرمجين تقريبًا تَوْثيقًا باِستخدَام نفس ذلك النظام لأي صندوق أدوات (toolbox) يُطوروه بالجافا. يُجهَز تَوْثيق Javadoc بالاعتماد على مجموعة من التعليقات (comments) الخاصة، والتي تُكتَب بملفات الشيفرة المصدرية. كما تَعَلم، تُكتَب أحد أنواع تعليقات الجافا ضِمْن الترميزين ‎/*‎ و ‎*/‎. بالمثل، تُكتَب تعليقات Javadoc بنفس الصياغة لكنها تبدأ بالترميز ‎/**‎‎ بدلًا من الترميز ‎/*‎. لقد تَعرَّضت بالفعل لتعليقات مَكْتوبة بتلك الصياغة بكثير من الأمثلة بهذا الكتاب. ينبغي أن تُوْضَع تعليقات Javadoc قَبْل البرنامج الفرعي المَعنِي بالتعليق مباشرة. لاحظ ضرورة اتباع تلك القاعدة عمومًا وبَغْض النظر عن العنصر المَعنِي بالتَوْثيق. تُستخدَم تعليقات Javadoc عمومًا مع البرامج الفرعية (subroutines)، والمُتَغيِّرات الأعضاء (member variables)، والأصناف (classes)، وفي جميع تلك الحالات، لابُدّ دائمًا أن يَسبِق تعليق Javadoc العنصر المَعنِي بالتعليق مباشرة. عندما يُصرِّف (compile) الحاسوب ملفات الشيفرة المصدرية، فإنه يتجاهل تعليقات Javadoc مثلما يتجاهل أي تعليق عادي آخر. تَتوفَّر الأداة javadoc، والتي تَقْرأ ملفات الشيفرة المصدرية، لتَستخرِج منها تعليقات Javadoc، ثم تُنشِئ مجموعة من صفحات الانترنت التي تَتضمَّن تلك التعليقات بصياغة متناسقة ومترابطة. لاحظ أن تلك الأداة تَستخرِج افتراضيًا المعلومات المُتعلِّقة بكُلًا من الأصناف العامة والبرامج الفرعية العامة والمُتَغيِّرات الأعضاء العامة فقط، أي تلك المُصرَّح عنها باِستخدَام المُبدِّل public، ومع ذلك فهي تَسمَح بإنشاء تَوْثيق للعناصر غير العامة (non-public) من خلال خيار خاص. إذا لم تَعثُر الأداة javadoc على أية تعليقات Javadoc لعنصر معين، فإنها تُنشِئ واحدًا افتراضيًا يَتكوَّن من مُجرد معلومات بسيطة عن ذلك العنصر، مثل اسم المُتَغيِّر العضو ونوعه في حالة كان العنصر مُتَغيِّر عضو، أو كُلًا من اسم البرنامج الفرعي، ونوع القيمة المُعادة منه وقائمة مُعامِلاته في حالة كان العنصر برنامجًا فرعيًا. تعدّ تلك المعلومات مُجرد معلومات صياغية (syntactic)، أما لإضافة معلومات دلالية (semantics) واقعية، فلابُدّ من كتابة تعليق Javadoc. كمثال، يُمكِنك فَحْص "توثيق Javadoc للصنف TextIO". اُنشأت صفحة التَوْثيق تلك باِستخدَام أداة javadoc مع ملف الشيفرة المصدري TextIO.java. إذا حَمَّلت نسخة الكتاب المتاحة عبر الإنترنت، ستَجِدْ ذلك التوثيق بالمجلد TextIO_Javadoc. يُعدّ استخدام الترميز * ببداية كل سطر ضِمْن تعليقات Javadoc أمرًا اختياريًا؛ حيث تَحذفه أداة javadoc على أية حال. بالإضافة إلى النص العادي، قد تَحتوِي تعليقات Javadoc على ترميزات (codes) خاصة، مثل أوامر HTML الترميزية (HTML mark-up commands)‏. تُستخدَم لغة HTML عمومًا لإنشاء صفحات الانترنت، ولمّا كانت تعليقات Javadoc مَعنيَّة بالظهور على تلك الصفحات، فإن أداة javadoc تَنسَخ أي أوامر HTML بالتعليقات إلى صفحات الانترنت التي تُنشئها. لن يتناول الكتاب أيّ شرح تفصيلي عن HTML، لكن، كمُجرَّد مثال، تستطيع مثلًا إضافة <p> للإشارة إلى بداية فقرة جديدة. في حالة غياب أوامر HTML، تتجاهل الأداة الأسطر الفارغة والفراغات (spaces) الإضافية الموجودة بالتعليق. انتبه أيضًا للمحرفين & و ‎<‎؛ لأن لهما معنًى خاصًا بلغة HTML، ولذا لا ينبغي اِستخدَامهما بتعليقات Javadoc لمعنى غير تلك المَعانِي، ولكن يُمكِن كتابتهما باِستخدَام ‎&amp;‎ و ‎&lt‎;‎ على التوالي. يُوفِّر نظام التوثيق Javadoc ما يُعرَف باسم الوُسوم التوثيقية (doc tags). تُكتَب تلك الوسوم ضِمْن تعليقات Javadoc، بحيث تُعالجها أداة javadoc بعدّها نوعًا من الأوامر الخاصة. يبدأ أيّ وَسم تَوْثيقي (doc tag) بالمحرف @ مَتبوعًا باسمه. سنتناول ٤ وسوم (tags) فقط، هي كالتالي: الوُسوم ‎@author و ‎@param و ‎@return و ‎@throws. يُستخدَم الوسم ‎@author فقط مع الأصناف، وينبغي أن يُتبَع باسم المؤلف. تُستخدَم الوسوم الثلاثة الاخرى بتعليقات Javadoc التي تستهدف البرامج الفرعية بهدف تَوْفِير معلومات عن مُعاملاتها، وقيمها المُعادة، والاعتراضات (exceptions) التي قد تبلِّغ عنها. لابُدّ أن تُوْضَع تلك الأوسمة بنهاية التعليق، أي بَعْد وصف البرنامج الفرعي نفسه. تُكتَب بالصيغ (syntax) التالية: @param <parameter-name> <description-of-parameter> @return <description-of-return-value> @throws <exception-class-name> <description-of-exception> يُمكِن أن تمتد الأوصاف و و إلى عدة أسطر؛ حيث ينتهي أي وصف إما ببداية الوَسْم التوثيقي (doc tag) التالي أو بنهاية تعليق Javadoc بالكامل. يُمكِنك اِستخدَام الوَسْم ‎@param لوصف كل مُعامِل (parameter) يَستقبِله البرنامج الفرعي، وكذلك الوَسْم ‎@throws بقدر ما تريد تَوْثيقه من أنواع الاعتراضات (exception) المختلفة. وأخيرًا الوَسْم ‎@return والذي يُفْترَض كتابته للبرامج الفرعية التي تُعيد قيمة فعلية لا void. لا يُشترَط عمومًا كتابة تلك الوسوم (tags) بترتيب معين. اُنظر المثال التالي والذي يَستخدِم وُسوم التَوْثيق (doc tag) الثلاثة: /** * يحسب هذا البرنامج الفرعي مساحة المستطيل بفرض استقباله * لكلا من طول وعرض المستطيل. * ينبغي أن يكون كلا من طول وعرض المستطيل الممررين قيمة موجبة * @param width طول أحد جوانب المستطيل * @param height طول الجانب الآخر من المستطيل * @return مساحة المستطيل * @throws IllegalArgumentException إذا كان عرض أو طول المستطيل قيمة سالبة */ public static double areaOfRectangle( double height, double width ) { if ( width < 0 || height < 0 ) throw new IllegalArgumentException("Sides must have positive length."); double area; area = width * height; return area; } حَاوِل أن تَستخدِم تعليقات Javadoc بالشيفرة الخاصة بك، حتى لو لَمْ تَكُن تَنوِي إنشاء صفحة انترنت للتوثيق (documentation)؛ لأنها تُعدّ الصيغة القياسية لكتابة التوثيقات، لذا ستَكُون مألوفة لغالبية مُبرمجي الجافا. أما إذا أردت إنشاء صفحة انترنت للتَوْثيق، فستحتاج إلى اِستخدَام أداة javadoc، وهي متاحة كأمر بعُدة تطوير الجافا (JDK) التي ناقشناها بالقسم ٢.٦. تستطيع عمومًا اِستخدَام أداة javadoc بواجهة سطر الأوامر (command line interface) بنفس الطريقة التي تَستخدِم بها أوامر مثل javac و java. لاحِظ أنه يُمكِن تطبيق توثيق Javadoc أيضًا داخل بيئات التطوير المتكاملة (integrated development environments)، تُعرَف اختصارًا باسم IDE، التي تحدثنا عنها بالقسم ٢.٦. لن نَتَعرَّض لأيّ من تلك التفاصيل هنا، حيث يُمكِنك ببساطة مراجعة توثيق بيئة البرمجة (programming environment) الخاصة بك. الاستيراد الساكن (static import) كنقطة أخيرة بهذا القسم، سنتناول امتدادًا للمُوجِّه (directive)‏ import. رأيت بالفعل كيف مَكَّنَك المُوجِّه import من الإشارة إلى صَنْف مثل java.util.Scanner باِستخدَام الاسم البسيط للصَنْف Scanner. لكنك ما زلت مُضطرًا لاِستخدَام الأسماء المُركَّبة للإشارة إلى المُتَغيِّرات الأعضاء الساكنة (static member variables) مثل System.out، والتوابع الساكنة (static methods) مثل Math.sqrt. في الواقع، تَتَوفَّر صياغة آخرى من المُوجِّه import، تَستورِد الأعضاء الساكنة (static members) الموجودة بصَنْف معين بنفس الطريقة التي يَستورَد بها المُوجِّه import الأصناف (classes) من حزمة (package). تُسمَى تلك الصياغة من المُوجِّه باسم الاستيراد الساكن (static import). فمثلًا، لاستيراد اسم عضو ساكن (static member) واحد من صَنْف، نَكتُب الصيغة (syntax) التالية : import static <package-name>.<class-name>.<static-member-name>; بالمثل، قد تُكتَب الصيغة التالية لاستيراد جميع الأعضاء الساكنة العامة (public static members) ضِمْن صَنْف معين: import static <package-name>.<class-name>.*; على سبيل المثال، إذا كَتَبَت المُوجِّه import التالي قَبْل تعريف صَنْف (class definition) معين: import static java.lang.System.out; فإنك تستطيع بَعْدها -ضِمْن ذلك الصَنْف- اِستخدَام الاسم البسيط out بدلًا من الاسم المُركَّب System.out، وكذلك كتابة out.println بدلًا من System.out.println. إذا كنت ستَستخدِم الصَنْف Math بكثرة ضِمْن صَنْف معين، فلربما قد تُفضِّل اِستِباق تعريف ذلك الصنف بالمُوجِّه import التالي: import static java.lang.Math.*; وهو ما سيَسمَح لك بكتابة sqrt بدلًا من Math.sqrt، وكتابة log بدلًا من Math.log، وأيضًا PI بدلًا من Math.PI، وغيره. يُمكِنك أيضًا استيراد الدالة getlnInt من الصنف TextIO باِستخدَام: import static textio.TextIO.getlnInt; تنبيه: يتَطلَّب مُوجِّه الاستيراد الساكن (static import directive) تَخْصيص اسم الحزمة حتى مع الأصناف المُعرَّفة بالحزمة القياسية java.lang، مما يَعنِي أنك لن تَكُون قادرًا على تَّنْفيذ أيّ استيراد ساكن (static import) من صَنْف مُعرَّف بالحزمة الافتراضية (default package)؛ لأن ليس لها اسم. ترجمة -بتصرّف- للقسم Section 6: APIs, Packages, Modules, and Javadoc من فصل Chapter 4: Programming in the Large I: Subroutines من كتاب Introduction to Programming Using Java.
  12. يُخْزَّن البرنامج الفرعي (subroutine) بذاكرة الحاسوب على هيئة سِلسِلة نصية طويلة مُكوَّنة من الرقمين صفر وواحد، ولذلك فإنه لا يَختلف كثيرًا عن البيانات (data)، مثل الأعداد الصحيحة والسَلاسِل النصية والمصفوفات، والتي تُخْزَّن أيضًا بنفس الطريقة. ربما اِعتدت التفكير بكُلًا من البرامج الفرعية والبيانات على أساس أنهما شيئان مختلفان تمامًا، ولكن في الواقع يَتعامَل الحاسوب مع البرنامج الفرعي على أساس أنه مجرد نوع آخر من البيانات. تَسمَح حتى بعض اللغات البرمجية بالتَعامُل مع البرامج الفرعية بنفس الطريقة التي يَتعامَل معها الحاسوب، وهو ما أُضيف إلى الجافا على هيئة تعبيرات لامدا (lambda expressions) منذ الإصدار ٨. أصبحت تعبيرات لامدا أكثر شيوعًا ببرامج الجافا، وهي مفيدة في العموم، ولكن تَبرُز أهميتها بصورة أكبر ببرمجة واجهات المُستخدِم الرُسومية (GUI)، كالتَعامُل مع "أدوات تَحكُّم واجهة المُستخدَم الرسومية JavaFX ‏(JavaFX GUI toolkit)"، وهو ما سنتناوله بالفصل السادس، كما سنَتَعرَّض لها مجددًا بنهاية الفصل الخامس، لذا يُمكِنك تَخطِّي هذا القسم إذا أردت إلى ذلك الحين. الدوال الكيانية (first-class functions) يُمكِننا كتابة دالة لحِسَاب قيمة مربع العدد، بحيث تَستقبِل ذلك العدد كمُعامِل صُّوري أو وهمي (dummy parameter)، كالتالي: static double square( double x ) { return x*x; } كالعادة، لابُدّ من تَخْصيص اسم لتلك الدالة، وفي تلك الحالة كان square، وعليه تُصبِح تلك الدالة جزءًا دائمًا من البرنامج (program)، وهو ما قد لا يَكُون مُلائمًا؛ بالأخص إذا كنت تَنوِي اِستخدَام الدالة مرة واحدة فقط. لامدا (lambda) هو أحد حروف الأبجدية الإغريقية (Greek alphabet)، والذي اِستخدَمه عالم الرياضيات ألونزو تشرتش (Alonzo Church) أثناء دراسته للدوال القابلة للحِسَاب (computable functions). على سبيل المثال، إذا كنا نُريد حِسَاب قيمة مربع عدد، وليكن x، فقد تَظُنّ أن الترميز x2 يُمثِل "دالة" تَحسِب مربع x، لكنه، في الواقع، ليس سوى تعبير (expression) يُمثِل "نتيجة" حِسَاب مربع x. قَدَّمَ ألونزو تشرتش (Alonzo Church) ترميز لامدا (lambda notation) والذي يُمكِن اِستخدَامه لتعريف دالة (function) بدون تَخْصيص اسم لها. بالتحديد اِستخدَم الترميز lambda(x).x2 (في الواقع يُستخدَم الحرف الإغريقي لامدا ذاته وليس الكلمة نفسها) للإشارة إلى "دالة x معطاة بواسطة x2". يُعدّ هذا الترميز دالة مجرَّدة (function literal)، أيّ تُمثِل قيمة من النوع "دالة (function)" بنفس الطريقة التي تُعدّ بها القيمة 42 عددًا صحيحًا مُجرَّدًا (integer literal) يُمثِل قيمة من النوع int. ينبغي أن تَدْفَعك الدوال المُجرَّدة (function literals) إلى التَفكير بالدوال (function) على أنها مُجرَّد نوع آخر من القيم، وعندما تَصِل إلى تلك المرحلة، فإنك ستَكُون قادرًا على تَطْبِيق نفس تلك الأشياء التي اعتدت إجرائها على القيم الآخرى على الدوال أيضًا، كإِسْناد دالة إلى مُتَغيِّر، أو تمرير دالة كمُعامِل (parameter) إلى برنامج فرعي (subroutine)، أو إعادة دالة كقيمة من برنامج فرعي، أو حتى إنشاء مصفوفة دوال (array of functions). تَسمَح بعض لغات البرمجة بالقيام بكل تلك الأشياء، ويُقال عندها أنها تُدعِّم "الدوال الكيانيَّة (first-class functions)" أو أنها تُعامِل الدوال بعدّها كائنات كيانيَّة (first-class objects). تُوفِّر لغة الجافا كل تلك الأشياء من خلال تعبيرات لامدا (lambda expressions)، والتي تَختلِف صيغتها عن الترميز الذي اِستخدَمه ألونزو تشرتش (Alonzo Church)، فهي الواقع، لا تَستخدِم حتى كلمة لامدا (lambda) على الرغم من مُسماها. تُكتَب دالة حِسَاب مربع العدد بتعبير لامدا (lambda expression) بلغة الجافا، كالتالي: x -> x*x يُنشِئ العَامِل ‎->‎ تعبير لامدا، بحيث تَقَع المُعامِلات الوهمية أو الصُّوريّة (dummy parameter) على يسار العَامِل، بينما يَقَع التعبير (expression) المسئول عن حِسَاب قيمة الدالة على يمينه. قد ترى تعبير لامدا بالأعلى مُمرَّرًا كمُعامِل فعليّ (actual parameter) إلى برنامج فرعي، أو مُسنَد إلى مُتَغيِّر، أو مُعاد من دالة. إذًا، هل الدوال بلغة الجافا كيانيَّة (first-class)؟ لا تَتَوفَّر إجابة واضحة على هذا السؤال، لأن لغة الجافا لا تُدعِّم بعض الأشياء الآخرى المُتوفِّرة ضِمْن لغات برمجية آخرى. فمثلًا، في حين تستطيع إِسْناد تعبير لامدا المُعرَّف بالأعلى إلى مُتَغيِّر اسمه sqr، فإنك لن تستطيع بَعْدها اِستخدَام ذلك المُتَغيِّر كما لو كان دالة فعليّة، أيّ لا تستطيع كتابة sqr(42)‎. لغة الجافا عمومًا هي لغة صارمة في تَحْدِيد النوع (strongly typed)، ولذلك لكي تَتَمكَّن من إِنشاء المُتَغيِّر sqr، فعليك التَّصْريح عنه أولًا وهو ما يَتضمَّن تَخْصيص نوعه، فيا تُرى ما هو ذلك النوع الذي يَتناسب مع قيمة هي عبارة عن دالة؟ تُوفِّر الجافا ما يُعرَف باسم واجهة نوع الدالة (functional interface) لهذا الغرض، وسنناقشها بَعْد قليل. ملاحظة أخيرة: على الرغم من ارتباط مصطلح الدالة (function) بتعبيرات لامدا (lambda expressions) بدلًا من مصطلحات مثل برنامج فرعي (subroutine) أو تابع (method)، فإنها في الواقع لا تَقْتصِر على تمثيل الدوال (functions)، وإنما يُمكِنها تمثيل أي برنامج فرعي (subroutines)، وبصورة عشوائية. واجهات نوع الدالة (functional interfaces) كي تَتَمكَّن من اِستخدَام برنامج فرعي (subroutine) معين، فلابُدّ أن تَعرِف كُلًا من اسمه، وعدد المُعامِلات (parameters) التي يَستقبِلها، وأنواعها، بالإضافة إلى نوع القيمة المُعادة (return type) من ذلك البرنامج الفرعي. تُوفِّر لغة الجافا ما يُعرَف باسم واجهات نوع الدالة (functional interface)، وهي أَشْبَه بالأصناف (class)، حيث تُعرَّف بملف امتداده هو ‎.java كأي صَنْف، وتَتضمَّن تلك المعلومات المذكورة بالأعلى عن برنامج فرعي وحيد. انظر المثال التالي: public interface FunctionR2R { double valueAt( double x ); } تُصرِّح الشيفرة بالأعلى عن واجهة نوع الدالة FunctionR2R، والموجودة بملف يَحمِل اسم FunctionR2R.java. تُصرِّح تلك الواجهة عن الدالة valueAt، والتي تَستقبِل مُعامِل وحيد من النوع double، وتُعيد قيمة من النوع double. تَفرِض قواعد الصيغة تَخْصيص اسم للمُعامِل -x بالأعلى- على الرغم من أنه في الواقع ليس ذا أهمية هنا، وهو ما قد يَكُون مزعجًا نوعًا ما. ها هو مثال آخر: public interface ArrayProcessor { void process( String[] array, int count ); } تَتضمَّن لغة الجافا الكثير من واجهات نوع الدالة القياسية (standard functional interfaces) بصورة افتراضية. تُعدّ واجهة نوع الدالة Runnable واحدة من أهم تلك الواجهات، وأبسطها، والمُعرَّفة كالتالي: public interface Runnable { public void run(); } سنَستخدِم واجهات نوع الدالة (functional interfaces) الثلاثة بالأعلى للأمثلة التالية بهذا القسم. تُوفِّر لغة الجافا أيضًا ما يُعرَف باسم الواجهات (Interfaces)، والتي هي في الواقع أعم وأشمل، وربما أكثر تعقيدًا من واجهات نوع الدالة (functional interfaces). سنتعلَّّم المزيد عنها بالقسم ٥.٧. سنُقصِر حديثنا بهذا القسم على واجهات نوع الدالة (functional interfaces)؛ لارتباطها بموضوعنا الرئيسي: تعبيرات لامدا (lambda expressions). يُعدّ اسم واجهة نوع الدالة نوعًا (type)، تمامًا كالأنواع String و double، وكأيّ نوع، فإنه يُمكِن اِستخدَامه للتَّصْريح عن المُتَغيِّرات، والمُعامِلات، وكذلك لتَخْصيص نوع القيمة المُعادة (return type) من الدوال، ويُمكِن إِسْناد تعبيرات لامدا (lambda expression) كقيم للمُتَغيِّرات من ذلك النوع. في الواقع، تَتضمَّن واجهة نوع الدالة (functional interface) قالبًا (template) لبرنامج فرعي وحيد، وينبغي لتعبير لامدا (lambda expression) المُسنَد أن يَكُون مُتطابِقًا مع ذلك القالب. تعبيرات لامدا (lambda Expressions) يُعدّ أي تعبير لامدا (lambda expression) تمثيلًا لبرنامج فرعي مجهول الاسم (anonymous subroutine)، أيّ لا يَمتلك اسمًا، ولكنه في المقابل، وكأيّ برنامج فرعي، لديه قائمة من المُعامِلات الصُّوريّة (formal parameter list)، بالإضافة إلى التعريف (definition) نفسه، ويُكتَب عمومًا بالصيغة (syntax) التالية: ( <parameter-list> ) -> { <statements> } كما هو الحال مع البرامج الفرعية العادية، يُمكِن لقائمة المُعامِلات -بالأعلى- أن تَكُون فارغة، أو قد تتكوَّن من تَّصْريح عن مُعامِل (parameter declaration) وحيد أو أكثر، بحيث يُفصَل بين كل تَّصْريح والذي يليه بفاصلة (comma)، وبحيث يَتكوَّن كل تَّصْريح من نوع المُعامِل متبوعًا باسمه. في الواقع، تُبسَّط عادة تلك الصيغة وفقًا لمجموعة من القواعد: أولًا: يُمكِن حَذْف أنواع المُعامِلات إذا كان استنباطها من السِّياق مُمكِنًا. على سبيل المثال، إذا كان هناك تعبير لامدا مُعرَّف على أنه من النوع FunctionR2R، فإن مُعامِل التعبير لابُدّ وأن يَكُون من النوع double، ولذلك فإنه، في تلك الحالة، ليس من الضروري تَخْصيص نوع المُعامِل ضِمْن تعبير اللامدا. ثانيًا، يُمكِن حَذْف الأقواس () حول قائمة المُعامِلات (parameter list) إذا كانت مُكوَّنة من مُعامِل وحيد غَيْر مُحدَّد النوع. ثالثًا، يُمكِن حَذْف الأقواس {} الموجودة على الجانب الأيمن من العَامِل ‎->‎ إذا كانت تَحتوِي على تَعْليمَة استدعاء برنامج فرعي (subroutine call) وحيدة. أخيرًا، إذا كان الجانب الأيمن من العَامِل ‎->‎ مكتوبًا بالصيغة { return <expression>;‎ }، فيُمكِن تبسيطها إلى التعبير وحَذْف أي شيء آخر. بفَرْض أننا نريد كتابة دالة لحِسَاب مربع القيم من النوع double، فإن نوع تلك الدالة هو من نفس نوع واجهة نوع الدالة FunctionR2R المُعرَّفة بالأعلى. الآن، إذا كان sqr مُتَغيِّرًا من النوع FunctionR2R، فإننا نستطيع أن نُسنِد تعبير لامدا (lambda expression) كقيمة لذلك المُتَغيِّر، ويُعدّ عندها ذلك التعبير تمثيلًا للدالة المطلوبة. يُمكِن القيام بذلك بأي من الصِيَغ التالية: sqr = (double x) -> { return x*x; }; sqr = (x) -> { return x*x; }; sqr = x -> { return x*x; }; sqr = x -> x*x; sqr = (double fred) -> fred*fred; sqr = (z) -> z*z; تُعرِّف تعبيرات لامدا (lambda expressions) الستة بالأعلى نفس الدالة بالضبط، وأُضيفت آخر تَعْليمَتين (statements) خصيصًا؛ للتأكيد على أن أسماء المُعامِلات غير مهمة، فهى بالنهاية مجرد مُعامِلات وهمية أو صُّوريّة (dummy parameters). لمّا كان المُصرِّف (compiler) على علم بكون المُتَغيِّر sqr من النوع FunctionR2R، ولأن النوع FunctionR2R يتطلَّب مُعامِلًا من النوع double، فقد تَمَكَّنا من حَذْف نوع المُعامِل double. يُمكِن اِستخدَام تعبيرات لامدا فقط ضِمْن السِّياقات التي يستطيع فيها المُصرِّف (compiler) استنباط نوعها، ولذلك لابُدّ من كتابة نوع المُعامِل إذا كان حَذْفه سيَتسبَّب بغموض نوع التعبير. لا يُعدّ المُتَغيِّر sqr -كما هو مُعرَّف بالأعلى- دالة (function) تمامًا، وإنما هو قيمة من النوع FunctionR2R، وبحسب ما هو مُخصَّص بتعريف الواجهة (interface definition)‏ FunctionR2R، فإن ذلك المُتَغيِّر لابُدّ وأن يَحتوِي على دالة تَحمِل اسم valueAt. يمكن استدعاء تلك الدالة من خلال اسمها الكامل sqr.valueAt، فمثلًا يُمكِن كتابة sqr.valueAt(42)‎ أو sqr.valueAt(x) + sqr.valueAt(y)‎. إذا كانت قائمة مُعامِلات تعبير لامدا مُكوَّنة من أكثر من مُعامِل واحد، فإن الأقواس () المُحيطة بقائمة المُعامِلات (parameters list) لم تَعُدْ اختيارية. اُنظر المثال التالي، والذي يَستخدِم واجهة نوع الدالة ArrayProcessor، كما يُبيِّن طريقة كتابة تعبير لامدا عندما يكون تعريفها مُتعدِّد الأسطر (multiline definition): ArrayProcessor concat; concat = (A,n) -> { // الأقواس هنا مطلوبة String str; str = ""; for (int i = 0; i < n; i++) str += A[i]; System.out.println(str); }; // الفاصلة المنقوطة هنا ليست جزءًا من تعبير لامدا // وإنما تشير إلى انتهاء تعليمة الإسناد String[] nums; nums = new String[4]; nums[0] = "One"; nums[1] = "Two"; nums[2] = "Three"; nums[3] = "Four"; for (int i = 1; i < nums.length; i++) { concat.process( nums, i ); } مما سينتج عنه الخَرْج التالي: One OneTwo OneTwoThree OneTwoThreeFour تُصبِح الأمور أكثر تشويقًا عند تمرير تعبير لامدا كمُعامِل فعليّ (actual parameter)، وهو في الواقع الاِستخدَام الأكثر شيوعًا لتعبيرات لامدا. افترض أن لدينا الدالة التالية مثلًا: /** * احسب قيمة التعبير‫ f(start) + f(start+1) + ... + f(end) * حيث‫ f عبارة عن دالة تُستقبل كمعامل * ‫قيمة المعامل end لابد أن تكون أكبر من أو تساوي start */ static double sum( FunctionR2R f, int start, int end ) { double total = 0; for (int n = start; n <= end; n++) { total = total + f.valueAt( n ); } return total; } لمّا كانت f من النوع FunctionR2R، فإن قيمة f عند n تُكتَب على الصورة f.valueAt(n)‎. يُمكِن تمرير تعبير لامدا (lambda expression) كقيمة للمُعامِل الأول عند استدعاء الدالة sum، كالتالي: System.out.print("The sum of n squared for n from 1 to 100 is "); System.out.println( sum( x -> x*x, 1, 100 ) ); System.out.print("The sum of 2 raised to the power n, for n from 1 to 10 is "); System.out.println( sum( num -> Math.pow(2,num), 1, 10 ) ); كمثال آخر، افترض أن لدينا برنامجًا فرعيًا (subroutine) ينبغي أن يُنفِّذ مُهِمّة (task) مُعطاة عدة مرات، يُمكِننا ببساطة تمرير تلك المُهِمّة كمُعامِل من النوع Runnable، كالتالي: static void doSeveralTimes( Runnable task, int repCount ) { for (int i = 0; i < repCount; i++) { task.run(); // نفذ المهمة } } نستطيع الآن طباعة السِلسِلة النصية "Hello World" عشر مرات باستدعاء التالي: doSeveralTimes( () -> System.out.println("Hello World"), 10 ); تَتَكوَّن قائمة مُعامِلات (parameter list) تعبيرات لامدا من النوع Runnable من زوج فارغ من الأقواس ()؛ وذلك لتتماشى مع البرنامج الفرعي المُصرَّح عنه ضِمْن واجهة نوع الدالة Runnable. اُنظر المثال التالي: doSeveralTimes( () -> { int count = 5 + (int)(21*Math.random()); for (int i = 1; i <= count; i++) { System.out.print(i + " "); } System.out.println(); }, 100); على الرغم من أن الشيفرة بالأعلى قد تبدو معقدة نوعًا ما، إلا أنها، في الواقع، مُكوَّنة من تَعْليمَة وحيدة هي تَعْليمَة استدعاء البرنامج الفرعي doSeveralTimes. يَتَكوَّن مُعامِل البرنامج الفرعي الأول من تعبير لامدا (lambda expression) قد امتد تعريفه إلى عدة أسطر، أما مُعامِله الثاني فهو القيمة ١٠٠، وتُنهِي الفاصلة المنقوطة (semicolon) بنهاية آخر سطر تَعْليمَة استدعاء البرنامج الفرعي (subroutine call). اطلعنا على عدة أمثلة تُسنَد فيها تعبيرات لامدا (lambda expression) إلى مُتَغيِّرات، وآخرى تُمرَّر فيها تعبيرات لامدا كمُعامِلات فعليّة (actual parameter). تَبَقَّى الآن أن نراها مُستخدَمة كقيمة مُعادة (return value) من دالة. اُنظر المثال التالي: static FunctionR2R makePowerFunction( int n ) { return x -> Math.pow(x,n); } وبالتالي، ستُعيد makePowerFunction(2)‎ قيمة من النوع FunctionR2R تَحسِب مربع قيمة المُعامِل المُمرَّر، بينما ستُعيد makePowerFunction(10)‎ قيمة من النوع FunctionR2R تَحسِب قيمة المُعامِل المُمرَّر مرفوعًا للأس ١٠. يُوضِح هذا المثال أيضًا أن تعبيرات لامدا (lambda expression) تستطيع اِستخدَام مُتَغيِّرات آخرى إلى جانب مُعامِلاتها، مثل n في المثال بالأعلى. (في الواقع هناك بعض القيود لتَحْدِيد متى يُمكِن القيام بذلك). مراجع التوابع (Method References) لنفْترِض أننا نريد كتابة تعبير لامدا (lambda expression) من النوع FunctionR2R، بحيث يُمثِل دالة تَحسِب الجذر التربيعي. نستطيع كتابته على الصورة x -> Math.sqrt(x)‎، ولكننا إذا دققنا النظر، سنَجِدْ أن تعبير لامدا، في تلك الحالة تحديدًا، ليس سوى مجرد مُغلِّف (wrapper) بسيط للدالة Math.sqrt الموجودة بالفعل. ولهذا تُوفِّر الجافا ما يُعرَف باسم مَراجِع التوابع (method reference)؛ لاِستخدَامها في الحالات المشابهة (تَذَكَر أن "التابع [method]" هو مجرد كلمة آخرى للإشارة إلى "البرنامج الفرعي [subroutine]" بلغة الجافا). نستطيع مثلًا اِستخدَام المَرجِع التابعي Math::sqrt، والذي يُعدّ اختصارًا لتعبير اللامدا المذكور سلفًا، أينما أَمْكَن اِستخدَام تعبير اللامدا المناظر، كتمريره مثلًا كمُعامِل للدالة sum المُعرَّفة بالأعلى، كالتالي: System.out.print("The sum of the square root of n for n from 1 to 100 is "); System.out.println( sum( Math::sqrt, 1, 100 ) ); فقط لو أَمْكَننا اِستخدَام الاسم Math.sqrt هنا بدلًا من الاستعانة بترميز (notation) جديد ::! في الواقع، الترميز Math.sqrt مُعرَّف بالفعل ضِمْن الصَنْف Math لكن للإشارة إلى مُتَغيِّر اسمه sqrt. يُمكِن عمومًا اِستخدَام مَراجِع التوابع (method reference) بدلًا من تعبيرات لامدا (lambda expression) التي يَقْتصِر دورها على اِستدعاء أحد التوابع الساكنة (static method) الموجودة مُسْبَّقًا. تُكتَب مَراجِع التوابع على الصورة التالية: <classname>::<method-name> لا يَقْتصِر هذا الترميز (notation) على التوابع الساكنة، أي تلك المُنتمية للأصناف (classes)، وإنما يَمْتَدّ إلى تلكم المُنتمية للكائنات (objects) كذلك. على سبيل المثال، إذا كان str متغير من النوع String، فإن str بطبيعة الحال سيَحتوِي على التابع str.length()‎، وفي هذه الحالة، يُمكِن اِستخدَام المَرجِع التابعي str::length كتعبير لامدا من واجهة نوع الدالة SupplyInt المُعرَّفة كالتالي: public interface SupplyInt { int get( ); } ترجمة -بتصرّف- للقسم Section 5: Lambda Expressions من فصل Chapter 4: Programming in the Large I: Subroutines من كتاب Introduction to Programming Using Java.
  13. يُطلَق على البرامج الفرعية (subroutine) التي تُعيد قيمة اسم "الدوال (function)"، والتي يُمكِنها أن تُعيد قيمة من نوع واحد مُحدَّد ضِمْن التعريف ويُسمى النوع المُعاد (return type) من الدالة. بإِمكَانك استدعاء الدوال (function call) في العموم بأيّ مكان يَتوقَّع فيه الحاسوب قيمة (value)، مثل الجانب الأيمن من تَعْليمَات الإِسْناد (assignment statement)، أو كمُعامِل فعليّ (actual parameter) بتَعْليمَات استدعاء البرامج الفرعية، أو كجُزء من تعبير (expression) أكبر. يُمكِن حتى اِستخدَام الدوال من النوع boolean، أي التي تُعيد قيمة من ذلك النوع، مَحل الشَّرْط (condition) ضِمْن تَعْليمَات مثل if و while و for و do..while. يُمكِن أيضًا اِستخدَام تَعْليمَة استدعاء دالة (function call)، كما لو كانت برنامجًا فرعيًا، أيّ كما لو أنها لا تُعيد قيمة. في تلك الحالة، يَتجاهَل الحاسوب القيمة المُعادة من البرنامج الفرعي، وهو ما يَكُون منطقيًا في بعض الأحيان. على سبيل المثال، تَقرأ الدالة TextIO.getln()‎ سَطْرًا مُدْخَلًا مِنْ قِبَل المُستخدِم وتُعيده كقيمة من النوع String. عادة، يُسنَد السَطْر المُعاد من تلك الدالة إلى مُتَغيِّر؛ لكي يُستخدَم لاحقًا، كما في التَعْليمَة name = TextIO.getln();‎. لكن، يُمكِن اِستخدَام تلك الدالة ضِمْن تَعْليمَة استدعاء برنامج فرعي على الصورة TextIO.getln();‎، والتي، كالعادة، ستَستمِر بقراءة مُدْخَلات المُستخدِم إلى أن تَصِل إلى محرف العودة إلى بداية السطر (carriage return). لمّا لم تُسنَد القيمة المُعادة من تلك الدالة إلى مُتَغيِّر، أو تُستخدَم ضِمْن تعبير (expression)، فإنها تُستبعَد ببساطة، لذا يَكُون تأثير استدعاء البرنامج الفرعي في تلك الحالة هو مجرد قراءة بعض المُدْخَلات واِستبعَادها، وهو الشيء الذي قد تَرغَب به في بعض الأحيان. تعليمة return بوصولك إلى هنا، فأنت قد تَعلَّمت طريقة استدعاء الدوال (functions) -التي تُعيد قيم فعليّة- مثل Math.sqrt()‎ و TextIO.getInt()‎، لكنك لَمْ تَكتُب أيّ دالة خاصة بك بَعْد. في الواقع، ستبدو كتابة الدوال أمرًا مألوفًا بالنسبة لك؛ فهي بالنهاية مُشابهة تمامًا لصيغة كتابة أيّ برنامج فرعي (subroutine) عادي باستثناء ضرورة اِستخدَام التَعْليمَة return؛ لتَخْصِيص القيمة التي سيُعيدها البرنامج الفرعي (return value). تُكتَب تَعْليمَة return بالصيغة (syntax) التالية: return <expression>; يُمكِن اِستخدَام تَعْليمَة return بالأعلى داخل تعريف الدوال فقط (function definition)، ولابُدّ لنوع التعبير المَكْتُوب بَعْد التَعْليمَة أن يَتماشى مع النوع المُعاد (return type) المُخصَّص ضِمْن تعريف الدالة، أيّ لابُدّ أن يؤول ذلك التعبير إلى قيمة هي من نوع يُمكِن إِسْناده -بواسطة تَعْليمَة إِسْناد (assignment)- إلى النوع المُعاد (return type) من الدالة والمُخصَّص بتعريفها (definition). عندما يُنفِّذ الحاسوب تَعْليمَة return، فإنه يَحسِب قيمة التعبير المُخصَّص، ثم يُنهِي تَّنْفيذ الدالة، وأخيرًا، يُعيد قيمة التعبير لتُصبِح القيمة المُعادة (return value) من الدالة. اُنظر تعريف الدالة التالي على سبيل المثال: static double pythagoras(double x, double y) { return Math.sqrt( x*x + y*y ); } بفَرْض تَّنْفيذ الحاسوب لتَعْليمَة إِسْناد تَتضمَّن تَعْليمَة استدعاء الدالة بالأعلى، مثل totalLength = 17 + pythagoras(12,5);‎، فإنه سيُحاوِل تَحْصِيل قيمة تَعْليمَة الاستدعاء pythagoras(12,5)‎، لذلك سيبدأ بإِسْناد قيم المُعامِلات الفعليّة (actual parameters) ‏المُمرَّرة، أيّ ١٢ و ٥، إلى المُعامِلات الصُّوريّة (formal parameters)‏ x و y بتعريف الدالة، ثم سينتقل إلى تَّنْفيذ مَتْن الدالة (function body)، وبالتالي سيضطرّ إلى تَحْصِيل قيمة التعبير Math.sqrt(12.0*12.0 + 5.0*5.0)‎ والتي تؤول إلى القيمة 13.0، ثم سيُعيد هذه القيمة لتَكُون القيمة المُعادة (return value) من الدالة، وعليه تُستبدَل القيمة 13.0 بتَعْليمَة استدعاء الدالة، فتصبح تَعْليمَة الإِسْناد وكأنها مَكْتُوبة على الصورة totalLength = 17+13.0. أخيرًا، تُضاف القيمة المُعادة (return value) إلى العدد 17 ثم تُخْزَن النتيجة 30.0 بالمُتَغيِّر totalLength. لاحظ أنه وبينما تُنفَّذ الدالة، فإنك قد تَتَمكَّن من تَحْصِيل القيمة المُفْترَض إعادتها منها، وفي تلك الحالة، تستطيع اِستخدَام تَعْليمَة return؛ لإعادة تلك القيمة مباشرة، ومِنْ ثَمَّ تَخَطِّي أية تَعْليمَات آخرى بمَتْن الدالة، دون الحاجة إلى الانتظار إلى نهاية المَتْن، أيّ أنه ليس من الضروري أن تَكُون تَعْليمَة return هي آخر تَعْليمَة بمَتْن الدالة، بل يُمكِن اِستخدَامها بأي مكان ضِمْن المَتْن. ولكن، لابُدّ دائمًا من إعادة قيمة ما من الدالة وذلك بجميع مسارات التَّنْفيذ المُحتمَلة التي قد تَتَّخذها الدالة. كما ذَكَرَنا مُسْبَّقًا، لابُدّ دائمًا من كتابة تَعْليمَة return مَتبوعة بتعبير (expression) ضِمْن تعريف أي دالة (function)؛ لأن الدوال دائمًا ما تُعيد قيمة. في المقابل، لا يُعدّ ذلك ضروريًا مع البرامج الفرعية من غير الدوال (non-function)، أي تلك التي تَستخدِم void ضِمْن تعريفها للتَّصْريح عن نوعها المُعاد، فهي في النهاية لا تُعيد قيمة، ومع ذلك يَظِلّ اِستخدَام تَعْليمَة return ضِمْن هذا السِّياق مُمكنًا، بحيث يَقْتصِر دورها على إنهاء تَّنْفيذ البرنامج الفرعي بمكان ما وسط المَتْن وإعادة التَحكُّم (return control) مرة آخرى إلى سَطْر الشيفرة المسئول عن استدعاء البرنامج الفرعي. تُكتَب تَعْليمَة return في تلك الحالة على الصورة return;‎ بدون تعبير (expression). بالإضافة إلى ما سبق، قد تُستخدَم تَعْليمَة return أيضًا داخل حَلْقة تَكْرار (loop) لإنهاء كُلًا من الحَلْقة (loop) والبرنامج الفرعي الحاضن لها. بالمثل، قد تُستخدَم بتَعْليمَة switch، للخروج من كُلًا من تَعْليمَة switch والبرنامج الفرعي الحاضن لها، أيّ ستَجِدْ أحيانًا تَعْليمَة return مُستخدَمة ضِمْن سِّياقات اعْتَدْت فيها استخدام تَعْليمَة break. أمثلة على الدوال (functions) تَعرَّضنا لمسألة حِسَاب قيم عناصر مُتتالية الأعداد"3N+1" عدة مرات بما في ذلك القسم السابق. يُمكِننا تعريف دالة (function) بسيطة جدًا، تَستقبِل قيمة العنصر الحالي بالمُتتالية، ثم تُعيد قيمة عنصر المتتالية التالي. اُنظر الشيفرة التالية: static int nextN(int currentN) { if (currentN % 2 == 1) // ‫إذا كان currentN فرديًا return 3*currentN + 1; // أعد تلك القيمة else return currentN / 2; // إذا لم يكن، أعد تلك القيمة } نُلاحِظ أن تعريف الدالة بالأعلى يَتضمَّن تَعْليمَتي return. ستُنفَّذ دائمًا تَعْليمَة return واحدة فقط بِغَضّ النظر عن عدد تلك التَعْليمَات المُستخدَمة ضِمْن التعريف. يُفضِّل بعض المبرمجين كتابة تَعْليمَة return وحيدة بنهاية الدالة، ويحاولوا القيام بذلك قدر الإمكان؛ خاصة وأن ذلك يُسهِل من العثور على تَعْليمَة return ضِمْن التعريف. فمثلًا، يُمكِننا إعادة كتابة الدالة nextN()‎ -المُعرَّفة بالأعلى- مرة آخرى على النحو التالي: static int nextN(int currentN) { int answer; // متغير يحمل القيمة المعادة من الدالة if (currentN % 2 == 1) // ‫إذا كان currentN فرديًا answer = 3*currentN+1; // أسند القيمة إلى المتغير else answer = currentN / 2; // إذا لم يكن، أسند القيمة إلى المتغير return answer; // لا تنسى أن تعيد المتغير } دَعْنَا الآن نَكتُب برنامجًا فرعيًا (subroutine) لحِسَاب قيم عناصر المُتتالية، لكن بالاعتماد على الدالة nextN هذه المرة. نظرًا لكَوْن الدالة nextN قصيرة نوعًا ما، فإن التَحْسِينات المُجراة على البرنامج الفرعي ليست ذا شأن بالموازنة مع نسخة البرنامج بالقسم ٤.٣. في المقابل، إذا كانت الدالة nextN()‎ طويلة لكَوْنها مثلًا تَحسِب عملية معقدة، فعندها سيَكُون من المنطقي إخفاء ذلك التعقيد بداخل دالة منفصلة: static void print3NSequence(int startingValue) { int N; // أحد عناصر المتتالية int count; // عدد عناصر المتتالية N = startingValue; // أول قيمة بالمتتالية count = 1; System.out.println("The 3N+1 sequence starting from " + N); System.out.println(); System.out.println(N); while (N > 1) { // ‫احسب قيمة عنصر المتتالية التالي باستدعاء الدالة nextN N = nextN( N ); count++; // أزد عدد عناصر المتتالية بمقدار الواحد System.out.println(N); // اطبع قيمة العنصر } System.out.println(); System.out.println("There were " + count + " terms in the sequence."); } اُنظر المثال التالي للدالة letterGrade، والتي تَستقبِل درجة عددية (numerical grade)، وتُحوِّلها إلى نظيرتها المحرفية (letter grade) بالاستعانة بمقياس درجات (grading scale) تقليدي: /** * أعد الدرجة المحرفية المناظرة للدرجة العددية الممررة للدالة كمعامل */ static char letterGrade(int numGrade) { if (numGrade >= 90) return 'A'; // 90 or above gets an A else if (numGrade >= 80) return 'B'; // 80 to 89 gets a B else if (numGrade >= 65) return 'C'; // 65 to 79 gets a C else if (numGrade >= 50) return 'D'; // 50 to 64 gets a D else return 'F'; } // ‫نهاية الدالة letterGrade تستطيع الدوال أن تُعيد قيم من أي نوع، فمثلًا، نوع القيمة المُعادة (return type) من الدالة letterGrade()‎ -بالأعلى- هو النوع char، أما الدالة المُعرَّفة بالمثال التالي، فإنها تُعيد قيمة (return value) من النوع boolean. اُنظر الشيفرة التالية، واحْرِصْ على قراءة التعليقات (comments)؛ لأنها تُوضِح بعض النقاط المثيرة للاهتمام: /** * تعيد هذه الدالة القيمة المنطقية‫ true إذا كان N عدد أولي. * ‫العدد الأولي هو عدد صحيح أكبر من العدد 1 * بشرط ألا يكون قابلًا للقسمة ‫إلا على نفسه وعلى الواحد. * Nإذا كان لدى‫ N أي قاسم D يتراوح بين الواحد والعدد * ‫فسيكون لديه قاسم يتراوح بين العدد 2 والجذر التربيعي للعدد N * ‫لذلك سوف نختبر قيم القواسم المحتملة بداية من العدد 2 * ‫وحتى الجذر التربيعي للعدد N */ static boolean isPrime(int N) { int divisor; // عدد يحتمل أن يكون قاسما للعدد if (N <= 1) return false; // العدد الأولي لابد أن يكون أكبر من الواحد int maxToTry; // أكبر قيمة قاسم محتمل ينبغي اختبارها // ‫ينبغي إجراء عملية التحويل بين الأنواع لأن Math.sqrt // ‫تعيد قيمة من النوع double maxToTry = (int)Math.sqrt(N); // ‫سنحاول قسمة‫ N على الأعداد من 2 ووصولا الى قيمة maxToTry // إذا لم يكن‫ N قابل للقسمة على أي عدد من تلك الأعداد // ‫سيكون N عددا اوليا for (divisor = 2; divisor <= maxToTry; divisor++) { if ( N % divisor == 0 ) // ‫إذا تمكن divisor من تقسم N return false; // ‫إذًا N لا يمكن أن يكون عددًا أوليًا // لا حاجة للاستمرار } // ‫إذا وصلنا إلى هنا، فإن N حتما هو عدد أولي // ‫لأنه إذا لم يكن أوليًا، ستكون الدالة بالفعل قد انتهت // ‫باستخدام تعليمة return الموجودة بالحلقة السابقة return true; // ‫نعم N هو عدد أولي } // ‫نهاية الدالة isPrime نَسْتَعْرِض بالشيفرة التالية دالة آخرى، والتي تَستقبِل مُعامِل (parameter) من النوع String، وتُعيد قيمة (return value) من نفس النوع String. تَعْكِس تلك الدالة ترتيب محارف السِلسِلة النصية المُمرَّرة، وتُعيد نسخة مَعْكوسة منها، فمثلًا، النسخة المَعْكوسة من السِلسِلة النصية "Hello World" هي "dlroW olleH". يُمكِن توصيف الخوارزمية (algorithm) المطلوبة لعَكْسَ سِلسِلة نصية str على النحو التالي: ابدأ بسِلسِلة نصية فارغة (empty string)، ثم أَضِف إلى نهايتها كل محرف (character) موجود بالسِلسِلة النصية str، بحيث تبدأ بالمحرف الأخير من السِلسِلة str، وتعود للوراء حتى تَصِل إلى المحرف الأول. اُنظر الشيفرة التالية: static String reverse(String str) { String copy; // النسخة المعكوسة // الموضع الحالي من السلسلة النصية الممرة // يأخذ القيم بداية من طول السلسلة وحتى صفر int i; copy = ""; // ابدأ بسلسلة نصية فارغة for ( i = str.length() - 1; i >= 0; i-- ) { // أضف المحرف الحالي إلي نهاية النسخة المعكوسة copy = copy + str.charAt(i); } return copy; } لاحِظ أن بعض السَلاسِل النصية لا تتغَيَّر سواء قُرأت طردًا (forwards) أو عكسًا (backwards)، أي سواء قرأت من اليسار إلى اليمين أو العَكْس. يُعرَف ذلك باسم السِّياق المُتَناظِر أو القلب المستو (palindrome)، والذي يُمكِنك اختبار حُدُوثه على سِلسِلة نصية معينة word بالاستعانة بالدالة المُعرَّفة بالأعلى وباِستخدَام الاختبار if (word.equals(reverse(word)))‎ تحديدًا. يقع كثير من المبتدئين، أثناء كتابتهم للدوال، بخطأ طباعة الإجابة بدلًا من إعادتها، وهو ما يُمثِل سوء فهم تام لماهية الدوال. إن مُهِمّة الدالة (function) ببساطة هو مجرد حِسَاب قيمة ما، ثم العودة مرة آخرى إلى سَطْر الشيفرة المسئول عن اِستدعاء الدالة، حيث ينبغي اِستخدَام القيمة المُعادة. قد يَطبَع ذلك السَطْر من الشيفرة تلك القيمة أو يُسنِدها إلى مُتَغيِّر أو يَستخدِمها ضِمْن تعبير (expression)، أو أي شيء آخر قد يَتطلَّبه البرنامج، المهم هنا هو أن تُدرِك أن أيًا كانت الطريقة التي سيُوظِّف بها المُستدعِي تلك القيمة المُعادة فهي بالنهاية أمر لا يَخُصّ الدالة. مسألة متتالية "3N+1" سنُحاوِل بالمثال الأخير من هذا القسم اِستخدَام الدالة nextN()‎ التي عَرَفناها بالأعلى لتَعْديل البرنامج المسئول عن حِسَاب قيم عناصر المتتالية "3N+1". أُضيفَت بعض التَعْديلات الآخرى أيضًا، مثل طباعة قيم عناصر المتتالية بأعمدة، بحيث يَحتوِي كل سَطْر على ٥ عناصر فقط، مما سيُحسِن من هيئة الخَرْج الناتج عن البرنامج. تستطيع القيام بذلك ببساطة عن طريق الاحتفاظ بعدد العناصر المَطبوعة بالسَطْر الحالي، وبدء سَطْر خَرْج جديد عند وصول قيمة ذلك العدد إلى القيمة ٥. نُسِق الخَرْج بحيث تُصطَفَّ العناصر بأعمدة مُرتَّبة: input textio.TextIO; /** * برنامج يحسب قيم عناصر المتتالية ‫3N+1 ويطبعها. تُحدد قيمة * أول عنصر بالمتتالية من قبل المستخدم، ثم تُطبع القيم بأعمدة * ‫بحيث يحتوي كل سطر على 5 عناصر فقط. * كما تطبع عدد عناصر المتتالية بالنهاية */ public class ThreeN2 { public static void main(String[] args) { System.out.println("This program will print out 3N+1 sequences"); System.out.println("for starting values that you specify."); System.out.println(); int K; // اقرأ مدخل المستخدم do { System.out.println("Enter a starting value;"); System.out.print("To end the program, enter 0: "); K = TextIO.getlnInt(); // اقرأ قيمة أول عنصر بالمتتالية if (K > 0) print3NSequence(K); // ‫اعد تكرار الحلقة إذا كان k أكبر من الصفر } while (K > 0); } // ‫نهاية main /** * يطبع‫ print3NSequence قيم المتتالية 3N+1 إلى الخرج القياسي، * بحيث يكون أول عنصر بالمتتالية هو قيمة المعامل الممررة * كما يطبع أيضًا عدد عناصر المتتالية * ‫قيمة المعامل startingValue الممررة ينبغي أن تكون عددًا صحيحا موجبا */ static void print3NSequence(int startingValue) { int N; // أحد عناصر المتتالية int count; // عدد العناصر حتى الآن int onLine; // عدد العناصر المطبوعة بالسطر الحالي N = startingValue; // ابدأ المتتالية بقيمة المعامل الممررة count = 1; // لدينا عنصر واحد بالمتتالية System.out.println("The 3N+1 sequence starting from " + N); System.out.println(); System.out.printf("%8d", N); onLine = 1; // هناك عدد مطبوع بالسطر الآن while (N > 1) { N = nextN(N); // احسب قيمة العنصر التالي count++; // أزد عناصر المتتالية بمقدار الواحد if (onLine == 5) { System.out.println(); // اطبع محرف العودة الى بداية السطر onLine = 0; // أعد ضبط عدد العناصر المطبوعة بالسطر } System.out.printf("%8d", N); // اطبع قيمة العنصر الحالي onLine++; // أزد قيمة عدد العنصر بالسطر بمقدار الواحد } System.out.println(); // انهي السطر الحالي من الخرج System.out.println(); // اطبع سطر فارغ System.out.println("There were " + count + " terms in the sequence."); } // نهاية print3NSequence /** * ‫تحسب الدالة nextN قيمة عنصر المتتالية التالي وتعيده * ‫بحيث تستقبل قيمة عنصر المتتالية الحالي كمعامل currentN */ static int nextN(int currentN) { if (currentN % 2 == 1) return 3 * currentN + 1; else return currentN / 2; } // ‫نهاية الدالة nextN } // ‫نهاية الصنف ThreeN2 ينبغي أن تَقْرأ هذا البرنامج بتأنِّي وأن تُحاوِل اِستيعاب طريقة عَمَله. ترجمة -بتصرّف- للقسم Section 4: Return Values من فصل Chapter 4: Programming in the Large I: Subroutines من كتاب Introduction to Programming Using Java.
  14. إذا كان البرنامج الفرعي (subroutine) عبارة عن صندوق أسود، فإن المُعامِلات (parameter) هي ببساطة طريقة لتمرير بعض المعلومات إليه من العالم الخارجي، لذا فإنها تُعدّ جزءًا من واجهة (interface) البرنامج الفرعي، وتَسمَح لك بتَخْصيص طريقة عمل البرنامج الفرعي للتَكيُف مع موقف محدد. على سبيل المثال، إذا نظرنا لمنظم الحرارة كصندوق أسود، تتلخَّص مُهِمّته في الحفاظ على درجة حرارة معينة، فإن ذلك المنظم لديه مُعامِل، هو القرص، والذي يُستخدَم لتَخْصيص درجة الحرارة المطلوبة. سيُنفِّذ منظم الحرارة دائمًا نفس المُهِمّة، أيّ الحفاظ على درجة حرارة ثابتة، لكن قيمة درجة الحرارة تلك يُمكِن تَخْصيصها من خلال القرص. استخدام المعاملات سنتَطَرَّق مجددًا لمسألة متتالية الأعداد "3N+1"، والتي قد تَعرَّضنا لها بالقسم الفرعي ٣.٢.٢. تُحسَّب قيم مُتتالية الأعداد "3N+1" وفقًا للقاعدة التالية: "إذا كان N عَدَدًا فرديًا، اِحسب حاصل ضربه في العَدَد ٣، ثُمَّ أَزِد قيمة حاصل الضرب بمقدار ١، أمَا إذا كان زوجيًا، اِحسب حاصل قِسمته على العَدَد ٢، بحيث تستمر بحِسَاب قيم عناصر مُتتالية الأعداد بنفس الطريقة حتى تُصبِح قيمة N مُساوِية للعَدَد ١. على سبيل المثال، إذا كانت قيمة N المبدئية تُساوِي ٣، سنَحصُل على مُتتالية الأعداد التالية: ٣، ١٠، ٥، ١٦، ٨، ٤، ٢، ١." اُكْتُب برنامجًا فرعيًا (subroutine) يَطبَع قيم مُتتالية الأعداد تلك. لاحِظ أن دور البرنامج هو طباعة متتالية الأعداد "3N+1" في العموم، أي أن القيم ذاتها التي سيَطبَعها لابُدّ وأن تعتمد على قيمة N المبدئية، ولهذا سنُمرِّر تلك القيمة كمُعامِل (parameter) للبرنامج الفرعي. يُمكِن كتابة البرنامج الفرعي كالتالي: static void print3NSequence(int startingValue) { int N; // أحد قيم عناصر المتتالية int count; // عدد عناصر المتتالية N = startingValue; // أول عنصر هو القيمة الممررة count = 1; // لدينا قيمة واحدة هي القيمة الممررة System.out.println("The 3N+1 sequence starting from " + N); System.out.println(); System.out.println(N); while (N > 1) { // ‫إذا كان N عدد فردي if (N % 2 == 1) N = 3 * N + 1; else N = N / 2; count++; // أزد عدد عناصر المتتالية بمقدار الواحد System.out.println(N); // اطبع العنصر } System.out.println(); System.out.println("There were " + count + " terms in the sequence."); } // ‫نهاية print3NSequence تَتكوَّن قائمة المعاملات (parameter list) -بتعريف البرنامج الفرعي بالأعلى- من المُعامِل int startingValue، مما يَعنِي أن ذلك البرنامج الفرعي سيَستقبِل مُعامِلًا (parameter) وحيدًا من النوع العَدَدَي الصحيح int، وسيَتَمكَّنْ من اِستخدَام اسم ذلك المُعامِل بداخل المَتْن، بنفس الطريقة التي يَستخدِم بها اسم أيّ مُتَغيِّر آخر. لاحِظ أننا لم نُسنِد أي قيمة للمُعامِل أثناء عملية التعريف ذاتها، فهو يَحصُل على قيمته المبدئية من مصدر خارجي أثناء عملية الاستدعاء، لذا لابُدّ للمُستدعِي أن يُمرِّر قيمة معينة لذلك المُعامِل ضِمْن تَعْليمَة الاستدعاء (subroutine call statement)، وعندها فقط ستُسنَد إلى startingValue قبل تَّنْفيذ المَتْن. مثلًا، عندما يُنفِّذ الحاسوب تَعْليمَة استدعاء البرنامج الفرعي print3NSequence(17);‎، فإنه يُسنِد أولًا القيمة ١٧ إلى startingValue، ثم يُنفِّذ بَعْدها التَعْليمَات الموجودة بمَتْن ذلك البرنامج، والتي تَطبَع قيم مُتتالية الأعداد "3N+1" بداية من العدد ١٧. وبصورة أعم، إذا كان K مُتَغيِّرًا من النوع العَدََدَي الصحيح int، يُمكِن اِستخدَام التَعْليمَة print3NSequence(K);‎ بهدف اِستدعاء البرنامج الفرعي، مما سيؤدي إلى إِسْناد قيمة المُتَغيِّر K إلى startingValue، ومِنْ ثَمَّ تَّنْفيذ مَتْن البرنامج الفرعي. يُمكِن اِستدعاء البرنامج الفرعي print3NSequence بواسطة أيّ برنامج فرعي -البرنامج main()‎ أو غيره- مُعرَّف ضِمْن نفس الصَنْف المُتَضمِّن لتعريف البرنامج الفرعي print3NSequence. فمثلًا، يَطبَع البرنامج main()‎ -بالمثال التالي- قيم عناصر المُتتالية "3N+1" أكثر من مرة وبقيم مبدئية مختلفة، والتي تُخصَّص من قِبَل المُستخدِم: public static void main(String[] args) { System.out.println("This program will print out 3N+1 sequences"); System.out.println("for starting values that you specify."); System.out.println(); int K; // اقرأ مدخل المستخدم do { System.out.println("Enter a starting value."); System.out.print("To end the program, enter 0: "); K = TextIO.getInt(); // اقرأ عنصر المتتالية الأول من المستخدم if (K > 0) // اطبع المتتالية print3NSequence(K); } while (K > 0); // ‫إستمر إذا كانت قيمة k أكبر من الصفر } // نهاية main تنبيه: لابُدّ أن يُعرَّف كُلًا من البرنامجين main و print3NSequence ضِمْن نفس الصَنْف؛ كي تتمكَّن من تَشْغِيل البرنامج بالأعلى. المعاملات الصورية (formal) والفعلية (actual) في الواقع، تُستخدَم كلمة "مُعامِل (parameter)" للإشارة إلى مفهومين، مُرتبطين نوعًا ما، لكنهما بالنهاية مختلفان. أولهما هو ذلك الذي نُشير إليه ضِمْن تعريفات البرامج الفرعية (definitions of subroutines)، مثل startingValue بالأعلى. والآخر هو ما نُشير إليه ضِمْن تَعْليمَات استدعاء البرامج الفرعية (subroutine call statements)، بحيث يُمرَّر إليها، مثل K بالتَعْليمَة print3NSequence(K);‎. يُطلَق على "المُعامِلات" من النوع الأول اسم المُعامِلات الصُّوريّة (formal parameters) أو المُعامِلات الوهمية (dummy parameters)، بينما يُطلَق على "المُعامِلات" من النوع الثاني اسم المُعامِلات الفعليّة (actual parameters) أو الوُسَطاء (arguments). عندما تَستدعِي برنامجًا فرعيًا، يُحصِّل الحاسوب قيم المُعامِلات الفعليّة بتَعْليمَة الاستدعاء، ثم يُسنِد تلك القيم إلى المُعامِلات الصُّوريّة المُصرَّح عنها بتعريف ذلك البرنامج الفرعي، وأخيرًا، يُنفِّذ مَتْن البرنامج الفرعي. المُعامِل الصُّوريّ (formal parameter) عبارة عن "اسم"، أو بتعبير آخر، مُعرِّف بسيط (simple identifier)، فهو في الواقع أشبه ما يَكُون بالمُتَغيِّر، وكأي مُتَغيِّر، لابُدّ أن يكون له نوع مثل int أو boolean أو String أو double[]‎. في المقابل، المُعامِل الفعلي (actual parameter) هو مجرد "قيمة" يُفْترَض اِسْنادها لمُتَغيِّر المُعامِل الصُّوريّ المُناظِر عند الاستدعاء الفعليّ للبرنامج الفرعي، لذا يُمكِن اِستخدَام أي تعبير (expression) طالما كان يَؤول إلى قيمة هي من نوع يمكن إِسْناده -بواسطة تَعْليمَة إِسْناد (assignment)- إلى نوع المُعامِل الصُّوريّ المُناظِر. فمثلًا، إذا كان لدينا مُعامِل صُّوريّ من النوع double، ونظرًا لإمكانية إِسْناد قيمة من النوع int إلى المُتَغيِّرات من النوع double، فإن قيمة المُعامِل الفعليّ المُمرَّرة لذلك المُعامِل الصُّوريّ يُمكِن أن تَكُون من النوع int. لاحِظ أنه من الضروري تَمرير مُعامِل فعليّ لكل مُعامِل صُّوريّ أثناء الاستدعاء الفعليّ للبرنامج الفرعي. اُنظر البرنامج الفرعي التالي كمثال: static void doTask(int N, double x, boolean test) { // تعليمات تنفيذ المهمة } تستطيع اِستدعاء البرنامج الفرعي بالأعلى باِستخدَام تَعْليمَة الاِستدعاء التالية: doTask(17, Math.sqrt(z+1), z >= 10); يُمكِن تَوصِيف ما يُنفِّذه الحاسوب -أثناء تَّنْفيذه لتَعْليمَة الاستدعاء بالأعلى- إلى تَعْليمَة الكُتلة (block statement) التالية: { int N; // خصص مساحة بالذاكرة للمعاملات الصورية double x; boolean test; // اسند القيمة 17 للمعامل الصوري الأول N = 17; // احسب قيمة التعبير واسندها للمعامل الصوري الثاني x = Math.sqrt(z+1); // احسب قيمة التعبير المنطقي واسندها للمعامل الصوري الثالث test = (z >= 10); // تعليمات تنفيذ المهمة } إلى جانب الاختلاف الجَلِيّ بحجم الشيفرة التي تَتَطلَّبها كِلا الطريقتين، هناك أيضًا اختلافات آخرى مُتعلقة بنِطاق (scope) المُتَغيِّرات، وكيفية التَعامُل في حالة وجود عدة مُتَغيِّرات أو مُعامِلات بنفس الاسم. قد تَكُون فكرة اِستخدَام مُعامِل (parameter) لتمرير بعض المعلومات إلى برنامج فرعي (subroutine) معين بديهية نوعًا ما، ولذلك فإن اِستدعاء التوابع الفرعية المُعرَّفة مُسْبَّقًا ليس بمشكلة، وهو ما يَختلِف تمامًا عن كتابة تعريف البرنامج الفرعي ذاته (subroutine definition)، والذي عادة ما يُرْبِك دَارسِي البرمجة المبتدئين، بالأخص ما يتعلق منها بكيفية عَمَل المُعامِلات. في الواقع، يَقع الكثيرون منهم في خطأ إِسْناد قيم إلى المُعامِلات الصُّوريّة (formal parameters) ببداية مَتْن البرنامج الفرعي، وهو ما يُمثِل سوء فهم تام لماهية مُعامِلات البرامج الفرعية؛ فعندما يُنفِّذ الحاسوب تَعْليمَة اِستدعاء برنامج فرعي معين، فإنه يُسنِد قيم المُعامِلات الفعليّة (actual parameters) المُمرَّرة بتَعْليمَة الاستدعاء إلى المُعامِلات الصُّوريّة، وذلك قَبْل البدء بتَّنْفيذ مَتْن البرنامج الفرعي، أيّ أنه حينما يبدأ الحاسوب بتَّنْفيذ ذلك المَتْن، تَكُون المُعامِلات الصُّوريّة قد هُيئت بالفعل بقيمها المبدئية المُمرَّرة، ولذلك لا معنى من بدء المَتْن بإِسْناد قيم إلى تلك المُعامِلات. تَذكَّر أن البرنامج الفرعي ليس مُستقلًا؛ فبالنهاية هو يُستدعَى بواسطة برنامج آخر (routine)، ولذا تَقع مسئولية تَوْفِير قيم مُعامِلاته على تَعْليمَة الاستدعاء، ومِنْ ثَمَّ على ذلك البرنامج المُستدعِي. التحميل الزائد (overloading) يَتَطلَّب استدعاء برنامج فرعي معين مَعرِفة بعض المعلومات عنه، والتي يُطلَق عليها اسم "بَصْمَة البرنامج الفرعي (subroutine's signature)". تَتَكوَّن تلك البَصْمَة من كُلًا من اسم البرنامج الفرعي، وعدد المُعامِلات الصُّوريّة (formal parameters) المُعرَّفة ضِمْن قائمة مُعامِلاته، بالإضافة إلى نوعها. على سبيل المثال، يُمكِنك كتابة بَصْمَة البرنامج الفرعي doTask المذكور بالأعلى كالتالي: doTask(int,double,boolean)‎. لاحِظ أن بَصْمَة البرنامج الفرعي لا تَتضمَّن أسماء المُعامِلات، وهو في الواقع أمر يَسهُل تبريره؛ فليس هناك أيّ نَفْع من مَعرِفة أسماء المُعامِلات الصُّوريّة إذا كان كل غرضك هو مجرد اِستخدَام ذلك البرنامج الفرعي، ولذلك لا تُعدّ أسماء المُعامِلات جزءًا من واجهة البرنامج الفرعي (subroutine's interface). تَسمَح لغة الجافا باِستخدَام نفس الاسم لأكثر من برنامج فرعي داخل نفس الصَنْف، بشَّرْط اختلاف البَصْمَة (signatures) الخاصة بهم، فيما يُعرَف باسم "التحميل الزائد (overloading)" لاسم البرنامج الفرعي؛ حيث أصبح ذلك الاسم يَمتلك عدة معاني مختلفة. لا يَخلِط الحاسوب بين تلك البرامج الفرعية التي تَحمِل نفس الاسم، ويَستطيع تَّمييز أي منها تَرغَب باستدعائه، وذلك بالاعتماد على عدد المُعامِلات الفعليّة المُمرَّرة ضِمْن تَعْليمَة الاستدعاء، وأنواع تلك المُعامِلات. في الواقع، يَستخدِم الكائن (object)‏ System.out التحميل الزائد (overloading)؛ حيث تَتَوفَّر له العديد من التوابع (methods) المختلفة، والتي تَحمِل جميعها الاسم println مع اختلاف بَصْمَاتها (signatures) بالتأكيد. اُنظر على سبيل المثال: println(int) println(double) println(char) println(boolean) println() يَعتمِد الحاسوب على نوع المُعامِل الفعليّ (actual parameter) المُمرَّر بتَعْليمَة الاستدعاء لمَعرِفة أي بَصْمَة من تلك البرامج الفرعية ترغب بتَّنْفيذها. فمثلًا، عند اِستخدَام التَعْليمَة System.out.println(17)‎، فإنه يَستدعِي البرنامج الفرعي ذو البَصْمَة println(int)‎، أما في حالة اِستخدَام التَعْليمَة System.out.println('A')‎، فإنه يَستدعِي البرنامج الفرعي ذو البَصْمَة println(char)‎. تُعدّ جميع البرامج الفرعية بالأعلى مرتبطة نوعًا ما من الناحية الدلالية، فجميعها يقوم بالطباعة، وهو ما يُبرِّر تسميتها جميعًا بنفس الاسم، ولكن فيما يتعلق بالحاسوب، فإن طباعة عدد صحيح من النوع int تختلف تمامًا عن طباعة محرف من النوع char، والتي بدورها تختلف عن طباعة قيمة منطقية من النوع boolean، وهو ما يُبرِّر تعريف برنامج فرعي منفصل لكُلًا من تلك العمليات المختلفة. لا يُعدّ نوع القيمة المُعادة من البرنامج الفرعي (return type) جزءًا من البَصْمَة، ولهذا لا يُسمَح بتعريف برنامجين فرعيين بصَنْف معين إذا كان لهما نفس الاسم والبَصْمَة (signature)، حتى مع اختلاف نوع القيمة المُعادة منهما. فمثلًا، سيُؤدي تعريف البرنامجين الفرعيين التاليين بنفس الصَنْف إلى حُدوث خطأ في بناء الجملة (syntax error): int getln() { ... } double getln() { ... } وفي الواقع، هذا هو السبب وراء عدم تسمية جميع البرامج الفرعية المُعرَّفة ضِمْن الصَنْف TextIO، والمُستخدَمة لقراءة الأنواع المختلفة، بنفس الاسم getln()‎، وإنما يُستخدَم اسم مختلف لكل نوع مثل getlnInt()‎ و getlnDouble()‎؛ لأن الصَنْف TextIO لا يُمكِن أن يَتَضمَّن أكثر من برنامج فرعي دون مُعامِلات، ويَحمِل نفس الاسم getln. أمثلة لبرامج فرعية تَتكوَّن البرمجة باستخدام البرامج الفرعية من شقّين، الأول هو تصميم البرنامج (program design)، أيّ تَقسِّيم مُهِمّة البرنامج (program) إلى مَهَامّ فرعية (subtasks) صغيرة بحيث يُسنَد كل منها إلى برنامج فرعي (subroutine)، أما الشقّ الآخر، فيَتعلَّق بعملية كتابة تعريف تلك البرامج الفرعية الصغيرة، والتي هي مسئولة عن تَّنْفيذ المَهَامّ الفرعية. سنقوم الآن بكتابة عدة أمثلة لبعض من تلك البرامج الفرعية الصغيرة، بينما سنُعود إلى مناقشة موضوع تصميم البرامج (program design) بالقسم ٤.٧. أول مثال هو كتابة برنامج فرعي يَستقبِل عددًا صحيحًا موجبًا كمُعامِل، ثم يَحسِب جميع قواسم (divisors) هذا العدد، ويَطبَعها. يُكتَب أيّ برنامج فرعي بالصيغة (syntax) التالية: <modifiers> <return-type> <subroutine-name> ( <parameter-list> ) { <statements> } تَتلخَّص كتابة أي برنامج فرعي (subroutine) في مَلْئ هذه الصيغة، لذا دَعْنَا نقوم بذلك لمسألة حِسَاب القواسم. أولًا، تَنُصّ المسألة على اِستقبال البرنامج الفرعي لمُعامِل (parameter) وحيد من النوع int، كما تُبيِّن المُهِمّة المطلوب تَّنْفيذها بواسطة التَعْليمَات المَكًتوبة بمَتْن ذلك البرنامج. ثانيًا، لمّا كان حديثنا في الوقت الراهن مقصورًا على البرامج الفرعية الساكنة (static subroutines)، فإننا سنَستخدِم المُبدِّل static بالتعريف. ثالثًا، قد نُضيف مُبدِّل وصول (access modifier) بالتعريف، مثل public أو private، ولكن نظرًا لعدم النَصّ بذلك صراحة ضِمْن نَّصّ المسألة، فلن نَستخدِم أيًا منهما. رابعًا، لم تَنُصّ المسألة على وجود أي قيمة مُعادة، ولذلك سيَكُون النوع المُعاد (return type) هو void. أخيرًا، لمّا لم تُحدِّد المسألة كُلًا من اسم البرنامج الفرعي (subroutine)، وأسماء المُعامِلات الصُّوريّة (formal parameter) صراحةً، فإننا سنَضَطرّ لاختيار تلك الأسماء بأنفسنا، ولذلك سيُستخدَم N كاسم للمُعامِل، و printDivisors كاسم للبرنامج الفرعي. اُنظر تعريف البرنامج الفرعي: static void printDivisors( int N ) { <statements> } ينبغي لنا الآن كتابة مجموعة التَعْليمَات التي ستُكوِّن مَتْن البرنامج (routine body)، وهو ليس أمرًا صعبًا. تَذكَّر فقط أن المُعامِل N سيَكُون لديه قيمة بالفعل ببداية تَّنْفيذ المَتْن. يُمكِن كتابة تَوصِيف الخوارزمية كالتالي: "لكل عدد يُحتمَل أن يَكُون قَاسِمًا D، أيّ بدايةً من الواحد ووصولًا للعَدَد N، إذا تَمكَّن العدد D من تَقسِّيم العَدَد N تَقسِّيمًا مُتعادلًا، اِطبَع قيمة D." تُصبح الخوارزمية كالتالي بعد ترجمتها إلى لغة الجافا: /** * اطبع قواسم‫ N * ‫بفرض أن N هو عدد صحيح موجب */ static void printDivisors( int N ) { int D; System.out.println("The divisors of " + N + " are:"); for ( D = 1; D <= N; D++ ) { if ( N % D == 0 ) // ‫إذا نجح D في تقسيم N تقسيما متعادلا System.out.println(D); } } يُمثِل التعليق (comment)، المُضاف قَبْل تعريف البرنامج الفرعي (subroutine definition)، ما يُعرَف باسم المواصفة الاصطلاحية للبرنامج (subroutine contract)، والمُكوَّنة من: بَيان هدف البرنامج الفرعي، بالإضافة إلى التَّصْريح عن أيّ اِفتراضات ينبغي مَعرفِتها قَبْل اِستخدَام ذلك البرنامج. مثلًا، في المثال بالأعلى، ذُكِر أن N ينبغي أن يَكُون عددًا صحيحًا موجبًا، ولذلك ينبغي لمُستدعِي البرنامج الفرعي التَأكُّد من تَوْفِية ذلك الفَرْض. لنَفْحَص مثالًا آخر، تَنُصّ المسألة على التالي: "اُكتب برنامجًا فرعيًا خاصًا private اسمه printRow، بحيث يَستقبِل ذلك البرنامج كُلًا من المُعامِلين: ch من النوع char، و N من النوع int. يَتلخَّص دور ذلك البرنامج بطباعة المحرف ch عدد N من المرات وذلك بسَطْر نصي مُنفصِل." بخلاف المسألة السابقة، نَصَّت هذه المسألة صراحةً على كُلًا من اسم البرنامج الفرعي وأسماء المُعامِلات، كما نَصَّت على ضرورة كَوْنه خاصًا، أيّ ينبغي اِستخدَام مُبدِّل الوصول private، ولذلك نحن مقيدين نوعًا ما فيما يتعلق بالسطر الأول من تعريف البرنامج الفرعي (subroutine definition). أخيرًا، مُهِمّة البرنامج بسيطة جدًا، ولذا يَسهُل كتابة مَتْن البرنامج الفرعي. اُنظر شيفرة البرنامج بالكامل: /** * ‫ اطبع المحرف `ch` عدد `N` من المرات وذلك بسَطْر نصي مُنفصِل * ‫إذا كان N أقل من أو يساوي الصفر، اطبع سطرًا فارغًا */ private static void printRow( char ch, int N ) { int i; for ( i = 1; i <= N; i++ ) { System.out.print( ch ); } System.out.println(); } لَمْ تُصَرِّح المواصفة الاصطلاحية (contract) للبرنامج بالأعلى عن وجود أية افتراضات فيما يَخُص المُعامِل N، ولكنها في المقابل أوْضَحَت طريقة استجابة البرنامج الفرعي لجميع الحالات المُمكِنة، بما فيها الحالة غَيْر المُتوقَّعة من كَوْن N <= 0. لنَفْحَص مثالًا آخر، ولكن في هذه المرة، سنُوضِح كيف يُمكِن لبرنامجين فرعيين أن يتفاعلا. تحديدًا، سنَكتُب برنامجًا فرعيًا يَستقبِل مُعامِلًا من النوع String، بحيث يَطبَع كل محرف بالسِلسِلة النصية (string) المُمرَّرة ٢٥ مرة وبسَطْر مُنفصِل. ينبغي أن تَستعين بالبرنامج الفرعي printRow()‎ الذي كتبناه للتو لطباعة ذلك الخَرْج. في هذا المثال، لمّا لم تُحدِّد المسألة كُلًا من اسم البرنامج الفرعي (subroutine)، وأسماء المُعامِلات الصُّوريّة (formal parameter) صراحةً، فإننا سنضطر لاختيار تلك الأسماء، ولذلك سيُستخدَم str كاسم للمُعامِل، وprintRowsFromString كاسم للبرنامج الفرعي. يُمكِن كتابة تَوصِيف الخوارزمية كالتالي: "لكل مَوْضِع i بالسِلسِلة النصية str، اِستدعِي البرنامج الفرعي printRow(str.charAt(i),25)‎؛ لطباعة سَطْر الخَرْج." تُصبِح الخوارزمية كالتالي بَعْد ترجمتها إلى لغة الجافا: /** * لكل محرف بالسلسلة النصية، اطبع سطر مكون من 25 نسخة من ذلك المحرف */ private static void printRowsFromString( String str ) { int i; for ( i = 0; i < str.length(); i++ ) { printRow( str.charAt(i), 25 ); } } نستطيع الآن استدعاء البرنامج الفرعي printRowsFromString المُعرَّف بالأعلى داخل البرنامج main()‎ كالتالي: public static void main(String[] args) { String inputLine; // السطر النصي المدخل من قبل المستخدم System.out.print("Enter a line of text: "); inputLine = TextIO.getln(); System.out.println(); printRowsFromString( inputLine ); } لاحِظ أنه من الضروري تَضْمِين تعريف البرامج الثلاثة main()‎ و printRowsFromString()‎ و printRow()‎ بنفس الصَنْف. على الرغم من كَوْن البرنامج بالأعلى عديم الفائدة نوعًا ما، لكنه على الأقل يُبيِّن طريقة اِستخدَام البرامج الفرعية. يُمكِنك الإطلاع على البرنامج كاملًا بالملف RowsOfChars.java. المصفوفات كمعامل بالإضافة إلى إمكانية تمرير المُعامِلات من الأنواع البسيطة (primitive types) إلى البرامج الفرعية، يُسمَح أيضًا بتمرير المُعامِلات من أنواع المصفوفة (array types)، ما يَعنِي تمرير مصفوفة كاملة من القيم (المُتَغيِّرات إذا شِئنا الدقة) من خلال مُعامِل وحيد. لنَكتُب، على سبيل المثال، برنامجًا فرعيًا يَستقبِل مُعامِلًا من نوع المصفوفة int[]‎؛ ثم يَطبَع جميع الأعداد الصحيحة الموجودة بها، بحيث يُفصَل بين كل عدد والذي يَليه فاصلة (comma)، وبحيث تُحاط جميع تلك الأعداد بزوج من الأقواس []، اُنظر تعريف البرنامج الفرعي: static void printValuesInList( int[] list ) { System.out.print('['); int i; for ( i = 0; i < list.length; i++ ) { if ( i > 0 ) System.out.print(','); System.out.print(list[i]); } System.out.println(']'); } لكي نَستدعِي البرنامج الفرعي بالأعلى، سنحتاج إلى مصفوفة فعليّة (actual). تُنشِئ الشيفرة التالية مصفوفة أعداد صحيحة (array of ints)، وتُمرِّرها كوسيط (argument) للبرنامج الفرعي: int[] numbers; numbers = new int[3]; numbers[0] = 42; numbers[1] = 17; numbers[2] = 256; printValuesInList( numbers ); سنَحصُل على الخَرْج [42,17,256]. وسطاء سطر الأوامر (command-line arguments) لمّا كان البرنامج (routine)‏ main يَستقبِل مُعامِل مصفوفة من النوع String[]‎، كان لزامًا على مُستدعِيه -أيّ النظام (system) في هذه الحالة- أن يُمرِّر مصفوفة سَلاسِل نصية (array of String) فعليّة (actual) كقيمة لذلك المُعامِل الصُّوريّ (formal parameter). لذا لابُدّ أن يَحصُل النظام على قيم السَلاسِل النصية بتلك المصفوفة بطريقة ما، فما الذي يعنيه ذلك؟ وكيف سيَتحصَّل النظام على تلك القيم؟ في الواقع، تَتَكوّن تلك القيم من وُسَطاء سطر الأوامر (command-line arguments)[*] المُمرَّرين إلى الأمر المُستخدَم لتَشْغِيل البرنامج، وبالتالي يُسنِد النظام قيم هؤلاء الوُسَطاء إلى مصفوفة سَلاسِل نصية (array of strings)، ثم يُمرِّرها إلى البرنامج main()‎. [*] يستطيع المُستخدِم عمومًا تَشْغِيل أحد البرامج من خلال كتابة أمر (command) معين بواجهة سَطْر الأوامر (command-line interface). يَتَكوّن الأمر عمومًا من اسم البرنامج المطلوب تَشْغِيله، ولكن يُمكِن أيضًا كتابة مُدْخَلات إضافية ضِمْن الأمر. تُسمَى تلك المُدْخَلات الإضافية باسم وُسَطاء سَطْر الأوامر (command-line arguments). فمثلًا، إذا كان اسم برنامج معين هو myProg، تستطيع تَشْغِيله باِستخدَام الأمر التالي: java myProg ولكن، في هذه الحالة، لا يوجد أي وُسَطاء (arguments)، في المقابل، تستطيع تمرير وسيط واحد أو أكثر باِستخدَام الأمر التالي: java myProg one two three في المثال بالأعلى، تُمثِل السَلاسِل النصية "one"، و "two"، و "three" قيم الوُسَطاء، ولذلك سيُسنِد النظام هذه السَلاسِل النصية إلى مصفوفة من النوع String[]‎، ثم يُمرِّرها كمُعامِل إلى البرنامج main()‎. على سبيل المثال، تَطبَع الشيفرة التالية قيمة أي وسيط (argument) أَدْخَله المُستخدِم: public class CLDemo { public static void main(String[] args) { System.out.println("You entered " + args.length + " command-line arguments"); if (args.length > 0) { System.out.println("They were:"); int i; for ( i = 0; i < args.length; i++ ) System.out.println(" " + args[i]); } } // ‫نهاية main } // ‫نهاية الصنف CLDemo إذا لم يُخصِّص المُستخدِم أي وسيط (arguments) بأمر تَشْغِيل البرنامج، فإن المُعامِل args سيَكُون عبارة عن مصفوفة فارغة، طولها (length) يُساوي الصفر. عمليًا، يُستخدَم وُسَطاء سَطْر الأوامر عادةً بهدف تمرير أسماء بعض الملفات إلى البرنامج. فمثلًا، يَنسَخ البرنامج التالي محتويات ملف نصي معين إلى ملف نصي آخر، ولذلك فإنه يَستخدِم الصَنْف TextIO ضِمْن حَلْقة تَكْرار، بحيث يَقْرأ سَطْرًا واحدًا من الملف الأصلي في كل مرة، ثم يَنسَخُه إلى الملف الآخر. يَستمِر في القيام بذلك حتى يَصِل إلى نهاية الملف، والتي يُستدَلّ عليها من القيمة المنطقية المُعادة من الدالة (function)‏ TextIO.eof()‎. input textio.TextIO; /** * يتطلب ذلك الأمر وسيطين، هما أسماء الملفات * الأول ينبغي أن يكون اسم ملف موجود * والثاني ينبغي أن يكون اسم الملف الذي سينسخ البرنامج إليه بيانات الملف الأول * سيقوم البرنامج بنسخ محتويات الملف الأول إلى الملف الثاني * تحذير: إذا كان الملف الثاني موجود عند تشغيل البرنامج، ستتم الكتابة على البيانات السابقة * يعمل هذا البرنامج مع الملفات النصية فقط */ public class CopyTextFile { public static void main( String[] args ) { if (args.length < 2 ) { System.out.println("Two command-line arguments are required!"); System.exit(1); } TextIO.readFile( args[0] ); // افتح الملف الأصلي بهدف القراءة TextIO.writeFile( args[1] ); // افتح الملف المراد النسخ إليه int lineCount; // عدد الأسطر المنسوخة lineCount = 0; while ( TextIO.eof() == false ) { // اقرأ سطر من الملف الأصلي وانسخه للملف الآخر String line; line = TextIO.getln(); TextIO.putln(line); lineCount++; } System.out.printf( "%d lines copied from %s to %s%n", lineCount, args[0], args[1] ); } } تُنفَّذ معظم البرامج حاليًا من خلال واجهة مُستخدِم رسومية (GUI environment)، ولذلك لم يَعُدْ وُسَطاء سَطْر الأمر (command-line arguments) بنفس ذات الأهمية في الوقت الحالي. مع ذلك ما يزال البرنامج بالأعلى مثالًا جيدًا لبيان كيفية اِستخدَام مُعامِلات المصفوفة (array parameters) على الأقل. التبليغ عن الاعتراضات (exceptions) ذَكَرَنا، منذ قليل، مصطلح المواصفة الاصطلاحية للبرنامج (subroutine contract)، والذي يُوضِح وظيفة البرنامج الفرعي في حالة تمرير قيم صالحة -وفقًا لما هو مُوضَّح بنفس المواصفة الاصطلاحية- لجميع المُعامِلات الخاصة به. تَتَبقَّى الحاجة إلى مَعرِفة كيفية تَصرُّف البرنامج الفرعي في حالة تمرير قيم غَيْر صالحة للمُعامِلات. تَعرَّضنا بالفعل لعدة أمثلة بالقسم ٣.٧، والتي تُبلِّغ فيها البرامج الفرعية عن اِعتراضات (throwing exceptions) في حالة تمرير قيم غَيْر صالحة، فمثلًا، تَنُصّ المواصفة الاصطلاحية للبرنامج الفرعي المَبنِى مُسْبَّقًا (built-in)‏ Double.parseDouble على ضرورة أن يَكُون المُعامِل المُمرَّر إليها عبارة عن تمثيل نصي (string representation) لعَدََدَ من النوع double. إذا كانت قيمة المُعامِل المُمرَّرة مُتماشية مع ذلك الشَّرْط، سيُحوِّل البرنامج الفرعي السِلسِلة النصية (string) المُمرَّرة إلى قيمتها العَدَدية المكافئة، أما إذا لم تَكُن مُتماشية معها، سيُبلِّغ البرنامج الفرعي عن اِعتراض (exception) من النوع NumberFormatException. تُبلِّغ الكثير من البرامج الفرعية عن اعتراض من النوع IllegalArgumentExceptions في حالة تمرير قيم غَيْر صالحة لمُعامِلاتها، وهو ما قد تَرغب في اتباعه بالبرامج الفرعية الخاصة بك. أيّ اعتراض (exception) هو بالنهاية عبارة عن كائن (object)، ستَضطرّ إلى إنشائه قَبْل التَبْليغ عنه باِستخدَام التَعْليمَة throw. سنتناول كيفية القيام بذلك تفصيليًا بالفصل الخامس، ولكن، إلى ذلك الحين، تستطيع اِستخدَام الصيغة (syntax) التالية لتَعْليمَة throw؛ للتَبْليغ عن اِعتراض من النوع IllegalArgumentException تحديدًا: throw new IllegalArgumentException( <error-message> ); تَتَكوّن رسالة الخطأ بالأعلى من سِلسِلة نصية (string) تَشرَح الخطأ المُكْتشَف بينما تُستخدَم new لإنشاء كائن الاعتراض (exception object). كل ما عليك القيام به هو فَحْص قيم المُعامِلات المُمرَّرة؛ لمَعرِفة ما إذا كانت صالحة أم لا، ثم التَبْليغ عن اعتراض باِستخدَام التَعْليمَة بالأعلى إذا لم تَكُن صالحة. على سبيل المثال، لابُدّ أن تَكُون قيمة المُعامِل المُمرَّرة إلى البرنامج الفرعي print3NSequence -المُعرَّف ببداية هذا القسم- عددًا صحيحًا موجبًا، لذلك يُمكِننا، في حالة انتهاك هذا الشَّرْط، تَعْدِيل تعريف البرنامج الفرعي (subroutine definition) بالصورة التالية؛ وذلك لكي نَتَمَكَّن من التَبْليغ عن الاِعتراض: static void print3NSequence(int startingValue) { if (startingValue <= 0) // إذا حدث انتهاك للمواصفة الاصطلاحية throw new IllegalArgumentException( "Starting value must be positive." ); // بقية البرنامج الفرعي مثلما بالأعلى إذا كانت القيمة المُمرَّرة إلى المُعامِل startingValue غَيْر صالحة، فستُنهِي تَعْليمَة throw بالأعلى البرنامج الفرعي فورًا بدون تَّنْفيذ بقية المَتْن، كما قد ينهار (crash) البرنامج بالكامل في حالة عدم اِلتقاط ذلك الاِعتراض (catching exception) بمكان آخر ضِمْن البرنامج باِستخدَام التَعْليمَة try..catch بحيث يُستدعَى البرنامج الفرعي print3NSequence داخل الجزء try، كما ناقشنا بالقسم ٣.٧. المتغيرات العامة (global) والمحلية (local) كملاحظة أخيرة بهذا القسم، يُمكِننا القول أننا نستطيع الآن اِستخدَام ثلاثة أنواع مختلفة من المُتَغيِّرات داخل أيّ برنامج فرعي، هي كالتالي: أولًا، المُتَغيِّرات المحليّة (local variables) والتي يُصرَّح عنها داخل البرنامج الفرعي. ثانيًا، أسماء المُعامِلات الصُّوريّة (formal parameter) ضِمْن تعريف البرنامج الفرعي. ثالثًا، المُتَغيِّرات الأعضاء الساكنة (static member variables)، والتي يُصرَّح عنها خارج البرنامج الفرعي. تُعدّ المُتَغيِّرات المحليّة (local variables) من صميم أعمال البرنامج الفرعي الداخلية، وليس لها أيّ اتصال مع ما هو خارج البرنامج الفرعي. تُمرِّر المُعامِلات الصُّوريّة (formal parameter) قيم خارجية إلى البرنامج الفرعي أثناء الاستدعاء، ولكنها مع ذلك تَتَصرَّف بطريقة مشابهة للمُتَغيِّرات المحليّة بمجرد بدء تَّنْفيذ البرنامج الفرعي. يَعنِي ذلك أنه في حالة حُدوث تَغْيِير بقيمة أحد المُعاملات الصُّوريّة (من النوع الأوَّليّ [primitive type] تحديدًا) أثناء تَّنْفيذ البرنامج الفرعي، فإن ذلك التَغْيِير لا يُؤثِر نهائيًا على بقية البرنامج. لاحِظ أنه في حالة كان المُعامِل الصُّوريّ عبارة عن مصفوفة أو كائن (object)، فإن الأمور تُصبِح أكثر تعقيدًا كما سنرى لاحقًا. أخيرًا، بخلاف المُتَغيِّرات المحليّة (local variables) المُعرَّفة داخل البرامج الفرعية، فإن المُتَغيِّرات العامة (global variables) تُعرَّف خارج أي برنامج فرعي، ولذا فإنها تُعدّ مستقلة عنها، كما أنها قابلة للاِستخدَام ضِمْن أجزاء عديدة من البرنامج (program). في الواقع، يُمكِن اِستخدَام المُتَغيِّر العام (global variable) بأيّ مكان داخل الصَنْف المُُعرَّف بداخله، بل يُمكِن استخدامه بأي صنف آخر طالما لم يُصرَّح عنه باِستخدَام المُبدِّل private. لاحظ أنه إذا حَدَثَ تغيير على مُتَغيِّر عام (global variable) داخل برنامج فرعي معين، فإن تأثير ذلك التغيير يمتد إلى ما هو أبعد من مجرد ذلك البرنامج الفرعي الذي أَحَدَث التغيير، وهو ما قد تعرضت له بالفعل بالمثال الأخير بالقسم السابق؛ حيث عَدَّلت إحدى البرامج الفرعية قيم المُتَغيِّرات العامة gamesPlayed و gamesWon، ثم طُبعت بواسطة برنامج فرعي آخر هو main()‎. إذا اِستخدَمت مُتَغيِّرًا عامًا (global variable) ضِمْن برنامج فرعي معين، فإن ذلك البرنامج الفرعي يُصبِح قادرًا على الاتصال مع بقية البرنامج (program) فيما يُشبه الاتصال السري (back-door communication)، ولهذا نستطيع القول أن المُتَغيِّرات العامة قد أصبحت بذلك جزءًا من واجهة البرنامج الفرعي (subroutine's interface). يُعدّ الاتصال السري عمومًا أقل وضوحًا وصراحةً من ذلك المُقام باستخدام المُعامِلات، ولهذا فإنه قد يَتَسبَّب بكسر القاعدة التي تَنُصّ على ضرورة كَوْن واجهة (interface) "الصندوق الاسود (black box)" صريحة وسهلة الفهم. قد يَدْفَعك ذلك إلى الظّنّ بأن اِستخدَام المُتَغيِّرات العامة داخل البرامج الفرعية أمر سيئ عمومًا، ولكن هذا غَيْر صحيح، فهناك على الأقل سببًا واحدًا جيدًا قد يَدْفَعك للقيام بذلك: مثلًا إذا فكرت بالصَنْف (class) بكَوْنه نوعًا من "الصندوق الاسود (black box)"، يُصبِح من المعقول تمامًا السماح للبرامج الفرعية الواقعة ضِمْن ذلك الصندوق بالاتصال سريًا مع بعضها البعض (back-door communication) خاصة إذا ساهم ذلك بتبسيط الصَنْف ككل من الخارج. لهذا يَنصَح الكاتب بعدم اتخاذ أيّ موقف عدائي مُطْلَق من اِستخدَام المُتَغيِّرات العامة (global variables) داخل البرامج الفرعية، فقط عليك أن تُفكِر مَلِيًّا قَبْل اِستخدَام مُتَغيِّر عام (global variable) داخل برنامج فرعي للتَأكُّد مما إذا كان ذلك ضروريًا. ترجمة -بتصرّف- للقسم Section 3: Parameters من فصل Chapter 4: Programming in the Large I: Subroutines من كتاب Introduction to Programming Using Java.
  15. تَسمَح بعض اللغات البرمجية -بل غالبيتها في الواقع- بتعريف البرامج الفرعية (subroutines) بصورة مستقلة خارج أي صَنْف، لكن تختلف لغة الجافا عنهم في تلك النقطة، حيث لابُدّ أن يُعرَّف أيّ برنامج فرعي (subroutine) بلغة الجافا ضِمْن صَنْف (class). الهدف من الصَنْف عمومًا هو تجميع البرامج الفرعية والمُتَغيِّرات المُرتبطة معًا ضِمْن وحدة واحدة. نظرًا لأن البرامج المكتوبة بلغة الجافا عادة ما تَستخدِم عددًا ضخمًا من البرامج الفرعية المَكْتوبة بواسطة مبرمجين مختلفين، فإن احتمالية الخلط بين أسمائها يَكُون كبيرًا، لذا حَرَص مُصمِّمي الجافا على وَضع تقييد حازم على الطرق التي يُمكِن بها تسمية الأشياء، ومن هنا كانت ضرورة تعريف البرامج الفرعية وتجميعها ضِمْن أصناف (classes) لها اسم وبالتبعية تجميع تلك الأصناف ضِمْن حزم (packages) لها اسم أيضًا كما سنرى لاحقًا. تُميز لغة الجافا بشكل واضح بين البرامج الفرعية الساكنة (static) وغَيْر الساكنة (non-static). يُمكِن لأي صَنْف أن يَتضمَّن كِلا النوعين، ولكن كيفية اِستخدَامهما مختلفة تمامًا. تُعدّ البرامج الفرعية الساكنة (static subroutine) عمومًا أسهل في الفهم، فببساطة أي برنامج فرعي ساكن هو عضو (member) ينتمي للصنف ذاته المُعرَّف بداخله. في المقابل، تعريف البرامج الفرعية غَيْر الساكنة (non-static subroutine) موجود فقط ضِمْن الصَنْف حتى تَتَمكَّن كائنات (objects) ذلك الصنف -عند إنشائها- من اِستخدَام تلك البرامج الفرعية، وتُصبِح عندها تلك البرامج أعضاء بتلك الكائنات (objects). يُمكِن تطبيق ذلك الاختلاف على المُتَغيِّرات (variables) الساكنة وغَيْر الساكنة بنفس الكيفية، بل وعلى أي شيء آخر قد يَقع ضمن تعريف الأصناف (class definition). سنتناول في هذا الفصل كُلًا من البرامج الفرعية الساكنة والمُتَغيِّرات الساكنة فقط، وسننتقل بالفصل التالي إلى البرمجة كائنية التوجه (object-oriented programming) وعندها سنستطيع مناقشة الأعضاء غَيْر الساكنة. عادة ما يُطلَق مصطلح "التابع (method)" على البرامج الفرعية (subroutines) المُعرَّفة ضِمْن صَنْف، وهو الاسم الذي يُفضِّله غالبية مبرمجي الجافا مع أن المصطلحان عمومًا مترادفان خاصة فيما يَتعلَّق بلغة الجافا. ستَجِدْ أيضًا البعض يَستخدِم مصطلحات آخرى للإشارة إلى البرامج الفرعية مثل "الإجراء (procedure)" أو "الدالة (function)". يُفضِّل الكاتب الاستمرار باِستخدَام المصطلح الأعم "البرنامج الفرعي (subroutine)" ضِمْن هذا الفصل للإشارة إلى البرامج الفرعية الساكنة على الأقل، ولكنه سيبدأ في اِستخدَام مصطلح "التابع (method)" من حين لآخر، كما أنه سيَستخدِم مصطلح "الدالة (function)" فقط للإشارة إلى البرامج الفرعية التي تَحسِب قيمة وتُعيدها مع أن بعض اللغات البرمجية الآخرى تَستخدِم ذلك المصطلح للإشارة إلى البرامج الفرعية (subroutines) بمفهومها الأعم. تعريف البرامج الفرعية ينبغي أن يُعرَّف (define) أيّ برنامج فرعي (subroutine)، تَنوِي اِستخدَامه، بمكان ما بالبرنامج، بحيث يَتضمَّن ذلك التعريف (subroutine definition) كُلًا من الآتي: اسم البرنامج الفرعي (subroutine name)، والمعلومات اللازمة لاستدعائه (subroutine call)، وأخيرًا، الشيفرة الفعليّة المطلوب تَّنْفيذها مع كل عملية استدعاء. يُكتَب تعريف البرنامج الفرعي (subroutine definition) بالجافا بالصياغة التالية: <modifiers> <return-type> <subroutine-name> ( <parameter-list> ) { <statements> } قد تكون الصيغة بالأعلى مألوفة بالنسبة لك؛ فقد تَعرَّضنا بالفعل خلال الفصول السابقة لبعض البرامج الفرعية، مثل البرنامج main()‎ المُعرَّف بأيّ برنامج، والبرنامج drawFrame()‎ ببرامج التحريكة (animation) بالقسم ٣.٩. ومع ذلك، لاِستيعاب ما تَعنيه تلك الصيغة تفصيليًا، سنحتاج إلى مُناقشتها باستفاضة، وهو ما سنفعله بغالبية هذا الفصل. أولًا، تُكوِّن التَعْليمَات -الواقعة بين القوسين المعقوصين {}- مَتْن البرنامج الفرعي (subroutine body)، وتُمثِل الجزء التَّنْفيذي (implementation) أو الداخلي "للصندوق الأسود (black box)" كما ذَكَرنا بالقسم السابق. يُنفِّذ الحاسوب تلك التَعْليمَات عند إجراء عملية اِستدعاء لذلك البرنامج (أو التابع [method]). لاحظ أن تلك التَعْليمَات قد تَتكوَّن من أي تَعْليمَة قد تَعرَّضنا لها خلال الفصلين الثاني والثالث. ثانيًا، تُكتَب المُبدِّلات ببداية التعريف، وهي عبارة عن كلمات تُستخدَم لضَبْط بعض خاصيات البرنامج الفرعي، مثل ما إذا كان ساكنًا (static) أم غير ساكن (non-static). تُوفِّر لغة الجافا عمومًا ستة مُبدِّلات (modifiers) يُمكنك اِستخدَامها، ولقد تَعرَّضنا لاثنين منها فقط حتى الآن، وهي المُبدِّل الساكن static، والمُبدِّل العام public. ثالثًا، يُستخدَم النوع المُعاد لتَخْصِيص نوع القيمة المُعادة من البرنامج الفرعي. تحديدًا، إذا كان البرنامج الفرعي بالأساس هو عبارة عن دالة (function) تَحسِب قيمة معينة، عندها قد تَستخدِم أي نوع، مثل String أو int أو حتى أنواع المصفوفة مثل double[]‎. أما إذا كان البرنامج الفرعي ليس بدالة، أيّ لا يُعيد قيمة، فعندها تُستخدَم القيمة الخاصة void، والتي تُشير إلى عدم وجود قيمة مُعادة، أيّ أنها إِما أن تَكُون فارغة (empty) أو غَيْر موجودة. سنتناول الدوال (functions)، والأنواع المُعادة (return types) تفصيليًا بالقسم ٤.٤. وأخيرًا، قائمة المُعامِلات الخاصة بالتابع (method). تُعدّ المُعامِلات جزءًا من وَاجهة البرنامج الفرعي (subroutine interface)، وتُمثِل المعلومات المُمرَّرة (passed) إليه من الخارج، والتي قد يَستخدِمها لحِسَاب بعض العمليات الداخلية الخاصة به. فمثلًا، بفَرْض وجود الصَنْف Television، والذي يَتضمَّن التابع changeChannel()‎. في تلك الحالة، يُفْترَض أن يُطرَح سؤالًا تلقائيًا عن رقم القناة التي تُريد من البرنامج الفرعي الانتقال إليها، وهنا يأتي دور المُعامِلات (parameter)؛ حيث يُمكِن اِستخدَام مُعامِل للإجابة على مثل هذا السؤال، ولمّا كان رقم القناة هو عدد صحيح، فسيَكُون نوع ذلك المُعامِل (parameter type) هو int، ولهذا يُمكِنك التَّصْريح (declaration) عن التابع changeChannel()‎ كالتالي: public void changeChannel(int channelNum) { ... } يُشير التَّصْريح بالأعلى إلى أن التابع changeChannel()‎ لديه مُعامِل يَحمِل اسم channelNum من النوع العددي الصحيح int. لاحظ أن قيمة المُعامِل channelNum غير مُتوفِّرة حتى الآن، وإنما تَتوفَّر عند عملية الاستدعاء الفعليّ للتابع الفرعي، على سبيل المثال: changeChannel(17);‎. قد تتكوَّن قائمة المُعامِلات (parameter list) -بتعريف البرنامج الفرعي- من تَّصْريح عن مُعامِل (parameter declaration) واحد أو أكثر، وذلك على الصورة ، بحيث يَتكوَّن كل تَّصْريح من مُعامِل وحيد، كما يُفصَل بين كل تَّصْريح والذي يليه باستخدام فاصلة (comma). على سبيل المثال، في حالة أردت التَّصْريح عن مُعامِلين من النوع double، فستضطر إلى كتابة double x, double y وليس double x, y. لاحِظ أن قائمة المُعامِلات قد تَكُون فارغة أيضًا. سنتناول المُعامِلات (parameters) تفصيليًا بالقسم التالي. ها هي التعريفات الخاصة ببعض البرامج الفرعية (subroutine definitions)، ولكن بدون الجزء التَّنْفيذي (implementation) منها، أي بدون التَعْليمَات الفعليّة التي تُعرِّف ما تُنفِّذه تلك البرامج الفرعية: public static void playGame() { // المبدلات هي‫ public و static // ‫النوع المعاد هو void // ‫اسم البرنامج الفرعي هو playGame // قائمة المعاملات فارغةً } int getNextN(int N) { // لا يوجد مبدلات // ‫النوع المعاد هو int // ‫اسم البرنامج الفرعي هو getNextN // ‫قائمة المعاملات تتضمن معامل اسمه N من النوع int } static boolean lessThan(double x, double y) { // ‫المبدلات هي static // ‫النوع المعاد هو boolean // ‫اسم البرنامج الفرعي هو lessThan // ‫قائمة المعاملات تتضمن معاملين x و y كلاهما من النوع double } بالمثال الثاني بالأعلى، لمّا كان تعريف التابع getNextN لا يَتضمَّن المُبدل static، فإنه يُعدّ بصورة افتراضية تابعًا غَيْر ساكن (non-static method)، ولذلك لن نَفْحصه بهذا الفصل! اُستخدِم المُبدل public بالمثال الأول، والذي يُشير إلى إِمكانية استدعاء التابع (method) من أي مكان بالبرنامج، حتى من خارج الصَنْف (class) الذي عُرِّف فيه هذا التابع. في المقابل، يَتوفَّر المُبدل private، والذي يُشير إلى إمكانية استدعاء التابع من داخل نفس الصَنْف فقط. يُطلَق على كُلًا من المُبدلين public و private مُحدِّدات الوصول أو مُبدِّلات الوصول (access specifiers/access modifier). يَتوفَّر مُبدل وصول (access modifier) أخير، هو protected، والذي ستتضح أهميته عندما ننتقل إلى البرمجة كائنية التَوجه (object-oriented programming) بالفصل الخامس. في حالة عدم تخصيص مُحدِّد وصول لتابع معين، فإنه، وبصورة افتراضية، يُصبِح قابلًا للاستدعاء من أيّ مكان بالحزمة (package) التي تَتضمَّن صَنْفه، ولكن ليس من خارج تلك الحزمة. سنُناقش الحزم (packages) خلال هذا الفصل، تحديدًا بالقسم ٤.٦. يَتَّبِع البرنامج main()‎، المُعرَّف بأي برنامج، نفس قواعد الصيغة (syntax rules) المُعتادة لأي برنامج فرعي: public static void main(String[] args) { ... } بفَحْص التعريف بالأعلى، تَجد أن المُبدلات المُستخدَمة هي public و static، أما النوع المُعاد فهو void، أما اسم البرنامج الفرعي فهو main، وأخيرًا، قائمة المعاملات هي String[] args، أيّ أن نوع المُعامِل المُمرَّر هو نوع المصفوفة String[]‎. إذا كنت قد قرأت الفصول السابقة، فلديك بالفعل الخبرة الكافية لكتابة الجزء التَّنْفيذي من البرنامج الفرعي (implementation of a subroutine). في هذا الفصل، سنتعلَّم كتابة التَعرِيف بالكامل بما في ذلك جزء الواجهة (interface). استدعاء البرامج الفرعية يُعدّ تعريف البرنامج الفرعي (subroutine definition) بمثابة إعلام للحاسوب بوجود ذلك البرنامج الفرعي وبالوظيفة التي يُؤديها، لكن يُرجئ التَّنْفيذ الفعليّ للبرنامج الفرعي إلى حين استدعائه (call). يَنطبِق ذلك حتى على البرنامج (routine)‏ main()‎، والذي يَستدعيه النظام (system) -لا أنت- عند تَّنْفيذه للبرنامج (program) ككل. تُستخدَم، مثلًا، تَعْليمَة استدعاء البرنامج الفرعي (subroutine call) التالية؛ لاستدعاء التابع playGame()‎ المذكور بالأعلى: playGame(); يُمكن عمومًا كتابة تَعْليمَة الاستدعاء بالأعلى بأيّ مكان داخل نفس الصَنْف (class) الذي عَرَّف التابع playGame()‎، سواء كان ذلك بالتابع‏ main()‎، أو بأيّ برنامج فرعي آخر. علاوة على ذلك، لمّا كان التابع playGame()‎ مُعرَّف باِستخدَام المُبدل public، وهو ما يَعنِي كَوْنه تابعًا عامًا، فإنه من الممكن لأيّ صَنْف آخر استدعائه أيضًا، ولكن ينبغي أن تُعلِم الحاسوب، في تلك الحالة، باسم التابع كاملًا أثناء الاستدعاء، أيّ بذكر الصَنْف الذي ينتمي إليه ذلك التابع. ولأن التابع playGame()‎ مُعرَّف باِستخدَام المُبدل static، وهو ما يَعنِي كَوْنه تابعًا ساكنًا، فإن اسمه الكامل يَتضمَّن اسم الصنف ذاته المُعرَّف بداخله. على سبيل المثال، إذا كان التابع playGame()‎ مُعرَّف بالصَنْف Poker، تُستخدَم التَعْليمَة التالية لاستدعائه من خارج هذا الصَنْف: Poker.playGame(); يُعلِم اسم الصَنْف -بالأعلى- الحاسوب بأيّ صَنْف ينبغي له أن يَجِد التابع. بالإضافة إلى ذلك، يُساعدنا وجود اسم الصَنْف أثناء الاستدعاء على التمييز بين التابع Poker.playGame()‎ وأي توابع اخرى لها نفس الاسم ومُعرَّفة بأصناف آخرى، مثل Roulette.playGame()‎ أو Blackjack.playGame()‎. تُكتَب عمومًا تَعْليمَة استدعاء أي برنامج فرعي ساكن static ‏(static subroutine call) بالجافا بالصيغة التالية إذا كان البرنامج الفرعي المُستدعَى مُعرَّفًا بنفس الصَنْف (class): <subroutine-name>(parameters); أو كالتالي إذا كان البرنامج الفرعي مُعرَّفًا بصَنْف آخر: <class-name>.<subroutine-name>(parameters); يَختلف ذلك عن التوابع غَيْر الساكنة (non-static methods) -سنتناولها لاحقًا-، والتي تنتمي إلى كائنات (objects) وليس أصناف (classes)، ولهذا يُستدعَى ذلك النوع من التوابع من خلال الكائنات (objects) ذاتها لا من خلال أسماء الأصناف. لاحظ أنه في حين يُمكِن لقائمة المُعامِلات (parameter list) أن تَكُون فارغة (empty)، كما هو الحال بالمثال playGame()‎، فإن كتابة الأقواس (parentheses) بتَعْليمَة الاستدعاء ما تزال ضرورية حتى مع كَوْن ما بينها فارغًا. أما في حالة تخصيص مُعامِل (parameter) أو أكثر بقائمة المُعامِلات (parameter list) بالتعريف الخاص ببرنامج فرعي ما (subroutine definition)، فينبغي عمومًا أن يَتطابَق عدد المُعامِلات المُمرَّرة أثناء استدعاء ذلك البرنامج الفرعي مع العَدَدَ المُخصَّص بذلك التعريف، كما لابُدّ بطبيعة الحال أن تَتطابَق أنواع تلك المُعامِلات المُمرَّرة بتَعْليمَة الاستدعاء مع نوعها المُناظِر بنفس ذلك التعريف. البرامج الفرعية بالبرامج سنُعطي الآن مثالًا عما قد يبدو عليه البرنامج (program) عند تَضمُّنه لبرنامج فرعي آخر غَيْر البرنامج main()‎. سنَكتُب تحديدًا برنامجًا للعبة تخمين، يَختار فيه الحاسوب عددًا عشوائيًا بين العددين ١ و ١٠٠، ثُمَّ يُحاول المُستخدِم تخمين ذلك العدد، ليُخبره الحاسوب بَعْدها عما إذا كان تخمينه أكبر أو أقل أو يُساوِي العَدَد الصحيح، وبحيث يَفوز المُستخدِم بالمباراة إذا تَمكَّن من تخمين العدد الصحيح خلال ٦ تخمينات كحد أقصى. أخيرًا، يَستطيع المُستخدِم اختيار الاستمرار بلعب مباراة إضافية بنهاية كل مباراة. لمّا كانت كل مباراة هي بالنهاية مُهِمّة مُترابطة مُفردة، كان بديهيًا كتابة برنامج فرعي playGame()‎ بهدف لعب مباراة تخمين واحدة مع المُستخدِم. سيَستخدِم البرنامج main()‎ حَلْقة تَكْرار (loop)، والتي ستَستدعِي ذلك البرنامج الفرعي مع كل مرة يختار فيها المُستخدِم الاستمرار بلعب مباراة إضافية. نحتاج الآن إلى تصميم البرنامج الفرعي playGame()‎ وكتابته، وفي الواقع، يُصمَّم أي برنامج فرعي بنفس طريقة تَصْميم البرنامج main()‎، أيّ نبدأ بكتابة توصيف للخوارزمية (algorithm)، ثم نُطبِق التَصْميم المُتدرج (stepwise refinement). اُنظر الخوارزمية التالية لبرنامج لعبة التخمين مكتوبًا بالشيفرة الوهمية: // اختر عددًا عشوائيًا Pick a random number // طالما لم تنته المباراة while the game is not over: // اقرأ تخمين المستخدم Get the user's guess // اخبر المستخدم عما إذا كان تخمينه صحيحًا أم أكبر أم أقل Tell the user whether the guess is high, low, or correct. يُعدّ الاختبار "طالما لم تنته المباراة" معقدًا نوعًا ما؛ وذلك لأن المباراة قد تنتهي لسببين: إما لأن تخمين المُستخدِم كان صحيحًا أو لوصوله للحد الأقصى من عدد التخمينات المُمكنة، أيّ ٦. أحد أسهل الطرائق للقيام بذلك هو اِستخدَام حَلْقة تَكْرار لا نهائية while (true)‎ تحتوي على تَعْليمَة break؛ بهدف إِنهاء الحَلْقة في الوقت المناسب. لاحِظ أننا سنحتاج إلى الاحتفاظ بعَدَد تخمينات المُستخدِم؛ حتى نَتمكَّن من إِنهاء المباراة في حالة وصول المُستخدِم للحد الأقصى من التخمينات. تُصبِح الخوارزمية كالتالي بعد إِجراء التعديلات: // أسند قيمة عشوائية بين 1 و 100 إلى المتغير computersNumber Let computersNumber be a random number between 1 and 100 // اضبط قيمة العداد المستخدم لعدّ عدد تخمينات المستخدم Let guessCount = 0 // استمر بتنفيذ الآتي while (true): // اقرأ تخمين المستخدم Get the user's guess // أزد قيمة العداد بمقدار الواحد Count the guess by adding 1 to guess count // إذا كان تخمين المستخدم صحيحًا if the user's guess equals computersNumber: // بلغ المستخدم بفوزه بالمباراة Tell the user he won // اخرج من الحلقة break out of the loop // إذا وصل العداد للحد الأقصى 6 if the number of guesses is 6: // بلغ المستخدم بخسارته للمباراة Tell the user he lost // اخرج من الحلقة break out of the loop // إذا كان كان تخمين المستخدم أقل من العدد if the user's guess is less than computersNumber: // بلغ المستخدم بكَوْن التخمين أقل من العدد Tell the user the guess was low // إذا كان كان تخمين المستخدم أعلى من العدد else if the user's guess is higher than computersNumber: // بلغ المستخدم بكَوْن التخمين أكبر من العدد Tell the user the guess was high يُستخدَم التعبير ‎(int)(100 * Math.random()) + 1 لاختيار عدد عشوائي يقع بين العددين ١ و ١٠٠. اُنظر الشيفرة التالية بلغة الجافا، والتي تَتضمَّن تعريف البرنامج playGame()‎ بعد التَّصْريح عن المُتَغيِّرات (variable declarations): static void playGame() { int computersNumber; // العدد العشوائي int usersGuess; // إحدى تخمينات المستخدم int guessCount; // عدد تخمينات المستخدم computersNumber = (int)(100 * Math.random()) + 1; guessCount = 0; System.out.println(); System.out.print("What is your first guess? "); while (true) { usersGuess = TextIO.getInt(); // اقرأ تخمين المستخدم guessCount++; if (usersGuess == computersNumber) { System.out.println("You got it in " + guessCount + " guesses! My number was " + computersNumber); break; // انتهت المباراة بفوز المستخدم } if (guessCount == 6) { System.out.println("You didn't get the number in 6 guesses."); System.out.println("You lose. My number was " + computersNumber); break; // انتهت المباراة بخسارة المستخدم } // بلغ المستخدم عما إذا كان تخمينه أكبر أم أصغر من العدد if (usersGuess < computersNumber) System.out.print("That's too low. Try again: "); else if (usersGuess > computersNumber) System.out.print("That's too high. Try again: "); } System.out.println(); } // end of playGame() الآن، وبعد انتهائنا من كتابة تعريف البرنامج الفرعي بالأعلى، فإننا سنحتاج إلى معرفة المكان الذي يُفْترَض أن نضع به هذا التعريف؟ ينبغي أن نضعه عمومًا ضِمْن نفس الصَنْف المُتضمِّن للبرنامج main()‎، ولكن ليس داخل البرنامج main ذاته؛ فمن غَيْر المسموح كتابة برنامج فرعي ضِمْن آخر (nested). لمّا كانت لغة الجافا لا تَشترِط أيّ ترتيب معين للبرامج الفرعية المُعرَّفة ضِمْن نفس الصَنْف، فيُمكِنك وضع تعريف playGame()‎ قَبْل البرنامج main()‎ أو بَعْده. لاحِظ أن البرنامج main()‎ سيَستدعِي البرنامج الفرعي playGame()‎، وهو ما يَعنِي مُجرد تَضمُّنه لتَعْليمَة استدعاء (call statement) لذلك البرنامج الفرعي، لا تَضمُّنه لتعريفها الكامل (definition). يتبقَّى لنا الآن كتابة البرنامج main، وهو ما قد تراه أمرًا بغاية السهولة خاصة مع رؤيتنا لكثير من الأمثلة المشابهة مُسْبَّقًا. سيبدو البرنامج كاملًا كالتالي مع مُراعاة إِمكانية إضافة المزيد من التعليقات (comments): import textio.TextIO; public class GuessingGame { public static void main(String[] args) { System.out.println("Let's play a game. I'll pick a number between"); System.out.println("1 and 100, and you try to guess it."); boolean playAgain; do { playGame(); // اِستدعي البرنامج الفرعي لإجراء مباراة System.out.print("Would you like to play again? "); playAgain = TextIO.getlnBoolean(); } while (playAgain); System.out.println("Thanks for playing. Goodbye."); } // ‫نهاية main static void playGame() { int computersNumber; // العدد العشوائي int usersGuess; // إحدى تخمينات المستخدم int guessCount; // عدد تخمينات المستخدم computersNumber = (int)(100 * Math.random()) + 1; guessCount = 0; System.out.println(); System.out.print("What is your first guess? "); while (true) { usersGuess = TextIO.getInt(); // اقرأ تخمين المستخدم guessCount++; if (usersGuess == computersNumber) { System.out.println("You got it in " + guessCount + " guesses! My number was " + computersNumber); break; // انتهت المباراة بفوز المستخدم } if (guessCount == 6) { System.out.println("You didn't get the number in 6 guesses."); System.out.println("You lose. My number was " + computersNumber); break; // انتهت المباراة بخسارة المستخدم } // بلغ المستخدم عما إذا كان تخمينه أكبر أم أصغر من العدد if (usersGuess < computersNumber) System.out.print("That's too low. Try again: "); else if (usersGuess > computersNumber) System.out.print("That's too high. Try again: "); } System.out.println(); } // ‫نهاية البرنامج الفرعي playGame } // ‫نهاية الصنف GuessingGame اِستغرِق الوقت الكافي لقراءة شيفرة البرنامج بالأعلى، وحَاول اِستيعاب طريقة عملها. رُبما تَكُون قد لاحَظت أن تَقسيم البرنامج إلى تابعين (methods) قد جَعل البرنامج عمومًا أسهل في القراءة، وربما حتى في الكتابة حتى مع كَوْنه بسيطًا، أما إذا لم تَلحَظ ذلك، فحاول إقناع نفسك به في الوقت الحالي. المتغيرات الأعضاء (Member Variables) قد تَتضمَّن الأصناف (classes) أعضاء (members) آخرى، غير البرامج الفرعية، كالمُتَغيِّرات (variables)، فيُمكِن لأي صَنْف التَّصْريح عن مُتَغيِّر ما (variable declaration)، ولا يُقصَد بذلك تلك المُتَغيِّرات المُعرَّفة داخل برنامج فرعي معين، والمَعروفة باسم المُتَغيِّرات المحليّة (local variable)، وإنما تلك المُعرَّفة بمكان يقع خارج أيّ برنامج فرعي ضِمْن الصَنْف. تُعرَف تلك المُتَغيِّرات باسم المُتَغيِّرات العامة (global variable) أو المُتَغيِّرات الأعضاء (member variables)؛ وذلك لكَوْنهم أعضاء (members) ضِمْن الصنف (class). مثلما هو الحال مع البرامج الفرعية، يُمكِن لأي مُتَغيِّر عضو (member variable) أن يُعرَّف بعدّه عضوًا ساكنًا (static) أو عضوًا غير ساكن (non-static). سنقتصر في هذا الفصل على الساكن منها. بدايةً، ينتمي أي مُتَغيِّر عضو، مُعرَّف باِستخدَام المُبدل static، إلى الصَنْف ذاته المُعرَّف بداخله -لا إلى كائنات ذلك الصَنْف-، فعندما يُحَمِّل مُفسر الجافا (Java interpreter) صَنْف معين، فإنه يُخصِّص مساحة بالذاكرة لكل مُتَغيِّر عضو ساكن ضِمْن ذلك الصَنْف. تُعدِّل أي تَعْليمَة إِسْناد (assignment) إلى واحد من تلك المُتَغيِّرات، وبغض النظر عن مكانها بالبرنامج، من محتوى نفس المساحة بالذاكرة، وكذلك يَلج (access) أي تعبير (expression) يَتضمَّن واحدًا من تلك المُتَغيِّرات، وبغض النظر عن مكانه بالبرنامج، إلى نفس المساحة بالذاكرة ويُعيد نفس القيمة، أيّ تتشارك البرامج الفرعية الساكنة المُعرَّفة بصَنْف ما قيم المُتَغيِّرات الأعضاء الساكنة المُعرَّفة ضِمْن نفس ذلك الصَنْف، فيُمكِن لبرنامج فرعي مُعين ضَبْط قيمة مُتَغيِّر عضو ساكن ما، بحيث يَستخدِمها برنامج فرعي آخر، وهو ما يَختلِف عن المُتَغيِّرات المحليّة المُعرَّفة (local variable) داخل أحد البرامج الفرعية؛ حيث تَتوفَّر فقط بينما يُنفَّذ ذلك البرنامج الفرعي، ثم لا يَعُدْ بالإمكان الوُلوج إليها (inaccessible) من خارج ذلك البرنامج الفرعي. تتشابه تَعْليمَة التَّصْريح (declaration) عن مُتَغيِّر عضو (member variable) مع تلك المَسئولة عن التَّصْريح عن أي مُتَغيِّر محليّ تقليدي باستثناء شيئين. الأول هو وقوع التَّصْريح عن المُتَغيِّر العضو بمكان خارج أي برنامج فرعي (مع ذلك ما يزال التَّصْريح ضِمْن الصَنْف نفسه)، والثاني هو إمكانية اِستخدَام المُبدلات (modifiers) مثل static و public و private ضِمْن التَّصْريح. يقتصر هذا الفصل على الأعضاء الساكنة فقط، ولهذا ستَتضمَّن أي تَعْليمَة تَّصْريح عن مُتَغيِّر عضو (member variable) المُبدل static، وربما قد يُستخدَم أيًا من المُبدلين public أو private. على سبيل المثال، انظر التالي: static String usersName; public static int numberOfPlayers; private static double velocity, time; إذا لم تَستخدِم المُبدل private ضِمْن تَعْليمَة التَّصْريح عن مُتَغيِّر عضو ساكن معين، فإنه يُعامَل افتراضيًا كعضو عام public، أي يُمكن الوُلوج إليه بأي مكان سواء من داخل الصَنْف المُعرَّف به أو من خارج ذلك الصنف. ولكن لاحِظ أنك ستحتاج إلى اِستخدَام مُعرِّف (identifier) مُركَّب على الصورة . عند محاولة الإشارة إليه من خارج الصَنْف. فمثلًا، يحتوي الصَنْف System على مُتَغيِّر عضو ساكن عام اسمه هو out، لذلك تستطيع الإشارة إلى ذلك المُتَغيِّر باِستخدَام System.out بأيّ صَنْف خاص بك. مثال آخر هو المُتَغيِّر العضو الساكن العام Math.PI بالصَنْف Math. مثال أخير، وبفَرْض أن لدينا الصَنْف Poker، والذي يُصَرِّح عن مُتَغيِّر عضو ساكن عام، وليَكُن numberOfPlayers، فإنه يُمكِن الإشارة إلى ذلك المُتَغيِّر داخل الصَنْف Poker ببساطة باِستخدَام numberOfPlayers، في المقابل، يُمكِن الإشارة إليه باِستخدَام Poker.numberOfPlayers من خارج الصَنْف. والآن، لنضيف عدة مُتَغيِّرات أعضاء ساكنة إلى الصَنْف GuessingGame الذي كتبناه مُسْبَّقًا بهذا القسم: أولًا، المُتَغيِّر العضو gamesPlayed بهدف الاحتفاظ بعَدَدَ المباريات التي لعبها المُستخدِم إجمالًا. ثانيًا، المُتَغيِّر العضو gamesWon بهدف الاحتفاظ بعَدَدَ المباريات التي كَسَبها المُستخدِم. يُصَرَّح عن تلك المُتغيرات كالتالي: static int gamesPlayed; static int gamesWon; ستَزداد قيمة المُتَغيِّر gamesPlayed بمقدار الواحد دائمًا مع كل عملية اِستدعاء للبرنامج playGame()‎، بينما ستَزداد قيمة المُتَغيِّر gamesWon بمقدار الواحد في حالة فوز المُستخدِم بالمباراة فقط، ثم تُطبَع قيمة المُتَغيِّرين بنهاية البرنامج main()‎. لمّا كان ضروريًا لكِلا البرنامجين الفرعيين playGame()‎ و main()‎ أن يَلجا إلى نفس قيمتي المُتَغيِّرين، فإنه يَستحِيل اِستخدَام المُتَغيِّرات المحليّة للقيام بنفس الشيء؛ ففي الواقع، يَقتصِر الولوج إلى قيمة مُتَغيِّر محليّ معين على برنامج فرعي وحيد، هو البرنامج الفرعي الذي عَرَّفه، وضِمْن نفس الاستدعاء؛ حيث تُسنَد قيمة جديدة للمُتَغيِّرات المحليّة (local variables) مع كل عملية استدعاء للبرنامج الفرعي الشامل لها، وهو ما يَختلِف عن المُتَغيِّرات العامة (global variables)، والتي تَحتفِظ بنفس قيمها بين كل استدعاء والاستدعاء الذي يَليه. تُهيَئ (initialized) المُتَغيِّرات الأعضاء تلقائيًا بقيم افتراضية بعد التَّصْريح عنها، وهو ما يَختلِف عن المُتَغيِّرات المحليّة ضِمْن البرامج الفرعية، والتي لابُدّ من إِسْناد قيمة لها صراحةً قَبْل اِستخدَامها. تلك القيم الافتراضية هي ذاتها القيم الافتراضية لعناصر المصفوفات، فتُسنَد القيمة صفر افتراضيًا إلى المُتَغيِّرات العددية، بينما تُسنَد القيمة false للمُتَغيِّرات من النوع boolean، في حين يُسنَد المحرف المقابل لقيمة ترميز اليونيكود (Unicode code)‏ ‎\u0000 للمُتَغيِّرات من النوع المحرفي char، أما القيمة الافتراضية المبدئية للكائنات (objects)، كالسَلاسِل النصية من النوع String، فهي القيمة الفارغة null. لمّا كان المُتَغيِّرين gamesPlayed و gamesWon من النوع int، فإنهما يُهيَئا أتوماتيكيًا إلى القيمة المبدئية صفر، وهو ما تَصادَف أن يَكُون القيمة المبدئية المناسبة لمُتَغيِّر يُنوَى اِستخدَامه كعَدَّاد (counter). مع ذلك، إذا لم تُناسبك القيمة المبدئية الافتراضية أو حتى إذا كنت تُريد مُجرد التَّصْريح عن نفس القيمة المبدئية لكن بصورة أكثر وضوحًا، فما يزال بإمكانك إِجراء عملية إِسْناد إلى أي من تلك المُتَغيِّرات ببداية البرنامج main()‎. اُنظر نسخة البرنامج GuessingGame.java بَعْد التعديل: import textio.TextIO; public class GuessingGame2 { static int gamesPlayed; // العدد الإجمالي للمباريات static int gamesWon; // عدد المباريات التي فاز فيها المستخدم public static void main(String[] args) { gamesPlayed = 0; gamesWon = 0; // لا فائدة فعلية من ذلك لأن الصفر هو القيمة الافتراضية System.out.println("Let's play a game. I'll pick a number between"); System.out.println("1 and 100, and you try to guess it."); boolean playAgain; do { playGame(); // استدع البرنامج الفرعي للعب مباراة System.out.print("Would you like to play again? "); playAgain = TextIO.getlnBoolean(); } while (playAgain); System.out.println(); System.out.println("You played " + gamesPlayed + " games,"); System.out.println("and you won " + gamesWon + " of those games."); System.out.println("Thanks for playing. Goodbye."); } // end of main() static void playGame() { int computersNumber; // العدد العشوائي int usersGuess; // إحدى تخمينات المستخدم int guessCount; // عدد تخمينات المستخدم gamesPlayed++; // أزد العدد الإجمالي للمباريات computersNumber = (int)(100 * Math.random()) + 1; guessCount = 0; System.out.println(); System.out.print("What is your first guess? "); while (true) { usersGuess = TextIO.getInt(); // اقرأ تخمين المستخدم guessCount++; if (usersGuess == computersNumber) { System.out.println("You got it in " + guessCount + " guesses! My number was " + computersNumber); gamesWon++; break; // انتهت المباراة بفوز المستخدم } if (guessCount == 6) { System.out.println("You didn't get the number in 6 guesses."); System.out.println("You lose. My number was " + computersNumber); break; // انتهت المباراة بخسارة المستخدم } // بلغ المستخدم عما إذا كان تخمينه أكبر أم أصغر من العدد if (usersGuess < computersNumber) System.out.print("That's too low. Try again: "); else if (usersGuess > computersNumber) System.out.print("That's too high. Try again: "); } System.out.println(); } // ‫نهاية البرنامج الفرعي playGame } // ‫نهاية الصنف GuessingGame2 بالمناسبة، لم يُستخدَم أي من المُبدلين public و private مع البرامج الفرعية أو المُتَغيِّرات الساكنة static بالأعلى، فما الذي يَعنيه ذلك؟ في الواقع، إذا لم يُخصَّص أي مُبدل وصول (access modifier) لمُتَغيِّر عام (global variable) أو لبرنامج فرعي (subroutine)، فإنه يُصبِح قابلًا للوصول بأيّ مكان يقع ضِمْن حزمة (package) الصَنْف الحاضن له، ولكن ليس بأي حزم آخرى. تقع الأصناف التي لا تُصَرِّح عن وجودها ضِمْن حزمة معينة بالحزمة الافتراضية (default package)، لذا تستطيع جميع الأصناف ضِمْن الحزمة الافتراضية -وهو ما يَشمَل أغلبية الأصناف بهذا الكتاب- الوصول إلى كِلا المُتَغيِّرين gamesPlayed و gamesWon وكذلك استدعاء البرنامج الفرعي playGame()‎. مع ذلك، يُعدّ اِستخدَام المُبدل private أثناء التَّصْريح عن كُلًا من المُتَغيِّرات الأعضاء والبرامج الفرعية عمومًا واحدًا من الممارسات الجيدة إلا إذا كان هناك سببًا يَدعوك لمخالفة ذلك، كما يُفضَّل تَجَنُّب اِستخدَام الحزمة الافتراضية (default package). ترجمة -بتصرّف- للقسم Section 2: Static Subroutines and Static Variables من فصل Chapter 4: Programming in the Large I: Subroutines من كتاب Introduction to Programming Using Java.
  16. تستطيع عمومًا أن تقتطع مجموعة من التَعْليمَات (instructions) المسئولة عن إنجاز مُهِمّة واحدة مُحدَّدة، وتُضمِّنها معًا تحت اسم معين، ومِنْ ثَمَّ، تستطيع التَعامُل مع تلك المُهِمّة كوحدة واحدة مهما بَلغت درجة تعقيدها، يُطلق على تلك الوحدات اسم البرنامج الفرعي (subroutine). والآن، لَمْ يَعُدْ هناك أيّ داعي للقلق بشأن أيّ من تلك الخطوات التي قد يَكُون الحاسوب مضطرًا لتَّنْفيذها أثناء إنجازه لمُهِمّة معينة، وإنما تحتاج فقط لتَذَكُّر اسم البرنامج الفرعي (subroutine) الخاص بتلك المُهِمّة؛ لكي تتَمكَّنْ من استدعائه (call). تُعدّ البرامج الفرعية عمومًا أحد أهم الأدوات الأساسية لتبسيط المسائل المعقدة. في الواقع، البرامج الفرعية (subroutine) هي أشبه ما تَكُون بـ"صندوق أسود (black box)"؛ لا يُمكِنك رؤية ما بداخله، أو بتعبير أدق، لا ترغب برؤيته؛ لأنك إذا فعلت، فستكون مُضطرًا للتعامل مع ذلك الكم الهائل من التعقيد الذي يُفْترَض لذلك البرنامج الفرعي إخفائه بالأساس. على الجانب الآخر، لا يُمكِن أن يَقْتصِر ذلك الصندوق على عالمه الداخلي، وإلا سيكون عديم الفائدة، بل ينبغي له التَفاعُل مع العالم الخارجي، ولهذا فإنه يحتاج إلى ما يُشبه الواجهة (interface) التي تَسمَح بتَفاعُل ما بداخل الصندوق مع ما هو خارجه. على سبيل المثال، قد يُوفِّر صندوق أسود مادي عدة أزرار قابلة للضغط، أو قرص قابل للضبط، أو عدة فتحات لتمرير المعلومات جيْئةَ وذَهابًا. يحاول الصندوق عمومًا التخلص من التعقيد بإخفائه، ولذلك فإن القاعدة الأولى لأيّ صندوق أسود هي كالتالي: "ينبغي لواجهة (interface) أيّ صندوق أسود أن تَكُون مُبسَّطة، وسَهلة الفهم، ومُعرَّفة بصورة جيدة." هل توجد صناديق سوداء ضِمْن عالمنا الذي نعيش فيه؟ في الواقع، إنها تُحيط بك من كل جانب: تلفازك، وسيارتك، وهاتفك المحمول، وثلاجتك هي مجرد أمثلة قليلة. على سبيل المثال، تَتَكوَّن واجهة (interface) التلفاز من عدة عناصر هي مفتاحي التَشْغِيل والغَلْق، وجهاز التحكم -ولا تنس بالطبع وضع قابس الكهرباء-، والتي تستطيع من خلالها غَلْق التلفاز، وتَشْغِيله، أو تَغْيِير القناة، أو ضَبْط الصوت، وذلك بدون أيّ فهم لكيفية عَمَل تلك الأشياء. يَنْطَبِق نفس الشيء على هاتفك المحمول على الرغم من كَوْن واجهته أكثر تعقيدًا بعدّة مراحل. يُسمَى الجزء الداخلي من الصندوق الأسود بالتَّنْفيذ (implementation)، والذي قد يَتمثَل في صورة مجموعة الالكترونيات داخل عُدَّة التلفاز أو في صورة شيفرة البرنامج الفرعي الفعليّة المسئولة عن تَّنْفيذ مهمة البرنامج، وعليه تَكُون القاعدة الثانية لأيّ صندوق أسود كالتالي: "لا حاجة لمَعرِفة أيّ شيء عن الجزء التَّنْفيذي (implementation) الخاص بصندوق أسود لكي تَتَمكَّنْ من اِستخدَامه، وإنما يَكفيك مَعرِفة واجهته (interface)." تستطيع عمومًا إعادة كتابة الجزء التَّنْفيذي (implementation) لبرنامج فرعي معين طالما لم تتأثر واجهته بذلك التَغْيِير. على سبيل المثال، عندما تغَيَّرت عُدَّة التلفاز الداخلية من اعتمادها على الصمامات المُفْرغَة (vacuum tubes) إلى الترانزستور (Transistor)، لَمْ يَكُن هناك داعي لمَعرِفة أي شيء عن ذلك أو مَعرِفة حتى ما قد يَعنِيه، وبالمثل، يُمكِن إعادة كتابة الجزء الداخلي لأيّ برنامج فرعي؛ ربما بهدف كتابة شيفرة أكثر كفاءة، وذلك بدون إِحداث أي تَغْيِير على البرامج المُستخدِمة لذلك البرنامج الفرعي (subroutine). تَحَصُّلك على صندوق أسود يَعنِي بالضرورة أن شخصًا ما كان قد صمم تَّنْفيذه (implementation) وبناه، وفي الواقع، يَستفيد كُلًا من المُنفِّذ (implementor) والمُستخدِم من فكرة وجود صندوق أسود، فبالنهاية، يُمكِن اِستخدَام ذلك الصندوق بعَدََدَ لا محدود من المواضع المختلفة، والتي لا يحتاج المُنفِّذ (implementor) أن يَعلم عنها شيئًا، وإنما يَقْتصِر دوره على التأَكُّد من أن ذلك الصندوق يُنفِّذ مُهِمّته المُوكلة إليه تَّنْفيذًا صحيحًا، مع تَوْفِيره لواجهة (interface) سليمة تَسمَح للصندوق بالتَفاعُل مع العالم الخارجي. وعليه تَكُون القاعدة الثالثة لأيّ صندوق أسود كالتالي: "لا يحتاج مُنفِّذ صندوق أسود معين مَعرِفة أيّ شيء عن تلك الأنظمة الأكبر التي ربما قد تُوظِّف ذلك الصندوق داخلها." يُمكِن القول أن أيّ صندوق أسود يُقسِّم العالم إلى جزئين: الجزء الداخلي أو التَّنْفيذي (implementation) والجزء الخارجي، بحيث تَقع الواجهة (interface) على الحد الفاصل بينهما وتربطهما معًا. لاحِظ أن الواجهة (interface) ليست مجرد رابط مادي بين صندوق معين وعالمه الخارجي، وإنما تَتضمَّن أيضًا تَوصِيفًا (specification) لما يُنجزه الصندوق بالإضافة إلى تبيان واضح لطريقة اِستخدَام تلك الواجهة. فمثلًا، لا يكفي أن تقول أن التلفاز يُوفِّر مفتاح تَشْغِيل، وإنما لابُدّ من تَحْدِيد إمكانية اِستخدَام ذلك المفتاح لأغراض تَشْغِيل التلفاز وغَلْقه. دَعْنَا نُعيد صياغة ما تَعنيه الواجهة (interface) بمصطلحات علم الحاسوب (computer science). تَتَكوَّن واجهة أيّ برنامج فرعي (subroutine) من مُكوّنين رئيسيين، الأول هو تَوصِيف صياغي أو نَحوِي (syntactic specification)، والآخر تَوصِيف دلالي (semantic specification). يخبرك الأول بالكيفية التي يُسْتَدعى (call) بها ذلك البرنامج الفرعي، بينما يُحدِّد الثاني المُهِمّة (task) المُوكَلة لذلك البرنامج والتي يُفْترَض له إنجازها. لابدّ من مَعرِفة التوصيف الصياغي (syntactic specification) لبرنامج فرعي معين؛ وذلك لاستدعائه استدعاءً سليمًا، بينما لابُدّ من مَعرِفة توصيفه الدلالي (semantic specification)؛ وذلك لفهم الغرض منه، واِستخدَامه بطريقة فعالة. عادةً ما يُستخدَم مصطلح المواصفة الاصطلاحية للبرنامج الفرعي (contract of subroutine) للإشارة إلى مُكوّني الواجهة -الصياغي والدلالي- معًا. تقول المواصفة الاصطلاحية (contract) لأيّ برنامج فرعي: "ها هو ما ينبغي القيام به لإستدعائي، وها هو ما سأُنْجِزُه بالمقابل". ينبغي للتعليقات (comments) المكتوبة لبرنامج فرعي معين أن تُبرز مُواصفته الاصطلاحية بصورة واضحة، وهو في الواقع ما لا يَحدُث عمليًا، مما يُؤدي إلى انزعاج المبرمجين الذين قد يكونوا مُضطرّين لاِستخدَام ذلك البرنامج الفرعي. سأنتقل ببقية هذا الفصل من مجرد الحديث عن الفكرة العامة للصندوق الأسود والبرامج الفرعية إلى تفاصيل كتابة تلك البرامج بلغة الجافا وكيفية اِستخدَامها. ولكن حَاوِل دومًا اِستحضار تلك الأفكار والمبادئ العامة بذهنك؛ لأنها تُبرز الغرض الأساسي من وجود البرامج الفرعية، وتُرشدك لكيفية اِستخدَامها. سيَكُون ذلك أكثر وُضوحًا بوصولنا للقسم ٤.٧ حيث سنناقش البرامج الفرعية كأداة لتطوير البرامج. تَذكَّر دائمًا أن البرامج الفرعية (subroutines) ليست الصناديق السوداء الوحيدة بعالم البرمجة. على سبيل المثال، يُعدّ الصنف (class) صندوقًا أسودًا، يَتكوَّن من جزئين، أحدهما عام (public) يُمثِل واجهته (interface)، والآخر خاص (private) يُمثِل تَّنْفيذه (implementation) الخَفي. تَنْطَبِق مبادئ الصناديق السوداء عمومًا على كُلًا من الأصناف (classes) والبرامج الفرعية (subroutines). ترجمة -بتصرّف- للقسم Section 1: Black Boxes من فصل Chapter 4: Programming in the Large I: Subroutines من كتاب Introduction to Programming Using Java.
  17. لقد تَعلَّمت، على مدار الفصلين السابقين، نوعية البرمجة المُستخدَمة أثناء كتابة برنامج فرعي وحيد (subroutine)، فيما أطلقنا عليه اسم "البرمجة في نطاق ضيق". سنُركز أكثر خلال الفصول المُتبقّية من الكتاب على بناء البرامج ضِمْن نطاق أوسع، ولكن ما يزال ما تَعلَّمته حتى الآن هو البذرة الأساسية والضرورية لكل ما ستَتَعرَّض له فيما بَعْد. سنرى، في هذا القسم، كيف يُمكِن تطبيق ما قد تَعلَّمته خلال الفصول السابقة ضِمْن سياق برمجة واجهات المُستخدِم الرسومية (graphical user interface)، والتي تُعرَف اختصارًا باسم GUI، وهو سِّياق مُختلف نوعًا ما عما اعتدته من برامج الطرفيّة النصية. ستَعتمِد برمجة الواجهات الرسومية (GUI) سواء التي سنَتَعرَّض لها خلال هذا القسم أو خلال بقية الكتاب على منصة JavaFX، والتي تَضُمّ مجموعة من الأصناف (classes) المُستخدَمة لكتابة هذه النوعية من البرامج، أيّ أن جميع الأصناف (classes) المذكورة بهذا القسم هي جزء من منصة JavaFX، وبالتالي يَنبغي أن تقوم باستيرادها (import) إلى البرنامج حتى تَتَمكَّن من اِستخدَامها. اُنظر القسم ٢.٦ لمزيد من المعلومات عن تَصرِّيف (compiling) البرامج المُستخدِمة لمنصة JavaFX وكيفية تَشْغِيلها. عند تَشْغِيل برامج واجهات المُستخدِم الرسومية (GUI)، ستُفتَح نافذة واحدة (window) أو أكثر على شاشة الحاسوب الخاصة بك. يُمكِنك، كمبرمج، التَحكُّم الكامل بما يَظهر على تلك النافذة، وكذلك بالكيفية التي يُمكِن للمُستخدِم التَفاعُل (interact) بها مع النافذة. سنَفْحَص خلال هذا القسم أمثلة بسيطة مثل طباعة بعض الأشكال البسيطة كالخطوط والمستطيلات على النافذة بدون أي تَفاعُل من المُستخدِم، فالنقطة المُهمّة، في الوقت الحالي، هو أن تتعرَّف على الطريقة التي تُستخدَم بها أساليب "البرمجة في نطاق ضيق" ضِمْن سياقات اخرى غَيْر برامج الطرفية المُعتمدة على النصوص، وسترى بنفسك أنه يمكن تطبيق نفس تلك الأساليب لكتابة أيّ برنامج فرعي (subroutine) وليس فقط البرنامج main. رسم الأشكال ستحتاج إلى أن تَكُون على دراية ببعض المفاهيم كالبكسل (pixels)، وأنظمة الإِحداثيَّات (coordinate systems)؛ كي يتَسَنَّى لك فهم الرسومات الحاسوبية (computer graphics)، ولذلك سنمر على بعض المفاهيم الأساسية سريعًا. تتكون عامةً شاشة الحاسوب (computer screen) من مربعات صغيرة تُسمى البكسل (pixels)، مُرَتَّبة بصورة صفوف وعواميد، بدقة تَصِل عادةً إلى ١٠٠ بكسل لكل بُوصَة (pixels per inch). تَحتوِي الكثير من الشاشات حاليًا على عدد أكبر بكثير من البكسلات المَلْموسة (physical pixels) لكل بُوصَة، لدرجة أنه مِنْ المُحتمَل لبكسل منصة JavaFX أن يُشير إلى بكسل مَلْموس (physical pixel) بمثل هذه الشاشات عالية الدقة (high-resolution)، ولكنه على الأرجح يُشيِر إلى بكسل مَنطقي (logical pixel)، والتي هي وَحدة قياس تُعادِل ٠.٠١ بوصة تقريبًا. لمّا كان باستطاعة الحاسوب التَحكُّم بلون البكسل، فإنه، في الواقع، يَرسِم (drawing) الأشكال عن طريق تَغْيِير ألوان البكسلات المُفردة (individual pixels). كل بكسل له زوج من الإِحداثيَّات (coordinates)، يُشار إليها عادة باسم الإِحداثيّ x والإِحداثيّ y، وتُستخدَم لتَحْدِيد المَوْضِع الأفقي (horizontal) والرأسي (vertical) للبكسل على الترتيب. عند الرسم بمساحة مستطيلية الشكل على الشاشة، تَكُون إِحداثيَّات البكسل بالرُكْن العُلوِي الأيسر (upper left corner) هي (٠،٠)، وبحيث تزداد قيمة الإِحداثيّ x من اليسار إلى اليمين، بينما تزداد قيمة الإِحداثيّ y من الأعلى إلى الأسفل. يُستخدَم البكسل لتَحْدِيد الأشكال وتَعرَيفها، فعلى سبيل المثال، يُعرَّف أيّ مستطيل من خلال الإِحداثيّ x والإِحداثيّ y بالرُكْن العُلوِي الأيسر للمستطيل، بالإضافة إلى كُلًا من عَرْضه (width)، وارتفاعه (height) بوَحدة البكسل. تَعرَض مساحة الرسم (drawing area) بالصورة التالية نطاق كلًا من الإِحداثيَّات x و y، ويُمثِل العَرْض والارتفاع بها حجم مساحة الرسم بوَحدة البكسل: بفَرْض أن مساحة الرسم (drawing area) -بالأعلى- مُكوَّنة من ٨٠٠*٥٠٠ بكسل، سيكون المستطيل، الواقع بالجزء العُلوِي الأيسر من الصورة، تقريبًا بعَرْض ٢٠٠ بكسل وارتفاع ١٥٠ بكسل، كما يَقع الرُكْن العُلوِي الأيسر (upper left corner) للمستطيل بالإِحداثيَّات (٥٠،٥٠) تقريبيًا. يَتمّ الرسم بلغة الجافا باِستخدَام كائن سِّياق رُسومي (graphics context) من النوع GraphicsContext. يَشتمِل هذا الكائن على بعض البرامج الفرعية (subroutines)، مثل برامج (routines) لرسم الأشكال البسيطة كالخطوط، والمستطيلات، والأشكال البيضاوية، والنصوص. (عندما يَظهر النص على الشاشة، يَرسِم الحاسوب حروف النص مثلما يَرسِم أيّ أشكال آخرى). بالإضافة إلى ذلك، يَشتمِل كائن السِّياق الرُسومي (graphics context) أيضًا على مجموعة من البيانات (data)، مثل نوع الخط المُختار حاليًا للرَسْم ولونه. (يُحدِّد نوع الخط كلًا من حجم وشكل الحروف). تَشتمِل بيانات كائن السِّياق أيضًا على سطح رسم (drawing surface)، وهو ما يَتمّ الرسم عليه، وفي حالتنا، سيكون سطح الرسم هو مساحة مُحتوَى النافذة بدون الحواف (border) وشريط العنوان (title bar)، ولكن تَتوفَّر أسطح رَسْم مختلفة يمكن الرَسْم عليها أيضًا. تُوفِّر منصة JavaFX طريقتين لرسم الأشكال: إِمّا بمَلْئ الشكل (filling) أو بتَحْدِيد حوافه (stroking). مَلْئ الشكل (filling) هو ضَبْط لون كل بكسل بداخله، أمَا تَحْدِيد حواف الشكل (stroking) فهو ضَبْط لون البكسلات الواقعة بحوافه (border)، وهو ما يُشبه عملية سَحب قلم على طول حواف الشكل، وفي هذه الحالة، تُعدّ صفات القلم -كحجمه (width/size) أو ما إذا كان يَستخدِم خط صلب (solid line) أو مُتقطِّع (dashed line)- خاصيات (properties) ضِمْن كائن السِّياق الرُسومي (graphics context). يُخصِّص كائن السِّياق الرُسومي أيضًا لونين مُنفصلين، أحدهما لمَلْئ الأشكال (filling)، والآخر لتَحْدِيد حوافها (stroking). لاحظ اقتصار بعض الأشكال -كالخطوط- على طريقة تَحْدِيد الحواف فقط. يُستخدَم مُتَغيِّر من النوع GraphicsContext لتَمثيِل السِّياق الرُسومي (graphics context)، ويَحمِل هذا المُتَغيِّر عادةً الاسم g. ليس هذا ضروريًا بالطبع، حيث يَتوقَف اسمه بالنهاية على المُبرمج. نَعرِض هنا بعض البرامج الفرعية (subroutines) المُتوفرة ضِمْن كائن السِّياق الرُسومي g. لاحظ أن كل قيم المُعامِلات العددية هي من النوع double: البرنامج الفرعي g.setFill(c)‎: يَضبُط اللون المُستخدَم لمَلْئ الأشكال (filling)، حيث المُعامِل c هو كائن من الصَنْف Color. تَتوفَّر الكثير من الثوابت (constants) المُمثِلة للألوان القياسية (standard colors)، والتي يُمكِن اِستخدَامها كمُعامِل لهذا البرنامج الفرعي. تتراوح الألوان القياسية من الألوان الشائعة مثل Color.BLACK و Color.WHITE و Color.RED و Color.GREEN و Color.BLUE و Color.YELLOW، إلى بعض الألوان الغريبة مثل Color.CORNFLOWERBLUE. (يُمكِنك أيضًا إِنشاء ألوان جديدة مُخصَّصة). على سبيل المثال، إذا أردت مَلْئ الأشكال باللون الأحمر، فإنك ستَستدعِي البرنامج الفرعي g.setFill(Color.RED);‎. لاحظ أن اللون المُخصَّص أثناء الاستدعاء سيُستخدَم لجميع عمليات المَلْئ التالية وحتى الاستدعاء التالي لنفس البرنامج الفرعي، أما الأشكال المَرسومة مُسْبَّقًا فلا تتأثر بهذا التَغْيِير. البرنامج الفرعي g.setStroke(c)‎: يَضبُط اللون المُستخدَم لتَحْدِيد حواف الأشكال (stroking)، ويَعمَل بصورة مشابهة للبرنامج الفرعي g.setFill. البرنامج الفرعي g.setLineWidth(w)‎: يَضبُط حجم القلم المُستخدَم خلال عمليات تَحْدِيد الحواف التالية (stroking). لاحظ أن المُعامِل w يَستخدِم وَحدة البكسل. البرنامج الفرعي g.strokeLine(x1,y1,x2,y2)‎: يَرسِم خطًا مُمتدًا من إِحداثيَّات نقطة البداية (x1,y1) وحتى إِحداثيَّات نقطة النهاية (x2,y2). يُرسَم الخط باللون الأسود وبحجم ١ بكسل افتراضيًا، ومع ذلك، يُمكِنك تَخْصِيص كلًا منهما باستدعاء g.setStroke()‎ و g.setLineWidth()‎ على الترتيب. البرنامج الفرعي g.strokeRect(x,y,w,h)‎: يَرسِم الحواف الخارجية (stroking) لمستطيل مع جوانبه الأفقية والرأسية، بحيث يَبعُد الرُكْن العُلوِي الأيسر (top-left corner) لهذا المستطيل مسافة قدرها x بوَحدة البكسل عن الحافة اليسرى لمساحة الرسم (drawing area)، ومسافة قدرها y بوَحدة البكسل عن حافتها العُلوِية. يُحدِّد كلًا من المُعامِلين w و h عَرْض المستطيل الأفقي وارتفاعه الرأسي بوَحدة البكسل على الترتيب. يُمكِن ضَبْط لون الخط المُستخدَم وحجمه باستدعاء g.setStroke()‎ و g.setLineWidth()‎ على الترتيب. البرنامج الفرعي g.fillRect(x,y,w,h)‎: يَعمَل بصورة مشابهة للبرنامج الفرعي g.strokeRect()‎ باستثناء أنه يَملْئ المستطيل (filling) بدلًا من رسم حوافه الخارجية (stroking). اِستدعي g.setFill لضَبْط اللون المُستخدَم. البرنامج الفرعي g.strokeOval(x,y,w,h)‎: يَرسِم الحواف الخارجية لشكل بيضاوي. يُرسَم الشكل البيضاوي بحيث يَقع ضِمْن المستطيل الذي كان سيُرسَم في حالة استدعاء g.strokeRect(x,y,w,h)‎ بنفس قيم المُعامِلات. لاحِظ أنه يُمكِنك اِستخدَام نفس القيمة لكُلًا من المُعامِلين w و h لرسم حواف دائرة. البرنامج الفرعي g.fillOval(x,y,w,h)‎: يَعمَل بصورة مشابهة للبرنامج الفرعي g.strokeOval()‎ باستثناء أنه يَملْئ الشكل البيضاوي بدلًا من رَسْم حوافه الخارجية. تُعدّ هذه البرامج الفرعية كافية لرَسْم بعض الصور باِستخدَام الجافا. لنبدأ بشئ بسيط مثل رَسْم عشرة خطوط متوازية، كالتالي: نحتاج أولًا لمجموعة افتراضات هي كالتالي: سيكون طول الخطوط حوالي ٢٠٠ بكسل، والمسافة بين كل خط والخط الذي يَليه حوالي ١٠ بكسل، وأخيرًا، سنفْترِض أن نقطة بداية (start) أول خط تقع بالإِحداثيَّات (١٠٠،٥٠). الآن، كل ما نحتاج إليه لرَسْم خط هو استدعاء البرنامج الفرعي g.strokeLine(x1,y1,x2,y2)‎ بقيم مُعامِلات مناسبة. نلاحِظ أن نقطة البداية (start) لجميع الخطوط لها نفس قيمة الإِحداثيّ x ‏(x-coordinate) وتُساوِي ١٠٠، ومِنْ ثَمَّ، سنَستخدِم قيمة ثابتة تُساوِي ١٠٠ كقيمة للمُعامِل x1. لمّا كانت جميع الخطوط بطول ٢٠٠ بكسل، فإننا سنَستخدِم قيمة ثابتة تُساوِي ٣٠٠ كقيمة للمُعامِل x2. في المقابل، تَختلف إِحداثيَّات y ‏(y-coordinates) بكل خط عن الخط الذي يَليه، ولكن يُمكِننا أن نرى أن قيمة الإِحداثيّ y بنقطتي البداية (start) والنهاية (end) لكل خط منها هو نفسه، وعليه، سنَستخدِم مُتَغيِّر وحيد لكُلًا من قيمتي y1 و y2، هو المُتَغيِّر y. الآن، أصبح أمر الاستدعاء لرَسْم أحد الخيوط كالتالي g.strokeLine(100,y,300,y)‎. اِفْترَضنا قبلًا أن قيمة المُتَغيِّر y لأول خط هي ٥٠، ثم ستزداد تلك القيمة بمقدار ١٠ مع كل انتقال للخط التالي، مما يَعنِي أننا سنحتاج إلى التأكد من أن قيمة y تأخذ القيمة الصحيحة من متتالية الأعداد. يُمكِننا اِستخدَام حَلْقة تَكْرار for، كالتالي: int y; // ‫إحداثي y للخط int i; // المتغير المتحكم بالحلقة y = 50; // ‫تبدأ y بالقيمة 50 لأول خط for ( i = 1; i <= 10; i++ ) { g.strokeLine( 100, y, 300, y ); y = y + 10; // ‫أزد y بمقدار 10 قبل رسم الخط التالي } نستطيع أيضًا اِستخدَام المُتَغيِّر y ذاته كمُتحكِّم بالحَلْقة (loop control variable). لاحِظ أن قيمة y للخط الأخير هي ١٤٠. انظر الشيفرة التالية: int y; for ( y = 50; y <= 140; y = y + 10 ) g.strokeLine( 100, y, 300, y ); إذا أردت تلوين الخطوط باللون الأزرق، اِستدعي البرنامج الفرعي g.setStroke(Color.BLUE)‎ قبل رَسْمها، حيث سيُستخدَم اللون الأسود افتراضيًا إذا قُمت برَسْمها دون ضَبْط اللون. أما إذا أردت أن يَكُون حجم تلك الخطوط ٣ بكسل، اِستدعي البرنامج الفرعي g.setLineWidth(3)‎ قَبْل رَسْمها. لننتقل إلى مثال أكثر تعقيدًا، فمثلًا، لنَرسِم عددًا كبيرًا من الدوائر بشكل عشوائي سواء فيما يَخُص مَوْضِعها (position) أو لونها. لمّا كنا على علم بعدد قليل من الألوان المُتوفرة، فإننا سنختار عشوائيًا واحدًا من الألوان التالية : الأحمر، والأخضر، والأزرق، والأصفر. سنستعمل تَعْليمَة switch بسيطة للاختيار، وذلك بطريقة شبيهة للمثال بالقسم الفرعي ٣.٦.٤: switch ( (int)(4*Math.random()) ) { case 0: g.setFill( Color.RED ); break; case 1: g.setFill( Color.GREEN ); break; case 2: g.setFill( Color.BLUE ); break; case 3: g.setFill( Color.YELLOW ); break; } لمّا كنا نريد للدوائر أن تكون عشوائية التَموْضع، فسنحتاج إلى اختيار مركز الدوائر (center of circles) بصورة عشوائية. بفَرْض أن عَرْض مساحة الرسم (drawing area) وارتفاعها مُعطيين من خلال المُتَغيِّرين width و height على الترتيب، فسينبغي للمَوْضِع الأفقي (horizontal position) للمركز أن يكون قيمة عشوائية تتراوح من القيمة ٠ وحتى width-1. بالمثل، يَنبغي للمَوْضِع الرأسي (vertical position) لمركز الدائرة أن يَكُون قيمة عشوائية تتراوح من القيمة ٠ وحتى height-1. أخيرًا، ما زِلنا بحاجة لتَحْدِيد حجم الدائرة. سنكتفي، في هذا المثال، باِستخدَام نصف قطر (radius) ثابت لجميع الدوائر مُساوِي للقيمة ٥٠ بكسل. تُرسَم الدائرة باِستخدَام التَعْليمَة g.fillOval(x,y,w,h)‎، لكن، في الواقع، لا يُمثِل المُعامِلان x و y، بهذا الأمر (command)، إِحداثيَّات مركز الدائرة؛ وإنما إِحداثيَّات الرُكْن العُلوِي الأيسر (upper left corner) للمستطيل المرسوم حول الدائرة، ولهذا سنحتاج إلى تَحرِيك مركز الدائرة بمسافة قدرها يُساوِي نصف قطر الدائرة أي ٥٠ بكسل؛ وذلك للحصول على قيم x و y المُناظِرة. في المقابل، يُمثِل المُعامِلان w و h عَرْض وارتفاع المستطيل على الترتيب، واللذين ستكون قيمتهما مُساوِية لضعف نصف قطر الدائرة أي ١٠٠ بكسل بهذا المثال. تُراعِي الشيفرة التالية جميع النقاط المذكورة بالأعلى، وتُستخدَم لرَسْم دائرة عشوائية واحدة: centerX = (int)(width*Math.random()); centerY = (int)(height*Math.random()); g.fillOval( centerX - 50, centerY - 50, 100, 100 ); لاحِظ أن الشيفرة بالأعلى تُستدَعى بَعْد استدعاء الشيفرة المسئولة عن ضَبْط اللون. تبدو الصورة عامةً بشكل أفضل بَعْد تَحْدِيد حافة الدائرة (border) باللون الأسود (stroking)، ولذلك أضيفت الشيفرة التالية: g.setStroke( Color.BLACK ); g.strokeOval( centerX - 50, centerY - 50, 100, 100 ); وأخيرًا، للحصول على عدد كبير من الدوائر، ضُمِّنت الشيفرة بالأعلى داخل حَلْقة تَكْرار for، ونُفِّذت ٥٠٠ مرة، فكانت الرسمة الناتجة عن البرنامج كالتالي: الرسم داخل برنامج كما تعلم، لا يُمكِن لأيّ شيفرة بلغة الجافا أن تكون مُستقلة بذاتها، فلابُدّ لها أن تُكتَب ضِمْن برنامج فرعي (subroutine)، والذي بدوره يَكُون مُعرَّفًا داخل صَنْف (class)، ولهذا تَعرِض الشيفرة التالية التَعرِيف الكامل لبرنامج فرعي (subroutine definition)، والذي يُستخدَم لرَسْم الصورة من المثال السابق : public void drawPicture(GraphicsContext g, int width, int height) { g.setFill(Color.WHITE); g.fillRect(0, 0, width, height); // املأ لون الخلفية بالأبيض // As an example, draw a large number of colored disks. // To get a different picture, erase this code, and substitute your own. int centerX; // ‫احداثي x لمركز القرص int centerY; // ‫احداثي y لمركز القرص int colorChoice; // قيمة اللون العشوائي int count; // المتغير التحكم بالحلقة for (count = 0; count < 500; count++) { centerX = (int)(width*Math.random()); centerY = (int)(height*Math.random()); colorChoice = (int)(4*Math.random()); switch (colorChoice) { case 0: g.setFill(Color.RED); break; case 1: g.setFill(Color.GREEN); break; case 2: g.setFill(Color.BLUE); break; case 3: g.setFill(Color.YELLOW); break; } g.fillOval( centerX - 50, centerY - 50, 100, 100 ); g.setStroke(Color.BLACK); g.strokeOval( centerX - 50, centerY - 50, 100, 100 ); } } // ‫نهاية drawPicture() هذه هي المرة الأولى التي تَتعرَّض فيها لتَعرِيف برنامج فرعي (subroutine definition) -إلى جانب main()‎-. سنتناول هذا الموضوع تفصيليًا بالفصل التالي، ولكن سنَمر عليه سريعًا هنا، يُتيِح السَطْر الأول من التَعرِيف الولوج لبعض القيم التي يَحتاجها البرنامج الفرعي، وهي السِّياق الرُسومي g، وكلًا من عَرْض وارتفاع مساحة الرسم width و height. يَستقبِل البرنامج الفرعي هذه القيم من مصدر خارجي، ويستطيع اِستخدَامها. ما يَهمّ هنا هو أن تُدرِك أنه لكي تَرِسم شيئًا (يَقصِد الكاتب أن هذا هو هدف البرنامج الفرعي، فاِسم البرنامج الفرعي هو drawPicture)، فستحتاج فقط إلى كتابة مُحتوَى البرنامج الفرعي، مثلما تَكتُب مُحتوَى البرنامج main()‎ عند كتابة برنامج (الهدف من main()‎). يَنبغي لتَعرِيف البرنامج الفرعي (subroutine definition) أن يَكُون بالصَنْف (class) الذي يُعرِّف البرنامج، وهو في هذه الحالة الصَنْف SimpleGraphicsStarter. شَّغِل البرنامج -مُتاح بالكامل بالملف SimpleGraphicsStarter.java- لترى الرَسمة، كما يُمكِنك اِستخدَام هذا البرنامج كنقطة بداية لرَسْم الصور الخاصة بك. لاحِظ أنك لن تَفهم كل الشيفرة المكتوبة بالبرنامج، لكن ما يزال بإمكانك التَعديل عليها، فلا حاجة إلى فهم الشيفرة بأكملها، كل ما قد يَعَنيك هو الشيفرة الموجودة بالبرنامج الفرعي drawPicture()‎. اِحذف تلك الشيفرة، وضَعْ مكانها شيفرة الرسوم خاصتك، وستَتمكَّن بعدها من عَرْض رسوماتك. بالمناسبة، قد تُلاحِظ أن الكلمة static مُستخدَمة بتَعرِيف البرنامج الفرعي main()‎، بعكس البرنامج الفرعي drawPicture()‎، الذي لا يَستخدِمها، وهو ما يَعنِي أن البرنامج الفرعي drawPicture()‎ موجود بكائن (object) وليس بصَنْف (class). تُعدّ البرامج الفرعية التي تَستخدِم الكلمة static بتَعرِيفعها ساكنة (static)، أما التي لا تَستخدِمها فتُعدّ غَيْر ساكنة (non-static). الفرق بينهما مُهِمّ، ولكنه ليس بالأمر الذي يَنبغي أن تَقْلَق حِياله في الوقت الحاضر؛ حيث سنتناوله تفصيليًا بالفصل الخامس على أية حال. التحريكة (Animation) يَعتمِد التَحرِيك الحاسوبي (computer animation) على متتالية من الصور المُنفصلة، يُطلَق على كُل منها اسم الإطار (frame). تُعرَض هذه الصور بشكل سريع واحدة تلو الآخرى، فإذا كان التَغْيِير بين كل صورة والصورة التي تَليها طفيفًا، ستبدو متتالية الصور وكأنها تَحرِيكة مُستمرة (continuous animation). يُمكِنك اِستخدَام المثال التوضيحي بالملف SimpleAnimationStarter.java كنقطة بداية، حيث يَحتوِي على البرنامج الفرعي drawFrame()‎ المَسؤول عن رَسْم إطار (frame) وحيد ضِمْن تَحرِيكة (animation)، بالإضافة إلى ذلك، يُنفَّذ البرنامج الفرعي drawFrame()‎ أتوماتيكيًا حوالي ٦٠ مرة بالثانية، مما يَضمَن استمرار عَرْض الأُطُر (frames)، أيّ أنك تستطيع إِنشاء تَحرِيكة (animation) بمُجرَّد إضافة الشيفرة إلى هذا البرنامج الفرعي. تستطيع تمييز المرة الحالية من التَّنْفيذ من خلال مُتَغيِّرين إضافيين -إلى جانب السِّياق الرُسومي وكُلًا من عَرْض وارتفاع مساحة الرسم- يَستقبِلهما البرنامج الفرعي، وهما frameNumber و elapsedSeconds؛ حيث يأخذ المُتَغيِّر frameNumber القيم ٠، ١، ٢، ٣، .. والتي تَزداد بمقدار الواحد مع كل اِستدعاء للبرنامج الفرعي، أمَا قيمة المُتَغيِّر elapsedSeconds فتُشيِر إلى عدد الثواني التي مَرَّت على تَّنْفيذ التَحرِيكة حتى الآن. إجمالًا، تَستطيع رَسْم صورة مختلفة في كل مرة يُستدَعى فيها البرنامج الفرعي (subroutine) بالاعتماد على قيمة أيًا من هذين المُتَغيِّرين. سنَرسِم بالمثال التالي مجموعة من المستطيلات المُتداخِلة (nested rectangles)، والتي ستنكمش باتجاه مركز الرَسْمة، مما سيُعطِي انطباعًا زائفًا بوجود حركة لا نهائية (infinite motion). تَعرِض الصورة التالية إِطارًا واحدًا من التَحرِيكة (animation): لنُفكر كيف يُمكِن رَسْم مثل هذه الصورة. عامةً، يُمكِن اِستخدَام حَلْقة التَكْرار while لرَسْم المستطيلات، بحيث تبدأ أولًا برَسْم المستطيل الخارجي، ثُمَّ تنتقل إلى الداخل وهكذا. يَنبغي الآن أن نُفكر بالمُتَغيِّرات التي سنحتاج إليها خلال حَلْقة التَكْرار (loop)، وكذلك بالطريقة التي ستَتغَيَّر بها قيم تلك المُتَغيِّرات من تَكْرار (iteration) معين إلى التَكْرار الذي يليه. ستساعدنا الملاحظات التالية على مَعرِفة تلك المُتَغيِّرات، أولًا، مع كل تَكْرار، يكون المستطيل المرسوم أصغر منه في المرة السابقة، كما أنه يَتحرك للداخل قليلًا. يتركز عامةً الفارق بين أيّ مستطيلين على حجمهما وإِحداثيَّات (coordinates) رُكْنيهما اليساريين العُلوِيين (upper left corners)، ولهذا سنحتاج، أولًا، إلى مُتَغيِّرين لتَمثيِل كلًا من عَرْض المستطيل وارتفاعه، وهما المُتَغيِّران rectWidth و rectHeight على الترتيب. أما بالنسبة لإِحداثيَّات الرُكْن الأيسر العُلوِي x و y، فيُمكِن تَمثيِل كليهما بمُتَغيِّر وحيد للمستطيل الواحد، هو المُتَغيِّر inset؛ لأن قيمتهما مُتساوِية؛ حيث يَبعُد أيّ مستطيل عن حافتي مساحة الرسم (drawing area) بنفس مقدار المسافة. نُلاحِظ أنه مع كل تَكْرار، تَنقُص قيمة كلًا من عَرْض المستطيل rectWidth وارتفاعه rectHeight، بينما تزداد المسافة inset التي يَبعُدها المستطيل عن الحافتين. أخيرًا، تنتهي حَلْقة التَكْرار while عندما يُصبِح عَرْض المستطيل أو ارتفاعه أقل من أو يُساوِي الصفر. اُنظر خوارزمية رَسْم إِطار (frame) وحيد: // املأ مساحة الرسم باللون الأبيض Fill the drawing area with white // ‫اضبط قيمة inset المبدئية للمستطيل الأول الخارجي Set the amount of inset for the first rectangle // اضبط قيمة عرض وارتفاع المستطيل الأول الخارجي Set the width and height for the first rectangle // اضبط اللون المستخدم لتحديد الحواف إلى اللون الأسود Set the stroke color to black // طالما كان العرض والارتفاع أكبر من الصفر while the width and height are both greater than zero: // ‫ارسم مستطيل باستخدام البرنامج الفرعي g.strokeRect draw a rectangle (using the g.strokeRect subroutine) // ‫أزد قيمة inset حتى ينتقل المستطيل التالي إلى الداخل increase the inset (to move the next rectangle over and down) // ‫انقص عرض وارتفاع المستطيل التالي حتى يصبح المستطيل التالي أصغر decrease the width and height (to make the next rectangle smaller) ضُبطَت هذه النسخة من البرنامج بحيث يَبعُد كل مستطيل مسافة قدرها ١٥ بكسل عن المستطيل المُحيِط به، ولهذا فإن قيمة المُتَغيِّر inset تَزداد بمقدار ١٥ بكسل مع كل تَكْرار. في المقابل، يَتقَلص المستطيل حوالي ١٥ بكسل يمينًا ويسارًا، أيّ يَنقُص عَرْض المستطيل بمقدار ٣٠ بكسل. وبالمثل، يَنقُص ارتفاعه بمقدار ٣٠ بكسل مع كل تَكْرار ضِمْن الحلقة. يَسهُل إعادة كتابة الخوارزمية بلغة الجافا، لكن تتبقَى فقط حاجتنا إلى معرفة القيم المبدئية للمُتَغيِّرات inset و width و height لأول مستطيل-(المستطيل الخارجي). لحساب ذلك، سنُفكر في حقيقة كَوْن الصورة متحركة (animated)، أي يَعتمِد ما نَرسِمه بطريقة ما على رقم الإِطار (frame number) الحالي. لمّا كان الرُكْن الأيسر العُلوِي (top-left corner) للمستطيل الخارجي يتحرك للأسفل وللداخل من أيّ إِطار إلى الإِطار الذي يَليه، فإن قيمة المُتَغيِّر inset المبدئية تزداد مع كل إِطار. قد تُفكر إذًا بضَبْط قيمة المُتَغيِّر inset المبدئية إلى القيمة ٠ بالإِطار رقم ٠، وإلى القيمة ١ بالإطار رقم ١ وهكذا. للأسف، لن يكون هذا صالحًا إلى الأبد؛ فعندما تَصِل التحريكة للإِطار ١٥، يَنبغِي أن يَظهر مستطيل خارجي جديد بمساحة الرَسْم (drawing area)، هو في الواقع ليس جديدًا، وإنما أُعيد فقط ضَبْط قيمة المُتَغيِّر inset المبدئية إلى القيمة ٠. إجمالًا، يَنبغِي لقيمة المُتَغيِّر inset أن تأخذ القيم ٠، ١، ٢، ٣،… حتى تَصِل إلى القيمة ١٤، لتُعاد الكَرَّة من جديد، وهو ما يُمكِن إنجازه باِستخدَام الشيفرة التالية: inset = frameNumber % 15; لاحِظ أن المستطيل يَملأ مساحة الرسم باستثناء حافة (border) تُحيِط به، عَرْضها يُساوِي قيمة المُتَغيِّر inset، أي بعبارة آخرى، عَرْض المستطيل هو عَرْض مساحة الرسم مطروحًا منه ضعف قيمة المُتَغيِّر inset، وبالمثل لارتفاعه. انظر شيفرة البرنامج الفرعي drawFrame()‎ كاملة بالأسفل والمسئولة عن تَحرِيك المستطيل: public void drawFrame(GraphicsContext g, int frameNumber, double elapsedSeconds, int width, int height) { g.setFill(Color.WHITE); g.fillRect(0,0,width,height); // املأ مساحة الرسم باللون الأبيض // المسافة بين بين المستطيل الخارجي ومساحة الرسم double inset; double rectWidth, rectHeight; // عرض وطول أحد المستطيلات // اضبط اللون المستخدم لرسم حواف المستطيل g.setStroke(Color.BLACK); // إضافة القيمة 0.5 هو أسلوب للحصول على صورة أكثر وضوحًا inset = frameNumber % 15 + 0.5; rectWidth = width - 2*inset; rectHeight = height - 2*inset; while (rectWidth >= 0 && rectHeight >= 0) { g.strokeRect(inset, inset, rectWidth, rectHeight); inset += 15; // تبعد المستطيلات عن بعضها بمقدار 15 بكسل rectWidth -= 30; rectHeight -= 30; } } البرنامج مُتاح بالكامل بالملف MovingRects.java. يُمكِنك أيضًا الإِطلاع على مثال توضيحي آخر للتحريك (animation) بالملف RandomCircles.java، والذي يُضيِف قرصًا ملونًا (colored disk) بشكل عشوائي مع كل إِطار جديد. سيُظهِر لك هذا المثال أن صورة الإِطار لا تُحذَف تلقائيًا قبل إِعادة رسم الإِطار التالي. ترجمة -بتصرّف- للقسم Section 9: Introduction to GUI Programming من فصل Chapter 3: Programming in the Small II: Control من كتاب Introduction to Programming Using Java.
  18. تناولنا، بالأقسام السابقة من هذا الفصل، جميع بُنَى التحكُّم (control structures) المُدعَّمة بلغة الجافا، ولكن قبل الانتقال إلى الفصل التالي، سنُلقِي نظرة مبدئية على موضوعين إِضافيين مُرتبطين نوعًا ما ببُنَى التحكُّم. سنُلقِي في هذا القسم نظرة خاطفة على المصفوفات (arrays)، والتي تُعدّ واحدة من أكثر هياكل البيانات (data structure) شيوعًا. بالإضافة إلى ذلك، تَتطلَّب معالجة المصفوفات (array processing) اِستخدَام بُنَى التحكُّم، وهو ما سيمنحك الفرصة لتطبيق بعضًا مما تَعلَّمته عن بُنَى التحكُّم. سنتحدث في الفصل التالي عن الرسومات الحاسوبية (computer graphics)، والتي ستسمح لك أيضًا باستخدام بُنَى التحكُّم، ولكن بسياق مُختلِف نوعًا ما. إنشاء المصفوفات واستخدامها يتكون الهيكل البياني (data structure) من مجموعة من العناصر (items)، مُجمَّعة معًا بحيث يُمكِن التَعامُل معها كوحدة واحدة (unit). تُعدّ المصفوفة (array) هيكلًا بيانيًا (data structure)، تُرتَّب فيه العناصر كمتتالية مُرقَّمَة (numbered sequence)، بحيث يُمكِن الإشارة إلى كل عنصر مُفرد بها بواسطة رقم المَوْضِع (position number) خاصته. تَشترِط لغة الجافا -بخلاف بعض لغات البرمجة الأخرى- أن تكون جميع عناصر (items) المصفوفة من نفس النوع، كما يبدأ العدّ فيها دائمًا من الصفر. ستحتاج إلى التَعرُّف على عدة مصطلحات جديدة لتتمكَّن من الحديث عن المصفوفات: أولًا، طول/حجم المصفوفة (length/size of array) هو عدد العناصر (items) الموجودة بها، أما نوع المصفوفة الأساسي (base type of array) فهو نوع عناصرها المُفردة، وأخيرًا، فهرس العنصر (index) هو رقم المَوْضِع (position number) الخاص بالعنصر داخل المصفوفة. لنفْترِض أنك تريد كتابة برنامج يُمكِنه معالجة الأسماء الخاصة بألف شخص، يَعنِي ذلك أنك في حاجة إلى طريقة للتَعامُل مع كل هذه البيانات. ربما ظننت -قبل تَعرُّفك على هيكل المصفوفة البياني (array)- أن البرنامج سيحتاج إلى ألف مُتَغيِّر ليَحمِل أسماء كل هذه الأشخاص، وأنك ربما ستحتاج إلى ألف تَعْليمَة طباعة كي تَتَمكَّن من طباعة كل هذه الأسماء. سيكون ذلك ضَرْبًا من العبث بكل تأكيد. تستطيع، في الواقع، وضع جميع الأسماء بمصفوفة (array)، يُمثِلها مُتَغيِّر وحيد يَحمِل قائمة الأسماء كاملة. في هذا المثال، لمّا كان هناك ألف اسم مفرد، فإن طول المصفوفة (length) سيُساوِي 1000. ولمّا كان كل عنصر بالمصفوفة من النوع String، فإن نوع المصفوفة الأساسي (base type) هو الصَنْف String. وأخيرًا، سيكون أول اسم داخل المصفوفة بالفهرس 0 (index)، بينما سيكون ثاني اسم بالفهرس 1، وهكذا حتى نَصِل إلى الاسم الأخير، والذي سيكون بالفهرس 999. يُمكِن لنوع المصفوفة الأساسي (base type of array) أن يكون أي نوع مُدعَّم بلغة الجافا، ولكننا سنكتفي حاليًا بمعالجة المصفوفات التي إِمّا أن يكون نوعها الأساسي هو النوع String أو أن يكون واحدًا من الأنواع الأوَّليّة (primitive types) الثمانية. يُطلَق اسم مصفوفة الأعداد الصحيحة (array of ints) على المصفوفات التي يكون نوعها الأساسي هو int، بينما يُطلَق اسم مصفوفة السَلاسِل النصية (array of Strings) على المصفوفات التي يكون نوعها الأساسي هو String. مع ذلك، لا تُعدّ المصفوفة -إذا أردنا تَحرِي الدقة- قائمة من قيم الأعداد الصحيحة (integers) أو قيم السَلاسِل النصية (strings) أو حتى أيّ قيم اخرى، فمن الأفضل أن تُفكِر بها كقائمة من المُتَغيِّرات من النوع العددي int أو كقائمة من المُتَغيِّرات من النوع String أو من أي نوع آخر، فدائمًا ما يكون هناك خَلطًا (confusion) مُحتمَلًا بين اِستخدَام المُتَغيِّرات (variables) كاسم لمَوْضِع ما بالذاكرة (memory location)، واِستخدَامها كاسم للقيمة المُخزَّنة بهذا المَوْضِع. يُعامَل أي مَوْضِع (position) بالمصفوفة كمُتَغيِّر (variable)، فيُمكِن لهذا المَوْضِع أن يَحمِل قيمة من نوع معين (نوع المصفوفة الأساسي) مثلما يُمكِن لأيّ مُتَغيِّر أن يَحمِل قيمة، كما يُمكِن لتلك القيمة أن تَتغيَّر بأيّ وقت مثلما يُمكِن لقيمة أي مُتَغيِّر أن تَتغيَّر. عادة ما يُطلَق اسم عناصر المصفوفة (elements of array) على مجموعة المُتَغيِّرات المُفردة (individual variables) الموجودة بالمصفوفة وتُكَوِّنها إجمالًا. عندما تُستخدَم مصفوفة ببرنامج ما، فيُمكِنك -كما ذَكَرت مُسْبَّقًا- الإشارة إليها ككل باِستخدَام مُتَغيِّر، لكنك عادةً ما ستحتاج إلى الإشارة إلى عناصر المصفوفة المُفردة (elements of array) من خلال اسم معين، والذي يَعتمِد على كُلًا من اسم المصفوفة ككل وفهرس (index) العنصر، فتكون صياغة (syntax) الاسم كالتالي namelist[7]‎، حيث namelist هو المُتَغيِّر الذي يُمثِل المصفوفة ككل، بينما يُشيِر namelist[7]‎ إلى العنصر الموجود بالفهرس ٧ بتلك المصفوفة. أي يُستخدَم اسم المصفوفة متبوعًا بفهرس العنصر ضِمْن أقواس معقوفة (square brackets) [ ] للإشارة إلى هذا العنصر بتلك المصفوفة. يُعامَل اسم العنصر بهذه الصياغة كأي مُتَغيِّر آخر، فيمكنك أن تُسنِد قيمة إليه، أو أن تَطبَعه، أو أن تَستخدِمه بأيّ تعبير (expression). تَحتوِي أيّ مصفوفة على مُتَغيِّر -نوعًا ما- يُمثِل طولها (length). فعلى سبيل المثال، إذا كان لديك مصفوفة namelist، فإنك تَستطيع اِستخدَام namelist.length للإشارة إلى طولها. ومع ذلك، لا يُمكِنك إسْنَاد (assign) قيمة إلى ذلك المُتَغيِّر؛ لأنه لا يُمكِن تَغْيِير طول المصفوفة. يجب أولًا أن تُصَرِّح (declaration) عن مُتَغيِّر مصفوفة (array variable)؛ حتى تَتَمكَّن من اِستخدَامه للاشارة إلى مصفوفة معينة. تَتوفَّر أنواع المصفوفة (array types) لتَخْصِيص نوع تلك المُتَغيِّرات، وبشكل عام، فإن نوع مصفوفة معينة (array type) يتكون من نوع المصفوفة الأساسي (base type) متبوعًا بزوج من الأقواس المعقوفة (square brackets) الفارغة. فمثلًا، يكون مُتَغيِّر مصفوفة (array variable) من النوع String[]‎ في حالة إشارته إلى مصفوفة سَلاسِل نصية (array of Strings)، بينما يكون من النوع int[]‎ في حالة إشارته إلى مصفوفة أعداد صحيحة (array of ints). اُنظر المثال التالي: String[] namelist; int[] A; double[] prices; يُمكِن للمُتَغيِّرات المُصرَّح عنها بتلك الطريقة الإشارة إلى المصفوفات، لكن لاحِظ أن مُجرَّد التَّصْريح عن المُتَغيِّر (variable declaration) لا يَتسبَّب بالإنشاء الفعليّ للمصفوفة. ينبغي إِسْناد قيمة لمُتَغيِّر المصفوفة (array variable) قبل اِستخدَامه -مثلما هو الحال مع جميع المُتَغيِّرات-، وفي هذه الحالة، تكون القيمة عبارة عن مصفوفة. تَتوفَّر صياغة خاصة (special syntax) للإِنشاء الفعليّ للمصفوفة؛ وذلك لأن المصفوفات -بلغة الجافا- هي بالأساس كائنات (objects)، وهو ما سنؤجل الحديث عنه؛ فهو غَيْر ذي صلة هنا. إجمالًا، يُستخدَم العَامِل new لإنشاء المصفوفات، انظر الأمثلة التالية: namelist = new String[1000]; A = new int[5]; prices = new double[100]; تكون الصياغة كالتالي: <array-variable> = new <base-type>[<array-length>]; يُمكِن اِستخدَام عدد صحيح (integer) أو تعبير من النوع العددي (integer-valued expression) لتَحْدِيد طول المصفوفة . بَعْد تَّنْفيذ تَعْليمَة الإِسْناد A = new int[5];‎، أَصبح المُتَغيِّر A يُشير إلى مصفوفة مُكوَّنة من ٥ عناصر (elements) من النوع العددي (integer)، هي A[0]‎ و A[1]‎ و A[2]‎ و A[3]‎ و A[4]‎، كما أصبح A.length يَحمِل القيمة ٥. اُنظر الصورة التالية: عند إِنشاء مصفوفة أعداد صحيحة (array of int)، تُهيَئ (initialized) جميع عناصر المصفوفة أُتوماتيكيًا بحيث تَحمِل القيمة صفر، مما يَعنِي أن قيم جميع عناصر أيّ مصفوفة أعداد تكون مُساوِية للصفر بمُجرَّد إِنشاؤها. في المقابل، تَحمِل جميع عناصر أيّ مصفوفة قيم منطقية (array of boolean) القيمة المنطقية false بمُجرَّد إِنشاؤها. أمّا عناصر أيّ مصفوفة محارف (array of char) فتحتوي على المحرف المقابل لقيمة ترميز اليونيكود (Unicode code) رقم صفر ‎\u0000 بمُجرَّد إِنشاؤها. وأخيرًا، فإن القيمة المبدئية لعناصر أيّ مصفوفة سَلاسِل نصية (array of String) تكون القيمة الفارغة null (تُستخدَم تلك القيمة مع الكائنات [objects] ولن نتَعرَّض لها حتى القسم ٥.١ لاحقًا). المصفوفات وحلقات التكرار For تُعدّ إِمكانية اِستخدَام مُتَغيِّر من النوع العددي (integer) أو حتى اِستخدَام تعبير من النوع العددي (integer-valued expression) كفهرس للعنصر (index of an element) واحدة من أهم مميزات المصفوفات. فعلى سبيل المثال، إذا كان لديك مصفوفة list، ومُتَغيِّر من النوع العددي الصحيح i، فبإمكانك اِستخدَام list‎ أو حتى list[2*i+1]‎ كأسماء مُتَغيِّرات، بحيث تُؤثِر قيمة i على ما يُشيِر إليه المُتَغيِّر فعليًا. يُساعدك ذلك في حالة أردت إِجراء معالجة معينة على جميع عناصر المصفوفة؛ حيث ستستطيع القيام بذلك ضِمْن حَلْقة التَكْرار for. فمثلًا، لطباعة جميع عناصر المصفوفة list، يُمكننا كتابة الآتي: int i; // فهرس المصفوفة for (i = 0; i < list.length; i++) { System.out.println( list[i] ); } في أول مرة تُنْفَّذ فيها الحَلْقة (loop) -أيّ خلال أول تَكْرار (iteration)-، ستكون قيمة i مُساوِية للصفر، أيّ سيُشيِر list‎ إلى list[0]‎، ولذا تُطبَع القيمة المُخزَّنة بالمُتَغيِّر list[0]‎. في المرة الثانية، ستكون قيمة i مُساوِية للواحد، ولذا تُطبَع القيمة المُخزَّنة بالمُتَغيِّر list[1]‎. لمّا كان طول المصفوفة list يُساوِي ٥، فستنتهي حَلْقة التَكْرار (loop) بعد طباعة قيمة المُتَغيِّر list[4]‎؛ لأن قيمة i ستُصبِح مُساوِية للقيمة ٥، مما يَعنِي أن الشَّرْط الاستمراري (continuation condition) للحلقة i < list.length‎‎ لم يَعُدْ مُتحقِّقًا بَعْد الآن. لاحِظ أن الشيفرة بالأعلى تُعدّ مثالًا نموذجيًا لاِستخدَام حَلْقة تَكْرار بغرض معالجة مصفوفة. لنفْحَص عدة أمثلة اخرى، بفَرْض أن A هي مصفوفة أعداد حقيقية (array of double)، وكنا نُريد حِسَاب قيمة متوسط (average) جميع عناصر تلك المصفوفة. يُمكِننا ببساطة اِستخدَام حَلْقة التَكْرار for لحِسَاب حاصل مجموع الأعداد، ومِنْ ثَمَّ نُقسمها على طول المصفوفة، كالتالي: double total; // حاصل مجموع الأعداد بالمصفوفة double average; // قيمة متوسط الأعداد int i; // فهرس المصفوفة total = 0; for ( i = 0; i < A.length; i++ ) { // ‫أضف قيمة العنصر برقم الموضع i إلى حاصل المجموع total = total + A[i]; } average = total / A.length; // ‫A.length هي عدد العناصر مِثال آخر هو محاولة إِيجاد أكبر عَدَد بالمصفوفة A من المثال السابق. سنُنشِئ مُتَغيِّر max، بحيث نُخزِن فيه قيمة أكبر عَدَد مرَّرنا به حتى الآن، ثم سنبدأ بالمرور على جميع عناصر المصفوفة، وأينما وَجدنا عَدَد أكبر من قيمة المُتَغيِّر max الحالية، فإننا سنُسنِد ذلك العَدَد إلى المُتَغيِّر max. بعد الانتهاء من معالجة المصفوفة بالكامل، سيَحتوِي المُتَغيِّر max حتمًا على أكبر عدد داخل المصفوفة ككل. ولكن يَبقى السؤال التالي، ما هي القيمة الأوَّليّة للمُتَغيِّر max؟ ربما ببساطة نَستخدِم قيمة أول عنصر بالمصفوفة، أي A[0]‎، للتهيئة المبدئية للمُتَغيِّر max، ومِنْ ثَمَّ نبدأ عملية البحث عن قيمة أكبر منها بباقي عناصر المصفوفة، أي بدءً من العنصر A[1]‎: double max; // اكبر عدد حتى الآن max = A[0]; // في البداية، أكبر عدد هو قيمة العنصر‫ A[0] int i; for ( i = 1; i < A.length; i++ ) { if (A[i] > max) { max = A[i]; } } // ‫بالوصول إلى تلك النقطة، يحتوي max على قيمة أكبر عدد قد تَحتاج أحيانًا إلى معالجة بعض عناصر المصفوفة (elements of the array) وليس كلها. تُستخدَم تَعْليمَة if، في هذه الحالة، بداخل حَلْقة التَكْرار for لتَحْدِيد ما إذا كُنت تُريد معالجة العنصر الحالي أم لا. دعنا نُلقِي نظرة أخرى على مسألة حِسَاب قيمة متوسط (average) عناصر مصفوفة معينة، ولكن في هذه المرة، لنفْترِض أننا نُريد حِسَاب قيمة المتوسط فقط للعناصر غَيْر الصفرية (non-zero elements)، أيّ التي لا تحتوي على القيمة صفر. في هذه الحالة، قد يكون عدد تلك العناصر أقل من طول المصفوفة (length)، ولهذا سنحتاج إلى عدّ العناصر غَيْر الصفرية، والتي أُضيفت فعليًا لحاصل المجموع بدلًا من الاعتماد على طول المصفوفة. انظر الشيفرة التالية: double total; // حاصل مجموع الأعداد غير الصفرية بالمصفوفة int count; // عدد الأعداد غير الصفرية double average; // متوسط الأعداد غير الصفرية int i; total = 0; count = 0; for ( i = 0; i < A.length; i++ ) { if ( A[i] != 0 ) { total = total + A[i]; // أضف قيمة العنصر إلى حاصل المجموع count = count + 1; // أزد قيمة العداد } } if (count == 0) { System.out.println("There were no non-zero elements."); } else { average = total / count; // اِقسم حاصل المجموع على عدد العناصر System.out.printf("Average of %d elements is %1.5g%n", count, average); } الجلب العشوائي (Random Access) اِستخدَمت جميع أمثلة معالجة المصفوفات -التي فَحْصناها حتى الآن- الجَلْب المُتتالي (sequential access)، أي عُولَجت عناصر المصفوفة (elements of the array) بنفس ترتيب حُدُوثها بالمصفوفة واحدًا تلو الآخر. مع ذلك، يُعدّ الجَلْب العشوائي (random access) واحدًا من أهم مميزات المصفوفات، حيث تستطيع الولوج لقيمة أيّ عنصر بالمصفوفة بأيّ وقت وبنفس الكفاءة وعلى قَدَم المُساواة. دعنا نُلقِي نظرة على إِحدى المسائل المشهورة والمعروفة باِسم مُعْضِلة يوم الميلاد (birthday problem). بفَرْض وجود مجموعة من الأشخاص داخل حجرة، وليَكُن عَدَدهم هو N، فما هي احتمالية أن يكون لاثنين من هؤلاء الأشخاص نفس يوم الميلاد؟ (بمعنى أنهما قد وُلدَا بنفس اليوم، والشهر، ولكن ليس ضروريًا ولادتهما بنفس العام). يُقَلِّل غالبية الناس من شأن هذه الاحتمالية بشدة. سنَفْحَص، في الواقع، نسخة أخرى شبيهة من نفس السؤال: بفَرْض اختيارك لمجموعة أشخاص بشكل عشوائي، بحيث تَفْحَص يوم ميلادهم، فكم عَدَد الأشخاص الذين ستحتاج إلى فَحْص يوم ميلادهم قبلما تَجِد اثنين لهما نفس يوم الميلاد؟ تَعتمِد الإجابة على مِثل هذا السؤال على عدة عوامل عشوائية بالتأكيد، ولكن مع ذلك، يُمكِننا مُحاكاة هذه التجربة باِستخدَام برنامج، بحيث نُنْفِّذه عدة مرات، وهو ما سيُعْطِينا مُؤِشرًا تقريبيًا لعدد هؤلاء الأشخاص. لمُحاكاة هذه التجربة، سنحتاج إلى معرفة جميع أيام الميلاد التي قد وَجدناها حتى الآن. لمّا كان هناك ٣٦٥ يوم ميلاد مُحتمَل (سنتجاهل الأعوام الكبيسة [leap years])، فإننا سنُنشِئ مصفوفة قيم منطقية (array of boolean) طولها (length) يُساوِي ٣٦٥، بحيث يُناظِر كل عنصر فيها يوم ميلاد مُختلِف، ويُعْلِمنا إذا ما كُنا قد وَجدنا شخصًا بنفس يوم الميلاد المُناظِر للعنصر أم لا. اُنظر الشيفرة التالية: boolean[] used; used = new boolean[365]; بدايةً، ستُرقَّم أيام العام من ٠ إلى ٣٦٤، بحيث يُحدِّد المُتَغيِّر used‎ -من النوع المنطقي- ما إذا سبق وأن وَجدنا شخصًا يوم ميلاده هو رقم اليوم (day number)‏ i أم لا، مما يَعنِي أنه سيَحمِل القيمة المنطقية true في تلك الحالة. مبدئيًا، ستكون جميع قيم عناصر المصفوفة used مُساوِية للقيمة المنطقية false، وهو في الواقع ما يَحدُث أتوماتيكيًا عند إِنشاء المصفوفة. عند اختيارنا لشخص ما، يوم ميلاده هو رقم اليوم i، فإننا سنَفْحَص أولًا قيمة المُتَغيِّر used‎، فإذا كانت مُساوِية للقيمة المنطقية true، سيَعنِي ذلك أننا قد وَجدنا الشخص الثاني بنفس يوم الميلاد، ومِنْ ثَمَّ فإننا قد انتهينا. أمّا إذا كانت قيمته مُساوِية للقيمة المنطقية false، فسنَضبُط قيمة المُتَغيِّر used‎ إلى القيمة المنطقية true، للإشارة إلى كَوْننا قد وجدنا الشخص الأول بيوم الميلاد ذاك. بَعْد ذلك، سنستمر بتَّنْفيذ البرنامج، فنختار الشخص التالي. اُنظر البرنامج بالأسفل (لاحِظ أننا لم نُحاكِي الأشخاص، فقط أيام الميلاد): public class BirthdayProblem { public static void main(String[] args) { // لتخزين أيام الميلاد التي وجدنا أشخاص ولدوا بها boolean[] used; // عدد الأشخاص الذين تم فحص أيام ميلادهم int count; // ‫القيمة المبدئية لجميع العناصر هي false used = new boolean[365]; count = 0; while (true) { // ‫اختر يوم ميلاد بصورة عشوائية من صفر وحتى 364 int birthday; // يوم الميلاد المختار birthday = (int)(Math.random()*365); count++; System.out.printf("Person %d has birthday number %d%n", count, birthday); if ( used[birthday] ) { // وجدنا يوم الميلاد هذا من قبل، انتهينا break; } used[birthday] = true; } // نهاية‫ while System.out.println(); System.out.println("A duplicate birthday was found after " + count + " tries."); } } // ‫نهاية الصنف BirthdayProblem ينبغي عليك أن تَقضِي بعض الوقت لدراسة البرنامج بالأعلى؛ وذلك حتى تَفهَم طريقة عمله، وطريقة اِستخدَامه للمصفوفة. شَغِّله أيضًا! ستكتشف أن احتمالية تَكْرار أيام الميلاد ربما هي أكبر مما كنت تَتوقَّع. المصفوفات الممتلئة جزئيًا (Partially Full) لنفْترِض أن لدينا تطبيقًا، بحيث يتغَيَّر -أثناء تَّنْفيذه- عَدَد العناصر المطلوب تخزينها بمصفوفة ما. لمّا كان من غَيْر المُمكن تَغْيِير طول/حجم (length/size) المصفوفة، كان لابُدّ لنا من اِستخدَام مُتَغيِّر مُنفصل لعدّ المَواضِع المُستخدَمة فعليًا بالمصفوفة. (تَحتوِي بالطبع جميع المَواضِع بأيّ مصفوفة على قيمة ما، ولكن ما يُهِمّنا هو عَدَد المَواضِع التي تَحتوِي على عناصر صالحة ومفيدة). على سبيل المثال، يَقرأ برنامج ما الأعداد الصحيحة الموجبة (positive integers)، المُدْخَلة مِنْ قِبَل المُستخدِم، بحيث يَتوقَف البرنامج عن القراءة عند إِدْخَال عدد أقل من أو يُساوِي الصفر. يحتاج البرنامج لتَخْزِين الأعداد المُدْخَلة بهدف مُعالجتها لاحقًا (later processing)، ولذلك فإنه يَحتفِظ بها داخل مصفوفة من النوع int[]‎، هي المصفوفة numbers. بفَرْض أنه لن يتمّ إِدْخَال أكثر من ١٠٠ عدد، فإن حجم (length/size) تلك المصفوفة سيكون ١٠٠. نحتاج الآن إلى الإجابة على السؤال التالي: عند إضافة قيمة عنصر (member) جديد بالمصفوفة، بأيّ مَوْضِع سنضعه تحديدًا؟ للإجابة على هذا السؤال، سنحتاج إلى معرفة عَدَد الأعداد التي تمّ فعليًا قرائتها وتَخْزِينها بالمصفوفة، أيّ عَدَد المَواضِع المُستخدَمة فعليًا بالمصفوفة، ولهذا سنَستخدِم مُتَغيِّر من النوع العددي (integer)، وليَكُن المُتَغيِّر count. سيَعمَل هذا المُتَغيِّر كعَدَّاد (counter)، أيّ أننا سنزيد قيمة هذا المُتَغيِّر بمقدار الواحد في كل مرة نُخزِّن فيها عدد جديد بالمصفوفة. الآن، لمّا كان العدد الفعليّ للعناصر يُساوي قيمة المُتَغيِّر count، فلابُدّ أن تلك العناصر مُخزَّنة بالأرقام المَوْضِعية (position numbers) ٠، ١، …، وحتى count - 1، ولذلك فإن رقم المَوْضِع (position number) المتاح التالي هو قيمة المُتَغيِّر count. وعليه، فإن هذا هو المَوْضِع الذي سنُخزِّن فيه العنصر الجديد. مثال آخر هو برنامج يَقرأ الأعداد المُدْخَلة من قِبَل المُستخدِم، وبحيث يَتوقَف عن القراءة عند إِدْخَال عدد يُساوِي الصفر. الهدف من البرنامج هو طباعة تلك الأعداد بترتيب مُعاكِس (reverse order) للترتيب الأصلي الذي أُدْخلت به. قد يبدو هذا المثال سخيفًا نوعًا ما، ولكنه على الأقل يَتطلَّب أن تكون الأعداد مُخزَّنة بمصفوفة، بعكس أنواع آخرى كثيرة من المعالَجات، والتي يُمكِن إِجراؤها دون الحاجة إلى الاِحتفاظ بقيم الأعداد المُفردة (individual numbers) بذاتها، مثل إِيجاد حاصل مجموع عناصر المصفوفة أو قيمة متوسط تلك العناصر أو قيمة أكبر عدد بها. import textio.TextIO; public class ReverseInputNumbers { public static void main(String[] args) { int[] numbers; // مصفوفة لتخزين القيم المدخلة int count; // عدد الأعداد المخزنة فعليًا بالمصفوفة int num; // أحد الأعداد المدخلة من قبل المستخدم int i; // متغير حلقة‫ for // مصفوفة أعداد صحيحة بطول 100 numbers = new int[100]; // لم يتم إدخال أي أعداد بعد count = 0; System.out.println("Enter up to 100 positive integers; enter 0 to end."); // اقرأ الأعداد وأضفها إلى المصفوفة while (true) { System.out.print("? "); num = TextIO.getlnInt(); // الصفر هو إشارة لانتهاء عملية الإدخال if (num <= 0) { break; } numbers[count] = num; // ‫خزن العدد برقم الموضع count count++; // أزد العداد بمقدار واحد } System.out.println("\nYour numbers in reverse order are:\n"); for ( i = count - 1; i >= 0; i-- ) { System.out.println( numbers[i] ); } } // ‫نهاية main(); } // ‫نهاية الصنف ReverseInputNumbers مِنْ المُهم أن تُدرِك الدور المزدوج الذي يؤديه المُتَغيِّر count. فبالإضافة إلى كَوْنه يَحمِل عدد العناصر التي أُدْخلت فعليًا إلى المصفوفة، فإنه أيضًا يُمثِل فهرس (index) المَوْضِع التالي المُتاح بالمصفوفة. عندما يَحيِن موعد طباعة الأعداد الموجودة بالمصفوفة، يكون رقم المَوْضِع الأخير المُستخدَم فعليًا بالمصفوفة count - 1، ولذا تَطبَع حَلْقة التَكْرار for قيم عناصر المصفوفة بداية من رقم المَوْضِع count - 1، ونزولًا إلى المَوْضِع صفر. يُعدّ هذا مثالًا جيدًا لمعالجة عناصر مصفوفة ما بترتيب مُعاكِس (reverse order). قد تتساءل، ماذا سيَحدُث بالبرنامج إذا حاول المُستخدِم إِدْخَال أكثر من ١٠٠ عدد؟ ستكون النتيجة حُدوث خطأ (error) يَتسبَّب بانهيار (crash) البرنامج؛ فعندما يُدْخِل المُستخدِم العدد رقم ١٠١، سيُحاول البرنامج تَخْزِين ذلك العدد بعنصر مصفوفة number[100]‎. لكن، في الواقع، هذا العنصر غَيْر موجود؛ حيث يُوجد فقط ١٠٠ عنصر بالمصفوفة، وفهرس آخر عنصر بها هو ٩٩، ولذلك ستؤدي محاولة اِستخدَام number[100]‎ إلى حُدوث اِعتراض (exception) من النوع ArrayIndexOutOfBoundsException. تُعدّ الاعتراضات من هذا النوع مصدرًا شائعًا لحُدوث أخطاء وقت التَّنْفيذ (run-time errors) بالبرامج التي تَستخدِم المصفوفات. المصفوفات ثنائية البعد (Two-dimensional) تُعدّ المصفوفات التي تَعامَلنا معها حتى الآن أحادية البعد (one-dimensional)، مما يَعنِي أن المصفوفة تتكون من متتالية من العناصر، والتي يُمكِن تَخَيُّلها وكأنها مَوْضوعة على خط (line). تَتوفَّر أيضًا المصفوفات ثنائية البعد (two-dimensional)، والتي تُوضَع فيها العناصر داخل شبكة مستطيلة الشكل (rectangular grid). سنمر على هذا الموضوع باختصار هنا، ونعود إليه مُجددًا بالقسم ٧.٥. تُرتَّب عناصر المصفوفة ثنائية البعد (2D/two-dimensional array) بصورة صفوف (rows) وأعمدة (columns). على سبيل المثال، تتكون مصفوفة الأعداد الصحيحة ثنائية البعد (2D array of int) التالية من ٥ صفوف و ٧ أعمدة: تتكون الشبكة ٥*٧ بالأعلى من ٣٥ عنصر. تُرقَّم الصفوف بالمصفوفة ثنائية البعد كالتالي: ٠، ١، ٢، …، وحتى عدد الصفوف ناقص واحد. بالمثل، تُرقَّم العواميد من ٠ وحتى عدد العواميد ناقص واحد. يُمكِن الولوج لأيّ عنصر مُفرد (individual element) بالمصفوفة من خلال رقمي الصف (row number) والعمود (column number) خاصته. (لا تبدو المصفوفة بذاكرة الحاسوب كالصورة المَعروضة بالأعلى، فهي مُجرَّد توضيح للبناء المنطقي [logical structure] للمصفوفة) تُشبه صيغة (syntax) المصفوفات ثنائية البعد (two-dimensional arrays) بلغة الجافا نفس تلك الصيغة المُستخدَمة مع المصفوفات أحادية البعد (one-dimensional arrays)، باستثناء وجود فهرس (index) إِضافي؛ لأن الولوج لأيّ عنصر أَصبح يَتطلَّب كُلًا من رقمي الصف (row number) والعمود (column number). على سبيل المثال، إذا كانت A مصفوفة أعداد صحيحة (array of int) ثنائية البعد، فسيُشيِر A[3][2]‎ إلى العنصر الموجود بالصف رقم ٣ والعمود رقم ٢، والذي يَحمِل العدد ١٧، كما هو موضح بالمصفوفة بالأعلى. كذلك، سيكون نوع مُتَغيِّر المصفوفة، في هذه الحالة، هو كلمة int متبوعة بزوجين من الأقواس المعقوفة (square brackets) الفارغة، أيّ int[][]‎. يمكنك كتابة التالي للتَّصْريح (declare) عن مُتَغيِّر مصفوفة (array variable)، بالإضافة إلى إِنشاء تلك المصفوفة: int[][] A; A = new int[5][7]; يُنشِئ السطر الثاني مصفوفة ثنائية البعد (2D array) مُكوَّنة من ٥ صفوف و ٧ أعمدة. غالبًا ما تُستخدَم حَلْقات التَكْرار for المُتداخِلة (nested) لمعالجة المصفوفات ثنائية البعد. على سبيل المثال، تَطبَع الشيفرة التالية عناصر المصفوفة A: int row, col; // متغيرات التحكم بالحلقة احدهما للصف والآخر للعمود for ( row = 0; row < 5; row++ ) { for ( col = 0; col < 7; col++ ) { System.out.printf( "%7d", A[row][col] ); } System.out.println(); } يُمكِن أن يكون النوع الأساسي (base type) لمصفوفة ثنائية البعد أيّ شئ، أيّ أنك تستطيع إِنشاء مصفوفات ثنائية البعد من النوع العددي double، أو النوع String، وهكذا. توجد عدة اِستخدَامات طبيعية للمصفوفات ثنائية البعد (2D arrays)، والتي تَكُون فيها الشبكة (grid) واضحة مرئيًا. فمثلًا، قد تُخزِن مصفوفة ثنائية البعد محتويات اللوحة (board) بألعاب مثل الشطرنج أو الدَّامَا (checkers). يُوظِّف مثال آخر، بالقسم الفرعي ٤.٧.٣، مصفوفة ثنائية البعد لحفظ ألوان شبكة (grid) مُكوَّنة من مربعات ملونة. يُمكِنك أيضًا تَوظِيف المصفوفات ثنائية البعد بمسائل لا تكون فيها الشبكة واضحة مرئيًا. فمثلًا، لنفْترِض وجود شركة تَملك ٢٥ مَخزنًا. تَحتفِظ الشركة ببيانات الأرباح التي كَسَبَها كل مَخزن شهريًا طوال عام ٢٠١٨. إذا كانت المخازن مُرقَّمة من ٠ إلى ٢٤، وكانت الشهور الاثنى عشر، أي من يناير ٢٠١٨ وحتى ديسمبر ٢٠١٨، مُرقَّمة من ٠ إلى ١١، فمِنْ ثَمَّ، يُمكِن تَخْزِين هذه البيانات بمصفوفة تَحمِل اسم profit، وتُنشَئ كالتالي: double[][] profit; profit = new double[25][12]; يُمثِل المُتَغيِّر profit[3][2]‎ قيمة أرباح المَخزن رقم ٣ بشهر مارس. بصورة أعم، يُمثِل المُتَغيِّر profit[storeNum][monthNum]‎ قيمة أرباح المَخزن رقم storeNum بالشهر رقم monthNum (تذكر أن الترقيم يبدأ من صفر.) لنفْترِض أن مصفوفة الأرباح profit ممتلئة بالبيانات بالفعل. وعليه، يمكن معالجة هذه البيانات بطرائق كثيرة شيقة. على سبيل المثال، يُمكِن حِسَاب الأرباح الكلية للشركة -جميع المَخازن طوال عام ٢٠١٨- بحِسَاب قيمة حاصل مجموع جميع عناصر المصفوفة، كالتالي: double totalProfit; // الأرباح الكلية للشركة بعام 2018 int store, month; // متغيرات حَلقتي التكرار totalProfit = 0; for ( store = 0; store < 25; store++ ) { for ( month = 0; month < 12; month++ ) totalProfit += profit[store][month]; } تحتاج أحيانًا إلى معالجة صف وحيد أو عمود وحيد بالمصفوفة، وليس المصفوفة بكاملها. على سبيل المثال، تستطيع اِستخدَام حَلْقة التَكْرار التالية لحِسَاب قيمة الأرباح الكلية للشركة بشهر ديسمبر، أيّ بالشهر رقم ١١: double decemberProfit; int storeNum; decemberProfit = 0.0; for ( storeNum = 0; storeNum < 25; storeNum++ ) { decemberProfit += profit[storeNum][11]; } قد تجد أن المصفوفات ثنائية البعد (two-dimensional array) مفيدة في بعض الأحيان، ولكنها عمومًا أقل شيوعًا من المصفوفات أحادية البعد (one-dimensional). وفي الواقع، تَسمَح لغة الجافا كذلك بمصفوفات ذات أبعاد أعلى (higher dimension)، ولكن يَنْدُر اِستخدَامها عمليًا. ترجمة -بتصرّف- للقسم Section 8: Introduction to Arrays من فصل Chapter 3: Programming in the Small II: Control من كتاب Introduction to Programming Using Java.
  19. بالإضافة إلى بُنَى التحكُّم (control structures) المُستخدَمة لتَحْدِيد مَسار التحكُّم الطبيعي (flow of control) بالبرنامج، تُوفِّر لغة الجافا طريقة للتَعامُل مع الحالات الاِعتراضية (exceptional cases)، والتي يُمكِنها تَغْيِير مَسار التحكُّم الطبيعي. فمثلًا، يُعدّ إنهاء (terminate) البرنامج وما يَتبعه من طباعة رسالة الخطأ هو مَسار التحكُّم الافتراضي في حالة حُدُوث خطأ أثناء التَّنْفيذ، مع ذلك تَسمَح لغة الجافا بالتقاط (catch) مثل تلك الأخطاء بصورة تَمنع اِنهيار البرنامج (crashing)، وبحيث يَتمكَّن المُبرمِج من الرد بصورة ملائمة على الخطأ. تُستخدَم تَعْليمَة try..catch لهذا الغرض، والتي سنكتفي بإلقاء نظرة مَبدئية وغَيْر مُكتملة عليها بهذا القسم؛ ففي الواقع يُعدّ موضوع مُعالجة الأخطاء (error handling) موضوعًا مُعقدًا نوعًا ما، ولذلك سنتناوله تفصيليًا بالقسم ٨.٣، وعندها سنَسْتَعْرِض التَعْليمَة try..catch بمزيد من التفصيل، ونُوضِح قواعد صياغتها (syntax) كاملة. الاعتراضات يُشير مصطلح الاِعتراض (exception) إلى تلك النوعية من الأحَدََاث التي قد تَرغَب عادةً بمُعالجتها عن طريق تَعْليمَة try..catch. يُفضَّل اِستخدَام هذا المصطلح بدلًا من كلمات مثل خطأ (error)؛ وذلك لأن الاِعتراض في بعض الحالات قد لا يُمثِل خَطأ مِنْ الأساس. ربما يُمكِنك التفكير بالاِعتراض (exception) بعدّه اِستثناءًا بمَسار التحكُّم (flow of control) الطبيعي للبرنامج، أيّ أنه مُجرَّد وسيلة أخرى لتنظيم البرنامج. تُمثَل الاِعتراضات (exceptions) بلغة الجافا باِستخدَام كائنات (objects) من الصَنْف Exception. تُعرَّف (definition) عادةً أصناف فرعية (subclasses) مُشتقَّة من الصَنْف Exception؛ لتمثيل الاِعتراضات الفعليّة، بحيث يُمثِل كل صَنْف فرعي (subclass) نوعًا مختلفًا من الاِعتراضات. سنفْحَص بهذا القسم نوعين فقط من الاِعتراضات، هما: NumberFormatException و IllegalArgumentException. يُمكِن أن يَحدُث الاِعتراض NumberFormatException أثناء محاولة تَحْوِيل سِلسِلة نصية (string) إلى عَدَد (number). تُستخدَم الدالتين Integer.parseInt و Double.parseDouble لإِجراء مثل تلك التَحْوِيلات (انُظر القسم الفرعي ٢.٥.٧). فمثلًا، تَستقبِل الدالة Integer.parseInt(str)‎ مُعامِلًا (parameter) من النوع String، فإذا كانت قيمة المُتَغيِّر str -المُمرَّرة كقيمة لهذا المُعامِل- تُساوِي السِلسِلة النصية "٤٢"، فعندها ستتمكن الدالة من تَحْوِيلها إلى القيمة ٤٢ من النوع int تَحْوِيلًا صحيحًا. في المقابل، إذا كانت قيمة المُتَغيِّر str تُساوِي السِلسِلة النصية "fred"، فعندها ستَفشَل الدالة؛ لأن تلك السِلسِلة لا تُعدّ تَمثيلًا نصيًا (string representation) صالحًا لأيّ قيمة ممكنة من النوع العددي int، لذا سيَحدُث اِعتراض من النوع NumberFormatException في هذه الحالة، وسينهار (crash) البرنامج إذا لم يُعالَج (handle) هذا الاِعتراض. يُمكِن أن يَحدُث الاِعتراض IllegalArgumentException عندما تُمرِّر قيمة غَيْر صالحة كمُعامِل (parameter) إلى برنامج فرعي (subroutine). على سبيل المثال، إذا مَرَّرت قيمة سالبة (negative) كمُعامِل إلى برنامج فرعي، وكان هذا البرنامج الفرعي يَتطلَّب أن تكون قيمة ذلك المُعامِل أكبر من أو تُساوِي الصفر، فمِنْ المُحتمَل أن يَحدُث اِعتراض من النوع IllegalArgumentException. على الرغم من شيوع حُدُوث ذلك الاِعتراض في مثل تلك الحالات، فما يزال لا يُمكننا الجَزْم بحُدُوثه في كل مرة ستُمرِّر فيها قيمة غَيْر صالحة كمُعامِل لبرنامج فرعي؛ ففي الواقع تَتوقَف طريقة التَعامُل مع القيم غَيْر الصالحة على الشخص الذي كَتَب البرنامج الفرعي. تعليمة try..catch عندما يَحدُث اِعتراض (exception)، يُقَال أنه قد بُلِّغ (thrown) عنه. فعلى سبيل المثال، يُبلِّغ Integer.parseInt(str)‎ عن اِعتراض من النوع NumberFormatException إذا كانت قيمة المُتَغيِّر str المُمرَّرة إليه غَيْر صالحة. تُستخدَم تَعْليمَة try..catch لالتقاط (catch) الاِعتراضات، ومَنعها من التَسبُّب بانهيار (crashing) البرنامج. تُكتَب هذه التَعْليمَة بالصياغة (syntax) التالية في أبسط صورها: try { <statements-1> } catch ( <exception-class-name> <variable-name> ) { <statements-2> } قد يُشير الصَنْف -بالأعلى- إلى الصَنْف NumberFormatException أو IllegalArgumentException أو أيّ صَنْف (class) آخر طالما كان من النوع Exception. عندما يُنفِّذ الحاسوب تَعْليمَة try..catch، فإنه سيبدأ بتَّنْفيذ التَعْليمَات الموجودة داخل الجزء try -والمُشار إليها بالتعليمة -، فإذا لم يَحدُث اِعتراض أثناء تَّنْفيذها، فإنه سيتَخَطَّى الجزء catch ليَمضِي قُدُمًا لتَّنْفيذ بقية البرنامج. أما إذا حَدَث اِعتراض من النوع المُحدَّد وِفقًا للعبارة أثناء التَّنْفيذ، فإنه سيَقفِز مُباشرةً من النقطة التي حَدَث فيها الاِعتراض إلى الجزء catch ليُنفِّذ العبارة ، مُتَخطِّيًا بذلك أيّ تَعْليمَات مُتبقِّية ضِمْن العبارة داخل الجزء try. لاحِظ أن تَعْليمَة try..catch بالأعلى ستَلتقِط نوعًا واحدًا فقط من الاِعتراضات -النوع المُحدَّد وِفقًا للعبارة -، أما إذا حَدَث اِعتراض من أي نوع آخر، فسينهار (crash) البرنامج كالعادة. تُمثِل عبارة كائنًا من نوع الاِعتراض المُلتقَط (exception object)، والذي يَتضمَّن معلومات عن سبب حُدُوث الاِعتراض بما في ذلك رسالة الخطأ (error message). تستطيع طباعة قيمة هذا الكائن (object)، على سبيل المثال، أثناء تَّنْفيذ العبارة ، مما سيترتب عليه طباعة رسالة الخطأ. سيَمضِي الحاسوب لتَّنْفيذ بقية البرنامج بَعْد انتهاءه من تَّنْفيذ الجزء catch؛ فقد اُلتقَط (catching) الاِعتراض، وعُولَج (handling)، وعليه لم يَتَسبَّب بانهيار البرنامج. يُعدّ القوسين { و } المُستخدَمين ضِمْن تَعْليمَة try..catch جزءً أساسيًا من صياغة (syntax) التَعْليمَة، حتى في حالة اِحتوائها على تَعْليمَة واحدة، وهو ما يختلف عن بقية التَعْليمَات الآخرى التي مَررنا بها حتى الآن، والتي يكون فيها اِستخدَام الأقواس أمرًا اختياريًا في حالة التَعْليمَات المُفردة (single statement). بفَرْض أن لدينا مُتَغيِّر str من النوع String، يُحتمَل أن تكون قيمته مُمثِلة لعَدَد حقيقي (real) صالح، اُنظر الشيفرة التالية: (real)double x; try { x = Double.parseDouble(str); System.out.println( "The number is " + x ); } catch ( NumberFormatException e ) { System.out.println( "Not a legal number." ); x = Double.NaN; } إذا بَلَّغ استدعاء (call) الدالة‏ Double.parseDouble(str)‎ عن خطأ، فستُنفَّذ التَعْليمَات الموجودة بالجزء catch مع تَخَطِّي تَعْليمَة الخَرْج (output) بالجزء try. يُعالَج الاعتراض، في هذا المثال، بإِسْناد القيمة Double.NaN إلى المُتَغيِّر x. تُشير هذه القيمة إلى عدم حَمْل مُتَغيِّر من النوع double لقيمة عَدَدية. لا تَحتاج دائمًا إلى اِلتقاط الاِعتراضات (catch exceptions)، والاستمرار بتَّنْفيذ البرنامج. على العكس، فقد يُؤدي ذلك أحيانًا إلى حُدُوث فوضى أكبر فيما بَعْد، ولربما يَكُون عندها من الأفضل السماح ببساطة بانهيار (crash) البرنامج. مع ذلك، يَكُون التَعافِي من أخطاء معينة مُمكنًا في أحيان آخرى. لنفْترِض مثلًا أنك تَرغَب بكتابة برنامج يَحسِب متوسط (average) متتالية من الأعداد الحقيقية (real numbers)، التي يُدْخِلها المُستخدِم، وبحيث يُمكِنه الإشارة إلى نهاية المتتالية عن طريق إِدْخَال سَطْر فارغ. في الواقع، يُشبه هذا البرنامج المثال الذي تَعرَّضنا له بالقسم ٣.٣، مع الفارق في اِستخدَام القيمة صفر للإشارة إلى انتهاء المتتالية. قد تُفكِر باِستخدَام الدالة ‏(function) ‏TextIO.getlnInt()‎ لقراءة دَخْل المُستخدِم (input). ولكن لمّا كانت تلك الدالة تَتَخَطَّى الأسطر الفارغة، فإننا ببساطة لن نتَمكَّنْ من تَحْدِيد السَطْر الفارغ، ولذلك سنَستخدِم بدلًا منها الدالة TextIO.getln()‎، مما سيُمكِّننا من تَحْدِيد السَطْر الفارغ عند إِدْخاله. سنَستخدِم أيضًا الدالة Double.parseDouble لتَحْوِيل الدَخْل -إذا لم يكن سَطْرًا فارغًا- إلى عَدَد حقيقي، وسنستدعيها ضِمْن تَعْليمَة try..catch؛ لتَجَنُّب انهيار البرنامج في حالة إِدْخَال المُستخدِِم عددًا غَيْر صالح. اُنظر شيفرة البرنامج: import textio.TextIO; public class ComputeAverage2 { public static void main(String[] args) { String str; // مدخل المستخدم double number; // مدخل المستخدم بعد تحويله إلى عدد double total; // حاصل مجموع الأعداد المدخلة double avg; // متوسط الأعداد المدخلة int count; // عدد الأعداد المدخلة total = 0; count = 0; System.out.println("Enter your numbers, press return to end."); while (true) { System.out.print("? "); str = TextIO.getln(); if (str.equals("")) { break; // اخرج من الحلقة لأن المستخدم أدخل سطر فارغ } try { number = Double.parseDouble(str); // إذا حدث خطأ، سيتم تخطي السطرين التاليين total = total + number; count = count + 1; } catch (NumberFormatException e) { System.out.println("Not a legal number! Try again."); } } avg = total/count; System.out.printf("The average of %d numbers is %1.6g%n", count, avg); } } اعتراضات الصنف TextIO يستطيع الصَنْف TextIO قراءة البيانات من عدة مصادر. (انظر القسم الفرعي ٢.٤.٤). فمثلًا، عندما يُحاوِل قراءة قيمة عددية مُدْخَلة من قِبَل المُستخدِم، فإنه يتحقَّق من كون دَخْل المُستخدِم صالحًا، وذلك باِستخدَام طريقة شبيهة للمثال السابق، أيّ عن طريق اِستخدَام حَلْقة التَكْرار while ضِمْن تَعْليمَة try..catch، وبالتالي لا يُبلِّغ عن اِعتراض. في المقابل، عندما يُحاوِل القراءة من ملف، فلا توجد طريقة واضحة للتَعَافِي من وجود قيمة غَيْر صالحة بالدَخْل (input)، ولهذا فإنه يُبلِّغ عن اِعتراض. يُبلِّغ الصَنْف TextIO -بغرض التبسيط- عن اِعتراضات من النوع IllegalArgumentException فقط، وذلك بِغَضّ النظر عن نوع الخطأ الفعليّ الذي قد وَاجهه الصَنْف. فمثلًا، إذا حَاولت قراءة ملف تمَّت قراءة جميع محتوياته بالفعل، فسيَحدُث اِعتراض من الصَنْف IllegalArgumentException. إذا كان لديك رؤية أفضل لكيفية معالجة أخطاء الملفات غَيْر السماح للبرنامج بالانهيار (crash)، اِستخدِم تَعْليمَة try..catch لالتقاط الاِعتراضات من النوع IllegalArgumentException. لنفْحَص البرنامج التالي لحِسَاب قيمة متوسط (average) مجموعة من الأعداد. في هذا المثال، وبفَرْض وجود ملف يَحتوِي على أعداد حقيقية (real numbers) فقط، فإن البرنامج سيَقرأ قيم الأعداد من ذلك الملف تباعًا، ويَحسِب كُلًا من حاصل مجموعها ومتوسطها. لمّا كان عَدَد الأعداد الموجودة بالملف غَيْر مَعلوم، فإننا بحاجة إلى معرفة إلى متى سنستمر بالقراءة. أحد الحلول هو أن نستمر حتى نَصِل إلى نهاية الملف، وعندها سيَحدُث اِعتراض، والذي لا يُمكِن عدّه في تلك الحالة خطأً فعليًا، وإنما هو فقط الطريقة المُتَّبَعة للإشارة إلى عدم وجود بيانات آخرى بالملف، وعليه، يُمكِننا التقاط هذا الاِعتراض وإنهاء البرنامج. بعبارة آخرى، سنَقرأ البيانات داخل حَلْقة تَكْرار while لا نهائية، ونَخْرُج منها فقط عند حُدوث اِعتراض. يُوظِّف هذا المثال الاِعتراضات بعدّها جزءً متوقعًا من مَسار التحكُّم (flow of control) بالبرنامج، وهو ما قد يُمثِل طريقة غَيْر اعتيادية نوعًا ما لاِستخدَام الاِعتراضات. سنحتاج إلى معرفة اسم الملف حتى نَتَمكَّن من قراءته. سنَسمَح في الواقع للمُستخدِم بإِدْخَال اسم الملف، بدلًا من تَوْفِيره بالشيفرة (hard-coding) كقيمة ثابتة؛ وذلك بهدف تَعميم البرنامج. لكن قد يُدخِل المُستخدِم اسم ملف غَيْر موجود أساسًا. وعليه، سيُبلَّغ عن اِعتراض من النوع IllegalArgumentException عندما نَستخدِم الدالة TextIO.readfile لمحاولة فتح ذلك الملف. يُمكِننا أن نَلتقِط (catch) هذا الاِعتراض، ثُمَّ نَطلُب من المُستخدِم إِدْخَال اسم ملف آخر صالح. اُنظر الشيفرة التالية: import textio.TextIO; public class AverageNumbersFromFile { public static void main(String[] args) { while (true) { String fileName; // اسم الملف المدخل من قبل المستخدم System.out.print("Enter the name of the file: "); fileName = TextIO.getln(); try { TextIO.readFile( fileName ); // حاول فتح الملف break; // إذا نجحت عملية الفتح، أخرج من الحلقة } catch ( IllegalArgumentException e ) { System.out.println("Can't read from the file \"" + fileName + "\"."); System.out.println("Please try again.\n"); } } // ‫الصنف TextIO سيقرأ الملف double number; // عدد مقروء من الملف double sum; // حاصل مجموع الأعداد المقروءة حتى الآن int count; // عدد الأعداد المقروءة حتى الآن sum = 0; count = 0; try { while (true) { // تتوقف الحلقة عند حدوث اعتراض number = TextIO.getDouble(); count++; // يتم تخطي هذه العبارة في حالة حدوث اعتراض sum += number; } } catch ( IllegalArgumentException e ) { // نتوقع حدوث اعتراض عندما نفرغ من قراءة الملف // تم التقاط الاعتراض فقط حتى نمنع انهيار البرنامج // لكن ليس هناك ما نفعله لمعالجة الاعتراض لأنه ليس خطأ أساسا } // تمت قراءة جميع محتويات الملف عند هذه النقطة System.out.println(); System.out.println("Number of data values read: " + count); System.out.println("The sum of the data values: " + sum); if ( count == 0 ) System.out.println("Can't compute an average of 0 values."); else System.out.println("The average of the values: " + (sum/count)); } } ترجمة -بتصرّف- للقسم Section 7: Introduction to Exceptions and try..catch من فصل Chapter 3: Programming in the Small II: Control من كتاب Introduction to Programming Using Java.
  20. تتوفَّر تَعْليمَتين للتَفْرِيع (branching statements) بلغة الجافا، تناولنا تَعْليمَة if بالقسم السابق، والآن سننتقل إلى تَعْليمَة التَفْرِيع الثانية switch، والتي يُعدّ اِستخدَامها أقل شيوعًا من تَعْليمَة if، ومع ذلك فهي تكون مفيدة أحيانًا للتعبير عن نوع معين من التَفْرِيع المُتعدِّد (multiway branches). تعليمة switch تَفحْص تَعْليمَة switch قيمة تعبير (expression) معين، لتَقفِز بَعْدها مباشرة إلى مكان ما بالتَعْليمَة، يَتقرَّر ذلك المكان وِفقًا للقيمة الناتجة عن التعبير. لا يُمكِنك اِستخدَام أيّ تعبير (expression) مع تَعْليمَة switch؛ حيث يُسمَح بأنواع معينة فقط، منها الأنواع العددية الصحيحة الأوَّليّة (primitive) مثل int و short و byte، في المقابل لا يُسمَح بالأنواع العددية العَشريّة مثل double و float. تستطيع كذلك اِستخدَام تعبيرات من النوع المحرفي الأوَّليّ char والنوع String. وأخيرًا، يُسمَح باِستخدَام التعدادات (enums) أيضًا. اُنظر القسم الفرعي ٢.٣.٤ لقراءة المزيد عن التعدادات (enums). كما ذكرنا مُسْبَّقًا، يَقفِز الحاسوب -أثناء تَّنْفيذه لتَعْليمَة switch- إلى مكان ما ضِمْن التَعْليمَة، لذلك كان لابُدّ من وجود طريقة لتَخْصِيص الأماكن المَسموح بالقفز (نَقْل التَحكُّم بصورة أدق) إليها. تُستخدَم عناوين الحالات (case labels) لهذا الغرض، وتُكتَب بصيغة case constant:‎، بحيث يكون الثابت (constant) قيمة مُجرَّدة مُصنَّفة النوع (literal)، والتي لابُدّ أن تكون من نفس نوع التعبير المُستخدَم بتَعْليمَة switch. يَعمَل عنوان الحالة (case label) كعلامة للإشارة إلى المكان الذي يَنبغِي للحاسوب القَفز إليه عندما تؤول قيمة التعبير إلى قيمة الثابت المُخصَّصة. يَتوفَّر أيضًا عنوان الحالة الافتراضي default:‎، والذي يَقفِز الحاسوب إليه إذا لم يَجِد قيمة التعبير ضِمْن قائمة عناوين الحالات (case labels) المُخصَّصة بتَعْليمَة switch. عادة ما تُستخدَم تَعْليمَة switch بالصياغة التالية: switch ( <expression> ) { case <constant-1>: <statements-1> break; case <constant-2>: <statements-2> break; . . // أضف مزيد من الحالات . case <constant-N>: <statements-N> break; default: // حالة افتراضية <statements-(N+1)> } // نهاية تعليمة switch تُطابِق تَعْليمَة switch بالأعلى تَعْليمَة التَفْرِيع المُتعدِّد (multiway branch)‏ if التالية، وتؤدي نفس وظيفتها. تُعدّ تَعْليمَة switch مع ذلك أكثر كفاءة؛ حيث يُقَيِّم الحاسوب -أثناء تَّنْفيذها- تعبيرًا (expression) وحيدًا فقط، ويَقفِز بَعْدها مباشرةً إلى الحالة (case) المَعنيّة. في المقابل، فإنه قد يَضطر -عند مُعالجته لتَعْليمَة if- إلى تَقييم ما قد يَصِل إلى عدد N من التعبيرات، وذلك قَبْل أن يَتمكَن من تَحْدِيد أي من كُتل التَعْليمَات ينبغي له تَّنْفيذها: if ( <expression> == <constant-1> ) { // but use .equals for String!! <statements-1> } else if ( expression == <constant-2> ) { <statements-2> } . . . else if ( <expression> == <constant-N> ) { <statements-N> } else { <statements-(N+1)> } في الواقع، لا تَتطلَّب صياغة (syntax) تَعْليمَة switch اِستخدَام تَعْليمَة break في نهاية كل حالة (case) كالمثال بالأعلى، ولكنك إن تَغافلت عنها -وهو ما يُعدّ سليمًا من ناحية الصياغة-، فسيَمضِي الحاسوب قُدُمًا لتَّنْفيذ التَعْليمَات الخاصة بعنوان الحالة (case label) التالي بعدما ينتهي من تَّنْفيذ تَعْليمَات الحالة الصحيحة. نادرًا ما تكون طريقة التَّنْفيذ تلك هي ما يَنتويه المبرمج، ولذلك اُعتيد اِستخدَام تَعْليمَة break بنهاية كل حالة؛ لإجبار الحاسوب على القفز إلى نهاية تَعْليمَة switch، مُتَخطِّيًا بذلك جميع الحالات الآخرى. ملحوظة قد لا تستطيع استيعابها حتى تَقرأ الفصل التالي: تَحلّ تَعْليمَة return أحيانًا مَحَلّ تَعْليمَات break -المُستخدَمة ضِمْن تَعْليمَة switch- بالبرامج الفرعية (subroutine)، ويؤدي تَّنْفيذها إلى إنهاء كُلًا من تَعْليمَة switch وكذلك البرنامج الفرعي. يُمكِنك تَرك واحدة أو أكثر من عناوين الحالات (case labels) فارغة تمامًا بدون أيّ تَعْليمَات، وحتى بدون تَعْليمَة break، مما سيؤدي إلى وجود متتالية من عناوين الحالات (case label) بثوابت مختلفة. في تلك الحالة، سيَقفِز الحاسوب إلى نفس المكان ويُنفِّذ نفس التَعْليمَات إذا آلت قيمة التعبير إلى أيّ واحدة من تلك الثوابت. ليس هناك أي قواعد لترتيب ثوابت (constants) عناوين الحالات (case label)، ولكن من الضروري ألا تُكرِّر قيمة أيّ ثابت. قد لا يكون المثال التالي مفيدًا بحدّ ذاته، ولكن على الأقل يَسهُل تَتبُّعه بغرض التعلم: switch ( N ) { // ‫بفرض أن N متغير من النوع العددي case 1: System.out.println("The number is 1."); break; case 2: case 4: case 8: System.out.println("The number is 2, 4, or 8."); System.out.println("(That's a power of 2!)"); break; case 3: case 6: case 9: System.out.println("The number is 3, 6, or 9."); System.out.println("(That's a multiple of 3!)"); break; case 5: System.out.println("The number is 5."); break; default: System.out.println("The number is 7 or is outside the range 1 to 9."); } تَستعير لغة الجافا جميع بُنَى التحكُّم (control structures) التي تُدعِّمها من لغتي البرمجة الأقدم: السي C، والسي بلص بلص C++‎. تُعدّ تَعْليمَة switch بدائية نوعًا ما بالموازنة مع بقية بُنَى التحكُّم (control structures)؛ فمِنْ السهل أن تَقع في العديد من الأخطاء عند اِستخدَامها، ولهذا يَظُنّ الكاتب أنه كان من الضروري لمُصمِّمي لغة الجافا أن يُحسِّنوا من تَعْليمَة switch أثناء تَبَنّيها ضِمْن اللغة. القوائم وتعليمات switch تُعدّ معالجة القوائم (menu) واحدة من أشهر تطبيقات تَعْليمَة switch. ببساطة تُعرَض قائمة (menu) مُكوَّنة من مجموعة من الخيارات (options) -ومُرقَّمَة عادةً على الصورة ١، ٢، …إلخ- على المُستخدِم، والذي يختار واحدة منها، ليتَلقَّى ردًا (response) وفقًا لاختياره. يَعني ذلك أنه لابُدّ للحاسوب من الرد على كل خيار مُحتمَل بطريقة مختلفة، ولهذا يُمكِن اِستعمال رقم الخيار كثابت لعناوين الحالات (case labels) ضِمْن تَعْليمَة switch لتَحْدِيد الرد المناسب. المثال التالي عبارة عن برنامج طرفيّة (command-line program)، وهو في الواقع مُجرَّد نسخة مختلفة من مسألة "تَحْوِيل قياسات الطول" من القسم السابق، حيث يُطلَب من المُستخدِم اختيار وَحدة القياس (unit of measure) التي يَستخدِمها قياس الطول (measurement) المُدْخَل، ولهذا ستُعرَض قائمة (menu) مُرقَّمَة بوَحَدات القياس المتاحة، بحيث يُمكِن للمُستخدِم اختيار إحداها بكتابة رقمها المُناظر كمُدْخَل (input): int optionNumber; // رقم الخيار المختار من قبل المستخدم double measurement; // قياس الطول العددي المعطى من قبل المستخدم double inches; // نفس قياس الطول ولكن بوحدة البوصة. // اعرض قائمة واقرأ رقم الخيار المطلوب System.out.println("What unit of measurement does your input use?"); System.out.println(); System.out.println(" 1. inches"); System.out.println(" 2. feet"); System.out.println(" 3. yards"); System.out.println(" 4. miles"); System.out.println(); System.out.println("Enter the number of your choice: "); optionNumber = TextIO.getlnInt(); // اقرأ قياس الطول المعطى وحوله إلى البوصة switch ( optionNumber ) { case 1: System.out.println("Enter the number of inches: "); measurement = TextIO.getlnDouble(); inches = measurement; break; case 2: System.out.println("Enter the number of feet: "); measurement = TextIO.getlnDouble(); inches = measurement * 12; break; case 3: System.out.println("Enter the number of yards: "); measurement = TextIO.getlnDouble(); inches = measurement * 36; break; case 4: System.out.println("Enter the number of miles: "); measurement = TextIO.getlnDouble(); inches = measurement * 12 * 5280; break; default: System.out.println("Error! Illegal option number! I quit!"); System.exit(1); } // ‫نهاية switch // حول قياس الطول بالبوصة إلى وحدات القياس الأخرى يمكن أيضًا إعادة كتابة المثال السابق بحيث تَستخدَم تَعْليمَة switch تعبيرًا (expression) من النوع String، كالتالي: String units; // وحدة القياس المعطاة من قبل المستخدم double measurement; // قياس الطول العددي المعطى من قبل المستخدم double inches; // نفس قياس الطول ولكن بوحدة البوصة // اقرأ وحدة القياس المعطاة System.out.println("What unit of measurement does your input use?"); System.out.print("Legal responses: inches, feet, yards, or miles : "); units = TextIO.getln().toLowerCase(); // اقرأ قياس الطول المعطى وحوله إلى البوصة System.out.print("Enter the number of " + units + ": "); measurement = TextIO.getlnDouble(); switch ( units ) { case "inches": inches = measurement; break; case "feet": inches = measurement * 12; break; case "yards": inches = measurement * 36; break; case "miles": inches = measurement * 12 * 5280; break; default: System.out.println("Wait a minute! Illegal unit of measure! I quit!"); System.exit(1); } // ‫نهاية switch التعداد (Enum) كتعبير لتعليمات switch كما ذَكَرنا مُسْبَّقًا، يُسمَح باِستخدَام التعدادات (enums) كتعبيرات (expression) لتَعْليمَة switch. وفي هذه الحالة، يجب أن تكون ثوابت عناوين الحالات (case labels) واحدة من القيم المتاحة بنوع التعداد المُستخدَم. فمثلًا، لنفْترِض أن لدينا تعدادًا من النوع Season مُعرَّف كالتالي: enum Season { SPRING, SUMMER, FALL, WINTER } إذا كان التعبير (expression) المُستخدَم ضِمْن تَعْليمَة switch من ذلك النوع، فينبغي أن تكون الثوابت المُستخدَمة بعناوين الحالات (case labels) واحدة من القيم التالية Season.SPRING و Season.SUMMER و Season.FALL و Season.WINTER. لكن لاحظ أنه عندما يُستخدَم تعداد ثابت (enum constant) ضِمْن عنوان حالة (case label)، يُستخدَم فقط الاسم دون إعادة ذِكر نوع التعداد، مثل SPRING، وليس الاسم كاملًا، مثل Season.SPRING. لمّا كان ضروريًا لثوابت عناوين الحالات (case labels) أن تكون من نفس نوع تعبير التَعْليمَة، يستطيع الحاسوب أن يَستنتج نوع تلك الثوابت اعتمادًا على نوع التعبير. فلمّا كان هذا التعبير تعدادًا، افْترَض الحاسوب أن تلك الثوابت لابُدّ وأن تَندرِج تحت قيم نفس نوع التعداد، ولهذا لا حاجة لإعادة كتابة نوع التعداد بالثابت. مع ذلك، يَظِلّ هذا شذوذًا بالصياغة (syntax). على سبيل المثال، يُوضِح المثال بالأسفل طريقة اِستخدَام التعداد Season ضِمْن تَعْليمَة switch، وبفَرْض أن المُتَغيِّر currentSeason من النوع Season: switch ( currentSeason ) { case WINTER: // ‫وليس Season.WINTER System.out.println("December, January, February"); break; case SPRING: System.out.println("March, April, May"); break; case SUMMER: System.out.println("June, July, August"); break; case FALL: System.out.println("September, October, November"); break; } الإسناد المؤكد (Definite Assignment) وتعليمة switch يُعيد التعبير (int)(3*Math.random()‎) واحدًا من الأعداد الصحيحة ٠ و ١ و ٢ عشوائيًا وباحتمالية مُتساوية، ولذلك تَستخدِمه تَعْليمَة switch -بالمثال التالي- للقيام باختيار عشوائي ضِمْن ثلاث بدائل مُحتمَلة، حيث تُسنَد إحدى القيم الثلاثة "Rock" أو "Paper" أو "Scissors" إلى المُتَغيِّر computerMove، وفقًا لقيمة هذا التعبير وبإحتمالية تُساوِي الثلث لكلًا من القيم الثلاثة. switch ( (int)(3*Math.random()) ) { case 0: computerMove = "Rock"; break; case 1: computerMove = "Paper"; break; case 2: computerMove = "Scissors"; break; } تُعدّ تَعْليمَة switch بالأعلى سليمة تمامًا بهذه الطريقة، لكن بفَرْض قيامنا بإضافة تَعْليمَة لطباعة المُتَغيِّر computerMove كالتالي: String computerMove; switch ( (int)(3*Math.random()) ) { case 0: computerMove = "Rock"; break; case 1: computerMove = "Paper"; break; case 2: computerMove = "Scissors"; break; } System.out.println("The computer's move is " + computerMove); // خطأ‫!! الآن، أَصبح لدينا خطأ (error) بالسَطْر الأخير! يَرجِع سبب هذا الخطأ إلى ما يُعرَف باسم الإِسْناد المؤكد (definite assignment)، الفكرة ببساطة هي أنه من الضروري أن يَتمكَن مُصرِّف الجافا (compiler) من التأكد من أن أيّ محاولة لاِستخدَام قيمة مُتَغيِّر معين قد سَبَقها بالفعل عملية إِسْناد (assignment) قيمة لهذا المُتَغيِّر. تَعرَّضنا قبلًا لمصطلح الإِسْناد المؤكد (definite assignment) بالقسم الفرعي ٣.١.٤. بهذا المثال تَحْدِيدًا، تُغطِّي الحالات (cases) الثلاثة الموجودة ضِمْن تَعْليمَة switch جميع الاحتمالات، ومع ذلك فإن المُصرِّف (compiler) ليس ذكي كفاية لاستنتاج ذلك، فهو فقط يرى تَعْليمَة switch مُستخدَمة مع تعبير من النوع العددي (integer-valued expression)، وبذات الوقت، يرى أن الحالات (cases) المُخصَّصة ضِمْن التَعْليمَة لا تُغطِّي جميع الأعداد الصحيحة المُحتمَلة، وإنما فقط ثلاثة منها. أحد الحلول البسيطة هو استبدال الحالة الافتراضية default:‎ بالحالة (case) الأخيرة الموجودة ضِمْن تَعْليمَة switch. لمّا كان تَخْصِيص الحالة الافتراضية default يَعني بالضرورة أن جميع القيم المُحتمَلة لتعبير التَعْليمَة switch سوف تُغطََّى لا محالة، استطاع المُصرِّف (compiler) التأكد من حُدوث إِسْناد مُؤكد للمُتَغيِّر computerMove. انظر المثال التالي: String computerMove; switch ( (int)(3*Math.random()) ) { case 0: computerMove = "Rock"; break; case 1: computerMove = "Paper"; break; default: computerMove = "Scissors"; break; } System.out.println("The computer's move is " + computerMove); // OK! صياغة جديدة لتعليمة switch أُضيفت نسخة جديدة من تَعْليمَة switch إلى الإصدار ١٤ من لغة الجافا. تَستخدِم النسخة الجديدة السهم (arrow)‫ ‎->‎‎‎‎‏ بعد عنوان الحالة (case label)، بدلًا من النقطتان الرأسيتان (colon) :، وتَسمَح بتَعْليمَة واحدة فقط بعد السهم. إذا أردت كتابة أكثر من تَعْليمَة لنفس عنوان الحالة، يُمكِنك اِستخدَام تَعْليمَة كُتليّة (block statement) بتَضْمِين التَعْليمَات المطلوبة بين قوسين. لم تَعُدْ في حاجة إلى اِستخدَام تَعْليمَة break بنهاية كل حالة، وهو ما ساعد كثيرًا على تَجنُّب الخطأ الشائع من انتقال التَحكُّم غَيْر المقصود من الحالة (case) الصحيحة -بعد الانتهاء من تَّنْفيذها- إلى الحالة التالية بدلًا من إنهاء التَعْليمَة، وذلك في حالة عدم إضافة تَعْليمَة break. وأخيرًا، لم تَعُدْ مُضطرًا لاِستخدَام قيمة واحدة فقط لكل عنوان حالة (case label)؛ فقد أصبح بالإمكان تَخْصِيص أكثر من قيمة لنفس الحالة (case)؛ بحيث يُفصَل بينهما بفاصلة (comma). يُمكن إعادة كتابة تَعْليمَة switch الموجودة بأول مثال بهذا القسم بالصياغة الجديدة كالتالي: switch ( N ) { // ‫بفرض أن N متغير من النوع العددي case 1 -> System.out.println("The number is 1."); case 2, 4, 8 -> { System.out.println("The number is 2, 4, or 8."); System.out.println("(That's a power of 2!)"); } case 3, 6, 9 -> { System.out.println("The number is 3, 6, or 9."); System.out.println("(That's a multiple of 3!)"); } case 5 -> System.out.println("The number is 5."); default -> System.out.println("The number is 7 or is outside the range 1 to 9."); } تُعدّ النسخة الجديدة من وجهة نظر الكاتب إضافة ضخمة للغة. لاحِظ أنه ما تزال الصيغة (syntax) الأصلية من تَعْليمَة switch مُتاحة باللغة. إلى جانب التحسِّينات على تَعْليمَة switch، أُضيف أيضًا إلى لغة الجافا تعبيرًا من النوع switch (‏switch expression)، والذي -وكأيّ تعبير آخر (expression)- يُمكن تَحْصِيل قيمته (evaluation) بحيث تؤول إلى قيمة مُفردة (single value). تتشابه صياغة (syntax) هذا التعبير الجديد إلى حد كبير مع تَعْليمَة switch، مع فارق أن كل حالة أَصبحَت تُخصِّص تعبيرًا (expression) لا تَعْليمَة (statement). اُنظر المثال التالي: String computerMove = switch ( (int)(3*Math.random()) ) { case 1 -> "Rock"; case 2 -> "Paper"; default -> "Scissors"; }; لابُدّ أن يؤول التعبير من النوع switch (‏switch expression) إلى قيمة دائمًا، ومِنْ ثَمَّ فإنه تقريبًا دائمًا ما يَتضمَّن الحالة الافتراضية (default case). يُمكِن للتعبير (expression) الخاص بحالة (case) معينة أن يُستبدَل بكتلة (block) تَشتمِل على مجموعة من التَعْليمَات، وعِندها تُحسَب قيمة هذه الحالة باِستخدَام تَعْليمَة yield، مثل yield 42;‎، وليس بأي من التَعْليمَتين return أو break. ترجمة -بتصرّف- للقسم Section 6: The switch Statement من فصل Chapter 3: Programming in the Small II: Control من كتاب Introduction to Programming Using Java.
  21. تتوفَّر تَعْليمَتين للتَفْرِيع (branching statements) بلغة الجافا، وفي هذا القسم، سنتناول تَعْليمَة التَفْرِيع الأولى if، والتي قد سبق وتَعرَّضنا لها بالفعل بالقسم ٣.١. تُكتب تَعْليمَة التَفْرِيع الشَّرْطيّة if بالصياغة التالية: if ( <boolean-expression> ) <statement-1> else <statement-2> تُسمى هذه الصياغة من if بتَعْليمَة التَفْرِيع الثنائي (two-way branch)، وكما هو مُتوقَّع، يُمكِن للتعليمتين و -بالأعلى- أن تَكونا كُتلَتي البِنْية (block statement). لاحِظ أنه من المُمكن حَذْف الجزء else المُكوَّن من الكلمة else والتعليمة . مشكلة تَّدَلِّي else لمّا كانت تَعْليمَة if هي بالنهاية مُجرَّد تَعْليمَة، فإنه من المُمكن لإحدى التعليمتين أو أو حتى كلتيهما أن يَكونا تَعْليمَة if بذاتهما، ولكن قد يتَسبَّب ذلك بحُدوث مشاكل في بعض الأحيان. فمثلًا، إذا اِستخدَمت تَعْليمَة if بدون الجزء else محل التعليمة ، كما في الشيفرة التالية: if ( x > 0 ) if (y > 0) System.out.println("First case"); else System.out.println("Second case"); قد يُعطيك اِستخدَامك للمسافات البادئة (indentation) -ببداية كل سطر من الشيفرة- انطباعًا خاطئًا بأن الجزء else هو النصف الآخر من تَعْليمَة if (x > 0‎)‎، لكن هذا غَيْر صحيح. لا يُعطِي الحاسوب في الواقع أيّ اعتبار لتلك المسافات، وإنما يَتبِّع -في هذه الحالة بالتَحْدِيد- قاعدة بسيطة، وهي ربْط الجزء else بأقرب تَعْليمَة if ليس لديها جزء else خاص بها، مما يَعني أنه سيَربُط الجزء else بتَعْليمَة if (y > 0)‎؛ لأنها الأقرب. ولهذا يَقرأ الحاسوب التَعْليمَات بالأعلى كما لو كانت مُرَتَّبة على النحو التالي: if ( x > 0 ) if (y > 0) System.out.println("First case"); else System.out.println("Second case"); تستطيع مع ذلك إجبار الحاسوب على اِستخدَام التفسير الآخر بتَضْمِين تَعْليمَة if الداخلية داخل كُتلَة (block)، كالتالي: if ( x > 0 ) { if (y > 0) System.out.println("First case"); } else System.out.println("Second case"); تختلف بالطبع نتائج تَعْليمَتي if السابقتين. فمثلًا إذا كانت قيمة المُتَغيِّر x أقل من أو تُساوِي الصفر، لا تَطبع التَعْليمَة الأولى شيئًا، بينما تَطبع الثانية السِلسِلة النصية "Second case". التفريع المتعدد (Multiway Branching) من الجهة الآخرى، تُصبِح الأمور أكثر تشويقًا عندما تَستخدِم تَعْليمَة if محل التعليمة ، وفي هذه الحالة، ستبدو تَعْليمَة التَفْرِيع (branching) كالتالي -ربما بدون الجزء else الأخير-: if ( <boolean-expression-1> ) <statement-1> else if ( <boolean-expression-2> ) <statement-2> else <statement-3> ولمّا كان الحاسوب لا يَهتم بطريقة تَنْسِيق الشيفرة على الصفحة، فتقريبًا دائمًا ما يُكتَب المثال السابق بالصيغة التالية: if ( <boolean-expression-1> ) <statement-1> else if ( <boolean-expression-2> ) <statement-2> else <statement-3> تُسمى هذه الصياغة من if -بالأعلى- بتَعْليمَة التَفْرِيع الثلاثي (three-way branch)، وينبغي أن تُفكر بها كتَعْليمَة واحدة؛ فبالنهاية، ستُنفَّذ واحدة فقط من التعليمات الثلاثة و و أثناء تَّنْفيذ تلك التَعْليمَة. تُحسَب أولًا قيمة التعبير ، فإذا كانت تؤول إلى القيمة المنطقية true، سيُنفِّذ الحاسوب التعليمة ، ثم سيَقفِز مباشرة إلى نهاية تَعْليمَة if الخارجية مُتخَطِّيًا بذلك العبارتين الآخريتين تمامًا. أما إذا آلت قيمتها إلى القيمة المنطقية false، فإنه سيتَخَطَّى التعليمة فقط، وينتقل إلى تَعْليمَة if الثانية المُتداخِلة (nested)؛ ليَفحْص قيمة التعبير ، ويُقرِّر ما إذا كان سيُنفِّذ التعليمة أم . مثلًا، يَطبع المثال التالي واحدة من ثلاث رسائل مُحتمَلة وفقًا لقيمة المُتَغيِّر temperature: if (temperature < 50) System.out.println("It's cold."); else if (temperature < 80) System.out.println("It's nice."); else System.out.println("It's hot."); على سبيل المثال، إذا كان المُتَغيِّر temperature يَحمِل القيمة ٤٢، فسيَتحقَّق أول شَّرْط، ومِنْ ثَمَّ تُطبَع الرسالة "It's cold"، لينتقل بَعْدها الحاسوب إلى ما بعد التَعْليمَة مُتخَطِّيًا بقيتها، وبدون حتى محاولة اختبار الشَّرْط الثاني. أما إذا كانت قيمته تُساوِي ٧٥، فلن يتحقَّق أول شَّرْط، لذلك سينتقل الحاسوب لاختبار الشَّرْط الثاني، الذي سيتحقَّق لتتم طباعة الرسالة "It's nice"، ثم سيتَخَطَّى بقية التَعْليمَة. أخيرًا إذا كانت قيمته تُساوِي ١٧٣، لا يتحقَّق أي من الشَّرْطين، وتُطبَع الرسالة "It's hot" (لو لم تَكن دوائره الكهربية قد اِحترقَت بفِعْل الحرارة). يُمكنك الاستمرار بإضافة المزيد من عبارات else-if بأيّ عَدَد، وفي تلك الحالة، يُطلَق عليها اسم تَعْليمَة التَفْرِيع المُتعدِّد (multiway branches)، كالتالي: if ( <test-1> ) <statement-1> else if ( <test-2> ) <statement-2> else if ( <test-3> ) <statement-3> . . // أضف المزيد . else if ( <test-N> ) <statement-N> else <statement-(N+1)> الاختبارات ** **-بالأعلى- هي تعبيرات منطقية (boolean expressions)، تُحسَب قيمها واحدة تلو الآخرى، إلى أن تؤول إحداها إلى القيمة المنطقية true، فيُنفِّذ عندها الحاسوب التعليمة ‎> التي تُناظِر الاختبار ‎> المُتحقِّق ويتَخَطَّى البقية. أمّا إذا لم تؤول أي واحدة منها إلى القيمة المنطقية true، تُنفَّذ عندها التعليمة (n+1)‎> الموجودة ضِمْن الجزء else. لاحِظ أنه دائمًا ما ستُنفَّذ واحدة فقط لا غَيْر من كل هذه التعليمات ‎>، وهذا هو سبب تسميتها باسم تَعْليمَة التَفْرِيع المُتعدِّد (multiway branch). يمُكنك أيضًا حَذْف الجزء else الأخير، ولكن -في تلك الحالة- إذا لم تتحقَّق أيّ من التعبيرات المنطقية، وآلت جميعا إلى القيمة المنطقية false، فلن تُنفَّذ أيّ عبارة نهائيًا. يُمكن بالطبع لأيّ من التعليمات ‎> أن تكون كُتليّة البِنْية (block)، أي مُكوَّنة من أيّ عدد من التَعْليمَات طالما أُحيطَت بزوج من الأقواس. ‎> قد يَنْتابك شُعور بالارتباك لوجود الكثير من قواعد الصياغة (syntax) الجديدة هنا، ولكن أَضمَن لك أنه مع قليل من الممارسة، ستعتاد عليها. يُمكنك أيضًا أن تُلقي نظرة على المُخطط التالي، والذي يُوضِح مَسار التحكُّم (flow control) أثناء تَّنْفيذ الصيغة الأعم من تَعْليمَة if..else if والمعروفة باسم تَعْليمَة التَفْرِيع المُتعدِّد: أمثلة لنفْترَض أن لدينا ثلاثة مُتَغيِّرات x و y و z من النوع العددي int، يَحتوِي كل منها على قيمة عددية معينة. المطلوب هو طباعة قيم المُتَغيِّرات الثلاثة بحيث تكون مُرَتَّبة ترتيبًا تصاعديًا. فمثلًا، إذا كانت قيم المُتَغيِّرات هي ٤٢ و ١٧ و ٢٠، فلابُدّ من طباعتها على الترتيب ١٧ و ٢٠ و ٤٢. لحل هذه المسألة، سنحاول الإجابة على السؤال التالي: "لنتخيل أن لدينا قائمة مُرَتَّبة بالفعل، فبأيّ مكان يَنبغي للمُتَغيِّر x أن يقع؟" ببساطة، إذا كانت قيمة المُتَغيِّر x أصغر من قيمة كلا المُتَغيِّرين y و z، فإنه سيأتي بأول القائمة، أما إذا كان أكبر من كليهما، فإنه سيأتي بذيل القائمة، وأخيرًا، سيأتي بوسط القائمة في أيّ حالة غَيْر تلك الحالتين. يُمكن التعبير عن الحالات الثلاثة السابقة بتَعْليمَة التَفْرِيع الثلاثي if، مع ذلك ما يزال أمامنا تحديًا لإيجاد طريقة نُحدِّد من خلالها ترتيب طباعة المُتَغيِّرين y و z. انظر الشيفرة الوهمية (pseudocode): if (x < y && x < z) { // ‫اطبع x متبوعة بكلا من y و z بترتيبها الصحيح output x, followed by y and z in their correct order } else if (x > y && x > z) { // ‫اطبع y و z بترتيبها الصحيح متبوعين بقيمة x output y and z in their correct order, followed by x } else { // ‫اطبع x بين y و z بترتيبها الصحيح output x in between y and z in their correct order } يتطلب تَحْدِيد ترتيب طباعة المُتَغيِّرين y و z تَعْليمَة if آخرى داخلية. انظر الشيفرة التالية بلغة الجافا: if (x < y && x < z) { // ‫إذا كانت x بالبداية if (y < z) System.out.println( x + " " + y + " " + z ); else System.out.println( x + " " + z + " " + y ); } else if (x > y && x > z) { // ‫إذا كانت x بالنهاية if (y < z) System.out.println( y + " " + z + " " + x ); else System.out.println( z + " " + y + " " + x ); } else { // ‫إذا كانت x في الوسط if (y < z) System.out.println( y + " " + x + " " + z); else System.out.println( z + " " + x + " " + y); } حَاوِل اختبار ما إذا كانت الشيفرة بالأعلى ستَعمَل بشكل سليم حتى في حالة وجود أكثر من مُتَغيِّر يَحمِل نفس القيمة. عمومًا إذا كان هناك مُتَغيِّرين بنفس القيمة، فإنه لم يَعُدْ مهمًا بأيّ ترتيب سيَتِمّ طباعتهما. بخلاف اللغات الطبيعية التي تَسمَح بكتابة جملة مثل "إذا كان x أصغر من y و z"، لا يُمكنك أن تَفعَل الشيء نفسه -أيّ كتابة التعبير if (x < y && z)‎- بلغة الجافا. لا تَسير الأمور بهذه البساطة، فلابُدّ أن يَستقبِل العَامِل (operator) المنطقي && مُعامِلين منطقيين (boolean)، ولهذا ستحتاج إلى إِجراء الاختبارين x<y و x<z بشكل منفصل، ثم تُطبِّق العَامِل المنطقي && على نتيجتيهما. ربما تَتَّخذ منحى آخر لحل نفس المسألة، وذلك بمحاولة الإجابة على السؤال: "بأيّ ترتيب يَنبغي طباعة المُتَغيِّرين x و y؟". بمُجرَّد أن تتَوصَّل لإجابة، سيتبقَّى فقط أن تُحدِّد مكان المُتَغيِّر z. انظر الشيفرة التالية بلغة الجافا: if ( x < y ) { // x comes before y if ( z < x ) // z comes first System.out.println( z + " " + x + " " + y); else if ( z > y ) // z comes last System.out.println( x + " " + y + " " + z); else // z is in the middle System.out.println( x + " " + z + " " + y); } else { // y comes before x if ( z < y ) // z comes first System.out.println( z + " " + y + " " + x); else if ( z > x ) // z comes last System.out.println( y + " " + x + " " + z); else // z is in the middle System.out.println( y + " " + z + " " + x); } تتوفَّر عادةً عدة طرائق وأساليب مختلفة لحل نفس المشكلة. فمثلًا، بالمثال السابق، تَعرَّضنا لحلّين فقط ضِمْن عدة حلول مُحتمَلة آخرى كثيرة. فمثلًا، قد يقوم حل آخر ثالث بتبديل قيمة (swap) المُتَغيِّرين x و y إذا كانت قيمة x أكبر من قيمة y، ومِنْ ثَمَّ سيكون من الضروري طباعة قيمة المُتَغيِّر x قبل y. سنحاول أخيرًا كتابة برنامج كامل يَستخدِم تَعْليمَة if بطريقة مُشوِّقة. هدف البرنامج عمومًا هو تَحْوِيل قياسات الطول (measurements of length) من إحدى وَحَدات قياس الطول (units of measure) إلى آخرى، فمثلًا من المِيل (miles) إلى اليَارْدَة (yards)، أو من البُوصَة (inches) إلى القَدَم (feet). سنحتاج بالتأكيد أن نكون أكثر تحديدًا بخُصوص مُتطلَّبات البرنامج؛ فما تزال المسألة غَيْر واضحة المعالِم حتى الآن، ولهذا سنفْترَض أن البرنامج يتَعامَل فقط مع وَحَدات القياس التالية: البُوصَة، والقَدَم، واليَارْدَة، والمِيل؛ وسيكون من السهل إضافة المزيد منها فيما بَعْد. سيُدْخِل المُستخدِم قياس الطول بأي وَحدة من وَحَدات القياس المسموح بها، كأن يَكتُب "١٧ قدم" أو "٢.٧٣ ميل"، ربما بَعْدها قد تَطلُب من المُستخدِم إِدْخَال اسم وَحدة القياس المطلوب التَحْوِيل إليها، بحيث تُطبَع كخَرْج (output) للبرنامج. لكننا في الواقع سنكتفي بطباعة قياس الطول بجميع وَحَدات القياس المسموح بها؛ وذلك في محاولة لتبسيط المسألة. انظر الخوارزمية التالية المبدئية: // اقرأ كلا من قياس الطول ووحدة القياس المستخدمة Read the user's input measurement and units of measure // حول قيمة قياس الطول المعطاة إلى وحدات القياس الأخرى Express the measurement in inches, feet, yards, and miles // اطبع قياسات الطول بالوحدات الأربعة Display the four results لابُدّ أن يَستقبِل البرنامج مُدْخَلين (input) من المُستخدِم، هما قياس الطول ذاته بالإضافة إلى وَحدة القياس المُستخدَمة (units of measure)، ولهذا تُستخدَم الدالتين TextIO.getDouble()‎ و TextIO.getlnWord()‎؛ لقراءة كلًا من قياس الطول العددي واسم وَحدة القياس من نفس السَطْر على الترتيب. سنبدأ دائمًا بتَحْوِيل قياس الطول المُعْطى إلى إِحدى وَحَدات قياس الطول -ولتكن البُوصَة-؛ وذلك لتوحيد الخطوات التالية المسئولة عن التَحْوِيل إلى وَحَدات القياس الأخرى. يَلزَمنا الآن فقط تَحدِّيد وَحدة القياس التي أَدْخَل بها المُستخدِم قياس الطول؛ وذلك لنتمكن من تَحْوِيلها إلى وَحدة البُوصَة. بَعْد إجراء ذلك التَحْوِيل، سيكون من السهل تَحْوِيل قياس الطول بوَحدة البُوصَة إلى كلًا من القَدَم، واليَارْدَة، والمِيل. اُنظر الخوارزمية التالية: Let measurement = TextIO.getDouble() Let units = TextIO.getlnWord() // إذا اسم وحدة القياس المعطى هو البوصة if the units are inches Let inches = measurement // إذا اسم وحدة القياس المعطى هو القدم else if the units are feet Let inches = measurement * 12 // 12 inches per foot // إذا اسم وحدة القياس المعطى هو الياردة else if the units are yards Let inches = measurement * 36 // 36 inches per yard // إذا اسم وحدة القياس المعطى هو الميل else if the units are miles Let inches = measurement * 12 * 5280 // 5280 feet per mile // إذا لم يكن اسم وحدة القياس المعطى أي من القيم الأربعة السابقة else // اسم وحدة القياس المعطى غير صالح The units are illegal! // اطبع رسالة خطأ واوقف المعالجة Print an error message and stop processing // إجراء التحويلات Let feet = inches / 12.0 Let yards = inches / 36.0 Let miles = inches / (12.0 * 5280.0) // اعرض النتائج Display the results لمّا كنا سنقرأ اسم وَحدة القياس المُعْطاة إلى المُتَغيِّر units، وهو من النوع String، فسنحتاج إلى إجراء موازنة نصية (comparison) لتَحدِّيد وَحدة القياس المُناظِرة. تُستخدَم الدالة units.equals("inches")‎ لفَحْص ما إذا كانت قيمة المُتَغيِّر units تُساوِي السِلسِلة النصية "inches"، لكن لا يَنطوِي إِلزام المُستخدِم على إِدْخَال الكلمة "inches" حرفيًا على أفضل تجربة للمُستخدِم (user experience)؛ فمن الأفضل أن نَسمَح له بكتابة كلمات أخرى مثل "inch" أو "in"، ولذلك سنُجري الفَحْص على التعبير المنطقي التالي: units.equals("inches") || units.equals("inch") || units.equals("in")‎ وذلك للسماح بالاحتمالات الثلاثة. يُمكننا أيضًا أن نَسمَح له بكتابة وَحدة القياس بحروف كبيرة (upper case) مثل "Inches" أو "IN" إِمّا عن طريق تَحْوِيل السِلسِلة النصية المُعْطاة units إلى حروف صغيرة (lower case) قبل إجراء الفَحْص، أو باِستخدَام الدالة units.equalsIgnoreCase للموازنة بدلًا من units.equals. تَسمَح النسخة النهائية من البرنامج للمُستخدِم بتَكْرار عمليتي الإِدْخَال والطباعة أكثر من مرة؛ حيث سيَتوقَف البرنامج فقط عند إِدْخَال القيمة صفر. كل ما تَطلبه الأمر هو إِحاطة الخوارزمية بالأعلى ضِمْن حَلْقة التَكْرار while، والتأكد من إِيقافها عندما يُدخِل المُستخدِم القيمة صفر. اُنظر الشيفرة بالكامل: import textio.TextIO; public class LengthConverter { public static void main(String[] args) { double measurement; // قياس الطول المعُطى String units; // اسم وحدة قياس المعطاة // قياس الطول بوحدات القياس الأربعة المتاحة double inches, feet, yards, miles; System.out.println("Enter measurements in inches, feet, yards, or miles."); System.out.println("For example: 1 inch 17 feet 2.73 miles"); System.out.println("You can use abbreviations: in ft yd mi"); System.out.println("I will convert your input into the other units"); System.out.println("of measure."); System.out.println(); while (true) { // اقرأ مُدخَل المستخدم System.out.print("Enter your measurement, or 0 to end: "); measurement = TextIO.getDouble(); if (measurement == 0) break; // ‫اِنهي حلقة while units = TextIO.getlnWord(); units = units.toLowerCase(); // صغر الحروف // حول قياس الطول المعطى إلى وحدة البوصة if (units.equals("inch") || units.equals("inches") || units.equals("in")) { inches = measurement; } else if (units.equals("foot") || units.equals("feet") || units.equals("ft")) { inches = measurement * 12; } else if (units.equals("yard") || units.equals("yards") || units.equals("yd")) { inches = measurement * 36; } else if (units.equals("mile") || units.equals("miles") || units.equals("mi")) { inches = measurement * 12 * 5280; } else { System.out.println("Sorry, but I don't understand \"" + units + "\"."); continue; // ‫عد إلى بداية حلقة while } // حوِّل قياس الطول بوحدة البوصة إلى وحدات القياس الآخرى feet = inches / 12; yards = inches / 36; miles = inches / (12*5280); // اطبع قياس الطول بوحدات القياس الأربعة System.out.println(); System.out.println("That's equivalent to:"); System.out.printf("%14.5g inches%n", inches); System.out.printf("%14.5g feet%n", feet); System.out.printf("%14.5g yards%n", yards); System.out.printf("%14.5g miles%n", miles); System.out.println(); } // ‫نهاية تعليمة while System.out.println(); System.out.println("OK! Bye for now."); } // ‫نهاية برنامج main } // ‫نهاية الصنف LengthConverter لمّا كنا غَيْر مُتحكِّمين بقيم الأعداد الحقيقية (real numbers) المُدْخَلة؛ حيث تَعتمِد على المُستخدِم الذي ربما قد يَرغَب بإِدْخَال قياسات صغيرة جدًا أو ربما كبيرة جدًا، فكان لابُدّ من صياغة الخَرْج باِستخدَام مُحدِّدات الصيغة ‫(format specifiers). اُستخدِم تحديدًا مُحدِّد الصيغة g -بالبرنامج-، والذي يَعمَل كالتالي: إذا كان العَدَد كبيرًا جدًا أو صغيرًا جدًا، فإنه يَطبَعه ‫بصيغة أسّية (exponential form)، أمّا إن لم يَكُن كذلك، فإنه يَطبَعه بالصيغة الرقمية (decimal form) العادية. تَذَكَّر أن القيمة 5، مثلًا، بمُحدِّد الصيغة ‎%14.5g‎ تُشير إلى العدد الكلي للأرقام المعنوية (significant digits) المَطبُوعة، وبذلك سنَحصُل دائمًا على نفس العَدَد من الأرقام المعنوية (significant digits) بالخَرْج بِغَضّ النظر عن قيمة العَدَد المُدْخَل. يَختلف ذلك عن مُحدِّد الصيغة f، والذي يَطبَع الخَرْج بصيغة رقمية (decimal form)، ولكن تُشير فيه القيمة 5، مثلًا، بمُحدِّد الصيغة ‎%14.5f إلى عَدَد الأرقام بَعْد العلامة العَشريّة (decimal point)، أيّ إذا كان لدينا العَدَد 0.000000000745482 فإن مُحدِّد الصيغة f سيَطبَعه إلى 0.00000، بدون أيّ أرقام معنوية (significant digits) على الإطلاق، أما مُحدِّد الصيغة g فسيَطبَعه 7.4549e-10. التعليمة الفارغة (Empty Statement) تُوفِّر لغة الجافا تَعْليمَة آخرى يُطلَق عليها اسم التَعْليمَة الفارغة (empty statement)، وهي ببساطة مُجرَّد فاصلة منقوطة ;، وتَطلُب من الحاسوب أن يَفعَل "لا شيء". اُنظر المثال التالي: if (x < 0) { x = -x; }; عملية إِضافة فاصلة منقوطة (semicolon) بَعْد القوس {، كما بالمثال السابق، هي عملية صحيحة تمامًا وِفقًا لقواعد بناء الجملة (syntax)، لكن، في هذه الحالة، لا يعدّها الحاسوب جزءًا من تَعْليمَة if، وإنما يُعامِلها معاملة تَعْليمَة فارغة (empty statement) مستقلة بذاتها. عمومًا ليس هذا الغرض من تَوفَّر التَعْليمَة الفارغة بلغة الجافا، ولن تجدها مُستخدَمة بهذه الطريقة إلا نادرًا، وإنما ستَستخدِمها أحيانًا عندما تُريد أن تَطلُب من الحاسوب ألا يَفعَل شيئًا. على سبيل المثال: if ( done ) ; // تعليمة فارغة else System.out.println( "Not done yet."); تَطلُب الشيفرة بالأعلى من الحاسوب ألا يَفعَل شيئًا إذا كانت قيمة المُتَغيِّر المنطقي done مساوية للقيمة المنطقية true، وأن يَطبَع السِلسِلة النصية "Not done yet" إذا كانت قيمته مُساوية للقيمة المنطقية false. لا يمكنك في هذه الحالة أن تَحذِف الفاصلة المنقوطة (semicolon)؛ لأن قواعد الصياغة بلغة الجافا (Java syntax) تَتَطلَّب وجود تَعْليمَة بين if و else. يُفضِّل الكاتب على الرغم من ذلك اِستخدَام كُتلَة (block) فارغة أي اِستخدَام قوسين فارغين في تلك الحالات. عادةً ما تَتسبَّب إضافة التَعْليمَة الفارغة ; دون قصد بحُدوث أخطاء (errors) يَصعُب إِيجادها. انظر المثال التالي: for (i = 0; i < 10; i++); System.out.println("Hello"); يَطبَع المثال بالأعلى كلمة "Hello" مرة واحدة فقط وليس عشر مرات كما قد تَظُنّ -بالأخص مع وجود المسافة البادئة (indentation) ببداية السطر الثاني-. السبب ببساطة هو وجود فاصلة منقوطة ; بنهاية تَعْليمَة for بنهاية السَطْر الأول. تُعدّ هذه الفاصلة المنقوطة ; -في هذه الحالة- تَعْليمَة بحد ذاتها، هي التَعْليمَة الفارغة (empty statement)، وهي في الواقع ما يتم تَّنْفيذه عشر مرات، أي أن حَلْقة التَكْرار for بالأعلى تَفعَل "لا شئ" عشر مرات. في المقابل، لا تُعدّ التَعْليمَة System.out.println جزءً من تَعْليمَة for أساسًا، ولذلك تُنفَّذ مرة واحدة فقط بعد انتهاء تَعْليمَة حَلْقة for. ترجمة -بتصرّف- للقسم Section 5: The If Statement من فصل Chapter 3: Programming in the Small II: Control من كتاب Introduction to Programming Using Java.
  22. قد تَكُون البرمجة (programming) صعبة نوعًا ما، وذلك كغَيْرها من الأنشطة المُفيدة والجديرة بالاهتمام، ومع ذلك، فهي عادة ما تَكُون مُجْزيّة ومُمتعة. عند كتابة برنامج (program)، لابُدّ أن تُخبِر الحاسوب بكُل تَفصيلة صغيرة يَنبغي له تَّنْفيذها، وبصورة صحيحة تمامًا؛ وذلك لأنه سيَتَّبِع البرنامج كما هو مَكتوب تمامًا وبطريقة عمياء. إذن، كيف تُكتَب البرامج غَيْر البسيطة؟ في الواقع، إنه ليس أمرًا غامضًا، فالأمر بِرُمَّته يَعتمِد فقط على تَعَلُّم طريقة التفكير الصحيحة. يُعدّ البرنامج (program) تعبيرًا عن فكرة (idea)، حيث يبدأ المُبرمج بفكرة عامة عن مُهِمّة (task) معينة يُريد من الحاسوب تَّنْفيذها، وعادةً ما يكون المُبرمج على عِلم بكيفية تَّنْفيذ تلك المُهِمّة يدويًا، أو لديه تَصَوُّر عام على الأقل. تتمحور المشكلة حول طريقة تَحْوِيل هذا التَصَوُّر العام إلى إِجراء (procedure)، واضح، ومُكتمِل، ومُحدَّد الخُطوات لتََّنْفيذ تلك المُهِمّة. يُسمَى مثل هذا الإِجراء باسم "الخوارزمية (algorithm)"، والتي هي إِجراء واضح، ومُحدَّد الخُطوات، والذي لابُدّ أن ينتهي دائمًا بَعْد عَدَد مُتناهي (finite number) من الخُطوات؛ فنحن لا نُريد عدّ الإِجراءات التي قد تَستمِر للأبد. لاحِظ أن الخوارزمية (algorithm) تَختلف عن البرنامج (program)، فالبرنامج يُكتَب بلغة برمجية معينة، أما الخوارزمية، فتُكتَب بأيّ لغة بما فيها الإنجليزية، وهي أَشْبَه بالفكرة وراء البرنامج، ويُقصَد بالفكرة هنا مجموعة الخُطوات المطلوب القيام بها حتى يَتمّ تَّنْفيذ المُهِمّة، وليس مُجرَّد مُلخَّص لما ينبغي للمُهِمّة إِنجازه في النهاية. عند وصف الخوارزمية، ليس من الضروري أن تَكُون الخُطوات مُفَصَّلة، وذلك طالما كانت واضحة بما فيه الكفاية لبَيَان أن تَّنْفيذها سوف يُنجِز المُهِمّة المطلوبة، ولكن بالطبع لا يُمكِن التعبير عنها كبرنامج (program) فِعليّ بدون مَلْئ جميع تلك التفاصيل. من أين تأتي الخوارزميات؟ ينبغي عادةً تطويرها، وهو ما يَتطلَّب كثيرًا من التفكير والعمل الجاد. يُمكِن القول أن عملية تطوير الخوارزميات (algorithm development) هي مهارة تُكتسَب مع المُمارسة المستمرة، ولكن، مع ذلك، تَتَوفَّر بعض التقنيات (techniques) والقواعد الإرشادية (guidelines) التي يُمكِنها مُساعدتك. سنتناول في هذا القسم بَعضًا منها، وبالأخص ما يَتعلَّق بالبرمجة "في نطاق ضيق"، كما سنعود للحديث عن نفس هذا الموضوع أكثر من مرة بالفصول القادمة. الشيفرة الوهمية والتصميم المتدرج عند البرمجة "في نطاق ضيق"، فأنت مُقيَّد نوعًا ما باِستخدَام عَدَد قليل من الأوامر، وهي كالتالي: المُتَغيِّرات (variables)، وتَعْليمَات الإِسْناد (assignment statements)، والبرامج (routines) الخاصة بعَمليتي الإِدْخال (input) والإِخراج (output). قد يَتوفَّر لك أيضًا اِستخدَام بعض البرامج الفرعية (subroutines)، والكائنات (objects)، أو رُبما حتى بعض اللَبِنات الأساسية الآخرى، ولكن بشَّرْط أن تَكُون قد كُتِبَت مُسْبَّقًا إِمّا بواسطتك أو بواسطة شخص آخر (لاحِظ أن برامج الإِدْخال والإِخراج تَقع ضِمْن هذا التصنيف). والآن، تَستطِيع بناء مُتتالية مِنْ تلك التَعْليمَات البسيطة، أو ربما قد تَدمجهم دِاخل بُنَى تحكُّم (control structures) أكثر تعقيدًا، مثل تَعْليمَة حَلْقة التَكْرار (loops)‫ while، وتَعْليمَة التَفْرِيع الشَّرْطيّة if. لنفْترِض أننا نريد برمجة الحاسوب ليُنفِّذ مُهِمّة (task) معينة، يُمكِننا البدء بكتابة تَوصِيف (description/specification) مَبدئي لتلك المُهِمّة، بحيث يُلخِّص وظيفة الخوارزمية (algorithm) المطلوب تَطْويرها، ثُمَّ نُضيف مزيدًا من الخطوات والتفاصيل إلى ذلك التَوصِيف تدريجيًا، وعبر سِلسِلة من التصميمات المُحسَّنة، إلى أن نَصِل إلى الخوارزمية الكاملة التي يُمكِن ترجمتها مباشرة إلى لغة برمجية. تُكتَب عادة تلك التَوصِيفات باِستخدَام ما يُعرَف باسم الشيفرة الوهمية (pseudocode)، وهي مجموعة من التَعْليمَات العَاميَّة، التي تُحاكِي بِنْية اللغات البرمجية، بصورة مُبسَّطة، وبدون قواعد الصيغة (syntax) الصارمة المُعتادة بالشيفرة الفعليّة. يُطلَق على هذا الأسلوب من كتابة البرامج اسم التصميم المُتدرج (stepwise refinement)، ويُصنَّف ضِمْن استراتيجيات التصميم من أعلى لأسفل (top-down design). لنفْحَص كيفية تَطبيق التصميم المتدرج لكتابة إحدى البرامج التي قد تَعرَّضنا لها خلال القسم السابق. يَحسِب هذا البرنامج قيمة الاستثمار خلال خمسة أعوام. يُمكِن تَوصِيف مُهِمّة البرنامج بالعبارة التالية: "اِحسب قيمة الاستثمار واطبعها لكل عام من الأعوام الخمسة التالية، بحيث تُحدَّد قيمة الاستثمار المبدئي وسعر الفائدة مِن قِبَل المُستخدِم". ربما نُفكر بكتابة الشيفرة الوهمية التالية: // اقرأ مدخل المستخدم Get the user's input // احسب قيمة الاستثمار بعد عام Compute the value of the investment after 1 year // اطبع القيمة Display the value // احسب قيمة الاستثمار بعد عامين Compute the value after 2 years // اطبع القيمة Display the value // احسب قيمة الاستثمار بعد ثلاث أعوام Compute the value after 3 years // اطبع القيمة Display the value // احسب قيمة الاستثمار بعد أربع أعوام Compute the value after 4 years // اطبع القيمة Display the value // احسب قيمة الاستثمار بعد خمس أعوام Compute the value after 5 years // اطبع القيمة Display the value على الرغم من أن الخوارزمية بالأعلى سليمة وستؤدي الغرض منها، لكنها مُكرَّرة، وهو ما يَعنِي ضرورة اِستخدَام حَلْقة تَكْرار (loop)؛ لأنها ستُمكِّننا من كتابة شيفرة مُعمَّمة أكثر وبعَدَد سطور أقل. يُقصَد بالتَعْميم (generalization) هنا أنه يُمكِن اِستخدَام نفس حَلْقة التَكْرار بغض النظر عن عدد الأعوام المطلوب مُعالجتها. والآن، سنُعيد كتابة مُتتالية الخطوات السابقة كالتالي: // اقرأ مدخل المستخدم Get the user's input // طالما ما يزال هناك عدد من الأعوام للمعالجة while there are more years to process: // احسب قيمة الاستثمار بعد العام التالي Compute the value after the next year // اطبع القيمة Display the value على الرغم من أن الخوارزمية (algorithm) بالأعلى سليمة، لكنها مُوجَزة، وربما مُبهَمة بشكل أكثر من اللازم. يَحتاج الحاسوب عمومًا إلى تَعْليمَات واضحة وصريحة، ولهذا سنحتاج، مثلًا، إلى شرح الخطوات: "اقرأ مُدْخَل المُستخدِم" و "احسب قيمة الاستثمار بَعْد العام التالي" و "ما يزال هناك عَدَد من الأعوام للمعالجة". فمثلًا، نستطيع إعادة تَوصِيف الخطوة "اِقْرأ مُدْخَل المُستخدِم" إلى التالي: // اسأل المستخدم عن قيمة الاستثمار المبدئي Ask the user for the initial investment // اقرأ مدخل المستخدم Read the user's response // اسأل المستخدم عن قيمة سعر الفائدة Ask the user for the interest rate // اقرأ مدخل المستخدم Read the user's response أمَا بخُصوص الخطوة "احسب قيمة الاستثمار بَعْد العام التالي"، فسنحتاج إلى معرفة طريقة حِسَابها (ينبغي أن تَطلُب في تلك الحالة مزيد من التوضيح من أستاذك أو مُديرك)، ولكن دَعْنَا الآن نفْترِض أن قيمة الاستثمار تُحسَب بإضافة قيمة فائدة معينة إلى قيمة الاستثمار السابقة، وبالتالي يُمكِننا إِعادة كتابة حَلْقة التَكْرار while كالتالي: // طالما ما يزال هناك عدد من الأعوام للمعالجة while there are more years to process: // احسب الفائدة Compute the interest // أضف الفائدة إلى قيمة الاستثمار Add the interest to the value // اطبع القيمة Display the value نحتاج الآن إلى توضيح الاختبار الموجود بالخطوة "ما يزال هناك عَدَد من الأعوام للمعالجة"، وهو ما يُمكِن القيام به عن طريق عدّ الأعوام بأنفسنا، سنَستخدِم عَدَّادًا قيمته تُساوِي الصفر، ثم نُزيِد قيمة هذا العَدَّاد بمقدار الواحد بَعْد كل مرة نُعالِج فيها عامًا جديدًا، ونَتوقَف عندما تُصبِح قيمة العَدَّاد مُساوِية للعَدَد المطلوب من الأعوام. يُطلَق على ذلك عادةً اسم "حَلْقة العدّ (counting loop)"، وهي أحد الأنماط الشائعة، ولذلك تَوقَّع أن تَستخدِم شيئًا مُشابهًا بكثير من البرامج. تُصبِح الآن حَلْقة التَكْرار while كالتالي: // ابدأ بعدد أعوام يساوي الصفر years = 0 // طالما عدد الأعوام أقل من الخمسة while years < 5: // أزد عدد الأعوام بمقدار الواحد years = years + 1 // احسب الفائدة Compute the interest // أضف الفائدة إلى تلك القيمة Add the interest to the value // اطبع القيمة Display the value نَحتاج إلى أن نكون أكثر توضيحًا بخُصوص طريقة حِسَاب الفائدة، وسنفْترِض أنها تُساوِي حاصل ضرب سعر الفائدة بقيمة الاستثمار الحالية. نُضيِف هذا الإيضاح إلى ذلك الجزء من الخوارزمية المسئول عن قراءة مُدْخَلات المُستخدِم، وبهذا، نَحصُل على الخوارزمية الكاملة: // اسأل المستخدم عن قيمة الاستثمار المبدئي Ask the user for the initial investment // اقرأ مدخل المستخدم Read the user's response // اسأل المستخدم عن قيمة سعر الفائدة Ask the user for the interest rate // اقرأ مدخل المستخدم Read the user's response // ابدأ بعدد أعوام يساوي الصفر years = 0 // طالما عدد الأعوام أقل من الخمسة while years < 5: // أزد عدد الأعوام بمقدار الواحد years = years + 1 // احسب الفائدة بحيث تساوي حاصل ضرب القيمة مع سعر الفائدة Compute interest = value * interest rate // أضف الفائدة إلى تلك القيمة Add the interest to the value // اطبع القيمة Display the value وَصلنا إلى النقطة التي يُمكِن معها الترجمة المُباشرة إلى لغة برمجة مناسبة، فقط نحتاج إلى اِختيار أسماء المُتَغيِّرات (variables)، وتَقْرير نص العبارات التي سنَطبَعها للمُستخدِم، وهكذا. نستطيع الآن كتابة الخوارزمية (algorithm) بلغة الجافا كالتالي: double principal, rate, interest; // التصريح عن المتغيرات int years; System.out.print("Type initial investment: "); principal = TextIO.getlnDouble(); System.out.print("Type interest rate: "); rate = TextIO.getlnDouble(); years = 0; while (years < 5) { years = years + 1; interest = principal * rate; principal = principal + interest; System.out.println(principal); } ما زال أمامنا بعض التَحسِّينات الإضافية، مِن بينها تَضْمِين هذه الشيفرة داخل برنامج كامل، وإضافة التعليقات (comments)، وطِباعة المَزيد من المعلومات للمُستخدِم، ولكنه يظِلّ نفس البرنامج بالقسم السابق. في حين تَستخدِم خوارزمية الشيفرة الوهمية (pseudocode algorithm) المسافات البادئة (indentation) لتوضيح التَعْليمَات الواقعة ضِمْن حَلْقة التَكْرار (loop)، تُهمِل لغة الجافا هذه المسافات البادئة تمامًا، ولهذا أَضفنا قوسين معقوصين (curly brackets/braces) {} لتوضيح أيّ مجموعة تَعْليمَات (statements) تقع ضِمْن حَلْقة التَكْرار. إذا لم تَستخدِم هذه الأقواس بشيفرة الجافا، فإن الحاسوب سيفْترِض أن التَعْليمَة الوحيدة الواقعة ضِمْن حَلْقة التَكْرار هي years = years + 1;‎، أمَا بقية التَعْليمَات فسيُنفِّذها مرة واحدة فقط بَعْد انتهاء حَلْقة التَكْرار. للأسف، لا يُبلِّغ الحاسوب عن هذه النوعية من الأخطاء، بنفس الطريقة التي يُبلِّغ بها عن حُدوث خطأ في حالة عدم اِستخدَام القوسين الهلاليين (rounded brackets/parentheses) () حول (years < 5)؛ وذلك لأن تلك الأقواس مطلوبة وِفقًا لصيغة (syntax) تَعْليمَة while، أمَا قوسيّ المعقوصين {}، فإنها مطلوبة فقط لأغراض دَلاليّة (semantics)، أيّ أغراض مُتعلِّقة بالمعنى. يَستطيع الحاسوب عمومًا تَمييز أخطاء بناء الجملة (syntax errors) فقط، لا الأخطاء الدَلاليّة (semantic errors). لاحِظ أن التَوصِيف الأصلي للمسألة التالي لم يَكُن مُكتملًا: ينبغي لك عمومًا، قَبْل بدء كتابة أيّ برنامج، أن تتأكد من أن لديك التَوصِيف الكامل لوظيفة البرنامج المطلوب كتابته، فلابُدّ أن تَعرِف المعلومات التي سيَقرأها البرنامج (input) وأيّ خَرْج (output) ينبغي أن يَطبعُه، وكذلك الحِسَابات التي ينبغي له القيام بها. ربما يُمكِننا إعادة تَوصِيف نفس البرنامج السابق بصورة أكثر معقولية كالتالي: مسألة متتالية "3N+1" لنفْحَص مثالًا آخرًا لم نتَعرَّض له من قَبْل، السؤال هنا عبارة عن مَسألة رياضية مُجرَّدة، والتي يَعُدّها الكاتب تَحْدِيدًا واحدة مِن تمارينه البرمجية المُفضلة. سنبدأ هذه المرة، بعكس المثال السابق، بتَوصِيف كامل (specification) لمُهِمّة (task) البرنامج: اُكْتُب برنامجًا يَقرأ عَدَدًا صحيحًا موجبًا من المُستخدِم، ثُمَّ يَطبَع مُتتالية الأعداد "3N+1"، بحيث تبدأ من العَدَد المُدْخَل، كما يَنبغي للبرنامج أن يعدّ عَدَد عناصر المُتتالية ويَطبعها." اُنظر الخوارزمية المبدئية التالية، والتي تُوضِح فقط التَصَوُّر العام لمثل هذا البرنامج: // اقرأ عدد صحيح موجب من المستخدم Get a positive integer N from the user. // احسب قيمة كل عنصر بالمتتالية، واطبعه وعدّه Compute, print, and count each number in the sequence. // اطبع عدد عناصر المتتالية Output the number of terms. يَتضح لنا أن الخطوة الثانية تَحتوِي على المَضمون الفِعليّ للبرنامج، وبالطبع تحتاج إلى مزيد من الإيضاح. لمّا كنا نُريد الاستمرار بحِسَاب قيم عناصر المُتتالية حتى تُصبِح قيمة N الحالية مُساوِية للعَدَد ١، فإننا سنحتاج ببساطة إلى اِستخدَام حَلْقة تَكْرار (loop)، ولذلك دَعْنَا نُعيد صياغة نفس الجملة السابقة بحيث تَتوافق مع حَلْقة التَكْرار while. إننا في حاجة إلى مَعرِفة متى نَستمِر بتَّنْفيذ حَلْقة التَكْرار ومتى نُوقِّفها، في الواقع، سنستمر طالما كانت قيمة N الحالية لا تُساوِي ١، ولهذا يُمكِننا إعادة كتابة خوارزمية الشيفرة الوهمية (pseudocode algorithm) كالتالي: // اقرأ عدد صحيح موجب من المستخدم Get a positive integer N from the user; // ‫‫طالما كانت قيمة `N` الحالية لا تساوي 1 while N is not 1: // ‫احسب قيمة عنصر المتتالية التالي واسنده إلى N Compute N = next term; // ‫اطبع قيمة N Output N; // عدّ عنصر المتتالية Count this term; // اطبع عدد عناصر المتتالية Output the number of terms; لمّا كان حِسَاب قيمة عنصر المُتتالية التالي يَعتمِد على ما إذا كانت قيمة N الحالية هي عَدَد زوجي (even) أم فردي (odd)، يَعنِي ذلك أن الحاسوب بحاجة إلى تَّنْفيذ حَدَثين مُختلفين لكل حالة، وهو ما يَعنِي أن اِستخدَام تَعْليمَة التَفْرِيع الشَّرْطيّة if بات ضروريًا؛ وذلك للاختيار ما بين تلك الحالتين، اُنظر الخوارزمية بَعْد التعديل: // اقرأ عدد صحيح موجب من المستخدم Get a positive integer N from the user; // ‫‫طالما كانت قيمة `N` الحالية لا تساوي 1 while N is not 1: // ‫إذا كان N عددًا زوجيًا if N is even: // ‫احسب قيمة العنصر التالي وأسنده إلى N Compute N = N/2; // ‫إذا كان N عددًا فرديًا else // ‫احسب قيمة العنصر التالي وأسنده إلى N Compute N = 3 * N + 1; // ‫اطبع قيمة N Output N; // عدّ عنصر المتتالية Count this term; // اطبع عدد عناصر المتتالية Output the number of terms; انتهينا تقريبًا، يَتبقَّى فقط العدّ (counting)؛ وذلك لطباعة عَدَد عناصر المُتتالية. يَعنِي العدّ ببساطة أن تبدأ بالقيمة صفر، ثم تُضيف المقدار واحد في كل مرة يَكُون لديك فيها ما تعدّه، ولهذا نحتاج إلى مُتَغيِّر (variable) للقيام بالعدّ، يُعرَف باسم العَدَّاد (counter). يَنبغي ضَبْط قيمة ذلك المُتَغيِّر إلى القيمة صفر قَبْل بداية الحَلْقة (loop)، بحيث تَزداد (increment) تلك القيمة أثناء تَّنْفيذ الحَلْقة. (يُعدّ ذلك أحد الأنماط الشائعة [common pattern]، ولذلك تَوقَّع أن تراه بكثير من البرامج). تُصبِح الخوارزمية، بَعْد إضافة العَدَّاد (counter)، كالتالي: // اقرأ عدد صحيح موجب من المستخدم Get a positive integer N from the user; // اضبط قيمة العداد إلى القيمة صفر Let counter = 0; // ‫‫طالما كانت قيمة N الحالية لا تساوي 1 while N is not 1: // ‫إذا كان N عددًا زوجيًا if N is even: // ‫احسب قيمة العنصر التالي وأسنده إلى N Compute N = N/2; // ‫إذا كان N عددًا فرديًا else // ‫احسب قيمة العنصر التالي وأسنده إلى N Compute N = 3 * N + 1; // اطبع قيمة‫ N Output N; // ‫أزد قيمة العداد بمقدار 1 Add 1 to counter; // اطبع عدد عناصر المتتالية Output the counter; ما يزال أمامنا مشكلة أخيرة بخُصوص الخطوة الأولى، وهي كيف نتأكد من أن المُستخدِم قد أَدْخَل عَدَدًا صحيحًا موجبًا؟ ففي الواقع، قد يُدْخِل المُستخدِم عَدَدًا سالبًا أو صفرًا، وعندها سيستمر تَّنْفيذ البرنامج للأبد؛ لأن القيمة المُدْخَلة N، في تلك الحالة، لن تُصبِح أبدًا مُساوِية للواحد. ربما قد لا يُعدّ ذلك مشكلة ضخمة في تلك الحالة تَحْدِيدًا، ولكن، مع ذلك، ينبغي عمومًا محاولة كتابة برامج غَيْر قابلة للخطأ. نستطيع حَل تلك المُشكلة عن طريق الاستمرار بقراءة الأعداد إلى أن يُدْخِل المُستخدِم عددًا صحيحًا موجبًا. // اطلب من المستخدم إدخال عدد صحيح موجب Ask user to input a positive number; // ‫أسند القيمة المدخلة إلى N Let N be the user's response; // طالما‫ N ليست موجبة while N is not positive: // اطبع رسالة خطأ Print an error message; // ‫اقرأ قيمة أخرى واسندها إلى N Read another value for N; // اضبط قيمة العداد إلى القيمة صفر Let counter = 0; // ‫‫طالما كانت قيمة `N` الحالية لا تساوي 1 while N is not 1: // ‫إذا كان N عددًا زوجيًا if N is even: // ‫احسب قيمة العنصر التالي وأسنده إلى N Compute N = N/2; // ‫إذا كان N عددًا فرديًا else // ‫احسب قيمة العنصر التالي وأسنده إلى N Compute N = 3 * N + 1; // اطبع قيمة‫ N Output N; // ‫أزد قيمة العداد بمقدار 1 Add 1 to counter; // اطبع عدد عناصر المتتالية Output the counter; لاحِظ أن حَلْقة while الأولى ستنتهي فقط عندما تُصبِح قيمة N زوجية. عند محاولة كتابة شيفرة التَوصِيف التالي: "إذا كانت قيمة N غَيْر زوجية، اُطلب من المُستخدِم إِدْخَال عدد آخر"، يقع الكثير من المبرمجين، وبالأخص المبتدئين، في خطأ اِستخدَام تَعْليمَة التَفْرِيع if بدلًا من تَعْليمَة حَلْقة التَكْرار while. تَظهر المشكلة تَحْدِيدًا عندما يُدْخِل المُستخدِم عددًا غَيْر زوجي مرة آخرى. لمّا كانت تَعْليمَة التَفْرِيع if تُنفَّذ مرة واحدة فقط، فإنه لا يَتمّ فَحْص مُدْخَل المُستخدِم إلا مرة واحدة فقط، مما يعني أن البرنامج سينتقل إلى تَّنْفيذ التَعْليمَة التالية بغض النظر عما إذا كانت قيمة المُدْخَل الثاني للمُستخدِم زوجية أم لا، وهو ما يَتسبَّب بحُدوث حَلْقة لا نهائية (infinite loop) كما ذَكرنا آنفًا. أمَا في حالة اِستخدَام حَلْقة التَكْرار while، فإن الحاسوب سيَقفِز (أو سيَنقِل التَحكُّم بتعبير أدق) إلى بداية الحَلْقة بَعْد كل عملية إِدْخَال؛ لاختبار ما إذا كانت القيمة المُدْخَلة زوجية أم لا، مما يَعنِي أنه سيستمر في طلب إِدْخَال عَدَد جديد إلى أن يُدْخِل المُستخدِم قيمة مقبولة، أيّ عدد زوجي. وبالتالي، في حالة انتقال البرنامج إلى تَّنْفيذ ما بَعْد حَلْقة while، فإن قيمة N هي زوجية حتمًا. ها هو نفس البرنامج بشيفرة الجافا. لاحِظ اِستخدَام العَامِلين (operators)‏ ‎<=‎ بمعنى "أقل من أو يُساوِي" و ‎!=‎ بمعنى "لا يُساوِي"، بالإضافة إلى اِستخدَام التعبير N % 2 == 0؛ لاختبار ما إذا كانت قيمة N زوجية. نُوقِشَت كل هذه العَوامِل في القسم ٢.٥. import textio.TextIO; public class ThreeN1 { public static void main(String[] args) { int N; // لحساب العناصر بالمتتالية int counter; // لعد عدد عناصر المتتالية System.out.print("Starting point for sequence: "); N = TextIO.getlnInt(); while (N <= 0) { System.out.print( "The starting point must be positive. Please try again: " ); N = TextIO.getlnInt(); } // ‫نعلم أن N هي عدد صحيح موجب عند هذه النقطة counter = 0; while (N != 1) { if (N % 2 == 0) N = N / 2; else N = 3 * N + 1; System.out.println(N); counter = counter + 1; } System.out.println(); System.out.print("There were "); System.out.print(counter); System.out.println(" terms in the sequence."); } // نهاية‫ main } // ‫نهاية الصنف ThreeN1 مُلاحظتان أخيرتان على هذا البرنامج: أولًا، ربما لاحَظت أن البرنامج لم يَطبَع قيمة أول عنصر بالمُتتالية -أيّ قيمة N المُدْخَلة من قِبَل المُستخدِم-، وكذلك لم يعدّها. هل هذا خطأ؟ يَصعُب القول. ربما ينبغي أن نَطرح سؤالًا آخر: هل كان تَوصِيف البرنامج (specification) صريحًا بما يَكفي بخُصوص تلك النقطة؟ في الواقع، للإجابة على مثل هذا السؤال، ستَحتاج إلى طَلَب مزيد من الإيضاح من أستاذك/مديرك. يُمكِن عمومًا حل هذه المشكلة -في حال كانت- بسهولة، فقط اِستبدل السَطْرين التاليين بتَعْليمَة counter = 0 قَبْل حَلْقة التَكْرار while : System.out.println(N); // print out initial term counter = 1; // and count it ثانيًا، لماذا تُعدّ هذه المسألة تَحْدِيدًا مثيرة؟ في الواقع، يَجِدْ كثير من علماء الرياضيات والحاسوب هذه المسألة مُشوقة؛ بسبب سؤال بسيط، يَخُص تلك المسألة، والذي لم يَتوصَّلوا للإجابة عليه بَعْد. السؤال هو "هل عملية حِسَاب قيم مُتتالية '3N+1' دومًا ما ستنتهي بَعْد عَدَد مُتناهي (finite) من الخطوات لجميع قيم N المبدئية؟" على الرغم من سهولة حِسَاب قيم المُتتاليات بشكل مُفرد، لم يُجِبْ أحد على السؤال الأعم حتى الآن، أيّ بصياغة آخرى، لا أحد يَعلم ما إذا كان من الصحيح تسمية عملية حِسَاب قيم مُتتالية "3N+1" بـ"الخوارزمية (algorithm)"؛ فبالنهاية، لابُدّ لأيّ خوارزمية أن تَنتهي بَعْد عَدَد مُتناهي من الخطوات. لاحظ: يَنطبق ذلك على الأعداد الصحيحة (integers) بمفهومها الرياضي، وليس القيم من النوع العددي الصحيح int! بمعنى أننا نفْترِض هنا أن قيمة N قد تَكُون أيّ عدد صحيح مُمكن مهما كَبُر، وهو ما لا يَنطبق على مُتَغيِّر من النوع int داخل برنامج بلغة الجافا. إذا أَصبحت قيمة N كبيرة جدًا ليَتمّ تَمثيلها بمُتَغيِّر من النوع int ‏(32 بت)، فلا يُمكِن عدّ قيم خَرْج البرنامج صحيحة رياضيًا، أيّ أن البرنامج لا يَحسِب قيم متتالية "3N+1" بشكل صحيح عندما تَكُون قيمة N كبيرة. اُنظر تمرين ٨.٢. كتابة الشيفرة (coding) والاختبار (testing) وتنقيح الأخطاء (debugging) بَعْد انتهائك من تَطوير خوارزمية البرنامج (algorithm)، سيَكُون من اللطيف لو كان بإمكانك الضغط فقط على زِر معين؛ لتَحصُل بَعْدها على برنامج قابل للتَّنْفيذ (working program) بصورة ممتازة. في الواقع، عملية تَحْوِيل الخوارزمية إلى شيفرة بلغة الجافا لا تَتمّ دومًا بمثل هذه السَلاسَة لسوء الحظ، وحتى عندما تَصِل إلى تلك المرحلة من الحُصول على برنامج قابل للتَّنْفيذ (working program)، فإنه غالبًا ما يَكُون قابلًا للتَّنْفيذ بمعنى أنه يُنفِّذ "شيء ما"، لا بالضرورة الشيء الذي تريده أن يَفعَله. بَعْد الانتهاء من تصميم البرنامج (program design)، يَحيِن موعد كتابة الشيفرة (coding): أيّ ترجمة التصميم إلى برنامج مكتوب بلغة الجافا أو بأيّ لغة برمجية اخرى. مَهمَا كنت حَريصًا أثناء كتابة الشيفرة، فعادةً ما ستَجِدْ بعض أخطاء بناء الجملة (syntax errors) طريقها إلى الشيفرة، ولذلك سيَرفُض مُصرِّف (compiler) الجافا البرنامج، وسيُبلِّغك عن نوع معين من رسائل الخطأ (error message). لاحِظ أنه في حين يستطيع المُصرِّف اكتشاف أخطاء بناء الجملة (syntax errors) دائمًا، فإنه لسوء الحظ ليس بنفس الكفاءة في اكتشاف مَاهية الخطأ، بل أنه قد لا يَتَمكَّن، في بعض الأحيان، من معرفة مكان حُدوث الخطأ الفِعليّ، فمثلًا، قد يَتسبَّب وجود خطأ إملائي أو نَسْيَان قوس "{" بالسطر رقم ٤٥ بتَوَقُّف المُصرِّف بالسطر ١٠٥. يَظِلّ، مع ذلك، الفهم الجيد لقواعد صياغة (syntax rules) اللغة البرمجية مع اِتباع بعض القواعد الإرشادية البرمجية البسيطة الطريقة الأفضل لتَلافِي كثير من تلك الأخطاء. لنسْتَعْرِض بعضًا من تلك القواعد، أولًا، لا تَكتُب أبدًا قوس حَاصِرة "{" بدون كتابة زوجه الآخر "}"، ثُمَّ عُد بَعْدها لكتابة التَعْليمَات بينهما؛ وذلك لأن نَسْيَان قوس أو إضافة قوس في غَيْر مَحَلّه يُعدّ من أكثر الأخطاء التي يَصعُب اكتشافها خاصة بالبرامج الضخمة. ثانيًا، اِستخدِم دائما المسافات البادئة (indentation) لتَنسيق الشيفرة، وإن عَدَّلت البرنامج، عَدِّل أيضًا المسافات البادئة بحيث تُصبِح مُتوافقة مع التَعْدِيل الجديد. ثالثًا، اِستخدِم نَمط تَسمية (naming scheme) ثابت؛ حتى لا تُعانِي بَعْد ذلك بينما تَتَذكَّر ما إذا كان اسم مُتَغيِّر ما (variable) هو "interestrate" أم "interestRate". رابعًا، عندما يُبلِّغك المُصرِّف بأكثر من رسالة خطأ (error message) واحدة، لا تُحاوِل إصلاح رسالة الخطأ الثانية حتى تَنتهي من إِصلاح الأولى؛ لأن المُصرِّف عادةً ما يَرتبك بعد إِيجاده لأول خطأ، ولذلك قد تَكُون رسائل الخطأ التالية هي مُجرَّد تَخمينات. وأخيرًا، وربما هي النصيحة الأفضل: خُذ الوقت الكافي لفِهم الخطأ قبل مُحاولة إِصلاحه؛ فالبرمجة، بالنهاية، ليست علمًا تجريبيًا (experimental science). إذا تمّ تَصرِيف برنامجك بنجاح، لا يَعنِي ذلك أنك قد انتهيت؛ فمن الضروري أن تَختبر (test) البرنامج لتتأكد مما إذا كان يَعمَل بشكل صحيح، وهو ما لا يَقْتصِر على مُجرَّد الحُصول على الخَرْج الصحيح (output) لعينة المُدْخَلات (inputs) التي أَعطاك إِياها الأستاذ، بل ينبغي للبرنامج أن يَعمَل بشكل سليم لجميع المُدْخَلات المقبولة، وفي حالة اِستقباله لمُدَْخَل غَيْر صالح، فينبغي أن يُوبِخ البرنامج المُستخدِم بلطف، لا أن يَنَهار (crashing) تمامًا. عمومًا، ينبغي أن تَختبِر البرنامج على نطاق واسع من المُدْخَلات. قد تُحاوِل أيضًا إِيجاد مجموعة المُدْخَلات التي بإِمكانها اِختبار جميع الوظائف التي أَدْرَجتها بالشيفرة. في حالة كتابة برامج كبيرة، حَاوِل تقسيمها إلى عدة مراحل، بحيث تَختبِر كل مرحلة قَبْل البدء بالمرحلة التالية، حتى لو اضطررت لكتابة شيفرة إِضافية تقوم بالاختبار مثل أن تَستدعِي أحد البرامج الفرعية التي قُمت بكتابتها للتو؛ فأنت حتمًا لا تُريد أن يَنتهي بك الحال بخُمسمائة سَطْر جديد من الشيفرة مَصحوبة بخطأ ما في مكان ما. الغرض من الاختبار (testing) هو مُحاولة العُثور على الأخطاء البرمجية (bugs)، وهي -بعكس أخطاء التَصرِيف (compilation errors)- أخطاء دَلاليّة (semantic errors)، أيّ تَكُون في صورة سُلوك غَيْر سليم. المُحزن في الأمر هو أنك غالبًا ما ستَجِدهم. تستطيع، مع ذلك، تَقْليل -لا التَفادِي التام- هذه النوعية من الأخطاء البرمجية (bugs)، من خلال الانتباه أثناء قيامك بكُلًا من التصميم (design)، وكتابة الشيفرة (coding). عندما تَكتشف خطأً برمجيًا (bug)، يَحيِن موعد تَنْقِيح الأخطاء (debugging)، بمعنى تَعَقُّب سبب الخطأ بالشيفرة بهدف التخلص منه. لاحظ أنك لن تُصبِح خبيرًا بتَنْقِيح الأخطاء إلا من خلال المُمارسة المستمرة؛ فهي مَهارة تكتسبها، مثلها مثل جميع نواحي البرمجة الآخرى، ولذلك لا تَكُن خائفا منها، وإنما تَعلَّم منها. تُعدّ القدرة على قراءة الشيفرة واحدة من أهم مهارات تَنْقِيح الأخطاء الاساسية، والمقصود بقراءة الشيفرة هنا: القدرة على تَنْحية تَصوراتك المُسْبَّقة عما ينبغي للشيفرة أن تقوم به، وبدلًا من ذلك، تَعقُّب الطريقة التي يُنفِّذها بها الحاسوب خَطوة بخَطوة؛ لتُدرِك ما تَقوم به فِعليًا. في الواقع، هذا ليس بالأمر السهل. ما يزال الكاتب يَذكُر تلك المرة التي قضى فيها عدة ساعات يَبحث عن خطأ برمجي ليَكتشِف بالنهاية أن سَطْرًا ما بالشيفرة، كان قد نَظَر إليه عشرات المرات، يَحتوِي على القيمة ١ بدلًا من الحرف i، أو تلك المرة التي كَتَب فيها برنامجًا فرعيًا (subroutine) اسمه هو WindowClosing، والذي كان سيُؤدي غرضه تمامًا لولا أن الحاسوب كان يَبحث عن البرنامج الفرعي windowClosing (بحرف w صغير). قد يُساعِدك أحيانًا الاستعانة بشخص آخر لفَحْص الشيفرة، خاصة وأنه لن يَملك نفس تَصوراتك المُسْبَّقة عنها. أحيانًا ما يَكُون مُجرَّد العثور على ذلك الجزء من البرنامج الذي يَكمُن فيه الخطأ مُشكلة بحد ذاته، ولهذا تُوفِّر مُعظم بيئات التطوير البرمجية (programming environments) برنامجًا يُسمَى مُنقِّح الأخطاء (debugger)؛ لمساعدتك بالعثور على الأخطاء البرمجية (bugs)، بحيث يَتمّ تَشْغِيل البرنامج (program) تحت تَحكُّم ذلك المُنقِّح، والذي يَسمَح لك بضَبْط ما يُعرَف باسم نقاط المُقاطعة (breakpoints)، وهي نقاط بالبرنامج سيَتوقَّف عندها المُنقِّح مؤقتًا (pause)؛ حتى تَتَمكَّن من فَحْص قيم مُتَغيِّرات البرنامج عند تلك النقطة، مما سيُساعدك في العُثور على المكان الذي بدأت فيه الأخطاء البرمجية بالظهور أثناء تَّنْفيذ البرنامج. بمُجرَّد تَحْدِيد ذلك الجزء من البرنامج الذي يَكمُن فيه الخطأ البرمجي (bug)، يَسمَح لك المُنقِّح أيضًا بتَّنْفيذ البرنامج سَطْرًا سَطْرًا، ومِنْ ثَمَّ، تستطيع مشاهدة ما يَحدُث تفصيليًا. يَعترِف الكاتب أنه لا يَستخدِم مُنقِّح الأخطاء دائمًا، وإنما يَتبِع المنهج التقليدي لتَنْقِيح الأخطاء، وذلك بإضافة تَعْليمَات تَنْقِيحيّة (debugging statements) داخل البرنامج، والتي هي مُجرَّد تَعْليمَات خَرْج تَطبَع معلومات عن حالة (state) البرنامج. عادةً ما تُكتَب أيّ تَعْليمَة تَنْقِيحيّة على الصورة التالية: System.out.println("At start of while loop, N = " + N); ينبغي لنص الخَرْج أن يُساعِدك على تَحْدِيد مكان تَعْليمَة الطباعة المسئولة عن ذلك الخَرْج، وكذلك معرفة قيم المُتَغيِّرات (variables) المُهمة. ستكتشف، في بعض الأحيان، أن الحاسوب لم يَمر حتى على ذلك الجزء من البرنامج الذي كنت تَظن أنه يُنفِّذه. تَذكَّر أن الهدف هو اكتشاف أول نُقطة بالبرنامج أَصبَحت فيها حالة البرنامج (state) مُخالِفة للحالة المُتوقَّعة، فهذا هو مكان الخطأ البرمجي (bug). وأخيرًا، تَذكَّر القاعدة الذهبية لتَنْقِيح الأخطاء: إذا كنت مُتأكدًا تمامًا أن كُل شئ بالبرنامج سليم، ومع ذلك ما يزال لا يَعمَل بالصورة المطلوبة، فلابُدّ أن واحدًا من تلك الأشياء هو ببساطة خطأ. ترجمة -بتصرّف- للقسم Section 3.1 Blocks, Loops, and Branches من فصل Chapter 3: Programming in the Small II: Control من كتاب Introduction to Programming Using Java.
  23. سنتناول في هذا القسم نوعًا آخر من الحَلْقات، هو تَعْليمَة الحَلْقة for. ينبغي أن تُدرك أنه يُمكن لأيّ حَلْقة تَكْرارية (loop) عامةً أن تُكتَب بأيّ من التَعْليمَتين for و while، وهو ما يَعني أن لغة الجافا لم تتَحَصَّل على أيّ مِيزَة وظيفية إضافية بتَدْعِيمها لتَعْليمَة for. لا يَعني ذلك أن تَعْليمَة for غَيْر مُهمة، على العكس تمامًا، ففي الواقع، قد يَتجاوز عَدَد حَلْقات for المُستخدَمة ببعض البرامج عَدَد حَلْقات while. (كما أن الكاتب على معرفة بأحد المبرمجين والذي لا يَستخدِم سوى حَلْقات for). الفكرة ببساطة أن تَعْليمَة for تكون أكثر ملائمة لبعض النوعيات من المسائل؛ حيث تُسهِل من كتابة الحَلْقات وقرائتها بالموازنة مع حَلْقة while. حلقة التكرار For عادةً ما تُستخدَم حَلْقة التَكْرار while بالصياغة التالية: <initialization> while ( <continuation-condition> ) { <statements> <update> } مثلًا، اُنظر لهذا المثال من القسم ٣.٢: years = 0; // هيئ المتغير while ( years < 5 ) { // شرط حلقة التكرار // نفذ التعليمات الثلاثة التالية interest = principal * rate; principal += interest; System.out.println(principal); // حدث قيمة المتغير years++; } ولهذا أضيفت تَعْليمَة for للتسهيل من كتابة هذا النوع من الحَلْقات، حيث يُمكِن إِعادة كتابة حَلْقة التَكْرار بالأعلى باستخدام تَعْليمَة for، كالتالي: for ( years = 0; years < 5; years++ ) { interest = principal * rate; principal += interest; System.out.println(principal); } لاحظ كيف دُمجَت كُُلًا من تعليمات التهيئة ، والشَّرْط الاستمراري لحَلْقة التَكْرار ، والتَحْدِيث جميعًا بسَطْر واحد هو السَطْر الأول من حَلْقة التَكْرار for. يُسهِل ذلك من قراءة حَلْقة التَكْرار وفهمها؛ لأن جميع تَعْليمَات التحكُّم بالحَلْقة (loop control) قد ضُمِّنت بمكان واحد بشكل منفصل عن مَتْن الحَلْقة الفعليّ المطلوب تَكْرار تَّنْفيذه. تُنفَّذ حَلْقة التَكْرار for بالأعلى بنفس الطريقة التي تُنفَّذ بها الشيفرة الأصلية، أي تُنفَّذ أولًا تعليمة التهيئة مرة وحيدة قبل بدء تَّنْفيذ الحَلْقة، ثم يُفحَص الشَّرْط الاستمراري للحَلْقة قَبْل كل تَكْرار (iteration/execution) لمَتْن الحَلْقة، بما في ذلك التَكْرار الأوَّليّ (first iteration)، بحيث تَتوقَف الحَلْقة عندما يؤول هذا الشَّرْط إلى القيمة false. وأخيرًا، تُنفَّذ تعليمة التَحْدِيث بنهاية كل تَكْرار (iteration/execution) قَبْل العودة لفَحْص الشَّرْط من جديد. تُكتَب تَعْليمَة حَلْقة التَكْرار for بالصياغة التالية: for ( <initialization>; <continuation-condition>; <update> ) <statement> أو كالتالي إذا كانت التعليمة كُتليّة البِنْية (block statement): for ( <initialization>; <continuation-condition>; <update> ) { <statements> } يُمكِن لأيّ تعبير منطقي (boolean-valued expression) أن يُستخدَم محل الشَّرْط الاستمراري . يُمكِن لأيّ تعبير (expression) -طالما كان صالحًا كتَعْليمَة برمجية- أن يُستخدَم محل تعليمة التهيئة ، وفي الواقع غالبًا ما تُستخدَم تَعْليمَة تَّصْريح (declaration) أو إِسْناد (assignment). يُمكِن لأي تَعْليمَة بسيطة (simple statement) أن تُستخدَم محل تعليمة التَحْدِيث ، وعادةً ما تكون تَعْليمَة زيادة/نقصان (increment/decrement) أو إِسْناد (assignment). وأخيرًا، يُمكِن لأي من تلك التعليمات الثلاثة بالأعلى أن تكون فارغة. لاحظ أنه إذا كان الشَّرْط الاستمراري فارغًا، فإنه يُعامَل وكأنه يُعيد القيمة المنطقية true، أي يُعدّ الشَّرْط مُتحقِّقًا، مما يعني تَّنْفيذ مَتْن حَلْقة التَكْرار (loop body) بشكل لا نهائي (infinite loop) إلى أن يتم إيقافها لسبب ما، مثل اِستخدَام تَعْليمَة break. يُفضِّل بعض المبرمجين في الواقع تَّنْفيذ الحَلْقة اللا نهائية (infinite loop) باِستخدَام الصياغة for (;;)‎ بدلًا من while (true)‎. يُوضح المخطط (diagram) التالي مَسار التحكُّم (flow control) أثناء تَّنْفيذ حَلْقة التَكْرار for: عادةً ما تُسْنِد تعليمة التهيئة قيمة ما إلى مُتَغيِّر معين، ثم تُعدِّل تعليمة التَحْدِيث قيمة هذا المُتَغيِّر إِمّا بواسطة تَعْليمَة إِسْناد (assignment) وإِمّا بعملية زيادة/نُقصان (increment/decrement)، وتُفْحَص تلك القيمة من خلال الشَّرْط الاستمراري لحَلْقة التَكْرار (continuation condition)، فتَتوقَف الحَلْقة عندما يؤول الشَّرْط إلى القيمة المنطقية false. يُطلق عادة على المُتَغيِّر المُستخدَم بهذه الطريقة اسم المُتحكِّم بالحَلْقة (loop control variable). في المثال بالأعلى، كان المُتحكِّم بالحَلْقة هو المُتَغيِّر years. تُعدّ حَلْقة العَدّ (counting loop) هي النوع الأكثر شيوعًا من حَلْقات التَكْرار for، والتي يأخذ فيها المُتَغيِّر المُتحكِّم بالحَلْقة (loop control variable) قيم جميع الأعداد الصحيحة (integer) الواقعة بين قيمتين إحداهما صغرى (minimum) والآخرى عظمى (maximum). تُكتَب حَلْقة العَدّ كالتالي: for ( <variable> = <min>; <variable> <= <max>; <variable>++ ) { <statements> } يُمكِن لأيّ تعبير يُعيد عددًا صحيحًا (integer-valued expressions) أن يُستخدَم محل و ، ولكن تُستخدَم عادةً قيم ثابتة (constants). يأخذ المُتَغيِّر -المشار إليه باستخدام بالأعلى والمعروف باسم المُتحكِّم بالحَلْقة (loop control variable)- القيم المتراوحة بين و ، أي القيم ‎<min>+1‎‎ و ‎<min>+2‎‎ ..وحتى . وغالبًا ما تُستخدَم قيمة هذا المُتَغيِّر داخل المَتْن (body). مثلًا، حَلْقة التَكْرار for بالأعلى هي حَلْقة عَدّ يأخذ فيها المُتَغيِّر المُتحكِّم بالحَلْقة years القيم ١ و ٢ و ٣ و ٤ و ٥. تطبع الشيفرة التالية قيم الأعداد من ١ إلى ١٠ إلى الخَرْج القياسي (standard output): for ( N = 1 ; N <= 10 ; N++ ) System.out.println( N ); مع ذلك، يُفضِّل مبرمجي لغة الجافا بدء العَدّ (counting) من ٠ بدلًا من ١، كما أنهم يميلون إلى اِستخدَام العَامِل > للموازنة بدلًا من ‎<=‎‎. تطبع الشيفرة التالية قيم الأعداد العشرة ٠، ١، ٢، …، ٩، كالتالي: for ( N = 0 ; N < 10 ; N++ ) System.out.println( N ); يُعدّ اِستخدَام عَامِل الموازنة > بدلًا من ‎<=‎‎ أو العكس مصدرًا شائعًا لحُدوث الأخطاء بفارق الواحد (off-by-one errors) بالبرامج. حاول دائمًا أن تأخذ وقتًا للتفكير إذا ما كنت تَرغَب بمعالجة القيمة النهائية أم لا. يُمكنك أيضًا إجراء العَدّ التنازلي، وهو ما قد يكون أسهل قليلًا من العَدّ التصاعدي. فمثلًا، لإجراء عَدّ تنازلي من ١٠ إلى ١. ابدأ فقط بالقيمة ١٠، ثم اِنقص المُتَغيِّر المُتحكِّم بالحَلْقة (loop control variable) بدلًا من زيادته، واستمر طالما كانت قيمة المُتَغيِّر أكبر من أو تُساوِي ١: for ( N = 10 ; N >= 1 ; N-- ) System.out.println( N ); في الواقع، تَسمَح صيغة (syntax) تَعْليمَة for بأن تَشتمِل كُلًا من تعليمتي التهيئة والتَحْدِيث على أكثر من تعبير (expression) مربوطين بفاصلة (comma). يَعنِي ذلك أنه من الممكن الإبقاء على أكثر من عَدَّاد بنفس الوقت، فمثلًا قد يكون لدينا عَدَّاد تصاعدي من ١ إلى ١٠، وآخر تنازلي من ١٠ إلى ١، كالتالي: for ( i=1, j=10; i <= 10; i++, j-- ) { // ‫اطبع قيمة i بخمس خانات System.out.printf("%5d", i); // ‫اطبع قيمة j بخمس خانات System.out.printf("%5d", j); System.out.println(); } كمثال أخير، نريد اِستخدَام حَلْقة التَكْرار for لطباعة الأعداد الزوجية (even numbers) الواقعة بين العددين ٢ و ٢٠، أي بالتحديد طباعة الأعداد ٢، ٤، ٦، ٨، ١٠، ١٢، ١٤، ١٦، ١٨، ٢٠. تتوفَّر أكثر من طريقة للقيام بذلك، نسْتَعْرِض منها أربعة حلول ممكنة (ثلاث منها هي حلول نموذجية تمامًا)؛ وذلك لبيان كيف لمسألة بسيطة مثل تلك المسألة أن تُحلّ بطرائق مختلفة: // (1) // المتغير المتحكم بالحلقة سيأخذ القيم من واحد إلى عشرة // وبالتالي سنطبع القيم 2*1 و 2*2 و ... إلى 2*10 for (N = 1; N <= 10; N++) { System.out.println( 2*N ); } // (2) // المتغير المتحكم بالحلقة سيأخذ القيم المطلوب طباعتها مباشرة // عن طريق إضافة 2 بعبارة التحديث بدلًا من واحد for (N = 2; N <= 20; N = N + 2) { System.out.println( N ); } // (3) // مر على جميع الأرقام من اثنين إلى عشرين // ولكن اطبع فقط الأعداد الزوجية for (N = 2; N <= 20; N++) { if ( N % 2 == 0 ) // is N even? System.out.println( N ); } // (4) // فقط اطبع الأعداد المطلوبة مباشرة // غالبًا سيغضب منك الأستاذ في حالة إطلاعه على مثل هذا الحل for (N = 1; N <= 1; N++) { System.out.println("2 4 6 8 10 12 14 16 18 20"); } من المهم أن نُعيد التأكيد على أنه -باستثناء تَعْليمَة التَّصْريح عن المُتَغيِّرات (variable declaration)- ليس مُمكنًا بأي حال من الأحوال تَّنْفيذ أي تَعْليمَة برمجية، بما في ذلك تَعْليمَة for، بشكل مستقل، وإنما ينبغي أن تُنفَّذ إما داخل البرنامج (routine) الرئيسي main أو داخل إحدى البرامج الفرعية (subroutine)، والمُعرَّفة ضِمْن صَنْف معين (class). لابُدّ أيضًا من التَّصْريح (declaration) عن أي مُتَغيِّر قبل إمكانية اِستخدَامه، بما في ذلك المُتَغيِّر المُتحكِّم بالحَلْقة (loop control variable) المُستخدَم ضِمْن حَلْقة التَكْرار for. صَرَّحنا عن هذا المُتَغيِّر بكونه من النوع العددي الصحيح (int) بالأمثلة التي فحصناها حتى الآن بهذا القسم. مع ذلك، فإنه ليس أمرًا حتميًا، فقد يكون من نوع آخر. فمثلًا، تَستخدِم حَلْقة التَكْرار for -بالمثال التالي- مُتَغيِّرًا من النوع char، وتعتمد على إمكانية تطبيق عَامِل الزيادة ++ على كُلًا من الحروف والأرقام: char ch; // المتغير المتحكم بالحلقة; for ( ch = 'A'; ch <= 'Z'; ch++ ) // اطبع حرف الأبجدية الحالي System.out.print(ch); System.out.println(); مسألة عد القواسم (divisors) سنُلقِي الآن نظرة على مسألة أكثر جدية، والتي يُمكِن حلّها باِستخدَام حَلْقة التَكْرار for. بفَرْض أن لدينا عددين صحيحين موجبين (positive integers)‏ N و D. إذا كان باقي قسمة (remainder) العدد N على العدد D مُساوٍ للصفر، يُقال عندها أن الثاني قَاسِمًا (divisor) للأول أو أن الأول مُضاعَفًا (even multiple) للثاني. بالمثل، يُقال -بتعبير لغة الجافا- أن العدد D قَاسِمًا (divisor) للعدد N إذا تَحقَّق الشَّرْط N % D == 0، حيث % هو عَامِل باقي القسمة. يَسمَح البرنامج التالي للمُستخدِم بإِدْخَال عدد صحيح موجب (positive integer)، ثُمَّ يَحسِب عَدَد القواسم (divisors) المختلفة لذلك العَدَد. لحِساب عَدَد قواسم (divisors) عَدَد معين N، يُمكننا ببساطة فَحْص جميع الأَعْدَاد التي يُحتمَل أن تكون قَاسِمًا للعَدَد N، أيّ جميع الأَعْدَاد الواقعة بدايةً من الواحد ووصولًا للعَدَد N (١، ٢، ٣، … ،N). ثم نَعدّ منها فقط تلكم التي أَمكنها التقسيم الفعليّ للعَدَد N تقسيمًا مُتعادلًا (evenly). على الرغم من أن هذه الطريقة ستُؤدي إلى نتائج صحيحة، فلربما هي ليست الطريقة الأكثر كفاءة لحلّ هذه المسألة. تَسْتَعْرِض الشيفرة التالية الخوارزمية بالشيفرة الوهمية (pseudocode): // ‫اقرأ قيمة عددية موجبة من المستخدم N Get a positive integer, N, from the user // هيئ عداد القواسم Let divisorCount = 0 // ‫لكل عدد من القيمة واحد وحتى القيمة العددية المدخلة testDivisor for each number, testDivisor, in the range from 1 to N: // إذا كان العدد الحالي قاسم للعدد المدخل if testDivisor is a divisor of N: // أزد قيمة العداد بمقدار الواحد Count it by adding 1 to divisorCount // اطبع قيمة العداد Output the count تَسْتَعْرِض الخوارزمية السابقة واحدة من الأنماط البرمجية (programming pattern) الشائعة، والتي تُستخدَم عندما يكون لديك مُتتالية (sequence) من العناصر، وتَرغَب بمعالجة بعضًا من تلك العناصر فقط، وليس كلها. يُمكِن تَعْمِيم هذا النمط للصيغة التالية: // لكل عنصر بالمتتالية for each item in the sequence: // إذا نجح العنصر الحالي بالاختبار if the item passes the test: // عالج العنصر الحالي process it يُمكننا تَحْوِيل حَلْقة التَكْرار for الموجودة ضِمْن خوارزمية عَدّ القواسم بالأعلى (divisor-counting algorithm) إلى لغة الجافا كالتالي: for (testDivisor = 1; testDivisor <= N; testDivisor++) { if ( N % testDivisor == 0 ) divisorCount++; } بإمكان الحواسيب الحديثة تَّنْفيذ حَلْقة التَكْرار (loop) بالأعلى بسرعة، بل لا يَسْتَحِيل حتى تَّنْفيذها على أكبر عَدَد يُمكن أن يَحمله النوع int، والذي يَصِل إلى ٢١٤٧٤٨٣٦٤٧، ربما حتى قد تَستخدِم النوع long للسماح بأعداد أكبر، ولكن بالطبع سيستغرق تَّنْفيذ الخوارزمية وقتًا أطول مع الأعداد الكبيرة جدًا، ولذلك تَقَرَّر إِجراء تعديل على الخوارزمية بهدف طباعة نقطة (dot) -تَعمَل كمؤشر- بَعْد كل مرة ينتهي فيها الحاسوب من اختبار عشرة مليون قَاسِم (divisor) مُحتمَل جديد. سنضطر في النسخة المُعدَّلة من الخوارزمية إلى الإبقاء على عَدَّادين (counters) منفصلين: الأول منهما لعَدّ القواسم (divisors) الفعليّة التي تحصَّلَنا عليها، والآخر لعَدّ جميع الأعداد التي اُختبرت حتى الآن. عندما يَصل العَدَّاد الثاني إلى قيمة عشرة ملايين، سيَطبع البرنامج نقطة .، ثُمَّ يُعيد ضَبْط قيمة ذلك العَدَّاد إلى صفر؛ ليبدأ العَدّ من جديد. تُصبِح الخوارزمية باِستخدَام الشيفرة الوهمية كالتالي: // اقرأ عدد صحيح موجب من المستخدم Get a positive integer, N, from the user Let divisorCount = 0 // عدد القواسم التي تم العثور عليها Let numberTested = 0 // عدد القواسم المحتملة والتي تم اختبارها // ‫اقرأ رد المستخدم إلى المتغير str // ‫لكل عدد يتراوح من القيمة واحد وحتى قيمة العدد المدخل for each number, testDivisor, in the range from 1 to N: // إذا كان العدد الحالي قاسم للعدد المدخل if testDivisor is a divisor of N: // أزد عدد القواسم التي تم العثور عليها بمقدار الواحد Count it by adding 1 to divisorCount // أزد عدد الأعداد المحتملة التي تم اختبارها بمقدار الواحد Add 1 to numberTested // إذا كان عدد الأعداد المحتملة المختبر يساوي عشرة ملايين if numberTested is 10000000: // اطبع نقطة print out a '.' // أعد ضبط عدد الأعداد المختبرة إلى القيمة صفر Reset numberTested to 0 // اطبع عدد القواسم Output the count وأخيرًا، يُمكننا تَحْوِيل الخوارزمية إلى برنامج كامل بلغة الجافا، كالتالي: import textio.TextIO; public class CountDivisors { public static void main(String[] args) { int N; // القيمة العددية المدخلة من قبل المستخدم int testDivisor; // ‫عدد يتراوح من القيمة واحد وحتى N int divisorCount; // عدد قواسم‫ N التي عثر عليها حتى الآن int numberTested; // عدد القواسم المحتملة التي تم اختبارها // اقرأ قيمة عدد صحيح موجبة من المستخدم while (true) { System.out.print("Enter a positive integer: "); N = TextIO.getlnInt(); if (N > 0) break; System.out.println("That number is not positive. Please try again."); } // عِدّ القواسم واطبع نقطة بعد كل عشرة ملايين اختبار divisorCount = 0; numberTested = 0; for (testDivisor = 1; testDivisor <= N; testDivisor++) { if ( N % testDivisor == 0 ) divisorCount++; numberTested++; if (numberTested == 10000000) { System.out.print('.'); numberTested = 0; } } // اعرض النتائج System.out.println(); System.out.println("The number of divisors of " + N + " is " + divisorCount); } // ‫نهاية البرنامج main } // ‫نهاية الصنف CountDivisors حلقات for المتداخلة كما ذَكَرنا مُسْبَّقًا، فإن بُنَى التحكُّم (control structures) بلغة الجافا هي ببساطة تَعْليمَات مُركَّبة، أي تَتضمَّن مجموعة تَعْليمَات. في الحقيقة، يُمكن أيضًا لبِنْية تحكُّم (control structure) أن تَشتمِل على بِنْية تحكُّم أخرى أو أكثر، سواء كانت من نفس النوع أو من أيّ نوع آخر، ويُطلَق عليها في تلك الحالة اسم بُنَى التحكُّم المُتداخِلة (nested). لقد مررنا بالفعل على عدة أمثلة تَتضمَّن هذا النوع من البُنَى، فمثلًا رأينا تَعْليمَات if ضِمْن حَلْقات تَكْرارية (loops)، كما رأينا حَلْقة while داخلية (inner) مُضمَّنة بداخل حَلْقة while آخرى خارجية. لا يَقتصر الأمر على هذه الأمثلة؛ حيث يُسمَح عامةً بدمج بُنَى التحكُّم بأي طريقة ممكنة، وتستطيع حتى القيام بذلك على عدة مستويات من التَدَاخُل (levels of nesting)، فمثلًا يُمكن لحَلْقة while أن تَحتوِي على تَعْليمَة if، والتي بدورها قد تَحتوِي على تَعْليمَة while آخرى؛ فلغة الجافا Java لا تَضع عامةً أي قيود على عدد مستويات التَدَاخُل المسموح بها، ومع ذلك يَصعُب عمليًا فهم الشيفرة إذا اِحْتَوت على أكثر من عدد قليل من مستويات التَدَاخُل (levels of nesting). تَستخدِم كثير من الخوارزميات حَلْقات for المُتداخِلة (nested)، لذا من المهم أن تفهم طريقة عملها. دعنا نَفحْص عدة أمثلة، مثلًا، مسألة طباعة جدول الضرب (multiplication table) على الصورة التالية: 1 2 3 4 5 6 7 8 9 10 11 12 2 4 6 8 10 12 14 16 18 20 22 24 3 6 9 12 15 18 21 24 27 30 33 36 4 8 12 16 20 24 28 32 36 40 44 48 5 10 15 20 25 30 35 40 45 50 55 60 6 12 18 24 30 36 42 48 54 60 66 72 7 14 21 28 35 42 49 56 63 70 77 84 8 16 24 32 40 48 56 64 72 80 88 96 9 18 27 36 45 54 63 72 81 90 99 108 10 20 30 40 50 60 70 80 90 100 110 120 11 22 33 44 55 66 77 88 99 110 121 132 12 24 36 48 60 72 84 96 108 120 132 144 نُظِّمت البيانات بالجدول إلى ١٢ صف و ١٢ عمود. اُنظر الخوارزمية التالية -بالشيفرة الوهمية (pseudocode)- لطباعة جدول مُشابه: for each rowNumber = 1, 2, 3, ..., 12: // اطبع بسَطر منفصل المضاعفات الاثنى عشر الأولى من قيمة المتغير Print the first twelve multiples of rowNumber on one line // اطبع محرف العودة الى بداية السطر Output a carriage return في الواقع، يُمكن للسطر الأول بمَتْن حَلْقة for بالأعلى "اطبع بسَطر منفصل المضاعفات الاثنى عشر الأولى من قيمة المتغير الحالية" أن يُكتَب على صورة حَلْقة for أخرى منفصلة كالتالي: for N = 1, 2, 3, ..., 12: Print N * rowNumber تحتوي الآن النسخة المُعدَّلة من خوارزمية طباعة جدول الضرب على حَلْقتي for مُتداخِلتين، كالتالي: for each rowNumber = 1, 2, 3, ..., 12: for N = 1, 2, 3, ..., 12: Print N * rowNumber // اطبع محرف العودة الى بداية السطر Output a carriage return يُمكن اِستخدَام مُحدِّدات الصيغة (format specifier) عند طباعة خَرْج ما (output)؛ بهدف تخصيص صيغة هذا الخَرْج، ولهذا سنَستخدِم مُحدِّد الصيغة ‎%4d‎ عند طباعة أيّ عَدَد بالجدول؛ وذلك لجعله يَحْتلَّ أربعة خانات دائمًا دون النظر لعَدَد الخانات المطلوبة فعليًا، مما يُحسِن من شكل الجدول النهائي. بفَرْض أنه قد تم الإعلان عن المُتَغيِّرين rowNumber و N بحيث يَكُونا من النوع العددي int، يمكن عندها كتابة الخوارزمية بلغة الجافا، كالتالي: for ( rowNumber = 1; rowNumber <= 12; rowNumber++ ) { for ( N = 1; N <= 12; N++ ) { // اطبع الرقم بأربع خانات بدون طباعة محرف العودة الى بداية السطر System.out.printf( "%4d", N * rowNumber ); } // اطبع محرف العودة الى بداية السطر System.out.println(); } ربما قد لاحظت أن جميع الأمثلة التي تَعْرَضنا لها -خلال هذا القسم- حتى الآن تَتعامَل فقط مع الأعداد، لذلك سننتقل خلال المثال التالي إلى معالجة النصوص (text processing). لنفْترِض أن لدينا سِلسِلة نصية (string)، ونريد كتابة برنامج لتَحْدِيد الحروف الأبجدية (letters of the alphabet) الموجودة بتلك السِلسِلة. فمثلًا، إذا كان لدينا السِلسِلة النصية "أهلًا بالعالم"، فإن الحروف الموجودة بها هي الألف، والباء، والعين، واللام، والميم، والهاء. سيَستقبِل البرنامج، بالتَحْدِيد، سِلسِلة نصية من المُستخدِم، ثم يَعرِض قائمة بكل تلك الحروف المختلفة الموجودة ضمن تلك السِلسِلة، بالإضافة إلى عَدَدها. كالعادة، سنبدأ أولًا بكتابة الخوارزمية بصيغة الشيفرة الوهمية (pseudocode)، كالتالي: // اطلب من المستخدم إدخال سلسلة نصية Ask the user to input a string // ‫اقرأ رد المستخدم إلى المتغير str Read the response into a variable, str // هيئ عداد لعدّ الحروف المختلفة Let count = 0 (for counting the number of different letters) // لكل حرف أبجدي for each letter of the alphabet: // إذا كان الحرف موجودًا بالسلسلة النصية if the letter occurs in str: // اطبع الحرف Print the letter // أزد قيمة العداد Add 1 to count // اطبع قيمة العداد Output the count سنَستخدِم الدالة TextIO.getln()‎ لقراءة السَطْر الذي أَدْخَله المُستخدِم بالكامل؛ وذلك لحاجتنا إلى معالجته على خطوة واحدة. يُمكننا تَحْوِيل سَطْر الخوارزمية "لكل حرف أبجدي" إلى حَلْقة التَكْرار for كالتالي for (letter='A'; letter<='Z'; letter++)‎. في المقابل، سنحتاج إلى التفكير قليلًا بالطريقة التي سنكتب بها تَعْليمَة if الموجودة ضِمْن تلك الحَلْقة. نُريد تَحْدِيدًا إيجاد طريقة نتحقَّق من خلالها إذا ما كان الحرف الأبجدي الحالي بالتَكْرار (iteration)‏ letter موجودًا بالسِلسِلة النصية str أم لا. أحد الحلول هو المرور على جميع حروف السِلسِلة النصية str حرفًا حرفًا، لفَحْص ما إذا كان أيًا منها مُساو لقيمة الحرف الأبجدي الحالي‏ letter، ولهذا سنَستخدِم الدالة str.charAt(i)‎ لجَلْب الحرف الموجود بموقع معين i بالسِلسِلة النصية str، بحيث تَتراوح قيمة i من الصفر وحتى عدد حروف السِلسِلة النصية، والتي يُمكن حِسَابها باِستخدَام التعبير str.length() - 1. سنواجه مشكلة أخرى، وهي إمكانية وجود الحرف الأبجدي بالسِلسِلة النصية str على صورتين، كحرف كبير (upper case) أو كحرف صغير (lower case). فمثلًا قد يكون الحرف A على الصورة A أو a، ولذلك نحن بحاجة لفَحْص كلتا الحالتين. قد نتجنب، في المقابل، هذه المشكلة بتَحْوِيل جميع حروف السِلسِلة النصية str إلى الحروف الكبيرة (upper case) قبل بدء المعالجة، وعندها نستطيع فَحْص الحروف الكبيرة (upper case) فقط. والآن نُعيد صياغة الخوارزمية كالتالي: // اطلب من المستخدم إدخال سلسلة نصية Ask the user to input a string // ‫اقرأ رد المستخدم إلى المتغير str Read the response into a variable, str // كَبِّر حروف السلسلة النصية Convert str to upper case // هيئ عداد لعدّ الحروف المختلفة Let count = 0 for letter = 'A', 'B', ..., 'Z': for i = 0, 1, ..., str.length()-1: if letter == str.charAt(i): // اطبع الحرف Print letter // أزد قيمة العداد Add 1 to count // اخرج من الحلقة لتجنب إعادة عدّ الحرف أكثر من مرة break Output the count لاحظ اِستخدَامنا لتَعْليمَة break داخل حَلْقة تَكْرار for الداخلية؛ حتى نتجنب طباعة حرف الأبجدية الحالي‏ letter وعَدّه مجددًا إذا كان موجودًا أكثر من مرة بالسِلسِلة النصية. تُوقِف تَعْليمَة break حَلْقة التَكْرار for الداخلية فقط (inner loop)، وليس الخارجية (outer loop)، والتي ينتقل الحاسوب في الواقع إلى تَّنْفيذها بمجرد خروجه من الحَلْقة الداخلية، ولكن مع حرف الأبجدية التالي. حاول استكشاف القيمة النهائية للمُتَغيِّر count في حالة حَذْف تَعْليمَة break. تَسْتَعْرِض الشيفرة التالية البرنامج بالكامل بلغة الجافا: import textio.TextIO; public class ListLetters { public static void main(String[] args) { String str; // السطر المدخل من قبل المستخدم int count; // عدد الحروف المختلفة الموجودة بالسلسلة النصية char letter; System.out.println("Please type in a line of text."); str = TextIO.getln(); str = str.toUpperCase(); count = 0; System.out.println("Your input contains the following letters:"); System.out.println(); System.out.print(" "); for ( letter = 'A'; letter <= 'Z'; letter++ ) { int i; // موضع الحرف بالسِلسِلة النصية for ( i = 0; i < str.length(); i++ ) { if ( letter == str.charAt(i) ) { System.out.print(letter); System.out.print(' '); count++; break; } } } System.out.println(); System.out.println(); System.out.println("There were " + count + " different letters."); } // ‫نهاية البرنامج main } // ‫نهاية الصنف ListLetters في الواقع، تتوفَّر الدالة str.indexOf(letter)‎ المبنية مُسْبَّقًا (built-in function)، والمُستخدَمة لاختبار ما إذا كان الحرف letter موجودًا بالسِلسِلة النصية str أم لا. إذا لم يكن الحرف موجودًا بالسِلسِلة، ستُعيد الدالة القيمة -1، أما إذا كان موجودًا، فإنها ستُعيد قيمة أكبر من أو تُساوي الصفر. ولهذا كان يمكننا ببساطة إجراء عملية فَحْص وجود الحرف بالسِلسِلة باِستخدَام التعبير if (str.indexOf(letter) >= 0)‎، بدلًا من اِستخدَام حَلْقة تَكْرار مُتداخِلة (nested loop). يتضح لنا من خلال هذا المثال كيف يمكننا اِستخدَام البرامج الفرعية (subroutines)؛ لتبسيط المسائل المعقدة ولكتابة شيفرة مَقْرُوءة. ترجمة -بتصرّف- للقسم Section 4: The for Statement من فصل Chapter 3: Programming in the Small II: Control من كتاب Introduction to Programming Using Java.
  24. تُصنَّف التَعْليمات البرمجية (statements) بأي لغة برمجة -ومنها الجافا Java- إلى تَعْليمات بسيطة (simple) وأخرى مُركَّبة (compound). تُعدّ التَعْليمات البسيطة -مثل تَعْليمَة الإِسْناد (assignment) وتَعْليمَة اِسْتدعاء البرامج الفرعية (subroutine call)- اللَبِنة الأساسية لأيّ برنامج. في المُقابل، تتكون التَعْليمات المُركَّبة -مثل تَعْليمَة حَلْقة التَكْرار while وتَعْليمَة التَفْرِيع الشَّرطيّة if- من عدد من التَعْليمات البسيطة، وتُعرَف باسم بُنَى التحكُّم (control structures)؛ نظرًا لأنها تَتَحكَّم بترتيب تَّنْفيذ التَعْليمات. سنتناول بُنَى التحكُّم (control structures) المُدَعَّمة بلغة الجافا Java بشئٍ من التفصيل خلال الأقسام الخمسة التالية، بحيث نبدأ في هذا القسم بتَعْليمتي الحَلْقة while و do..while. سنتَعرَّض، خلال ذلك، لكثير من الأمثلة البرمجية التي تُوظِّف بُنَى التحكُّم تلك، كما أننا وبينما نقوم بذلك، سنُطبِّق التقنيات المُتَّبَعة بتصميم الخوارزميات (algorithms)، والتي قد تَعرَّضنا لها بالفصل السابق. تَعْليمَة while تُكتَب تَعْليمَة حَلْقة التَكْرار (loop)‏ while -والتي قد تَعرَّضت لها مُسْبقًا بالقسم ٣.١- بالصياغة التالية: while ( <boolean-expression> ) <statement> لا يُشترَط أن تتكوَّن التعليمة -المُشار إليها بالأعلى، والمعروفة باسم مَتْن حَلْقة التَكْرار (loop body)- من تَعْليمَة واحدة فقط، فبالطبع يُمكنها أن تكون كُتليّة البِنْية (block statement)، بحيث تَتضمَّن عدة تَعْليمَات مُحَاطة بزوج من الأقواس. يَتكرَّر تَّنْفيذ مَتْن الحَلْقة (body of the loop) طالما كان التعبير -المُشار إليه بالأعلى- مُتحقِّقًا. تتكون تلك العبارة من تَعبير منطقي (boolean expression)، وتُعرَف باسم الشَّرْط الاستمراري (continuation condition) لحَلْقة التَكْرار أو باسم اختبار حَلْقة التَكْرار. نحتاج الآن لإِيضاح عدة نقاط بشيء من التفصيل. أولًا، ماذا يَحدُث إذا لم يَتحقَّق شَّرْط حَلْقة التَكْرار ولو لمرة واحدة على الأقل، بمعنى أنه لم يَكُن متحقِّقًا قبل التَّنْفيذ الأوَّليّ لمَتْن الحَلْقة؟ في هذه الحالة، لا يُنفَّذ مَتْن الحَلْقة (body) نهائيًا، وهو ما يعني أن مَتْن حَلْقة while يُمكِن أن يُنفَّذ أيّ عدد من المرات بما في ذلك الصفر. ثانيًا، ماذا لو كان شَّرْط حَلْقة التَكْرار مُتحقِّقًا، لكن وبينما يُنفَّذ مَتْن الحَلْقة لم يَعُدْ الشَّرْط كذلك؟ هل يَتوقَف تَّنْفيذ الحَلْقة بمجرد حُدُوث ذلك؟ ببساطة لا؛ حيث يَستمِر الحاسوب بتَّنْفيذ مَتْن الحَلْقة بالكامل حتى يَصِل إلى نهايته، وعِندها فقط يَقفز عائدًا إلى بداية حَلْقة التَكْرار لفَحْص شَّرْطها (continuation condition) مُجددًا، ومِنْ ثَمَّ، يَستطيع في تلك اللحظة فقط إِيقاف الحَلْقة وليس قبل ذلك. لنفْحَص إِحدى المشكلات التي تُحلّ باِستخدَام حَلْقة التَكْرار while: حِساب قيمة متوسط (average) مجموعة من الأعداد الصحيحة الموجبة (positive integers)، والتي يَتم إِدْخالها من قِبَل المُستخدِم. تُحسَب قيمة المتوسط (average) عمومًا بحِساب حاصل مجموع الأعداد ثم قِسمته على عَدَدها. سيَطلب البرنامج من المُستخدِم إِدْخال عدد صحيح (integer) وحيد في المرة، وسيَحتفِظ دومًا بقيمة المجموع الكلي للأعداد الصحيحة المُدْخَلة حتى اللحظة الراهنة، وكذلك عَدَدها، كما سيُبقي هذه القيم مُحْدَثة مع كل عملية إِدْخال. هاك خوارزمية البرنامج (algorithm) مَكتوبة بأسلوب الشيفرة الوهمية (pseudocode): Let sum = 0 Let count = 0 // طالما لا يوجد المزيد من الأعداد الصحيحة للمعالجة while there are more integers to process: // اقرأ عدد صحيح Read an integer // ‫أضف قيمته إلى المتغير sum Add it to the sum // أزد قيمة العداد Count it // ‫اقسم المجموع sum على count لحساب قيمة المتوسط Divide sum by count to get the average // اطبع قيمة المتوسط Print out the average لكن كيف سنتحقَّق فعليًا من شَّرْط الحَلْقة بالأعلى "طالما لا يوجد المزيد من الأعداد الصحيحة للمعالجة"؟ أحد الحلول هو إِبلاغ المُستخدِم بأن يُدخِل القيمة صفر بعد أن يَنتهِي من إِدْخال جميع البيانات الفعليَّة. ستَنجح تلك الطريقة نظرًا لأننا نَفترِض أن البيانات الفعليَّة المُدْخَلة لابُدّ وأن تَكون من الأعداد الصحيحة الموجبة، وهو ما يَعني أن الصفر يُعدّ قيمة غيْر صالحة من الأساس. لاحِظ أن الصفر هنا ليس قيمة بحدّ ذاتِها يَنبغي تَضْمِينها مع الأعداد المطلوب حِساب مُتوسطها، وإِنما هي فقط مطلوبة كعلامة للإشارة إلى نهاية البيانات الفعليَّة. يُطلق أحيانًا على قِيَم البيانات المُستخدَمة بطريقة مُشابهة اسم بَيَان البداية/النهاية (sentinel value). سنُعدِّل الآن اختبار حَلْقة التَكْرار while ليُصبِح "طالما العدد الصحيح المُدخل لا يساوي الصفر". ستُواجهنا مشكلة آخرى، عندما يُنفَّذ اختبار حَلْقة التَكْرار (loop) لأول مرة، أي قَبْل التَّنْفيذ الأوَّليّ لمَتْن الحَلْقة، لن يكون هناك عدد صحيح قد قُرأ بَعْد، بل ليس هناك أيّ بيانات قد قُرِأت أساسًا، مما يَعني أنه لا يوجد "عدد صحيح مُدخل". ولهذا، يُصبِح اختبار الحَلْقة "طالما العدد الصحيح المُدخل لا يساوي الصفر" -ضِمْن هذا السياق- غَيْر ذي مَعنى. لذلك يَنبغي القيام بأمر ما قَبل البدء بتَّنْفيذ حَلْقة التَكْرار while للتأكد من صلاحية الاختبار حتى مع أول تَّنْفيذ له. تُعرَف هذه العملية باسم التهيئة المبدئية (priming the loop) لحَلْقة التَكْرار، وفي هذه الحالة بالتحديد، يُمكننا ببساطة تهيئة الحَلْقة عن طريق قراءة أول عدد صحيح مُدْخَل قَبل بدء تَّنْفيذ حَلْقة التَكْرار. هاك الخوارزمية (algorithm) المُعدَّلة: Let sum = 0 Let count = 0 // اقرأ عدد صحيح Read an integer // طالما العدد الصحيح المُدخل لا يساوي الصفر while the integer is not zero: // ‫أضف قيمته إلى المتغير sum Add the integer to the sum // أزد قيمة العداد Count it // اقرأ عدد صحيح Read an integer // ‫اقسم المجموع sum على count لحساب قيمة المتوسط Divide sum by count to get the average // اطبع قيمة المتوسط Print out the average لاحِظ إعادة ترتيب تَعْليمَات مَتْن حَلْقة التَكْرار (loop body)؛ لأنه لمّا أصْبَحت أول محاولة لقراءة عدد صحيح (integer) تَحدُث قَبْل بداية حَلْقة التَكْرار (loop)، كان لابُدّ لمَتْن الحَلْقة من أن يبدأ أولًا بمُعالجة ذلك العدد. ثُمَّ بنهاية المَتْن، سيُحاول الحاسوب قراءة عدد صحيح جديد، ليَقفِز بَعْدها عائدًا لبداية الحَلْقة ليَختبِر العدد الجديد المَقروء للتو. عندما يَقرأ الحاسوب قيمة بَيَان النهاية (sentinel value)، ستَتوقَف الحَلْقة، ومِنْ ثَمَّ لن تُعالَج تلك القيمة، كما أنها لن تُضاف إلى حاصل المجموع sum، أو هذه هى الطريقة التي ينبغي أن تَعمَل بها الخوارزمية على الأقل؛ لأن قيمة بَيَان النهاية ليست جزءً من البيانات الفعليَّة كما ذَكَرنا مُسْبَّقًا. لا تَعمَل الخوارزمية الأصلية طِبقًا لذلك بطريقة صحيحة -بفَرْض إِمكانية تَّنْفيذها بدون إِجراء التهيئة المبدئية (priming) بطريقة ما-؛ لأنها ستَحسِب حاصل مجموع جميع الأعداد الصحيحة وكذلك عَدَدها (count) بما يتَضمَّن قيمة بَيَان النهاية (sentinel value). لمّا كانت تلك القيمة -المُتفق عليها- تُساوي الصفر، فإن حاصل المجموع سيظِلّ لحسن الحظ صحيحًا، ولكن في المقابل سيكون عَدَدها (count) خطأ بفارق واحد. يُطلَق على الأخطاء المُشابهة اسم الأخطاء بفارق الواحد (off-by-one errors)، وهي واحدة من أكثر الأخطاء شيوعًا. اِتضح أن العَدّ أصعب مما قد يبدو عليه. سنُحوِّل الآن الخورازمية إلى برنامج كامل، نُلاحِظ أولًا أنه لا يُمكِن اِستخدَام التَعْليمَة average = sum/count;‎ لحساب قيمة المتوسط (average)؛ لأنه لمّا كانت قيمة كُلًا من المُتَغيِّرين sum و count من النوع العددي الصحيح int، فإن حاصل قِسمة الأول على الثاني sum/count ستكون أيضًا من النوع العددي الصحيح int، في حين ينبغي للمتوسط أن يكون عددًا حقيقيًا (real number). لقد واجهنا هذه المشكلة مِن قَبْل، ويَتلخَّص حَلّها بضرورة تَحْوِيل واحد من القيمتين إلى النوع double؛ وذلك لإجبار الحاسوب على حِسَاب قيمة حاصل القِسمة (quotient) كعَدَد حقيقي (real number). يُمكِن القيام بذلك عن طريق إجراء عملية التَحْوِيل بين الأنواع (type-casting) على أحد المُتَغيِّرين على الأقل، بحيث يُحوَّل إلى النوع double عن طريق اِستخدَام ‎(‎‎double)sum‎. ولهذا يَحسِب البرنامج قيمة المتوسط باستخدام التعبير average = ((double)sum) / count;‎. يَتوفَّر حلّ آخر، وهو التَّصْريح (declaration) عن المُتَغيِّر sum بحيث يكون أساسًا من النوع double. حُلَّت مشكلة آخرى بالبرنامج التالي، وهي أنه في حالة إِدْخال المُستخدِم القيمة صفر كأول قيمة مُدْخَل، فلن تتوفَّر أيّ بيانات للمُعالجة، ولهذا يُمكننا اختبار حُدُوث تلك الحالة بفَحْص ما إذا كانت قيمة المُتَغيِّر count ما تزال مُساوية للصفر حتى بَعْد انتهاء حَلْقة التَكْرار while. قد تبدو هذه المشكلة ثانوية، ولكن لابُدّ أن يُغطي أيّ مبرمج مُنتبِه جميع الحالات المُمكنة. انظر شيفرة البرنامج بالكامل: import textio.TextIO; public class ComputeAverage { public static void main(String[] args) { int inputNumber; // إحدى القيم المدخلة من قبل المستخدم int sum; // حاصل مجموع الأعداد الصحيحة الموجبة int count; // عدد الأعداد الصحيحة double average; // قيمة متوسط الأعداد الصحيحة الموجبة // هيئ قيمة كلا من متغير المجموع والعداد sum = 0; count = 0; // اقرأ مدخل من المستخدم لمعالجته System.out.print("Enter your first positive integer: "); inputNumber = TextIO.getlnInt(); while (inputNumber != 0) { // أضف قيمة المتغير للمجموع الحالي sum += inputNumber; // أزد قيمة العداد بمقدار الواحد count++; System.out.print("Enter your next positive integer, or 0 to end: "); inputNumber = TextIO.getlnInt(); } // اعرض النتائج if (count == 0) { System.out.println("You didn't enter any data!"); } else { average = ((double)sum) / count; System.out.println(); System.out.println("You entered " + count + " positive integers."); System.out.printf("Their average is %1.3f.\n", average); } } // ‫نهاية البرنامج main } // ‫نهاية الصنف ComputeAverage تعليمة do..while تَفحْص تَعْليمَة حَلْقة التَكْرار while الشَّرْط الاستمراري لحَلْقة التَكْرار (continuation condition) ببداية الحَلْقة، ولكن أحيانًا ما يكون من الأنسب فَحْصه بنهاية الحَلْقة لا بدايتها، وهذا في الواقع ما تُوفِّره تَعْليمَة do..while. تَتشابه صياغة (syntax) تَعْليمَة الحَلْقة do..while مع تَعْليمَة while تمامًا باستثناء بعض التَغْيِيرات الطفيفة، حيث تُنْقَل كلمة while مصحوبة مع الشَّرْط إلى نهاية الحَلْقة، في حين تُضاف كلمة do في بداية الحَلْقة بدلًا منها. تُكتَب تَعْليمَة حَلْقة التَكْرار do..while بالصياغة التالية: do <statement> while ( <boolean-expression> ); إذا كانت التعليمة كُتلَة (block) بذاتها، تُصاغ التَعْليمَة كالتالي: do { <statements> } while ( <boolean-expression> ); لاحِظ وجود الفاصلة المَنقوطة (semicolon) ; بنهاية التَعْليمَة do..while. تُعدّ هذه الفاصلة جزءً أساسيًا من التَعْليمَة do..while، مثلما تُعدّ الفاصلة بنهاية أي تَعْليمَة أخرى -كالإِسْناد (assignment) أو التَّصْريح (declaration)- جزءً أساسيًا منها. ولهذا سيؤدي حَذْف الفاصلة المنقوطة ;، في هذه الحالة، إلى التَسبُّب بحُدوث خطأ في بناء الجملة (syntax error). ينبغي عمومًا أن تنتهي أيّ تَعْليمَة برمجية -بلغة الجافا Java- إِمّا بفاصلة منقوطة ; أو بقوس مُغلِق {. عندما يُنفَّذ الحاسوب تَعْليمَة حَلْقة التَكْرار do..while، فإنه يبدأ أولًا بتَّنْفيذ مَتْنها (loop body)-التَعْليمَة أو مجموعة التَعْليمَات بداخل الحَلْقة-، ثم يَحسِب قيمة التعبير المنطقي (boolean expression)، فإذا كانت مُساوية للقيمة المنطقية true، فإنه يَقفِز عَائدًا إلى بداية الحَلْقة ويُعيد تَّنْفيذ مَتْنها، لتستمر العملية بَعْدها بنفس الطريقة. أما إذا كانت قيمة التعبير المنطقي مُساوية للقيمة المنطقية false، فإنه يَتوقَف عن تَّنْفيذ الحَلْقة، وينتقل إلى الجزء التالي من الشيفرة. لاحظ أنه لمّا كان شَّرْط الحَلْقة لا يُفحْص إلا بنهاية الحَلْقة، فإن مَتْن حَلْقة التَكْرار do..while دائمًا ما يُنفَّذ مرة واحدة على الأقل. على سبيل المثال، اُنظر الشيفرة الوهمية (pseudocode) التالية لبرنامج لعبة. ستجد أن استخدام تَعْليمَة الحَلْقة do..while أكثر مُلائمة في هذه الحالة؛ لأنها ستَضمَن أن المُستخدِم قد لعِبَ مباراة واحدة على الأقل. علاوة على ذلك، فإن شَّرْط حَلْقة التَكْرار بالأسفل لن يكون له أيّ مَعنى في حالة تَّنْفيذه في بداية الحَلْقة. do { // العب مباراة Play a Game // اسأل المستخدم إذا ما كان يريد اللعب مرة آخرى Ask user if he wants to play another game // اقرأ رد المستخدم Read the user's response // أعد التكرار طالما كان رد المستخدم هو نعم } while ( the user's response is yes ); سنُحوِّل الآن هذه الشيفرة الوهمية إلى لغة الجافا. بدايةً ولكي نتجنب الخَوْض في تفاصيل ليست ذا أهمية، سنفترض أن لدينا صَنْف (class) يَحمِل اسم Checkers. أحد أعضاء (member) هذا الصَنْف هو البرنامج الفرعي الساكن (static subroutine)‏ playGame()‎. يُغلِّف هذا البرنامج الفرعي التفاصيل الخاصة باللعبة، ويُمثِل استدعائه إجراء مباراة دَّامَا (checkers) واحدة مع المُستخدِم. يُمكننا الآن اِستخدَام تَعْليمَة استدعاء البرنامج الفرعي (subroutine call) الساكن Checkers.playGame();‎ كبديل عن سَطْر الشيفرة "العب مباراة". سنَستخدِم الصنف TextIO لتَلَقِّي رد المُستخدِم على سؤال من النوع نعم أم لا؛ حيث يُوفِّر هذا النوع الدالة‏ (function) ‏TextIO.getlnBoolean()‎، والتي تَسمَح للمُستخدِم بإِدْخال إحدى القيمتين "Yes/No" ضِمْن عدة ردود أُخرى صالحة، بحيث يؤول الرد "Yes" إلى القيمة المنطقية true، بينما يؤول الرد "No" إلى القيمة المنطقية false. سنحتاج الآن إلى مُتَغيِّر لتخزين رد المُستخدِم، والذي سيكون بطبيعة الحال من النوع المنطقي (boolean). يُمكن كتابة الخوارزمية كالتالي: // ‫تساوي True في حالة رغب المستخدم باللعب مجددًا boolean wantsToContinue; do { Checkers.playGame(); System.out.print("Do you want to play again? "); wantsToContinue = TextIO.getlnBoolean(); } while (wantsToContinue == true); وفقًا لشَّرْط حَلْقة التَكْرار بالأعلى، فإنه عندما تَتَساوى قيمة المُتَغيِّر المنطقي wantsToContinue مع القيمة المنطقية false، سيكون ذلك إشارة (signal) إلى ضرورة تَوقُف حَلْقة التَكْرار. يُطلَق عادةً على المُتَغيِّرات المنطقية (boolean variables) المُستخدَمة بهذه الطريقة اسم راية (flag) أو مُتَغيِّر راية -هي متغيرات تُضبَط (set) بمكان ما بالشيفرة لتُفحَص قيمتها بمكان آخر-. بالمناسبة، عادةً ما يَسخَر بعض المبرمجين -ربما قد يَصفهم البعض بالمُتَحذلِقين- من شَّرْط حَلْقة التَكْرار while (wantsToContinue == true)‎؛ وذلك لأنه مُكافئ تمامًا للشَّرْط while (wantsToContinue)‎؛ فاختبار ما إذا كان التعبير wantsToContinue == true يُعيد القيمة المنطقية true لا يَختلف نهائيًا عن اختبار ما إذا كان المُتَغيِّر wantsToContinue يَحمِل القيمة true ويُعيدها. بالمثل، -وبفَرْض أن لدينا مُتَغيِّر اسمه flag من النوع المنطقي (boolean variable)- يُكافئ التعبير flag == false، والذي يُعدّ أقل فَجاجة من التعبير السابق، تمامًا التعبير ‎!flag‎، حيث الحرف ! هو عَامِل النفي المنطقي (negation operator). يُفضَّل عمومًا كتابة while (!flag)‎ بدلًا من while (flag == false)‎، وبالمثل، كتابة if (!flag)‎ بدلًا من if (flag == false)‎. على الرغم من أن اِستخدَام تَعْليمَة حَلْقة التَكْرار do..while أحيانًا ما يكون أكثر ملائمة في بعض المسائل، فعمومًا لا يَجعل وجود نوعين مختلفين من تَعْليمَة الحَلْقة (loops) لغة البرمجة أكثر قوة؛ فبالنهاية، أي مشكلة تُحلّ بإحداهما، ستجد أنه من الممكن حلّها بالآخرى. في الواقع، بفرض أن العبارة تُمثل أي كُتلَة شيفرة، فإن التالي: do { <doSomething> } while ( <boolean-expression> ); يُكافئ تمامًا: <doSomething> while ( <boolean-expression> ) { <doSomething> } بالمثل: while ( <boolean-expression> ) { <doSomething> } يُكافئ: if ( <boolean-expression> ) { do { <doSomething> } while ( <boolean-expression> ); } بدون أي تَغْيِير بمَقصِد البرنامج نهائيًا. تعليمتي break و continue تَسمَح لك صيغتي حَلْقتي التَكْرار while و do..while باختبار الشَّرْط الاستمراري (continuation condition) إِمّا ببداية الحَلْقة أو بنهايتها على الترتيب. ولكن أحيانًا سترغب في إجراء عملية اختبار شَّرْط أثناء تَّنْفيذ الحَلْقة، أيّ داخل مَتْن الحَلْقة نفسها، أو حتى قد ترغب بإجراء أكثر من عملية اختبار بأماكن مختلفة داخل نفس الحَلْقة. تُستخدَم تَعْليمَة break بلغة الجافا لإيقاف تَّنْفيذ أيّ حَلْقة تَكْرار (loop)، والخروج منها، وذلك بمجرد استدعائها بأيّ مكان داخل الحَلْقة. تُكتَب كالتالي: break; عندما يُنفِّذ الحاسوب تَعْليمَة break داخل حَلْقة (loop)، فإنه سيَقفِز مُباشرةً خارج الحَلْقة، ويُنفِّذ الشيفرة التالية بالبرنامج والموجودة أسفل الحَلْقة. انظر المثال التالي: while (true) { // يبدو وكأنها ستنفذ بشكل لا نهائي System.out.print("Enter a positive number: "); N = TextIO.getlnInt(); // إما أن تكون قيمة المدخل سليمة أو اقفز خارج الحلقة if (N > 0) break; System.out.println("Your answer must be > 0."); } // ‫اكمل هنا بعد break في المثال بالأعلى، إذا أَدْخَل المُستخدِم عدد أكبر من الصفر، سيتحقَّق الشَّرْط، وستُنفَّذ تَعْليمَة break، ولهذا سيَقفِز الحاسوب مباشرة خارج الحَلْقة إلى ما بَعْدها. في المقابل، إذا لم يتحقَّق الشَّرْط، ستُطبَع السِلسِلة النصية "Your answer must be > 0"، ثم سيَقفِز الحاسوب عَائدًا إلى بداية الحَلْقة ليَقرأ مُدْخَل جديد. قد يبدو السَطْر الأول من الشيفرة بالأعلى while (true)‎ غَيْر مألوف نوعًا ما، ولكنه في الواقع سليم تمامًا. يُسمَح عمومًا لأيّ تعبير منطقي (boolean-valued expression) بأن يكون شَّرْطًا لحَلْقة التَكْرار، أيّ لابُدّ فقط أن تؤول قيمته النهائية إلى قيمة منطقية؛ ليَفحْصها الحاسوب لمعرفة ما إذا كانت مُساوية للقيمة true أم للقيمة false. تُعدّ القيمة المُجرَّدة (literal)‏ true تعبيرًا منطقيًا، والذي يؤول دائمًا إلى القيمة المنطقية true، ولذلك يُمكِن اِستخدَامها كشَّرْط للحَلْقة. يُستخدَم الشَّرْط while (true)‎ لكتابة حَلْقة لا نهائية (infinite loop) أو حَلْقة يُفْترَض الخروج منها باِستخدَام تَعْليمَة break. يُسمَح باِستخدَام حَلْقات التَكْرار المُتداخِلة (nested loops)، بمَعنى وجود تَعْليمَة حَلْقة تَكْرار داخل أُخرى، ولذلك لابُدّ لنا من فهم طريقة عَمَل تَعْليمَة break ضِمْن هذا السياق. كقاعدة عامة، تُوقِف تَعْليمَة break حَلْقة التَكْرار الأقرب لها فقط، بمَعنى أنها إذا اُستخدِمت بداخل الحَلْقة الداخلية (inner loop)، فإنها ستُوقِف فقط تلك الحَلْقة، لا الحَلْقة الخارجية (outer loop) التي تَشَمَلها. أما إذا أردت إيقاف تَّنْفيذ الحلقة الخارجية، يُمكِنك اِستخدَام ما يُعرَف باسم تَعْليمَة break المُعنوَنة ‏(labeled break). تَسمَح تَعْليمَة break المُعنوَنة بتحديد صريح للحَلْقة المطلوب الخروج منها. اِستخدَام هذه التَعْليمَة المُعنوَنة غَيْر شائع، ولذلك سنمر عليها سريعًا. تَعمَل العناوين (labels) كالتالي: يُمكِنك ببساطة عَنوَنة أيّ حَلْقة تَكْرار بوَضْع مُعرَّف (identifier) مَتبوع بنقطتان رأسيتان : بمقدمة الحَلْقة. فمثلًا، يمكن عَنوَنة حَلْقة تَكْرار while باِستخدَام mainloop: while...‎. تستطيع الآن اِستخدَام تَعْليمَة break المُعنوَنة بأيّ مكان داخل هذه الحَلْقة عن طريق اِستخدَام الصيغة break mainloop;‎، وذلك بهدف الخروج من هذه الحَلْقة تحديدًا. على سبيل المثال، تَفحْص الشيفرة بالأسفل ما إذا كانت السِلسِلتين النصيتين s1 و s2 تحتويان على حرف مُشتَرَك. في حالة تَحقُّق الشَّرْط، ستُسْنَد القيمة المنطقية false إلى مُتَغيِّر الراية nothingInCommon، ثُمَّ تُسْتَدعى تَعْليمَة break المُعنوَنة لإيقاف المعالجة عند تلك النقطة: boolean nothingInCommon; // افترض أن السلسلتين لا يشتركان بأي حرف nothingInCommon = true; // متغيرات حلقتي التكرار والتي ستأخذ قيم حروف السلسلتين int i,j; i = 0; bigloop: while (i < s1.length()) { j = 0; while (j < s2.length()) { // إذا كان هناك حرفًا مشتركا if (s1.charAt(i) == s2.charAt(j)) { // ‫اضبط قيمة المتغير إلى القيمة false nothingInCommon = false; // اخرج من الحلقتين break bigloop; } j++; // Go on to the next char in s2. } i++; //Go on to the next char in s1. } تُعدّ تَعْليمَة continue مرتبطة نوعًا ما بتَعْليمَة break، لكنها أقل اِستخدَامًا منها. تتَخَطَّى تَعْليمَة continue الجزء المُتبقي من التَكْرار الحالي (current iteration) فقط، أي أنها لا تتَسبَّب بالقفز خارج الحَلْقة تمامًا إلى ما بَعْدها كتَعْليمَة break؛ وإنما تَقفِز عائدة إلى بداية نفس الحَلْقة لتَّنْفيذ التَكْرار التالي (next iteration) -بالطبع إذا كان الشَّرْط الاستمراري (continuation condition) لحَلْقة التَكْرار ما زال مُتحقِّقًا. عندما تُستخدَم تَعْليمَة continue داخل حَلْقة مُتداخِلة (nested loop)، فإنها، مثل تَعْليمَة break، تُجرِي هذه العملية على حَلْقة التَكْرار الأقرب لها فقط، أيّ الحَلْقة الداخلية (inner nested loop). وبالمثل، تتوفَّر تَعْليمَة continue المُعنوَنة ‏(labeled continue)، وذلك لتخصيص حَلْقة التَكْرار المراد إجراء عملية continue عليها. يمكن اِستخدَام تَعْليمَتي break و continue داخل جميع حَلْقات التَكْرار مثل while، و do..while، و for. سنتحدث عن الأخيرة تفصيليًا بالقسم التالي. يُمكن أيضًا اِستخدَام تَعْليمَة break للخروج من تَعْليمَة switch، وهو ما سنتناوله بالقسم ٣.٦. يُسمَح أيضًا باِستخدَامها داخل تَعْليمَة التَفْرِيع الشَّرْطيّة if إذا كانت تَعْليمَة التَفْرِيع موجودة إِمّا ضِمْن حَلْقة تَكْراريّة أو ضِمْن تَعْليمَة switch، ولكن عندها لا يكون المقصود هو الخروج من تَعْليمَة التَفْرِيع if، وإنما الخروج من التَعْليمَة التي تَشتَملها، أي من تَعْليمَة الحَلْقة أو من تَعْليمَة switch. بالمثل، يُمكِن اِستخدَام تَعْليمَة continue داخل تَعْليمَة if بنفس الطريقة، وتتبِّع نفس القواعد بالأعلى. ترجمة -بتصرّف- للقسم Section 3: The while and do..while Statements من فصل Chapter 3: Programming in the Small II: Control من كتاب Introduction to Programming Using Java.
  25. تعتمد قدرة الحاسب على أداء مهامٍ معقدة على دمج التعليمات البسيطة ضمن بنى تحكمٍ. هناك 6 بنى تحكم كهذه في جافا، تستخدم لتحديد تدفق التحكم الطبيعي في البرنامج وتكفي ثلاث منها لكتابة برامج قادرة على أداء أيّة مهمة.بنى التحكم الستّ هي: الكتلة (block) وحلقة while وحلقة do..while وحلقة for وتعليمة if وتعليمة switch. تُعدّ كلّ واحدة من البنى السابقة "تعليمةً" مفردة، لكنها في الواقع تعليمة بنيوية قد تحتوي بداخلها تعليمة أو أكثر. الكتل الكتلة (Blocks) هي الشكل الأبسط للتعليمات البنيوية، وتهدف إلى تجميع سلسلة من التعليمات في تعليمة واحدة. تأخذ الكتلة الشكل التالي: { // ضع التعليمات هنا } بكلمات أخرى، تتألف الكتلة من سلسلة من التعليمات المُغلّفة بين قوسين من الشكل "{ }". يمكن ألّا تحتوي الكتلة أيّة تعليمة، وتدعى عندئذٍ بالكتلة الفارغة (empty block) ومن الوارد أن تلزمك أحيانًا. تتألف الكتلة الفارغة من زوج فارغ من الأقواس فقط. عادةً ما ترد كتل التعليمات ضمن تعليمات أخرى وتهدف إلى تجميع عدة تعليمات معًا في وحدةٍ واحدة. يمكنك في الحقيقة استخدام كتل التعليمات أينما وردت تعليمةٌ. بيد أنّ من الواجب استخدامها في حالة البرامج الفرعية -لاحظ استخدامنا لها سابقًا في حالة البرنامج الفرعي main . البرنامج الفرعيّ هو كتلةٌ بالتعريف، نظرًا لكونه سلسلةً من التعليمات المغلّفة ضمن زوج من الأقواس. من الجدير بالذكر هنا أنّ لغة البرمجة جافا هي لغةٌ حرة التنسيق وهذا يعني عدم وجود قواعد صياغة تحدد كيفيّة ترتيب اللغة على الصفحة. يمكنك إن أردت، على سبيل المثال، كتابة كتلةٍ كاملة على سطر واحد. مع ذلك، ينبغي عليك، اتباعًا لممارسات البرمجة الفضلى، تنظيم برنامجك على نحوٍ يجعل قراءته وفهمه أسهل ما يمكن. يقتضي هذا في الحالة العامة كتابةً تعليمةٍ واحدة فقط في السطر واستخدام المسافات البادئة للإشارة إلى التعليمات المُحتواة ضمن بنى التحكم. سيُستخدم التنسيق هذا في جميع أمثلة الكتاب.إليك مثالين عن الكتل: { System.out.print("الجواب هو: "); System.out.println(ans); } { // ‫تستبدل هذه الكتلة بين قيمتي المتغيرين x و y int temp; // مُتغير مؤقت لاستخدامه ضمن هذه الكتلة temp = x; // ‫احفظ نسخةً عن x صمن temp. x = y; // انسخ قيمة‫ y إلى x. y = temp; // ‫انسخ قيمة temp إلى y. } قمنا في المثال الثاني بالتصريح عن المتغيّر temp ضمن الكتلة. التصريح عن متغيّرٍ ضمن كتلةٍ أمرٌ جائزٌ لا بل ومستحسنٌ أيضًا لا سيما إن كان استخدام المتغير مقتصرًا على تلك الكتلة. لا يمكن الوصول للمتغير المُصرّح عنه ضمن كتلة خارجها ولا يكون مرئيًّا إلا فيها. عندما ينفّذ الحاسوب تعليمة التصريح عن متغيّر، يخصص موضعًا في الذاكرة لتخزين قيمة هذا المتغيّر (نظريًّا على الأقل). عندما تنتهي الكتلة، يتم التخلّص من موضع الذاكرة ذاك ليصبح مُتاحًا للاستخدام مجددًا. يقال أنّ هذا المتغير "محلّي" ضمن الكتلة. هناك مفهوم أكثر شمولًا يُصطلح على تسميته مجال المُعرّف (scope of an identifier). مجال المُعرّف هو جزء من البرنامج يكون فيه المعرّف صالحًا. يقتصر مجال المتغيّر المُعرّف ضمن كتلة على الكتلة نفسها، وعلى نحوٍ أدق، يقتصر مجال المتغيّر المعرّف ضمن كتلة على الجزء من الكتلة الذي يلي التصريح عنه. حلقة while الأساسيّة لا تؤثر تعليمة الكتلة بحدّ ذاتها على تدفق التحكم في البرنامج على عكس بنى التحكم الخمس المتبقية. تُصنّف هذه البنى إلى نوعين: تعليمات الحلقات وتعليمات التفريع. في الواقع، لا تحتاج لغة البرمجة عامّة الغرض سوى إلى بنية تحكم واحدة من كل نوعٍ وكل ما سواهما هو بغرض التسهيل والتبسيط ليس إلّا. سنتطرق في هذا القسم إلى حلقة while وتعليمة if ونؤجل الحديث عن بنى التحكم الثلاث المتبقية إلى أقسام لاحقة. تُستخدم حلقة while لتكرار تعليمة معيّنة مرة بعد مرة. من غير المرجّح أنّك ستحتاج تكرار العملية إلى الأبد، لكنّه أمرٌ ممكن ويدعى الحلقة اللانهائية وهو غير محبّذ في الحالة العامة. هناك قصة قديمة عن رائدة علوم الحاسوب غريس موراي هوبر التي قرأت التعليمات على علبة الشامبو وكانت تنصّ على: " ارغي، اشطفي، كرري العمليّة." اتبعت غريس التعليمات حسب قولها وانتهى بها الأمر بعلبة شامبو فارغة. روت غريس القصة كمزحة حول اتباع الحواسيب للتعليمات بدون تفكير. توخيًّا للدقة، نقول أنّ حلقة while تُكرر التعليمة مرةً بعد مرة طالما أنّ قيمة شرطٍ محدّدٍ هي true. تأخذ حلقة while الشكل الآتي: while (تعبير منطقي) //ضع تعليماتك هنا نظرًا لأنّه من الممكن والمرّجح أن التعليمة هي كتلة تعليمات، تأخذ معظم حلقات while الشكل الآتي: while (تعبير منطقي) { //ضع تعليماتك هنا } يظّن بعض المبرمجين أنّ من الواجب تضمين الأقواس دائمًا لضرورات تنسيقية حتى لو كانت ستُغلّف تعليمةً واحدة فحسب، لكننا لن نتبع هذه النصيحة في الكتاب. أمّا دلالة تعليمة while فهي كما يلي: عندما يصادف الحاسوب تعليمة while، يقوم بتقييم التعبير المنطقي لينتج عن ذلك قيمة إما ture أو false. إذا كانت القيمة false، يتجاوز الحاسوب بقيّة حلقة while ويتابع تنفيذ الأمر الذي يليها في البرنامج. أمّا إذا كانت قيمة التعبير ture، ينفّذ الحاسوب التعليمة أو كتلة التعليمات الواردة ضمن الحلقة. بعدئذٍ، يعود الحاسوب إلى بداية حلقة while ويكرر العملية (أي يعاود تقييم التعبير المنطقي، يُنهي التنفيذ إذا كانت قيمته false، ويستمر إذا ما كانت ture). تتكرر العملية مرةً بعد مرةٍ بعد مرة حتى تصبح قيمة التعبير false عند قيام الحاسوب بتقييمه. إن لم نصل لهذه النتيجة، فستستمر الحلقة إلى الأبد.إليك مثالًا عن حلقة while بسيطة تطبع الأرقام 1، 2، 3، 4، 5: int number; // ‫العدد المراد طباعته. number = 1; // ‫ابدأ من الرقم 1. while ( number < 6 ) { // ‫تابع طالما أن العدد أصغر من 6. System.out.println(number); number = number + 1; // ‫انتقل للعدد التالي. } System.out.println("انتهى التنفيذ"); ترجمة -بتصرّف- للقسم Section 2: Algorithm Development من فصل Chapter 3: Programming in the Small II: Control من كتاب Introduction to Programming Using Java.