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

معالجة المصفوفات Arrays في جافا


رضوى العربي

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

أمثلة على المعالجة

لابُدّ أن تكون حريصًا فيما يتعلَّق بالفهارس من خارج نطاق المصفوفة. لنَفْترِض مثلًا أن lines عبارة عن مصفوفة من النوع String[]‎، وأننا نريد مَعرِفة ما إذا كانت تلك المصفوفة تحتوي على أية عناصر مكررة بموضعين متتاليين. ينبغي إذًا أن نَفْحَص الاختبار lines.equals(lines[i+1])‎ لأي فهرس i. اُنظر المحاولة الخاطئة التالية:

boolean dupp = false;  // Assume there are no duplicates
for ( int i = 0; i < list.length; i++ ) {
    if ( lines[i].equals(lines[i+1]) ) {  // THERE IS AN ERROR HERE!
        dupp = true;   // we have found a duplicate!
        break;
    }
}

تبدو حلقة التكرار for بالأعلى مثل الكثير من الحلقات التي تعرضنا لها من قبل، لذلك ما هي المشكلة إذًا؟ يحدث الخطأ عندما تسند القيمة النهائية إلى i بالحلقة أي عندما i تساوي lines.length-1. في هذه الحالة، i+1 تساوي lines.length. لكن فهرس العنصر الأخير بالمصفوفة هو lines.length-1، لذلك lines.length ليست فهرس صالح. يعني ذلك أن lines[i+1]‎ يتسبب بحدوث اعتراض من النوع ArrayIndexOutOfBoundsException. يمكنك إصلاح ذلك بسهولة عن طريق إيقاف حلقة التكرار قبل i+1 من النطاق المسموح به، كالتالي:

boolean dupp = false;  // Assume there are no duplicates
for ( int i = 0; i < list.length - 1 ; i++ ) {
    if ( lines[i].equals(lines[i+1]) ) { 
        dupp = true;   // we have found a duplicate!
        break;
    }
}

يظهر أحيانًا نوع شبيه من الأخطاء عند العمل مع المصفوفات المملوءة جزئيًا (partially full arrays) -اُنظر القسم الفرعي 3.8.4-، ولكنه قد لا يَكُون بهذا الوضوح. إذا كان جزء معين فقط من المصفوفة مملوءًا بحيث يُستخدَم عداد (counter) لمعرفة عدد الخانات المُستخدَمة فعليًا ضِمْن تلك المصفوفة. لا تتعلَّق المشكلة في تلك الحالة بمحاولة الوصول لعنصر بموضع خارج المصفوفة ولكنها تتعلق بفحص جزء من المصفوفة قيد الاستخدام بالفعل. عندما يُحاوِل البرنامج الوصول إلى مَوْضِع خارج المصفوفة، ينهار (crash) البرنامج على الأقل مما يَسمَح لك برصد المشكلة لكن في حالة المصفوفات المملوءة جزئيًا، لن تتمكن من رصد الخطأ بسهولة.

لقد رأينا الكيفية التي يُمكِننا بها أن نُضيف عناصر إلى مصفوفة مملوءة جزئيًا، ولكننا لم نَتعرَّض لطريقة حَذْف تلك العناصر؟ لنَفْترِض مثلًا أننا نَكْتُب برنامجًا عبارة عن لعبة يستطيع اللاعبون الانضمام إليها ومغادرتها بأي وقت. يُمكِننا إذًا أن نُنشِئ الصَنْف Player لتمثيل كل لاعب موجود باللعبة. سنُخزِّن بطبيعة الحال قائمة اللاعبين الموجودين باللعبة داخل مصفوفة من النوع Player[]‎، وليَكُن اسمها هو playerList. نظرًا لأن عدد اللاعبين قد يَتَغيَّر بأي وقت، سنُطبِق نمط المصفوفة المملوءة جزئيًا، لذا سنُعرِّف مُتغيِّر playerCt لتَخْزِين عدد اللاعبين الموجودين باللعبة. بفَرْض عدم احتمالية وجود أكثر من 10 لاعبين بأي لحظة، يُمكِننا إذًا أن نُصرِّح عن المُتْغيِّرات كالتالي:

Player[] playerList = new Player[10];  // Up to 10 players.
int      playerCt = 0;  // At the start, there are no players.

