اذهب إلى المحتوى

تعرف على المصفوفات (Arrays) في جافا


رضوى العربي

ناقشنا أساسيات المصفوفات (arrays) بالفصول السابقة، ولكن ما يزال هنالك بعض التفاصيل الأخرى الخاصة بقواعد الصيغة (syntax) التي نحتاج إلى تناولها كما أن هنالك الكثير لذِكره بشأن اِستخدَامات المصفوفات في العموم. سنَفْحَص خلال هذا القسم بعضًا من تلك التفاصيل.

المصفوفة عبارة عن متتالية من العناصر المُرقَّمة بحيث يُمثِل كل عنصر مُتْغيِّرًا منفصلًا. لابُدّ أن تَكُون جميع العناصر ضِمْن مصفوفة معينة من نفس النوع. يُطلق على هذا النوع اسم نوع المصفوفة الأساسي (base type) كما أن المصفوفة ككل لها نوع أيضًا. فمثلًا، إذا كان النوع الأساسي لمصفوفة هو btype، فإن المصفوفة ككل تَكُون من النوع btype[]‎. يَملُك كل عنصر فهرسًا (index) يُمثِل مَوضِعه العددي بمتتالية العناصر. إذا كان لدينا مثلًا مصفوفة A، فإننا نَستخدِم A للإشارة إلى عنصر المصفوفة الموجود برقم المَوضِع i. يُطلَق على عدد العناصر الموجودة ضِمْن مصفوفة اسم طول المصفوفة (length)، ويُستخدَم A.length لمَعرِفة طول مصفوفة A. لاحِظ أنه من غَيْر الممكن تَغيِير طول مصفوفة معينة بَعْد انشائها. يُشار إلى عناصر مصفوفة A باستخدام A[0]‎ و A[1]‎ و .. حتى A[A.length-1]‎، وتُؤدِي أي محاولة للإشارة إلى عنصر مصفوفة برقم فهرس خارج النطاق المسموح به أي خارج نطاق يتراوح بين 0 و A.length-1 إلى حُدوث اِعتراض من النوع ArrayIndexOutOfBoundsException.

المصفوفات بلغة الجافا عبارة عن كائنات (objects) أي يستطيع أي مُتْغيِّر مصفوفة (array variable) أن يُشير إلى مصفوفة معينة لا أن يَحتوِي قيمة المصفوفة نفسها. قد تَكُون قيمة المُتْغيِّر فارغة (null)، وفي تلك الحالة، لا يُشير المُتْغيِّر إلى أي مصفوفة، وستَتَسبَّب إذًا أي محاولة للإشارة إلى عنصر ضِمْن تلك المصفوفة مثل A‎ بحدوث اعتراض من النوع NullPointerException. تُنشَئ المصفوفات باِستخدَام صيغة خاصة من العامل new كالتالي:

int[] A = new int[10];

تُنشِئ التَعْليمَة بالأعلى مصفوفة جديدة نوعها الأساسي (base type) هو int وطولها يُساوِي 10 ثم تَضبُط المُتْغيِّر A لكي يشير إلى تلك المصفوفة الجديدة المُنشئة للتو.

حلقات تكرار For-each

تعالج المصفوفات عادةً باستخدام حلقات التكرار (loop). تسهل حلقات التكرار من معالجة كل عنصر بالمصفوفة. على سبيل المثال، إذا كان namelist مصفوفة من النوع String، يمكننا طباعة قيم جميع عناصر تلك المصفوفة كالتالي:

for (int i = 0; i < namelist.length; i++) {
    System.out.println( namelist[i] );
}

هذا النوع من المعالجة شائع جدًا لدرجة وجود صياغة مختلفة من حلقات التكرار تسهل أكثر من ذلك. يطلق على تلك الصياغة اسم for-each. يستخدم المثال التالي حلقة التكرار for-each لطباعة جميع قيم عناصر مصفوفة من النوع String:

for ( String name : namelist ) {
    System.out.println( name );
}

