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

مفهوم المصفوفات الديناميكية (ArrayLists) في جافا


رضوى العربي

يُمكِننا أن نُضمِّن نمط المصفوفة الديناميكية (dynamic array) داخل صَنْف كما رأينا بالقسم الفرعي 7.2.4، ولكن بدا الأمر كما لو أننا سنحتاج إلى تعريف صَنْف مختلف لكل نوع من البيانات (data type). في الحقيقة، تُوفِّر جافا خاصية تُعرَف باسم "الأنواع ذات المعاملات غَيْر مُحدَّدة النوع (parameterized types)"، والتي يُمكِنها أن تُجنِّبنا مشكلة تعدد الأصناف كما تُوفِّر جافا الصَنْف ArrayList الذي يُنفِّذ (implement) نمط المصفوفة الديناميكية لجميع أنواع البيانات.

الصنف ArrayList والأنواع ذات المعاملات غير محددة النوع (Parameterized Types)

تُوفِّر جافا النوع القياسي ArrayList<String>‎ لتمثيل مصفوفة ديناميكية (dynamic arrays) من النوع String. وبالمثل، يُمثِل النوع ArrayList<Button>‎ مصفوفة ديناميكية من النوع Button. بنفس الكيفية، إذا كان Player صنفًا (class) يُمثِل اللاعبين ضِمْن لعبة، فإن النوع ArrayList<Player>‎ يُمثِل مصفوفة ديناميكية من النوع Player.

قد يبدو الأمر كما لو أننا نَستخدِم أصنافًا كثيرة، ولكن، في الواقع، هنالك صَنْف (class) واحد فقط هو الصَنْف ArrayList المُعرَّف بحزمة java.util. يُعدّ ذلك الصَنْف صَنْفًا ذي مُعامِلات غَيْر مُحدَّدة النوع (parameterized type) -نوع يُمكِنه أن يَأخُذ معامل نوع (type parameter)-. يُمكِننا ذلك من اِستخدَام صَنْف واحد فقط للحصول على عدة أنواع مثل ArrayList<String>‎ و ArrayList<Button>‎ وحتى ArrayList<T>‎. لابُدّ أن يَكُون معامل النوع T نوعًا كائنيًا (object type) أي اسم صَنْف (class) أو اسم واجهة (interface)، ولا يُمكِنه أن يَكُون نوعًا أساسيًا (primitive type). يعني ذلك أنك لا تستطيع الحصول على ArrayList من النوع int أو ArrayList من النوع char.

يُمكِننا مثلًا أن نَستخدِم النوع ArrayList<String>‎ للتّصرِيح (declare) عن مُتْغيِّر كالتالي:

ArrayList<String> namelist;

أو قد نَستخدِمه كنوع لمعامل صوري (formal parameter) ضِمْن تعريف برنامج فرعي (subroutine) أو كنوع مُعاد (return type) من برنامج فرعي. علاوة على ذلك، قد نَستخدِمه مع العامل new لإنشاء كائنات (objects):

namelist = new ArrayList<String>();

يَكُون الكائن المُنشَئ في هذه الحالة من النوع ArrayList<String>‎، ويُمثِل قائمة ديناميكية من السَلاسِل النصية (strings). يُوفِّر الصَنْف مجموعة من توابع النُسخ (instance methods) مثل التابع namelist.add(str)‎ لإضافة سِلسِلة نصية من النوع String إلى القائمة، والتابع namelist.get(i)‎ لجَلْب السِلسِلة النصية الموجود برقم الفهرس i، والتابع namelist.size()‎ لحِسَاب عدد العناصر الموجودة بالقائمة حاليًا.

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

ArrayList<Player> playerList = new ArrayList<Player>();

والآن، يُمكِنك أن تَستخدِم playerList.add(plr)‎ لإضافة لاعب plr إلى اللعبة أو قد تَستخدِم playerList.remove(k)‎ لحَذْف اللاعب الموجود برقم الفهرس k.