بَعْد انضمام بعض اللاعبين إلى اللعبة، سيُصبِح المُتْغيِّر playerCt أكبر من الصفر كما ستُخزَّن الكائنات (objects) المُمثِلة للاعبين داخل عناصر المصفوفة playerList[0]‎ و playerList[1]‎ .. وحتى playerList[playerCt-1]‎. لاحِظ أننا لم نُشِر إلى العنصر playerList[playerCt]‎. يُمثِل المُتْغيِّر playerCt كُلًا من عدد العناصر الفعلية الموجودة داخل المصفوفة، وكذلك فهرس (index) الخانة التالية المتاحة بالمصفوفة. تُضيِف الشيفرة التالية كائنًا جديدًا newPlayer إلى اللعبة:

playerList[playerCt] = newPlayer; // Put new player in next
                                  //     available spot.
playerCt++;  // And increment playerCt to count the new player.

في المقابل، قد يَكُون حَذْف لاعب من اللعبة أصعب قليلًا، لأننا لا نُريد بالطبع أن نَترُك مَوْضِع اللاعب المطلوب حَذْفه فارغًا. لنَفْترِض مثلًا أننا نُريد حَذْف اللاعب الموجود بالفهرس k داخل المصفوفة playerList. يَعنِي ذلك أن عدد الخانات المُستخدَمة فعليًا بالمصفوفة سيُصبِح أقل بمقدار الواحد. إذا لم يَكُن ترتيب اللاعبين داخل المصفوفة مُهِمًا، يُمكنِنا إذًا أن نَنقِل اللاعب الأخير الموجود فعليًا داخل المصفوفة إلى المَوْضِع k ثُمَّ نُنقِص قيمة playerCt بمقدار الواحد كالتالي:

playerList[k] = playerList[playerCt - 1];
playerCt--;

لم يَعُد اللاعب الذي كان موجودًا مُسْبَقًا بالمَوْضِع k موجودًا بالمصفوفة، فقد حَذْفناه ببساطة من القائمة. في المقابل، أصبح اللاعب الذي كان موجودًا مُسْبَقًا بالموضع playerCt - 1 موجودًا بالمصفوفة مرتين، ولكن نظرًا لأننا انقصنا المُتْغيِّر playerCt بمقدار الواحد، فإن واحدة منهما فقط تَقَع ضِمْن الجزء المُستخدَم فعليًا بالمصفوفة. يَحمِل أي عنصر بالمصفوفة في العموم قيمة ما، ولكن القيم من الموضع 0 وحتى playerCt - 1 هي فقط الصالحة أي يُمكِننا معالجتها. حاول أن تُفكِر بما سيَحدُث إذا كان اللاعب المطلوب حَذْفه هو اللاعب الأخير بالمصفوفة، وهل ترى أن الشيفرة بالأعلى ستَظِل صالحة؟

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

for (int i = k+1; i < playerCt; i++) {
    playerList[i-1] = playerList[i];
}
playerCt--;

تُوضِح الصورة التالية طريقتي حَذْف عنصر -اللاعب "C" تحديدًا- من مصفوفة مملوءة جزئيًا (partially full array):

001Delete_From_Array.png

يترك ذلك السؤال مفتوحًا عما سيَحدُث إذا كانت المصفوفة المملوءة جزئيًا (partially full) مملوءة بالكامل بينما تَرغَب بإضافة عناصر جديدة إليها؟ بالطبع، لا يُمكِننا أن نُغيِّر حجم المصفوفة (array size)، ولكن يُمكِننا أن نُنشِئ مصفوفة جديدة أكبر ثُمَّ نَنَسَخ البيانات الموجودة بالمصفوفة القديمة إلى المصفوفة الجديدة. ينقلنا ذلك إلى السؤال التالي: ما الذي يعنيه نَسخ مصفوفة بالأساس؟

لنَفْترِض أن A و B عبارة عن مُتْغيِّري مصفوفة (array variables) لهما نفس النوع الأساسي (base type). تُشير A بالفعل إلى مصفوفة، ونريد الآن جَعْل B تُشيِر إلى نُسخة من A. أول ما ينبغي أن تُدركه هو أن تَعْليمَة الإِسْناد (assignment statement) التالية:

B = A;

لا تُنشِئ نسخة من A. نظرًا لأن المصفوفات عبارة عن كائنات (objects)، يَحمِل أي مُتْغيِّر مصفوفة (array variable) -إن لم يَكُن فارغًا- مؤشِرًا (pointer) إلى مصفوفة. تَنَسَخ تَعْليمَة الإِسْناد (assignment) بالأعلى ذلك المُؤشر من A إلى B، وبالتالي سيُشيِر كُلًا من A و B إلى نفس ذات المصفوفة، فيُعدّ مثلًا كُلًا من A[0]‎ و B[0]‎ أسماءً مختلفة لنفس عناصر المصفوفة (array element). في المقابل، إذا كنا نريد جَعْل B يُشيِر إلى نسخة من A، ينبغي إذًا أن نُنشِئ مصفوفة جديدة كليًا ثُمَّ نَنَسخ العناصر من A إلى B. بِفَرض أن A و B من النوع double[]‎، يُمكِننا إذًا كتابة ما يَلي لإنشاء نسخة من A:

double[] B;
B = new double[A.length];  // Make a new array with the same length as A.
for ( int i = 0; i < A.length; i++ ) {
    B[i] = A[i];
}

لكي نَتَمكَّن من إضافة عناصر جديدة إلى مصفوفة مملوءة جزئيًا (partially full array) ممتلئة، ينبغي أن نُنشِئ مصفوفة جديدة أكبر، وعادةً ما تَكُون بحجم يُساوِي ضعف المصفوفة الحالية. لما كان مُتْغيِّر المصفوفة هو ما يَسمَح لنا بالوصول إلى البيانات التي أصبحت موجودة بالمصفوفة الجديدة، ينبغي إذًا أن نُعدِّل ذلك المُتْغيِّر لكي يُشير إلى المصفوفة الجديدة. يُمكِننا أن نُجرِي ذلك لحسن الحظ بتَعْليمَة إِسْناد (assignment statement) بسيطة. بِفَرْض اِستخدَام playerList و playerCt لتَخْزِين اللاعبين باللعبة -كالمثال بالأعلى-، تُوضِح الشيفرة التالية طريقة إضافة لاعب جديد newPlayer إلى اللعبة حتى لو كانت المصفوفة playerList ممتلئة:

if ( playerCt == playerList.length ) {
        // The number of players is already equal to the size of the array.
        // The array is full.  Make a new array that has more space.
    Player[] temp;   // A variable to point to the new array.
    temp = new Player[ 2*playerList.length ];  // Twice as big as the old array.
    for ( int i = 0; i < playerList.length; i++ ) {
        temp[i] = playerList[i];  // Copy item from old array into new array.
    }
    playerList = temp;  // playerList now points to the new, bigger array.    
}
// At this point, we know that there is room in the array for newPlayer.
playerList[playerCt] = newPlayer;
playerCt++;

الآن، لم يَعُد هناك أي مُتْغيِّر يُشير إلى المصفوفة القديمة، ولذلك يَتَولَّى كانس المُهملات (garbage collector) مُهِمَة تَحرِيرها.

بعض التوابع القياسية للمصفوفات

تحتاج الكثير من البرامج إلى عمليات مثل نَسْخ مصفوفة، ولهذا تُوفِّر الجافا العديد من التوابع (methods) القياسية لمعالجة المصفوفات. هذه التوابع مُعرَّفة كتوابع ساكنة (static methods) بصَنْف اسمه Arrays ضِمْن حزمة java.util. اُنظر المثال التالي:

Arrays.copyOf( list, lengthOfCopy )

تُعيد الدالة copyOf مصفوفة جديدة طولها يُساوِي قيمة المعامل lengthOfCopy، وبحيث تحتوي على عناصر منسوخة من المُعامل list. إذا كان lengthOfCopy أكبر من list.length، ستَحمِل الخانات الإضافية بالمصفوفة الجديدة قيمها الافتراضية (أي صفر في حالة المصفوفات العددية، وnull في حالة مصفوفات الكائنات [array objects]، ..إلخ). في المقابل، إذا كان lengthOfCopy أقل من list.length، تُنسَخ العناصر بمقدار ما يَتَناسَب مع حجم المصفوفة الجديدة. على سبيل المثال، إذا كانت A مصفوفة، فإن الشيفرة التالية:

B = Arrays.copyOf( A, A.length );

تَضبُط B لكي تُشيِر إلى نُسخة مُتطابِقة مع A، أما الشيفرة التالية:

playerList = Arrays.copyOf( playerList, 2*playerList.length );

يُمكِنها أن تُستخدَم لمضاعفة حجم المساحة المتاحة بمصفوفة مملوءة جزئيًا (partially full array). علاوة على ذلك، قد نَستخدِم نفس ذلك التابع Arrays.copyOf لإنقاص حجم مصفوفة مملوءة جزئيًا، وهو ما يُساعِدنا على تَجنُّب وجود خانات إضافية كثيرة غَيْر مُستخدَمة. يُمكِننا أن نُطبِق ذلك أثناء حَذْف لاعب k من قائمة اللاعبين كالتالي:

playerList[k] = playerList[playerCt-1];
playerCt--;
if ( playerCt < playerList.length/4 ) {
        // More than 3/4 of the spaces are empty. Cut the array size in half.
    playerList = Arrays.copyOf( playerList, playerList.length/2 );
}

يحتوي الصَنْف Arrays في الواقع على نُسخ مختلفة من التابع copyOf: نُسخة لكل نوع أساسي (primitive type)، بالإضافة إلى نُسخة آخرى للكائنات (objects). ملحوظة: عند نَسْخ مصفوفة كائنات (objects)، تُنسَخ فقط مؤشرات (pointers) الكائنات لا محتوياتها إلى المصفوفة الجديدة، ويُمثِل ذلك عمومًا القاعدة العامة لإِسْناد مؤشر (pointer) إلى آخر.

إذا كان كل ما تُريده هو مُجرد نسخة بسيطة من مصفوفة بنفس حجمها الأصلي، فهناك طريقة سهلة للقيام بذلك. في الواقع، تَملُك أي مصفوفة تابع نُسخة (instance method) اسمه هو clone()‎. يُنشِئ ذلك التابع نُسخة من المصفوفة. يُمكِنك مثلًا أن تَستخدِم الشيفرة التالية لإنشاء نسخة من مصفوفة من النوع int:

int[] B  =  A.clone();

يَحتوِي الصَنْف Array على توابع (methods) مفيدة آخرى سنَعرِض بعضًا منها هنا. بالمثل من التابع Arrays.copyOf، تَتَوفَّر نُسخ متعددة من كل تلك التوابع للأنواع المختلفة من المصفوفات:

  • Arrays.fill( array, value )‎: تَملَئ مصفوفة كاملة بقيمة محددة. لابُدّ أن يَكُون value من نوع مُتوافق مع النوع الأساسي للمصفوفة array. إذا كان numlist عبارة عن مصفوفة من النوع double[]‎ مثلًا، ستَملَئ التَعْليمَة Arrays.fill(numlist,17)‎ جميع عناصر تلك المصفوفة بالقيمة 17.

  • Arrays.fill( array, fromIndex, toIndex, value )‎: تَملَئ جزءًا من المصفوفة array من الفهرس fromIndex حتى الفهرس toIndex-1 بالقيمة value. لاحِظ أن الفهرس toIndex غَيْر مُضمَّن.

  • Arrays.toString( array )‎: عبارة عن دالة (function) تُعيِد سِلسِلة نصية من النوع String مُكوَّنة من قيم المصفوفة array مفصولة بفاصلة (comma) ومُحاطَة بأقواس مربعة. تُحوَّل القيم إلى سَلاسِل نصية (strings) بنفس الطريقة التي تُحوَّل بها تلك القيم أثناء طباعتها.

  • Arrays.sort( array )‎: تُعيد ترتيب جميع القيم الموجودة بمصفوفة ترتيبًا تصاعديًا. لاحِظ أنها لا تَعمَل مع جميع المصفوفات، فلابُدّ أن تكون قيم المصفوفة قابلة للموازنة لمعرفة أيهما أصغر. افتراضيًا، تَعمَل مع المصفوفات من النوع String والأنواع البسيطة (primitive types) باستثناء النوع boolean. سنناقش خوارزميات ترتيب المصفوفات (array sorting) بالقسم 7.4.

  • Arrays.sort( array, fromIndex, toIndex )‎: تُرتِّب العناصر من array[fromIndex]‎ إلى array[toIndex-1]‎.

  • Arrays.binarySearch( array, value )‎: تبحث تلك الدالة عن value داخل array، وتُعيد قيمة من النوع int عبارة عن فهرس (index) عنصر المصفوفة المُتضمِّن للقيمة المُمرَّرة إذا كانت موجودة. إذا لم تَكُن تلك القيمة موجودة بالمصفوفة، تُعيد تلك الدالة القيمة -1. ملحوظة: لابُدّ أن تَكُون المصفوفة مُرتَّبة ترتيبًا تصاعديًا. سنُناقِش خوارزمية البحث الثنائي (binary search) بالقسم 7.4.

7.2.3 برنامج RandomStrings

صَمَّمنا -بالقسم الفرعي 6.2.4- برنامج واجهة مُستخدِم رسومية (GUI) يَعرِض نُسخًا مُتعددة من سِلسِلة نصية بمواضع عشوائية وبألوان وخطوط عشوائية، وعندما يَنقُر المُستخدِم على نافذة البرنامج، تَتَغيَّر كُلًا من مَواضِع وألوان وخطوط تلك السَلاسِل النصية (strings) إلى قيم عشوائية. لنَفْترِض الآن أننا نُريد أن نُنشِئ تحريكة (animation) تتحرك خلالها تلك السَلاسِل عبر النافذة. سنحتاج في تلك الحالة إلى تَخْزِين خاصيات كل سِلسِلة نصية منها لكي نَتَمكَّن من إعادة رسمها بكل إطار ضِمْن التحريكة. يُمكِنك الإطلاع على نسخة البرنامج الجديدة بالملف RandomStringsWithArray.java.