يُقصَد من التَعْليمَة for (String name : namelist)‎ "نَفِّذ ما يلي لكل عنصر name من النوع String ضِمْن المصفوفة namelist". يُسنَد إلى المُتْغيِّر name خلال كل تَكْرار (iteration) إحدى قيم عناصر المصفوفة namelist ثُم يُنفَّذ مَتْن الحلقة. يُمثِل مُتْغيِّر التَحكُّم بالحلقة name قيمة عنصر المصفوفة وليس فهرسه (index) أي أننا لا نَملُك فهرس (index) العنصر داخل الحلقة.

تجنبك حَلْقة التَكْرار for-each التعقيد المُرتبط بفهارس عناصر المصفوفة. إلى جانب ذلك، يُمكِنك في العموم اِستخدَامها لمعالجة مجموعة من القيم ضِمْن شتى هياكل البيانات (data structure) كما سنرى بالفصل 10 حيث تَسمَح بمعالجة تلك القيم بغض النظر عن هيكل البيانات المُستخدَم.

تُنفِّذ حلقة التكرار for-each نفس العملية على كل قيمة مُخزَّنة بالمصفوفة. على سبيل المثال، إذا كان itemArray مصفوفة من النوع BaseType[]‎، يُمكنك كتابة حَلْقة التَكْرار for-each بالصياغة التالية:

for ( BaseType item : itemArray ) {
   .
   .  // عالج العنصر
   .
}

الأقواس بالأعلى اختيارية في حالة وجود تَعْليمَة واحدة فقط ضِمْن المَتْن. نظرًا لأن النوع الأساسي للمصفوفة (base type) هو BaseType، صَرَّحنا (declare) عن المُتْغيَّر المُتحكِّم بالحلقة (loop control variable)‏ item ليَكُون من النوع BaseType. لاِحظ أن التَّصريح (declare) عن المُتْغيِّر المُتحكِّم بحَلْقات التَكْرار for-each لابُدّ أن يَقَع داخل حَلْقة التَكْرار، فلا يُمكِنك اِستخدَام مُتْغيِّر مُصرَّح عنه مُسبَقًا خارج الحلقة (loop). عند تّنفيذ الحلقة، تُسنَد كل قيمة بالمصفوفة إلى المُتْغيِّر item ثم يُنْفَّذ مَتْن الحلقة (loop body) لتلك القيمة. يُمكِنك في الواقع استخدام for لكتابة نفس حَلْقة التَكْرار بالأعلى كما يلي:

for ( int index = 0; index < itemArray.length; index++ ) {
   BaseType item;
   item = itemArray[index];  // اجلب قيمة أحد عناصر المصفوفة
     .
     .  // process the item
     .  
}

على سبيل المثال، إذا كان المُتْغيِّر A عبارة عن مصفوفة من النوع int[]‎، يُمكِننا طباعة جميع قيم A باستخدام for-each كالتالي:

for ( int item : A )
   System.out.println( item );

ويُمكِننا حساب مجموع قيم الأعداد الصحيحة الموجبة داخل المصفوفة A كالتالي:

int sum = 0;   // حاصل مجموع الأعداد الموجبة ضمن المصفوفة
for ( int item : A ) {
   if (item > 0)
      sum = sum + item;
}

قد لا يَكون اِستخدَام حَلْقة التَكْرار for-each ملائمًا ببعض الأحيان. على سبيل المثال، لا توجد طريقة بسيطة لاستخدامها لمعالجة العناصر الموجودة بجزء معين من مصفوفة أو لمعالجة عناصر مصفوفة بترتيب معكوس. في المقابل، هي في العموم أكثر ملائمة في حالة أردت معالجة جميع قيم عناصر مصفوفة معينة بنفس ترتيب وجودها داخل تلك المصفوفة؛ لأنها تُجنِّبك اِستخدَام الفهارس (indices).