علاوة على ذلك، إذا كان playerList مُتْغيِّرًا محليًا (local variable)، يُمكِنك أن تَستخدِم صيغة التّصرِيح (declaration) المختصرة -اُنظر القسم الفرعي 4.8.2- كما يلي:

var playlerList = new ArrayList<Player>();

يَعتمِد مُصرِّف (compiler) الجافا على القيمة المبدئية المُسنَدة إلى playerList لاستنتاج أن نوعه هو ArrayList<Player>‎.

عندما تَستخدِم نوعًا (type) مثل ArrayList<T>‎، فإن المُصرِّف يتأكد من أن جميع الكائنات (objects) المضافة إليه من النوع T، وستُعدّ أي محاولة لإضافة كائن (object) من نوع آخر خطأً في بناء الجملة (syntax error)، ولا يُصرَّف عندها البرنامج. لمّا كانت الكائنات المنتمية لصَنْف فرعي من T هي بالنهاية من النوع T، فإنه من المُمكن إضافتها أيضًا إلى القائمة. فمثلًا، يُمكِن لمُتْغيِّر من النوع ArrayList<Pane>‎ أن يَحمِل كائنات من النوع BorderPane أو TilePane أو GridPane أو أي صنف فرعي (subclass) آخر من الصَنْف Pane (في الواقع، يُشبِه ذلك طريقة عمل المصفوفات حيث يستطيع كائن من النوع T[]‎ أن يَحمِل أي كائنات تنتمي لصَنْف فرعي من T). بالمثل، إذا كان T عبارة عن واجهة (interface)، فمن الممكن إضافة أي كائن (objects) إلى القائمة طالما كان يُنفِّذ (implement) تلك الواجهة T.

تَملُك الكائنات من النوع ArrayList<T>‎ جميع توابع النسخ (instance methods) التي قد تَتَوقَّعها من مصفوفة ديناميكية (dynamic array). بِفَرْض أن list عبارة عن مُتْغيِّر من النوع ArrayList<T>‎، اُنظر التوابع التالية:

  • list.size()‎: يُعيد حجم القائمة أي عدد العناصر الموجودة بها حاليًا. قد يَكون حجم القائمة مُساويًا للصفر. فمثلًا، يُنشِئ باني الكائن الافتراضي new ArrayList<T>()‎ قائمة حجمها يُساوي صفر، وفي العموم، تتراوح أرقام المواضع الصالحة من 0 وحتى list.size()-1.

  • list.add(obj)‎: يُضيِف كائنًا (object) إلى نهاية القائمة مع زيادة حجمها بمقدار الواحد. لاحِظ أن المُعامل obj لابُد أن يَكُون كائنًا من النوع T أو قد يَكُون فارغًا.

  • list.get(N)‎: يَستقبِل المُعامل N الذي لابُدّ أن يكون عددًا صحيحًا (integer) يتراوح من 0 إلى list.size()-1 ثُمَّ يُعيد القيمة المُخزَّنة بالمَوْضِع N، والتي بطبيعة الحال تَكُون من النوع T. إذا كان N خارج النطاق المسموح به، يَقَع اعتراض من النوع IndexOutOfBoundsException. في الواقع، تُشبه تلك الدالة الأمر A[N]‎ -بِفَرْض أن A عبارة عن مصفوفة- بفارق أنه لا يُمكِن اِستخدَام list.get(N)‎ بالجانب الأيسر من أي تَعْليمَة إِسْناد (assignment statement).

  • list.set(N, obj)‎: يُسنِد الكائن obj إلى عنصر المصفوفة بالمَوْضِع N أي يَحلّ ذلك الكائن محلّ الكائن المُخزَّن مُسْبَقًا بذلك الموضع. لابُدّ أن يَكُون المُعامل obj من النوع T كما ينبغي أن يَكُون N عددًا صحيحًا (integer) يتراوح بين 0 و list.size()-1. في الواقع، تُشبِه تلك الدالة الأمر A[N] = obj بِفَرْض أن A عبارة عن مصفوفة.

  • list.clear()‎: يحذِف جميع العناصر الموجودة بالقائمة، ويَضبُط حجمها إلى الصفر.

  • list.remove(N)‎: يَحذِف العنصر الموجود بالمَوْضِع N من القائمة، ويُنقِص حجمها بمقدار الواحد كما يَنقِل العناصر الموجودة بَعْد العنصر المحذوف مَوْضِعًا للأعلى. لاحِظ أن المُعامل N لابُدّ أن يَكُون عددًا صحيحًا (integer) يتراوح بين 0 و list.size()-1.

  • list.remove(obj)‎: يَحذِف الكائن المُمرَّر من القائمة إذا كان موجودًا بها، ويُنقِص حجمها بمقدار الواحد كما يَنقِل العناصر الموجودة بَعْد العنصر المحذوف مَوضِعًا للأعلى. لاحِظ أنه في حالة وجود الكائن obj أكثر من مرة ضِمْن القائمة، فإنه يَحذِف أول حدوث له فقط أما إذا لم يَكُن موجودًا، فإنه لا يَفعَل أي شيء أي لا يُعدّ ذلك بمثابة خطأ.

  • list.indexOf(obj)‎: يَبحَث عن الكائن obj داخل القائمة، ويُعيد أول مَوْضِع لحدوثه إذا كان موجودًا أما إذا لم يَكُن موجودًا، فإنه يُعيِد العدد -1.