سنَرسِم 25 سِلسِلة نصية. لكل سِلسِلة نصية منها، ينبغي أن نُخزِّن إحداثيات مَوْضِعها (x,y) ولون ونوع الخط المُستخدَم للرَسْم. لكي نَتَمكَّن من تَحرِيك السَلاسِل النصية، سنُخزِّن سرعة الحركة الخاصة بكل سِلسِلة نصية بهيئة عددين dx و dy. بكل إطار (frame)، ينبغي أن تزداد قيمة الإحداثي x لكل سِلسِلة نصية بمقدار dx الخاص بتلك السِلسِلة بينما ستزداد قيمة الإحداثي y بمقدار dy المناظر. سنحتاج إذًا إلى الست مصفوفات التالية لتَخْزِين بيانات البرنامج:

double[] x = new double[25];  
double[] y = new double[25];
double[] dx = new double[25];  
double[] dy = new double[25];
Color[] color = new Color[25];
Font[] font = new Font[25];

ستُملَئ هذه المصفوفات بقيم عشوائية. يَرسَم التابع draw()‎ -المسئول عن رَسْم الحاوية (canvas)- السَلاسِل النصية. تُرسَم كل سِلسِلة نصية i بإحداثيات نقطة تُساوِي (x,y)‎ وبلون يُساوِي color‎ وبنوع خط يُساوِي font‎. لم نَستخدِم كُلًا من dx و dy بالتابع draw()‎، وإنما سنَستخدِمهما أثناء تَحدِيث البيانات أثناء رَسْم الإطار التالي. يُمكِننا إذًا كتابة تعريف التابع draw()‎ كالتالي:

public void draw() {
    GraphicsContext g = canvas.getGraphicsContext2D();
    g.setFill(Color.WHITE); // (Fill with white, erasing previous picture.)
    g.fillRect(0,0,canvas.getWidth(),canvas.getHeight());
    for (int i = 0; i < 25; i++) {
       g.setFill( color[i] );
       g.setFont( font[i] );
       g.fillText( MESSAGE, x[i], y[i] );
       g.setStroke(Color.BLACK);
       g.strokeText( MESSAGE, x[i], y[i] );
    }
}

يَستخدِم هذا الأسلوب المصفوفات المتوازية (parallel arrays) حيث قُسِّمت بيانات رسالة واحدة بين عدة مصفوفات، ولكي تَرَى جميع البيانات المُتعلِّقة برسالة واحدة، ستحتاج إلى وَضْع المصفوفات إلى جانب بعضها كخطوط مُتوازية، وبحيث تَقَع العناصر برقم المَوضِع i ضِمْن تلك المصفوفات إلى جوار بعضها البعض. قد لا يُعدّ اِستخدَام المصفوفات المتوازية (parallel arrays) بهذا المثال البسيط خطأً فادحًا، ولكنها في العموم لا تَتَبِع الفلسفة كائنية التوجه (object-oriented) فيما يَتَعلَّق بتَخْزِين البيانات المُرتبطة معًا ضِمْن كائن واحد. إذا اتبعنا تلك القاعدة، فإننا لن نحتاج إلى تَخيُل العلاقة بين تلك البيانات؛ لأنها ستَكُون موجودة بمكان واحد بالفعل. سنُعرِّف الصَنْف StringData لهذا الغرض تحديدًا:

private static class StringData {  // Info needed to draw one string.
    double x,y;       // location of the string;
    double dx,dy;     // velocity of the string;
    Color color;      // color of the string;
    Font font;        // the font that is used to draw the string
}

سنَستخدِم مصفوفة من النوع StringData[]‎ لتَخْزِين بيانات النُسخ المختلفة من الرسالة النصية. تُصرِّح التَعْليمَة التالية عن مصفوفة stringData بهيئة مُتْغيِّر نسخة (instance variable):

StringData[] stringData;

ستَكُون قيمة المُتْغيِّر stringData فارغة بالبداية. ينبغي إذًا أن نُنِشئ مصفوفة ونملأها بالبيانات ثم نُسنِدها (assign) إليه. لاحِظ أن كل عنصر بتلك المصفوفة عبارة عن كائن (object) من النوع StringData، والذي ينبغي أن نُنشِئه قبل اِستخدَامه. يُنشِئ البرنامج الفرعي (subroutine) التالي المصفوفة ويملؤها ببيانات عشوائية:

private void createStringData() {
    stringData = new StringData[25];
    for (int i = 0; i < 25; i++) {
        stringData[i] = new StringData();
        stringData[i].x = canvas.getWidth() * Math.random();
        stringData[i].y = canvas.getHeight() * Math.random();
        stringData[i].dx = 1 + 3*Math.random();
        if (Math.random() < 0.5) // 50% chance that dx is negative
            stringData[i].dx = -stringData[i].dx;
        stringData[i].dy = 1 + 3*Math.random();
        if (Math.random() < 0.5) // 50% chance that dy is negative
            stringData[i].dy = -stringData[i].dy;
        stringData[i].color = Color.hsb( 360*Math.random(), 1.0, 1.0 );
        stringData[i].font = fonts[ (int)(5*Math.random()) ];
    }
}

يُستدعَى التابع (method) السابق داخل start()‎ وكذلك عندما يَنقُر المستخدم على الزر لإنشاء بيانات عشوائية جديدة. يُمكِننا الآن أن نَستخدِم حَلْقة تَكْرار for لرَسْم السلاسل النصية كالتالي:

for (int i = 0; i < 25; i++) {
    g.setFill( stringData[i].color );
    g.setFont( stringData[i].font );
    g.fillText( MESSAGE, stringData[i].x, stringData[i].y );
    g.setStroke(Color.BLACK);
    g.strokeText( MESSAGE, stringData[i].x, stringData[i].y );
}

أو قد نَستخدِم حَلْقة التَكْرار for-each لكي نَتَجنَّب التَعامُل مع فهارس المصفوفة (array indices) كالتالي:

for ( StringData data : stringData ) {
    g.setFill( data.color );
    g.setFont( data.font);
    g.fillText( MESSAGE, data.x, data.y );
    g.setStroke( Color.BLACK );
    g.strokeText( MESSAGE, data.x, data.y );
}

أثناء كل تَكْرار (iteration)، سيَحمِل المُتْغيِّر المُتحكِّم بالحلقة data نسخة من إحدى قيم المصفوفة. تلك القيمة عبارة عن مَرجِع (reference) إلى كائن (object) من النوع StringData المُتْضمِّن لمُتْغيِّرات النُسخة color و font و x و y.

ناقشنا التحريكات (animations) بالقسم الفرعي 6.3.5. يُمكِنك أيضًا الإطلاع على شيفرة البرنامج بالكامل بالملف RandomStringsWithArray.java لمعرفة طريقة تّنْفيِذ التحريكة (animation).

ينبغي أن يختار البرنامج RandomStringsWithArray نوع الخط المُستخدَم لرَسْم الرسالة عشوائيًا من خمسة خطوط محتملة. بالنسخة الأصلية من البرنامج، صَرَّحنا عن خمسة مُتْغيِّرات أسمائها هي font1 و font2 و font3 و font4 و font5 من النوع Font لتمثيل تلك الخطوط ثُمَّ اِستخدَمنا تَعْليمَة switch لاختيار إحدى تلك الخطوط عشوائيًا كالتالي:

Font randomFont;  // One of the 5 fonts, chosen at random.
int rand;         // A random integer in the range 0 to 4.

fontNum = (int)(Math.random() * 5);
switch (fontNum) {
   case 0:
      randomFont = font1;
      break;
   case 1:
      randomFont = font2;
      break;
   case 2:
      randomFont = font3;
      break;
   case 3:
      randomFont = font4;
      break;
   case 4:
      randomFont = font5;
      break;
}

سنُخزِّن الخطوط الخمسة داخل مصفوفة fonts بالنسخة الجديدة من البرنامج. صَرَّحنا عنها بهيئة مُتْغيِّر نسخة (instance variable) من النوع Font[]‎ كالتالي:

Font[] fonts;

اِستخدمنا الباني (constructor) للإنشاء الفعليّ للمصفوفة بصيغة مُصنَّفة النوع (array literal) -اُنظر القسم الفرعي 7.1.3- كالتالي:

fonts= new Font[] {
        Font.font("Times New Roman", FontWeight.BOLD, 20),
        Font.font("Arial", FontWeight.BOLD, FontPosture .ITALIC, 28),
        Font.font("Verdana", 32),
        Font.font(40),
        Font.font("Times New Roman", FontWeight.BOLD, FontPosture .ITALIC, 60)
};