تُعالِج حَلْقة التَكْرار for-each "القيم (values)" الموجودة بالمصفوفة وليس "العناصر (elements)" أي أنها لا تُعالِج مواضع الذاكرة الفعلية لتلك القيم. على سبيل المثال، تُحاوِل الشيفرة التالية مَلْئ مصفوفة من الأعداد الصحيحة بالقيمة 17 بطريقة خاطئة:

int[] intList = new int[10];
for ( int item : intList ) {   // غير صحيح
   item = 17;
}

تُسنِد التَعْليمَة item = 17 القيمة 17 إلى المُتْغيِّر المُتحكِّم بالحَلْقة item، ولكن هذا ليس له أي علاقة بالمصفوفة. عند تّنفِيد مَتْن الحَلْقة (body loop)، تُنسَخ إحدى قيم عناصر المصفوفة إلى المُتْغيِّر item أي أن ما تحاول التَعْليمَة item = 17 تَعْدِيله هو تلك القيمة المَنسُوخَة، وبالتالي ليس لها أي تأثير على عنصر المصفوفة المَنسُوخ ذاته ولا تَتَغيَّر قيمته. تُكافِيء حَلْقة التَكْرار السابقة ما يلي:

int[] intList = new int[10];
for ( int i = 0; i < intList.length; i++ ) {
   int item = intList[i];
   item = 17;
}

والتي بوضوح لا تُغيِّر قيمة أي عنصر داخل المصفوفة.

التوابع متغيرة الرتبية (Variable Arity Methods)

قبل الإصدار الخامس من الجافا، كانت جميع التوابع (method) ثابتة الرتبية (fixed arity) -الرتبية عمومًا هي عدد المُعاملات المُمرَّرة لتابع ضِمْن تَعْليمَة الاستدعاء-. في حالة التوابع ثابتة الرتبية (fixed arity methods)، لابُدّ من تمرير نفس عدد المعاملات (parameters) بكل تعليمة استدعاء (call) لتابع ثابت الرتبية، كما لابُدّ أن يَكُون ذلك العدد هو نفسه عدد المُعاملات الصُوريَّة (formal parameters) ضِمْن تعريف ذلك التابع (method definition). منذ الإصدار الخامس من الجافا، تَتَوفَّر توابع مُتْغيِّرة الرتبية (variable arity methods)، والتي يُمكِن أن يُمرَّر إليها عدد مختلف من المعاملات (parameters). على سبيل المثال، يُعدّ التابع System.out.printf -ناقشناه بالقسم الفرعي 2.4.1- مُتْغيِّر الرتبية (variable arity). بالإضافة إلى مُعامله الأول الذي لابُدّ أن يَكُون عبارة عن سِلسِلة نصية من النوع String، فإنه يَستقبِل أي عدد ممكن من المعاملات الإضافية ومن أي نوع.

لا تختلف طريقة استدعاء تابع مُتْغيِّر الرتبية (variable arity method) عن طريقة استدعاء أي تابع آخر، ولكن تَتَطلَّب كتابته بعض قواعد الصيغة الجديدة (syntax). على سبيل المثال، إذا أردنا كتابة تابع (method) يَحسِب قيمة متوسط أي عدد من القيم من النوع double، يُمكِننا أن نُعرِّفه كالتالي:

public static double average( double...  numbers ) {

لاحظ اِستخدَام الترميز ... بَعْد اسم النوع double ضِمْن التعريف بالأعلى، وهو في الواقع ما يَجعَل ذلك التابع مُتْغيِّر الرتبية (variable arity) حيث يُشير ذلك الترميز إلى إمكانية تمرير أي عدد من القيم من النوع double بتعليمة استدعاء البرنامج الفرعي (subroutine) أي يُمكِنك اِستدعائه باِستخدَام average(1,4,9,16)‎ أو average(3.14,2.17)‎ أو average(0.375)‎ أو average()‎. يُمكِنك أيضًا أن تُمرِّر قيم من النوع int كمُعاملات فعليّة (actual parameters) للتابع حيث تُحوَّل كالعادة تلقائيًا إلى أعداد حقيقية (real).

عند استدعاء تابع مُتْغيِّر الرتبية (variable arity method)، تُوضَع قيم المُعاملات الفعليّة المناظرة لمعامل مُتْغيِّر الرتبية (variable arity) بمصفوفة ثم تُمرَّر تلك المصفوفة إلى التابع أي يبدو مُعامل مُتْغيِّر الرتبية من النوع T مثلًا بهيئة مصفوفة من النوع T[]‎ داخل مَتْن التابع، ويُحدِّد عدد المُعاملات الفعليّة (actual parameters) المُمرَّرة أثناء الاستدعاء طول تلك المصفوفة. بالنسبة للتابع average()‎، فإن مَتْنه (body) سيَرى مصفوفة اسمها numbers من النوع double[]‎، ويُحدِّد numbers.length عدد المعاملات الفعليّة بتَعْليمَة الاستدعاء، وتُمثِل numbers[0]‎ و numbers[1]‎ ..إلخ قيم تلك المعاملات المُمرَّرة. يُمكِننا تعريف (define) ذلك التابع كالتالي:

public static double average( double... numbers ) {
        // ‫داخل التابع، المتغير numbers من النوع double[]
   double sum;      // مجموع قيم المعاملات الفعلية
   double average;  // متوسط قيم المعاملات الفعلية
   sum = 0;
   for (int i = 0; i < numbers.length; i++) {
       // أزد أجد المعاملات الفعلية إلى حاصل المجموع
      sum = sum + numbers[i];  
   }
   average = sum / numbers.length;
   return average;
}

بالمناسبة، من الممكن تمرير مصفوفة واحدة إلى تابع متغير الرتبية (variable arity mehtod) بدلًا من قائمة من القيم المفردة. على سبيل المثال، لنفترض أن salesData عبارة عن متغير من النوع double[]‎، فإنه من الممكن استخدام الاستدعاء average(salesData)‎ لحساب قيمة متوسط جميع الأعداد بالمصفوفة. قد تتضمن قائمة المعاملات الصورية (formal parameter list) بتعريف تابع متغير الرتبية (variable-arity method) أكثر من معامل واحد، ولكن الترميز ... يمكن أن يطبق على المعامل الصوري الأخير فقط.

كمثال، إذا أردنا كتابة تابع (method) لرسم مضلع يُحدَّد موضعه عبر أي عدد من النقاط. تكون تلك النقاط من النوع Point، وبحيث يحتوي أي كائن (object) من ذلك النوع على مُتْغيِّري نُسخة (instance variables) هما x و y من النوع double يُحدِّدان موضع النقطة. سيَستقبِل التابع أولًا مُعاملًا عاديًا هو كائن السياق الرسومي (graphics context) المُستخدَم لرَسْم المضلع بالإضافة إلى مُعامل مُتْغيِّر الرتبية (variable arity parameter). يبدو ذلك المُعامل بهيئة مصفوفة من النوع Point ضِمن مَتْن التابع. يُمكننا كتابة تعريف التابع كالتالي:

public static void drawPolygon(GraphicsContext g, Point... points) {
    // نحتاج إلى نقطتين على الأقل لرسم أي شيء
    if (points.length > 1) {  
       for (int i = 0; i < points.length - 1; i++) {
           // ‫ارسم خطا من النقطة i إلى النقطة i+1
           g.strokeLine( points[i].x, points[i].y, points[i+1].x, points[i+1].y );
       }
        // ارسم خطًا عائدًا إلى نقطة البداية
       g.strokeLine( points[points.length-1].x, points[points.length-1].y,
                       points[0].x, points[0].y );
    }
}

لابُدّ أن تحتوي تَعْليمَة استدعاء ذلك البرنامج الفرعي (subroutine call) على مُعامل فعلي واحد من النوع GraphicsContext متبوعًا بأي عدد من المعاملات الفعلية من النوع Point.

كمثال أخير، يَضُمّ التابع (method) التالي قائمة من السَلاسِل النصية (strings) معًا ليُكوِّن سِلسِلة نصية واحدة طويلة. تَستخدِم الشيفرة التالية حَلْقة تَكْرار for-each لمعالجة المصفوفة:

public static String concat( String... values ) {
    // ‫استخدم الصنف StringBuilder لضم أكثر كفاءة
   StringBuilder buffer;  
   buffer = new StringBuilder();  // ابدأ بكائن فارغ
   for ( String str : values ) { // استخدم حلقة تكرار لمعالجة القيم
       buffer.append(str); // أضف سلسلة نصية إلى المخزن المؤقت
   }
   return buffer.toString(); // أعد محتويات المخزن المؤقت
}

يُعيد الاستدعاء concat("Hello", "World")‎ مثلًا السِلسِلة النصية "HelloWorld" بينما يُعيد الاستدعاء concat()‎ سِلسِلة نصية فارغة. لمّا كان مُمكِنًا تمرير مصفوفة كمعامل فعلي (actual parameter) لتابع مُتْغيِّر الرتبية (variable arity method)، فبإمكاننا أيضًا استدعاء concat(lines)‎ حيث lines عبارة عن مصفوفة من النوع String[]‎. سيَضُمّ (concatenate) التابع جميع العناصر الموجودة داخل المصفوفة إلى سِلسِلة نصية (string) واحدة.

مصفوفات مجردة مصنفة النوع (Array Literals)

لقد رأينا أنه بإمكاننا أن نُهيِئ (initialize) مُتْغيِّر مصفوفة (array variable) بقائمة من القيم أثناء التّصرِيح (declare) عنه كالتالي:

int[] squares = { 1, 4, 9, 16, 25, 36, 49 };

تُهيِئ التَعليمَة بالأعلى المُتْغيِّر squares لكي يُشيِر إلى مصفوفة جديدة تحتوي على قائمة من 7 قيم. يُمكِنك اِستخدَام مُهيِئ القائمة (list initializer) بالصياغة السابقة مع تَعْليمَات التّصرِيح (declaration statement) فقط لكي تُسنِد قيمة مبدئية لمُتْغيِّر المصفوفة (array variable) المُصرَّح عنه للتو. في المقابل، لا يُمكنِك اِستخدَامه مع تَعْليمَة إِسْناد (assignment statement) بغرض إِسْناد قيمة جديدة إلى مُتْغيِّر مَوجود مُسْبَقًا. تَتَوفَّر طريقة آخرى لإنشاء مصفوفات جديدة، ويُمكِنك حتى اِستخدَامها بمواضع آخرى غير التّصرِيح. تَستخدِم تلك الطريقة صياغة مختلفة من العامل new لإنشاء كائن مصفوفة جديد ومن ثَمَّ مَلْئه. يُمكِننا مثلًا أن نُسنِد قيمة جديدة إلى مُتْغيِّر مصفوفة cubes من النوع int[]‎ كالتالي:

cubes = new int[] { 1, 8, 27, 64, 125, 216, 343 };

لاحِظ أن التَعْليمَة بالأعلى هي تَعْليمَة إِسْناد (assignment) وليست تصريح (declaration) أي أنه من غَيْر المُمكن اِستخدَام مُهيِئ المصفوفة -بدون new int[]‎- هنا. تُكتَب تلك الصياغة في العموم على النحو التالي:

new base-type [ ] { list-of-values }

في الواقع، يُعدّ ما سبق تعبيرًا (expression) قيمته عبارة عن مَرجِع (reference) إلى كائن مصفوفة (array object) جديد، ويُمكِنك اِستخدَامه لتمثيل قيمة من ذلك النوع، لهذا يُمكِن عدّه ضِمْن هذا السياق مُصفوفة مُصنَّفة النوع (array literal). يَعنِي ذلك أنه من المُمكن اِستخدَامه أينما أمكن اِستخدَام كائن (object) من النوع base-type[]‎، فيُمكِنك مثلًا أن تُمرِّر المصفوفة الجديدة المنشئة للتو كمُعامل فعلي (actual parameter) لبرنامج فرعي (subroutine). تَستخدِم الشيفرة التالية مصفوفة من السَلاسِل النصية (strings) لإنشاء قائمة (menu):

/**
 * @param menuName  the name for the Menu that is to be created.
 * @param itemNames  an array holding the text that appears in each
 *     MenuItem.  If a null value appears in the array, the corresponding
 *     item in the menu will be a separator rather than a MenuItem.
 * @return  the menu that has been created and filled with items.
 */
public static Menu createMenu( String menuName, String[] itemNames ) {
    Menu menu = new Menu(menuName);
    for ( String itemName : itemNames ) {
        if ( itemName == null ) {
            menu.getItems().add( new SeparatorMenuItem() );
        }
        else {
            MenuItem item = new MenuItem(itemName);
            menu.getItems().add(item);
        }
    }
    return menu;
}

يَستقبِل التابع createMenu معاملًا ثانيًا عبارة عن مصفوفة من السَلاسِل النصية. يُمكننا أن نُنشِئ المصفوفة المُمرَّرة كمعامل فعلي (actual parameter) إلى ذلك التابع بمحلها باِستخدَام العامل new. على سبيل المثال، تُنشِئ التَعْليمَة التالية قائمة File كاملة:

Menu fileMenu = createMenu( "File", 
              new String[] { "New", "Open", "Close", null, "Quit" } );

ينبغي أن يُقنعِك المثال السابق بأهمية القدرة على إنشاء مصفوفة واِستخدَامها بنفس المكان، فلربما هي بنفس أهمية القدرة على اِستخدَام الأصناف الداخلية (inner classes) مجهولة الاسم (anonymous). ربما كان من الافضل أيضًا كتابة التابع createMenu()‎ بهيئة تابع مُتْغيِّر الرتبية (variable arity method).

تستطيع أيضًا أن تَستخدِم new BaseType[] { ... }‎ بدلًا من مُهيِئ المصفوفة (array initializer) أثناء التّصرِيح عن مُتْغيِّر مصفوفة (array variable). فمثلًا، بدلًا من كتابة:

int[] primes = { 2, 3, 5, 7, 11, 13, 17, 19 };

يُمكِنك أن تَستخدِم ما يلي:

int[] primes = new int[] { 2, 3, 5, 7, 11, 17, 19 };

يُفضِّل الكاتب في الواقع اِستخدَام الصياغة الثانية بدلًا من اِستخدَام صياغة خاصة تَعمَل فقط مع تَعْليمَات التّصرِيح (declaration statement).

ملحوظة أخيرة: يُمكِنك كتابة التصريح عن مصفوفة مثل:

int[] list;

على النحو التالي أيضًا:

int list[];

وهي صياغة مُستخدَمة بلغتي البرمجة C و C++‎، ولكنها مع ذلك غَيْر مُتِّسقَة مع لغة Java، ومن الأفضل تَجنُبها. فالغرض بالنهاية هو التّصرِيح عن مُتْغيِّر من نوع معين، وهذا النوع هو int[]‎، لذلك من الأفضل اتباع لمثل تلك التصريحات.

ترجمة -بتصرّف- للقسم Section 1: Array Details من فصل Chapter 7: Arrays and ArrayLists من كتاب Introduction to Programming Using Java.


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

أفضل التعليقات

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



انضم إلى النقاش

يمكنك أن تنشر الآن وتسجل لاحقًا. إذا كان لديك حساب، فسجل الدخول الآن لتنشر باسم حسابك.

زائر
أضف تعليق

×   لقد أضفت محتوى بخط أو تنسيق مختلف.   Restore formatting

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   جرى استعادة المحتوى السابق..   امسح المحرر

×   You cannot paste images directly. Upload or insert images from URL.


×
×
  • أضف...