ملحوظة: يَستخدِم آخر تابعين الاستدعاء obj.equals(item)‎ لموازنة obj -لو لم يَكُن obj فارغًا- مع عناصر القائمة أي أنهما يَفْحَصا تَساوِي السَلاسِل النصية بِفْحَص محتوياتها وليس مَواضِعها بالذاكرة.

تُوفِّر جافا أصنافًا كثيرة ذات مُعاملات غَيْر مُحدَّدة النوع (parameterized classes) لتمثيل هياكل البيانات المختلفة (data structure)، وتُشكِّل تلك الأصناف إطار جافا للتجميعات (Java Collection Framework). لقد ناقشنا هنا الصَنْف ArrayList فقط، ولكننا سنُعود لمناقشة هذا الموضوع على نحو أكثر تفصيلًا بالفصل 10.

ملحوظة: يُستخدَم الصَنْف ArrayList أيضًا بشكل عادي بدون مُعاملات غَيْر مُحدَّدة النوع (non-parametrized) أي يُمكنِنا أن نُصرِّح (declare) عن مُتْغيرِّات، ونُنشِئ كائنات من النوع ArrayList كالتالي:

ArrayList list = new ArrayList();

يُكافِئ ذلك التّصرِيح عن مُتْغيٍّر list من النوع ArrayList<Object>‎. يَعنِي ذلك أن list يُمكِنه أن يَحمِل أي كائن ينتمي إلى صَنْف فرعي من Object. ولأن أي صَنْف هو بالنهاية صَنْف فرعي (subclass) من Object، فإنه من الممكن إضافة أي كائن (object) إلى list.

الأصناف المُغلِّفة (Wrapper Classes)

كما أوضحنا مُسْبَقًا، لا تتوافق الأنواع ذات المعاملات غير محدَّدة النوع (parameterized types) مع الأنواع الأساسية (primitive types) أي لا يُمكِنك مثلًا أن تَستخدِم شيئًا مثل ArrayList<int>‎. مع ذلك، نظرًا لتوفُّر أصناف مُغلِّفة (wrapper classes) مثل Integer و Character، فلربما الأمر ليس مُقيَدًا تمامًا.