يُسهِل ذلك من عملية الاختيار العشوائي لنوع الخط المُستخدَم كالتالي:

Font randomFont;  // One of the 5 fonts, chosen at random.
int fontIndex;    // A random number in the range 0 to 4.
fontIndex = (int)(Math.random() * 5);
randomFont = fonts[ fontIndex ];

وبذلك نَكُون قد اِستخدَمنا أسطر قليلة من الشيفرة بدلًا من تَعْليمَة switch. يُمكِننا حتى أن نَضُم تلك الأسطر إلى سطر واحد كالتالي:

Font randomFont = fonts[ (int)(Math.random() * 5) ];

يُعدّ ذلك تطبيقًا نمطيًا على المصفوفات (arrays). لاحِظ أن التَعْليمَة السابقة تَستخدِم خاصية الجَلْب العشوائي (random access) بمعنى اختيار فهرس مصفوفة (array index) بشكل عشوائي لجَلْب قيمة العنصر بذلك الفهرس مباشرةً.

لنَفْحَص مثالًا آخر: عادةً ما تُخزَّن الأشهر بهيئة أعداد صحيحة: 1 و 2 و 3 .. وحتى 12. ستحتاج أحيانًا إلى تَحْوِيل تلك الأعداد إلى أسماء الأشهر المناظرة: يناير و فبراير و .. وحتى ديسمبر. يُمكِننا ببساطة أن نَستخدِم مصفوفة لإجراء ذلك التَحْوِيل. تُصرِّح (declare) التَعْليمَة التالية عن المصفوفة وتُهيِئها (initialize) كالتالي:

static String[] monthName = { "January", "February", "March",
                              "April",   "May",      "June",
                              "July",    "August",   "September",
                              "October", "November", "December" };

إذا كان mnth عبارة عن مُتْغيِّر يَحمِل عددًا صحيحًا يتراوح بين 1 و 12، فإن monthName[mnth-1]‎ يُمثِل اسم الشهر المناظر. ينبغي أن نُضيِف "-1" لأن الأشهر مُرقَّمة بدءًا من 1 بينما عناصر المصفوفة (array elements) مُرقَّمة بدءًا من صفر.

المصفوفات الديناميكية (Dynamic Arrays)

لقد ناقشنا الكيفية التي يُمكِننا بها اِستخدَام المصفوفات المملوءة جزئيًا (partially full array) لتَخْزِين قائمة من اللاعبين داخل لعبة، بحيث تَكبُر تلك القائمة أو تَتقلَّص أثناء تَشْغِيل اللعبة. يُطلَق الاسم "ديناميكي (dynamic)" على أي قائمة (list) يُمكِن أن يَتَغيَّر حجمها أثناء تّنْفِيذ البرنامج. تُعدّ القوائم الديناميكية شائعة الاستخدام، لذلك ربما من الأفضل كتابة صَنْف (class) يُمثِل ذلك المفهوم، وهو ما سيُمكِّننا من تَجنُّب إعادة كتابة نفس ذات الشيفرة بكل مرة نُريد فيها اِستخدَام هيكل بياني (data structure) مُشابِه. سنُصمِّم إذًا شيئًا مشابهًا للمصفوفة باستثناء أن يَكُون حجمها (size) قابلًا للتَغيِير. فَكِر بالعمليات التي نحتاج إلى إِجرائها على مصفوفة ديناميكية (dynamic array)، مثلًا:

  • إضافة عنصر إلى نهاية المصفوفة
  • حَذْف عنصر بمَوِضْع معين ضِمْن المصفوفة
  • جَلْب قيمة إحدى عناصر المصفوفة
  • ضَبْط قيمة إحدى عناصر المصفوفة
  • جَلْب عدد العناصر الموجودة حاليًا بالمصفوفة

عندما نُصمِّم ذلك الصَنْف، ستُصبِح تلك العمليات توابع نسخة (instance methods) داخل الصَنْف. سنُطبِق أيضًا نمط المصفوفة المملوءة جزئيًا (partially full array) لتَخْزِين العناصر بالمصفوفة الديناميكية (dynamic) أي ستُخزَّن العناصر بالنهاية داخل مصفوفة عادية. علاوة على ذلك، لابُدّ أن نُقرِّر ما ينبغي أن يَحدُث عند محاولة جَلْب عنصر مصفوفة غَيْر موجود، فقد نُبلِّغ مثلًا عن اِعتراض من النوع ArrayIndexOutOfBoundsException. بِفْرَض كَوْن عناصر المصفوفة من النوع int، يُمكِننا كتابة الشيفرة التالية:

import java.util.Arrays;

/**
 * Represents a list of int values that can grow and shrink.
 */
public class DynamicArrayOfInt {

    private int[] items = new int[8];  // partially full array holding the ints
    private int itemCt;

    /**
     * Return the item at a given index in the array.  
     * Throws ArrayIndexOutOfBoundsException if the index is not valid.
     */
    public int get( int index ) {
        if ( index < 0 || index >= itemCt )
            throw new ArrayIndexOutOfBoundsException("Illegal index, " + index);
        return items[index];
    }

    /**
     * Set the value of the array element at a given index. 
     * Throws ArrayIndexOutOfBoundsException if the index is not valid.
     */
    public void set( int index, int item ) {
        if ( index < 0 || index >= itemCt )
            throw new ArrayIndexOutOfBoundsException("Illegal index, " + index);
        items[index] = item;
    }

    /**
     * Returns the number of items currently in the array.
     */
    public int size() {
        return itemCt;
    }

    /**
     * Adds a new item to the end of the array.  The size increases by one.
     */
    public void add(int item) {
        if (itemCt == items.length)
            items = Arrays.copyOf( items, 2*items.length );
        items[itemCt] = item;
        itemCt++;
    }

    /**
     * Removes the item at a given index in the array.  The size of the array
     * decreases by one.  Items following the removed item are moved up one
     * space in the array.
     * Throws ArrayIndexOutOfBoundsException if the index is not valid.
     */
    public void remove(int index) {
        if ( index < 0 || index >= itemCt )
            throw new ArrayIndexOutOfBoundsException("Illegal index, " + index);
        for (int j = index+1; j < itemCt; j++)
            items[j-1] = items[j];
        itemCt--;
    }

} // end class DynamicArrayOfInt

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

بالمثال ReverseInputNumbers.java، اِستخدَمنا مصفوفة مملوءة جزئيًا (partially full array) من النوع int لطباعة قائمة من الأعداد المُدْخَلة من قِبَل المُستخدِم بترتيب معاكس. اِستخدَمنا تحديدًا مصفوفة عادية طولها يُساوِي 100 لحَمْل الأعداد. في الواقع، قد يَكُون حجم المصفوفة كبيرًا جدًا أو صغيرًا جدًا مما قد يَتَسبَّب بحُدوث اِعترَاض (exception). نستطيع الآن إعادة كتابة البرنامج باستخدام الصَنْف DynamicArrayOfInt، والذي سيَتَكيَف مع أي عدد معقول من المُدْخَلات. يُمكِنك الإطلاع على شيفرته بالملف ReverseWithDynamicArray.java. على الرغم من أنه برنامج بسيط جدًا، لكن بإمكانك تطبيق نفس المبدأ بأي تطبيق آخر لا يُمكِنك تَوقُّع مقدار البيانات التي قد يحتاجها مقدمًا. تستطيع هياكل البيانات الديناميكية (dynamic data structure) عمومًا أن تَتَكيَف مع أي مقدار من البيانات، ولكن بالطبع بما يتناسب مع مساحة الذاكرة (memory) المُتاحة للبرنامج.

يُعدّ الصَنف بالأعلى مثالًا جيدًا نوعًا ما على المصفوفات الديناميكية، ولكنه يُعانِي من مشكلة صغيرة. لنَفْترِض مثلًا أننا نريد أن نُنشِئ مصفوفة ديناميكية من النوع String. نظرًا لأن الصَنْف DynamicArrayOfInt مُصمَّم للتَعامُل مع النوع int فقط، لا يُمكِننا بطبيعة الحال أن نَستخدِم كائنًا (object) منه لحَمْل سلاسل نصية (strings). يبدو أننا سنضطّر إذًا إلى كتابة صَنْف جديد DynamicArrayOfString. بالمثل، إذا أردنا أن نُنشِئ مصفوفة ديناميكية لتَخْزِين لاعبين من النوع Player، سنضطّر إلى كتابة صَنْف جديد DynamicArrayOfPlayer، وهكذا. يَعنِي ذلك ضرورة كتابة صَنْف جديد لكل نوع من البيانات، وهو ما لا يُمكِن أن يَكُون أمرًا منطقيًا. في الواقع، توفِّر الجافا الصَنْف القياسي ArrayList كحلّ لتلك المشكلة حيث يُمثِل مصفوفة ديناميكية (dynamic arrays) يُمكِنها التَعامُل مع أي نوع من البيانات.

ترجمة -بتصرّف- للقسم Section 2: Array Processing من فصل 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.


×
×
  • أضف...