لقد تَعرضنا بالقسم 2.5 للأصناف Double و Integer. تُعرِّف تلك الأصناف التوابع الساكنة Double.parseDouble()‎ و Integer.parseInteger()‎ المُستخدَمة لتَحْوِيل سِلسِلة نصية (string) إلى قيمة عددية. تَتَضمَّن تلك الأصناف أيضًا ثوابتًا (constants) مثل Integer.MAX_VALUE و Double.NaN. لقد تَعرضنا أيضًا للصَنْف Character ببعض الأمثلة. يُعرِّف ذلك الصَنْف التابع الساكن Character.isLetter()‎ المُستخدَم لفْحَص ما إذا كانت قيمة معينة من النوع char عبارة عن حرف أبجدي (letter) أم لا. تَتوفَّر في الواقع أصناف مشابهة لكل نوع أساسي (primitive type) مثل Long و Short و Byte و Float و Boolean. تُعدّ جميع تلك الأصناف أصنافًا مُغلِّفة (wrapper classes)، وعلى الرغم من أنها تَحوِي بعض الأعضاء الساكنة (static members) المفيدة عمومًا، فإنها تُستخدَم أيضًا لغرض آخر: تَمثيِل الأنواع الأساسية ككائنات (objects).

تذكَّر دومًا أن الأنواع الأساسية ليست أصنافًا (classes)، وأن قيم تلك الأنواع ليست كائنات (objects). مع ذلك، قد يَكُون من المفيد أحيانًا التَعامُل مع قيمة من نوع أساسي كما لو كانت كائنًا (object)، فمثلًا قد نرغب بتَخْزِين قيمة من نوع أساسي (primitive type) داخل قائمة من النوع ArrayList. لا نستطيع في الواقع فِعِل ذلك حرفيًا، ولكن يُمكِننا التَحايُل على ذلك قليلًا بتَغْليف (wrap) تلك القيمة داخل كائن ينتمي إلى صَنْف مُغلِّف (wrapper classes).

على سبيل المثال، يحتوي أي كائن من النوع Double على مُتْغيِّر نُسخة (instance variable) وحيد من النوع double. يَعمَل ذلك الكائن بمثابة مُغلِّف (wrapper) للقيمة العددية. يُمكِنك إذًا أن تُنشِئ كائنًا (object) لتَغليف قيمة من النوع double مثل 6.0221415e23 كالتالي:

Double d = new Double(6.0221415e23);

في الواقع، يَحتوِي d على نفس المعلومات التي تَحتوِيها قيمة من النوع double، ولكنه كائن (object). يُمكِنك أن تَستدِعي d.doubleValue()‎ لاسترجاع القيمة العددية المُغلَّفة داخل الكائن. بالمثل، يُمكِنك أن تُغلِّف قيمة من النوع int ضِمْن كائن من النوع Integer أو قيمة من النوع boolean ضِمْن كائن من النوع Boolean، وهكذا.

إذا كنت تريد أن تُنشِئ كائنًًا من النوع Double لتَغْلِيف قيمة عددية x، يُفضَّل أن تَستخدِم التابع الساكن Double.valueOf(x)‎ بدلًا من أن تَستدعِي الباني new Double(x)‎؛ لأن استدعاء التابع Double.valueOf()‎ بنفس قيمة المُعامل (parameter) أكثر من مرة دائمًا ما يُعيِد نفس ذات الكائن. تحديدًا، بَعْد أول استدعاء لذلك التابع بقيمة مُعامِل معينة، فإنه سيُعيد نفس ذلك الكائن بالاستدعاءات المتتالية لنفس قيمة المُعامل. لا يُعدّ ذلك مشكلة كما قد تَظّن؛ لأن الكائنات من النوع Double غَيْر قابلة للتَعْدِيل (immutable) أي أن الكائنات التي لها نفس القيمة المبدئية ستَظِلّ دائمًا متطابقة. يُعدّ التابع Double.valueOf تابعًا مُصنِّعًا (factory method) مثل تلك التوابع التي تَعرَّضنا لها بالقسم 6.2 أثناء تَعامُلنا مع الصَنْفين Color و Font، وتحتوي جميع الأصناف المُغلِّفة عمومًا على تابع مُصنِّع مشابه مثل Character.valueOf(ch)‎ و Integer.valueOf(n)‎.

يَتَوفَّر أيضًا تحويل أتوماتيكي بين كل نوع أساسي (primitive type) وصَنْفه المُغلِّف (wrapper class) لتسهيل الاِستخدَام. على سبيل المثال، إذا كنت تَستخدِم قيمة من النوع int ضِمْن سياق يَتَطلَّب كائنًا (object) من النوع Integer، يُمكِنك أن تُغلِّف تلك القيمة ضِمْن كائن من النوع Integer أتوماتيكيًا كالتالي:

Integer answer = 42;

سيقرأ الحاسوب التَعْليمَة بالأعلى كما لو كانت مَكْتُوبة كالتالي:

Integer answer = Integer.valueOf(42);

يُطلَق على ذلك اسم "التغليف الأوتوماتيكي (autoboxing)"، ويَعمَل من الجهة الآخرى أيضًا. فمثلًا، إذا كان d يُشير إلى كائن من النوع Double، يُمكِنك أن تَستخدِم d بتعبير عددي (numerical expression) مثل 2*d. تُفَك (unboxing) القيمة العددية داخل d أتوماتيكيًا، ويُحسَب حاصل ضربها مع العدد 2. كثيرًا ما يَسمَح لك التَغْلِيف الأتوماتيكي (autoboxing) وفكه بتَجاهُل الفارق بين الأنواع الأساسية (primitive types) والكائنات (objects).

يُعدّ ما سبق صحيحًا فيما يَتَعلَّق بالأنواع ذات المعاملات غَيْر مُحدَّدة النوع (parameterized types). على الرغم من عدم توفُّر النوع ArrayList<int>‎، يَتَوفَّر ArrayList<Integer>‎ حيث تَحمِل مصفوفة من ذلك النوع كائنات من النوع Integer التي تُمثِل مُجرّد قيمة من النوع int ضِمْن مُغلِّف صغير. لنَفْترِض أن لدينا كائن (object) من النوع ArrayList<Integer>‎، اُنظر التالي:

ArrayList<Integer> integerList;
integerList = new ArrayList<Integer>();

يُمكِننا الآن أن نُضيف كائنًا مُمثلًا للعدد 42 مثلًا إلى integerList كالتالي:

integerList.add( Integer.valueOf(42) );

أو قد نعتمد على التَغْلِيف الأتوماتيكي (autoboxing) كالتالي:

integerList.add( 42 );

سيُغلِّف المُصرِّف (compiler) العدد 42 أتوماتيكيًا داخل كائن (object) من النوع Integer قبل إضافته إلى القائمة. بالمثل، يُمكِننا كتابة التالي:

int num = integerList.get(3);

يُعيد التابع integerList.get(3)‎ قيمة من النوع Integer، ولكن بِفَضل خاصية فك التَغْلِيف (unboxing)، سيُحوِّل المُصرِّف (compiler) القيمة المُعادة (return value) أتوماتيكيًا إلى قيمة من النوع int كما لو كنا قد كتبنا ما يلي:

int num = integerList.get(3).intValue();

نستطيع إذًا أن نَستخدِم المصفوفة integerList كما لو كانت مصفوفة ديناميكية (dynamic array) من النوع int لا من النوع Integer. وفي العموم، يَنطبِق نفس الأمر على القوائم (lists) من الأصناف المُغلِّفة الآخرى مثل ArrayList<Double>‎ و ArrayList<Character>‎. يَتَبقَّى لنا مشكلة أخيرة: يُمكِن لأي قائمة أن تَحمِل القيمة null، ولكن لا تَتَوفَّر قيمة من النوع الأساسي (primitive type) مناظرة للقيمة null. وبالتالي، إذا أعاد استدعاء مثل integerList.get(3)‎ ضِمْن التَعْليمَة int num = integerList.get(3);‎ القيمة null، سيَحدُث اِعتراض مُتعلِّق بوجود مُؤشر فارغ (null pointer). ينبغي إذًا أن تَأخُذ ذلك بالحسبان إذا لم تَكُن متأكدًا بشأن وجود قيم فارغة ضِمْن القائمة.

البرمجة باستخدام ArrayList

كمثال بسيط، سنُعيِد كتابة البرنامج ReverseWithDynamicArray.java من القسم السابق باِستخدَام النوع ArrayList بدلًا من الصَنْف المُخصَّص (custom) الذي كنا قد كتبناه. نظرًا لأننا سنُخزِّن أعدادً صحيحة (integers) ضِمْن القائمة، سنَستخدِم إذًا النوع ArrayList<Integer>‎. اُنظر شيفرة البرنامج بالكامل:

import textio.TextIO;
import java.util.ArrayList;

public class ReverseWithArrayList {

    public static void main(String[] args) {
        ArrayList<Integer> list;
        list = new ArrayList<Integer>();
        System.out.println("Enter some non-zero integers.  Enter 0 to end.");
        while (true) {
            System.out.print("? ");
            int number = TextIO.getlnInt();
            if (number == 0)
                break;
            list.add(number);
        }
        System.out.println();
        System.out.println("Your numbers in reverse are:");
        for (int i = list.size() - 1; i >= 0; i--) {
            System.out.printf("%10d%n", list.get(i));
        }
    }

}

كما أوضحنا مُسْبَقًا، عادةً ما تُستخدَم حَلْقة التَكْرار for لمعالجة المصفوفات الديناميكية من النوع ArrayList بنفس طريقة مُعالجة المصفوفات العادية. على سبيل المثال، تَطبَع حَلْقة التَكْرار (loop) التالية جميع العناصر بالمصفوفة namelist من النوع ArrayList<String>‎:

for ( int i = 0; i < namelist.size(); i++ ) {
    String item = namelist.get(i);
    System.out.println(item);
}

يُمكِنك أيضًا أن تَستخدِم حَلْقة التَكْرار for-each مع المصفوفات من النوع ArrayList كالتالي:

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

عند التَعامُل مع الأصناف المُغلِّفة (wrapper classes)، يُمكن للمُتْغيِّر المُتحكِّم بحَلْقة التَكْرار for-each أن يَكُون من النوع الأساسي (primitive type) بفضل خاصية فك التغليف (unboxing). على سبيل المثال، إذا كان numbers مُتْغيِّر من النوع ArrayList<Double>‎، يُمكِنك أن تَستخدِم حَلْقة التَكْرار التالية لحِسَاب حاصل مجموع القيم بالقائمة:

double sum = 0;
for ( double num : numbers ) {
   sum = sum + num;
}

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

double sum;
for ( Double num : numbers ) {
    if ( num != null ) {
        // يتم فك التغليف للحصول على قيمة‫ double
        sum = sum + num;  
    }
}

سنَفْحَص الآن البرنامج SimplePaint2.java الذي يُعدّ نسخة مُحسَّنة من البرنامج SimplePaint.java الذي كتبناه بالقسم الفرعي 6.3.3. بالبرنامج الجديد، يستطيع المُستخدِم أن يَرسِم منحنيات داخل مساحة رسم (drawing area) بالنَقْر على زر الفأرة والسَحب كما يستطيع أن يختار كُلًا من اللون المُستخدَم للرسم ولون خلفية مساحة الرسم من قائمة. تحتوي أيضًا قائمة "Control" على عدة أوامر منها: الأمر "Undo" المُستخدَم لحَذْف آخر منحنى رسمه المُستخدِم على الشاشة، والأمر "Clear" المُستخدَم لحَذْف جميع المنحنيات. يَتَوفَّر أيضًا مربع الاختيار "Use Symmetry" المسئول عن تَفْعِيل خاصية التماثلية أو تَعْطِيلها. تَنعكِس المنحنيات التي يَرسِمها المُستخدِم أثناء تَفْعِيل تلك الخاصية أفقيًا ورأسيًا لتُنتِج نمطًا متماثلًا.

بخلاف البرنامج الأصلي SimplePaint، تَستخدِم النسخة الجديدة هيكلًا بيانيًا (data structure) لتَخْزِين عدة معلومات عما رَسمه المُستخدِم. عندما يختار المُستخدِم لون خلفية جديد، تُملَئ الحاوية (canvas) بذلك اللون ثم يُعاد رَسْم جميع المنحنيات مجددًا على الخلفية الجديدة، ولذلك لابُدّ أن نُخزِّن كل المعلومات اللازمة لإعادة رَسْم تلك المنحنيات. بالمثل، عندما يختار المُستخدِم الأمر "Undo"، تُحَذَف بيانات آخر منحنى رسمه المُستخدِم ثُمَّ يُعاد رَسْم الصورة بالكامل مُجددًا بالاعتماد على البيانات المُتبقية.

سنعتمد في العموم على الهيكل البياني ArrayList لتَخْزِين تلك البيانات. تَتَكوَّن البيانات الأساسية لأي منحنى من قائمة من النقط الواقعة على المنحنى، والتي سنُخزِّنها بكائن من النوع ArrayList<Point2D>‎. لاحِظ أن الصَنْف Point2D هو صَنْف قياسي (standard) مُعرَّف بحزمة javafx.geometry، ويُمكِننا أن نُنشِئ كائنًا منه باِستخدَام قيمتين من النوع double يُمثِلان الإحداثي x والإحداثي y. يَمتلك أي كائن من النوع Point2D تابعي جَلْب pt.getX()‎ و pt.getY()‎ لاسترجاع قيمة x و y. إلى جانب ذلك، سنحتاج إلى مَعرِفة لون المنحنى، وما إذا كان ينبغي تطبيق خاصية التماثلية عليه أم لا. سنُخزِّن جميع البيانات المطلوبة لرَسْم منحنى ضِمْن كائن من النوع CurveData، والذي سنُعرِّفه كصَنْف مُتْدِاخل (nested class) بالبرنامج:

private static class CurveData {
   Color color;  // لون المنحنى
   boolean symmetric;  // خاصية التماثلية
   ArrayList<Point2D> points;  // نقاط المنحنى
}

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

ArrayList<CurveData> curves = new ArrayList<CurveData>();

لدينا هنا قائمة من الكائنات (objects)، ويَحتوِي كل كائن منها على قائمة من النقط كجزء من البيانات المُعرَّفة بداخله. سنَفْحَص الآن عدة أمثلة على معالجة ذلك الهيكل البياني (data structure). أولًا، عندما يَنقُر المُستخدِم بزر الفأرة على مساحة الرسم (drawing surface)، يَعنِي ذلك أنه يَرسِم منحنى جديدًا، ولذلك ينبغي أن نُنشِئ كائنًا جديدًا من النوع CurveData، ونُهيئ جميع مُتْغيِّرات النُسخ (instance variables) المُعرَّفة بداخله. بَفْرَض أن currentCurve هو مُتْغيِّر عام (global) من النوع CurveData، يُمكِننا تعريف البرنامج mousePressed()‎ كالتالي:

currentCurve = new CurveData();       // ‫أنشئ كائن من النوع CurveData

// ‫يُنسخ اللون المستخدم لرسم المنحنى من متغير النسخة الممثل للون 
// المستخدم للرسم
currentCurve.color = currentColor;    

// ‫تنسخ خاصية التماثلية أيضًا من القيمة الحالية للمتغير useSymmetry
currentCurve.symmetric = useSymmetry; 

currentCurve.points = new ArrayList<Point2D>();  // A new point list object.

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

curves.add( currentCurve );

عندما يُغيِّر المُستخدِم لون الخلفية أو يختار الأمر "Undo"، ينبغي أن نُعيِد رَسْم الصورة. يَحتوِي البرنامج على التابع redraw()‎ المسئول عن إعادة رَسْم الصورة بالكامل. يَعتمِد التابع على البيانات الموجودة بالمُتْغيِّر curves لإعادة رَسْم جميع المنحنيات، ويَستخدِم حَلْقة تَكْرار for-each لمعالجة بيانات كل منحنى على حدى كالتالي:

for ( CurveData curve : curves ) {
   .
       // ‫ارسم المنحنى الذي يمثله الكائن curve من النوع CurveData
   .  
}

لاحِظ أن curve.points عبارة عن مُتْغيِّر من النوع ArrayList<Point2D>‎ أي عبارة عن قائمة من النقط الواقعة على المنحنى. يُمكِننا على سبيل المثال أن نَسترجِع النقطة i على المنحنى باستدعاء التابع get()‎ المُعرَّف بالقائمة curve.points.get(i)‎. يُعيِد ذلك الاستدعاء قيمة من النوع Point2D التي تُعرِّف بدورها توابع الجَلْب getX()‎ و getY()‎.

يُمكِننا إذًا أن نُشير إلى الإحداثي x للنقطة i على المنحنى مباشرةً كالتالي:

curve.points.get(i).getX()

قد يبدو الاستدعاء السابق مُعقدًا، ولكنه مثال جيد على الأسماء المركبة (complex names). يُخصِّص ذلك الاسم مسارًا إلى جزء من البيانات: اذهب إلى الكائن curve. بداخل ذلك الكائن، إذهب إلى points. بداخل points، إذهب إلى العنصر برقم المَوْضِع i. من هذا العنصر، اِسترجِع الإحداثي x من خلال استدعاء تابعه getX()‎. اُنظر تعريف التابع redraw()‎ بالكامل:

private void redraw() {
    g.setFill(backgroundColor);
    g.fillRect(0,0,canvas.getWidth(),canvas.getHeight());
    for ( CurveData curve : curves ) {
        g.setStroke(curve.color);
        for (int i = 1; i < curve.points.size(); i++) {
            // ‫ارسم قطعة مستقيمة من رقم الموضع i-1 إلى رقم الموضع i
            double x1 = curve.points.get(i-1).getX();
            double y1 = curve.points.get(i-1).getY();
            double x2 = curve.points.get(i).getX();
            double y2 = curve.points.get(i).getY();
            drawSegment(curve.symmetric,x1,y1,x2,y2);
        }
    }
}

يَرسِم التابع drawSegment()‎ قطعة مستقيمة من (x1,y1) إلى (x2,y2). بالإضافة إلى ذلك، إذا كان المُعامل (parameter) الأول يُساوِي true، فإنه أيضًا يَرسِم انعكاسًا أفقيًا ورأسيًا لتلك القطعة.

لقد رَكَّزنا هنا على اِستخدَام الصَنْف ArrayList ضِمْن البرنامج، ولكن يُفضَّل بالطبع أن تَتَطلِع على الشيفرة الكاملة للبرنامج SimplePaint2.java. بالإضافة إلى كَوْنه مثال على اِستخدَام الأنواع ذات المُعاملات غَيْر محدَّدة النوع (parameterized types)، فإنه يُعدّ أيضًا مثالًا جيدًا على إنشاء القوائم (menus) واِستخدَامها. ينبغي عمومًا أن تَكُون قادرًا على فهم البرنامج بالكامل.

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


×
×
  • أضف...