مدخل إلى جافا السلاسل النصية String والأصناف Class والكائنات Object والبرامج الفرعية Subroutine في جافا


رضوى العربي

تحدثنا بالقسم السابق عن الأنواع الأساسية (primitive types) الثمانية بالإضافة إلى النوع String. هنالك فارق جوهري بينهما، وهو أن القيم من النوع String عبارة عن كائنات (objects). على الرغم من أننا لن نناقش الكائنات تفصيليًا حتى نَصِل إلى الفصل الخامس، فما يزال من المفيد أن نَطَّلِع قليلًا عليها وعلى مفهوم الأصناف (classes) المُرتبِط بها إلى حد كبير. لن يُمكِّننا استيعاب مفاهيم أساسية كالكائنات (objects) والأصناف (classes) على اِستخدَام السَلاسِل النصية من النوع String فقط، وإنما سيَفتَح لنا الباب لاستيعاب مفاهيم برمجية آخرى مُهِمّة مثل البرامج الفرعية (subroutines).

الدوال (functions) والبرامج الفرعية (subroutines) المبنية مسبقًا

تَذَكَّر أن البرنامج الفرعي (subroutine) ما هو إلا مجموعة من التَعْليمَات (instructions) مُضمَّنة معًا تحت اسم معين، ومُصمَّمة لتكون مسئولة عن إنجاز مُهِمّة واحدة مُحدَّدة. سنتعلَّم أسلوب كتابة البرامج الفرعية (subroutines) بالفصل الرابع، ومع ذلك يُمكِنك أن تُنجز الكثير فقط باستدعاء البرامج الفرعية التي كُتبَت بالفعل بواسطة مبرمجين آخرين. يُمكِنك أن تَستدعِي برنامجًا فرعيًا ليُنجز المُهِمّة المُوكَلة إليه باِستخدَام تَعْليمَة استدعاء برنامج فرعي (subroutine call statement). يُعرَّف أي برنامج فرعي بالجافا ضِمْن صَنْف (class) أو كائن (object)، وتَتَضمَّن بعض الأصناف القياسية بلغة الجافا برامجًا فرعية مُعرَّفة مُسْبَقًا يُمكِنك اِستخدَامها. فمثلًا، تحتوي القيم من النوع String التي هي عبارة عن كائن على برامج فرعية مبنية مُسْبَقًا يُمكِنها معالجة السَلاسِل النصية (strings)، والتي يُمكِنك أن تَستدعِيها دون فهم طريقة كتابتها أو كيفية عملها. في الواقع، هذا هو الغرض الأساسي منها: أي برنامج فرعي هو صندوق أسود (black box) يُمكِن اِستخدَامه بدون مَعرِفة ما يحدث داخله.

لنَفْحَص أولًا البرامج الفرعية (subroutines) التي تُعدّ جزءًا من صَنْف (class). يُستخدَم أي صَنْف عمومًا لتجميع بعض المُتْغيِّرات (variables) والبرامج الفرعية (subroutines) المُعرَّفة بذلك الصنف معًا، ويُعرَف كلاهما باسم "أعضاء الصَنْف الساكنة (static members)". لقد رأينا مثالًا لذلك بالفعل: إذا كان لدينا صنف يُعرِّف برنامجًا، فإن البرنامج main()‎ هو عضو ساكن (static member) بذلك الصَنْف. ينبغي أن تُضيف الكلمة المحجوزة static عندما تُعرِّف عضو ساكن مثل الكلمة static بالتَصْرِيح public static void main...‎.

عندما يحتوي صنف (class) معين على مُتْغيِّر أو برنامج فرعي ساكن (static)، يُعدّ اسم الصنف جزءًا من الاسم الكامل لذلك المُتْغيِّر أو لذلك البرنامج الفرعي. على سبيل المثال، يحتوي الصنف القياسي System على البرنامج الفرعي exit لذا يُمكِنك أن تُشير إليه باستخدام الاسم System.exit والذي يَتَكوَّن من كُلًا من اسم الصنف المُتْضمِّن للبرنامج الفرعي متبوعًا بنقطة ثم باسم البرنامج الفرعي نفسه. يَستقبِل البرنامج الفرعي exit مُعامِلًا من النوع int، لذلك يُمكِنك اِستخدَامه بكتابة تَعْليمَة استدعاء برنامج فرعي (subroutine call statement) كالتالي:

System.exit(0);

يُنهِي الاستدعاء System.exit البرنامج ويُغلِق آلة جافا الافتراضية (Java Virtual Machine)، لذا يُمكِنك أن تَستخدِمه إذا كنت تُريد إنهاء البرنامج قبل نهاية البرنامج main()‎. تُشير قيمة المُعامِل المُمرَّرة إلى سبب إغلاق البرنامج، فإذا كانت تُساوِي القيمة ٠، يَعنِي ذلك أن البرنامج قد انتهى بشكل طبيعي. في المقابل، تَعنِي أي قيمة آخرى انتهاء البرنامج لوجود خطأ مثل الاستدعاء System.exit(1)‎. تُرسَل قيمة المُعامِل المُمرَّرة إلى نظام التشغيل والذي عادةً ما يتجاهلها.

يُعدّ الصَنْف System واحدًا فقط من ضِمْن مجموعة من الأصناف القياسية التي تأتي مع الجافا. كمثال آخر، يَتَضمَّن الصنف Math مُتْغيِّرات ساكنة (static variables) مثل Math.PI و Math.E والتي قيمها هي الثوابت الرياضية π و e على الترتيب كما يُعرِّف عددًا كبيرًا من الدوال (functions) الحسابية. يُنفِّذ أي برنامج فرعي (subroutine) في العموم مُهِمّة مُحدّدة. لبعض البرامج الفرعية، تَحسِب المُهِمّة قيمة إحدى البيانات ثم تُعيدها، وفي تلك الحالة، تُعرَف تلك البرامج الفرعية باسم الدوال (functions)، ونقول عندها أن تلك الدالة تُعيد (return value) قيمة، والتي يُفْترَض استخدامها بطريقة ما ضِمْن البرنامج المُستدعِي للدالة.

لنَفْترِض مثلًا مسالة حِسَاب الجذر التربيعي (square root)، تُوفِّر لغة الجافا دالة (function) لهذا الغرض اسمها هو Math.sqrt. تُعدّ تلك الدالة عضو برنامج فرعي ساكن (static member subroutine) بالصنف Math. إذا كانت x هي أي قيمة عددية، فإن الاستدعاء Math.sqrt(x)‎ يَحسِب الجذر التربيعي (root) لتلك القيمة ثم يُعيدها. لمّا كانت الدالة Math.sqrt(x)‎ تُمثِل قيمة، فليس هناك أي مغزى من استدعائها بمفردها بتَعْليمَة استدعاء برنامج فرعي (subroutine call statement) كالتالي:

Math.sqrt(x);   

لا يَفعَل الحاسوب أي شيء بالقيمة المُعادة من الدالة (function) بالأعلى، أي أنه يَحسِبها ثم يتجاهلها، وهو أمر غَيْر منطقي، لذا يُمكِنك أن تُخبره مثلًا بأن عليه طباعتها على الأقل كالتالي:

System.out.print( Math.sqrt(x) );  // اعرض الجذر التربيعي

أو قد تَستخدِم تَعْليمَة إِسْناد (assignment statement) لتُخبره بأن عليه تَخْزِينها بمُتْغيِّر كالتالي:

lengthOfSide = Math.sqrt(x);

يُمثِل استدعاء الدالة Math.sqrt(x)‎ قيمة من النوع double أي يُمكِنك كتابة ذلك الاستدعاء أينما أَمْكَن اِستخدَام قيمة عددية مُصنَّفة النوع (numeric literal) من النوع double. يُمثِل x مُعامِلًا (parameter) يُمرَّر إلى البرنامج الفرعي (subroutine) والذي قد يَكُون مُتْغيِّرًا (variable) اسمه x أو قد يَكُون أي تعبير آخر (expression) بشَّرط أن يُمثِل ذلك التعبير قيمة عددية. على سبيل المثال، يَحسِب الاستدعاء Math.sqrt(2)‎ قيمة الجذر التربيعي للعَدَد ٢ أما الاستدعاء Math.sqrt(a*a+b*b)‎ فهو صالح تمامًا طالما كانت قيم a و b مُتْغيِّرات من النوع العددي.

يحتوي الصَنْف Math على العديد من الدوال الأعضاء الساكنة (static member functions) الأخرى. اُنظر القائمة التالية والتي تَعرِض بعضًا منها:

  • Math.abs(x)‎: تَحسِب القيمة المطلقة للمُعامِل x.

  • الدوال المثلثية العادية (trigonometric functions)‏ Math.sin(x)‎ و Math.cos(x)‎ و Math.tan(x)‎: تُقاس جميع الزوايا بوحدة قياس راديان (radians) وليس بوحدة الدرجات (degrees).

  • الدوال المثلثية العكسية (inverse trigonometric functions) ‏Math.asin(x)‎ و Math.acos(x)‎ و Math.atan(x)‎: تُقاس القيمة المُعادة (return value) من تلك الدوال بوحدة قياس راديان وليس بوحدة الدرجات.

  • تَحسِب الدالة الأسية Math.exp(x)‎ قيمة العدد e مرفوعة للأس x أما دالة اللوغاريتم الطبيعي Math.log(x)‎ فتَحسِب لوغاريتم x بالنسبة للأساس e.

  • Math.pow(x,y)‎ تحسب قيمة x مرفوعة للأس y.

  • Math.floor(x)‎: تُقرِّب x لأكبر عدد صحيح أقل من أو يُساوِي x. تَحسِب تلك الدالة عددًا صحيحًا بالمفهوم الرياضي، ولكنها مع ذلك تُعيد قيمة من النوع double بدلًا من النوع int كما قد تَتَوقَّع. فمثلًا يُعيد استدعاء الدالة Math.floor(3.76)‎ القيمة 3.0 بينما يُعيد استدعاء الدالة Math.floor(-4.2)‎ القيمة ‎-5. علاوة على ذلك، تَتَوفَّر أيضًا الدالة Math.round(x)‎ والتي تُعيد أقرب عدد صحيح للمُعامِل x وكذلك الدالة Math.ceil(x)‎ والتي تُقرِّب x لأصغر عدد صحيح أكبر من أو يُساوِي x.

  • Math.random()‎ تُعيد قيمة عشوائية من النوع double ضِمْن نطاق يتراوح من ٠ إلى ١. تَحسِب تلك الدالة أعدادًا شبه عشوائية (pseudorandom number) أي أنها ليست عشوائية تمامًا وإنما إلى درجة كافية لغالبية الأغراض. سنكتشف خلال الفصول القادمة أن تلك الدالة لها استخدامات أخرى كثيرة مفيدة.

تَستقبِل الدوال (functions) بالأعلى مُعامِلات (parameters) -أي x أو y داخل الأقواس- بأي قيم طالما كانت من النوع العددي أما القيم المُعادة (return value) من غالبيتها فهي من النوع double بغض النظر عن نوع المُعامِل (parameter) باستثناء الدالة Math.abs(x)‎ والتي تَكُون قيمتها المُعادة من نفس نوع المُعامِل x، فإذا كانت x من النوع int، تُعيد تلك الدالة قيمة من النوع int أيضًا وهكذا. على سبيل المثال، تُعيد الدالة Math.sqrt(9)‎ قيمة من النوع double تُساوِي 3.0 بينما تُعيد الدالة Math.abs(9)‎ قيمة من النوع int تُساوِي 9.

لا تَستقبِل الدالة Math.random()‎ أي مُعامِلات (parameter)، ومع ذلك لابُدّ من كتابة الأقواس حتى وإن كانت فارغة لأنها تَسمَح للحاسوب بمَعرِفة أنها تُمثِل برنامجًا فرعيًا (subroutine) لا مُتْغيِّرًا (variable). تُعدّ الدالة System.currentTimeMillis()‎ من الصنف System مثالًا آخرًا على برنامج فرعي (subroutine) ليس له أي مُعامِلات (parameters). عندما تُنفَّذ تلك الدالة، فإنها تُعيد الوقت الحالي مُقاس بحساب الفارق بين الوقت الحالي ووقت آخر قياسي بالماضي (بداية عام ١٩٧٠) بوحدة المللي ثانية. تَكُون القيمة المعادة من System.currentTimeMillis()‎ من النوع long (عدد صحيح ٦٤ بت). يُمكِنك اِستخدَام تلك الدالة لحِساب الوقت الذي يَستَغْرِقه الحاسوب لتّنْفيذ مُهِمّة معينة. كل ما عليك القيام به هو أن تُسجِّل كُلًا من الوقت الذي بدأ فيه التّنْفيذ وكذلك الوقت الذي انتهى به ثم تَحسِب الفرق بينهما. للحصول على توقيت أكثر دقة، يُمكِنك اِستخدَام الدالة System.nanoTime()‎ والتي تُعيد الوقت الحالي مُقاس بحساب الفارق بين الوقت الحالي ووقت آخر عشوائي بالماضي بوحدة النانو ثانية. لكن لا تَتَوقَّع أن يَكُون الوقت دقيقًا بحق لدرجة النانوثانية.

يُنفِّذ البرنامج بالمثال التالي مجموعة من المهام الحسابية ويَعرِض الوقت الذي يَستَغْرِقه البرنامج لتّنْفيذ كُلًا منها:

/**
 * This program performs some mathematical computations and displays the
 * results.  It also displays the value of the constant Math.PI.  It then 
 * reports the number of seconds that the computer spent on this task.
 */
public class TimedComputation {

    public static void main(String[] args) {

        long startTime; // وقت البدء بالنانو ثانية
        long endTime;   // وقت الانتهاء بالنانو ثانية
        long compTime;  // زمن التشغيل بالنانو ثانية
        double seconds; // فرق الوقت بالثواني

        startTime = System.nanoTime();

        double width, height, hypotenuse;  // جوانب المثلث
        width = 42.0;
        height = 17.0;
        hypotenuse = Math.sqrt( width*width + height*height );
        System.out.print("A triangle with sides 42 and 17 has hypotenuse ");
        System.out.println(hypotenuse);

        System.out.println("\nMathematically, sin(x)*sin(x) + "
                + "cos(x)*cos(x) - 1 should be 0.");
        System.out.println("Let's check this for x = 100:");
        System.out.print("      sin(100)*sin(100) + cos(100)*cos(100) - 1 is: ");
        System.out.println( Math.sin(100)*Math.sin(100) 
                + Math.cos(100)*Math.cos(100) - 1 );
        System.out.println("(There can be round-off errors when" 
                + " computing with real numbers!)");

        System.out.print("\nHere is a random number:  ");
        System.out.println( Math.random() );

        System.out.print("\nThe value of Math.PI is ");
        System.out.println( Math.PI );

        endTime = System.nanoTime();
        compTime = endTime - startTime;
        seconds = compTime / 1000000000.0;

        System.out.print("\nRun time in nanoseconds was: ");
        System.out.println(compTime);
        System.out.println("(This is probably not perfectly accurate!");
        System.out.print("\nRun time in seconds was:  ");
        System.out.println(seconds);

    } // ‫نهاية main()

} // ‫نهاية الصنف TimedComputation

الأصناف (classes) والكائنات (objects)

بالإضافة إلى استخدام الأصناف (classes) كحاويات للمُتْغيِّرات والبرامج الفرعية الساكنة (static). فإنها قد تُستخدَم أيضًا لوصف الكائنات (objects). يُعدّ الصنف ضِمْن هذا السياق نوعًا (type) بنفس الطريقة التي تُعدّ بها كلًا من int و double أنواعًا أي يُمكِننا إذًا أن نَستخدِم اسم الصنف للتَصْرِيح (declare) عن مُتْغيِّر (variable). تَحمِل المُتْغيِّرات في العموم نوعًا واحدًا من القيم يَكُون في تلك الحالة عبارة عن كائن (object).

ينتمي أي كائن إلى صنف (class) معين يُبلِّغه بنوعه (type)، ويُعدّ بمثابة تجميعة من المُتْغيِّرات والبرامج الفرعية (subroutines) يُحدِّدها الصنف الذي ينتمي إليه الكائن أي تتشابه الكائنات (objects) من نفس الصنف (class) وتحتوي على نفس تجميعة المُتْغيِّرات والبرامج الفرعية. على سبيل المثال، إذا كان لدينا سطح مستو، وأردنا أن نُمثِل نقطة عليه باِستخدَام كائن، فيُمكِن إذًا لذلك الكائن المُمثِل للنقطة أن يُعرِّف مُتْغيِّرين (variables)‏ x و y لتمثيل إحداثيات النقطة. ستُعرِّف جميع الكائنات المُمثِلة لنقطة قيمًا لكُلًا من x و y والتي ستكون مختلفة لكل نقطة معينة. في هذا المثال، يُمكِننا أن نُعرِّف صَنْفًا (class) اسمه Point مثلًا ليُعرِّف (define) البنية المشتركة لجميع الكائنات المُمثِلة لنقطة بحيث تَكُون جميع تلك الكائنات (objects) قيمًا من النوع Point.

لنَفْحَص الاستدعاء System.out.println مرة آخرى. أولًا، System هو عبارة عن صَنْف (class) أما out فهو مُتْغيِّر ساكن (static variable) مُعرَّف بذلك الصنف. ثانيًا، يشير المُتْغيِّر System.out إلى كائن (object) من الصَنْف القياسي PrintStream و System.out.println هو الاسم الكامل لبرنامج فرعي (subroutine) مُعرَّف بذلك الكائن. يُمثِل أي كائن من النوع PrintStream مقصدًا يُمكِن طباعة المعلومات من خلاله حيث يَتَضمَّن برنامجًا فرعيًا println يُستخدَم لإرسال المعلومات إلى ذلك المقصد. لاحِظ أن الكائن System.out هو مُجرد مقصد واحد محتمل، فقد تُرسِل كائنات (objects) آخرى من النوع PrintStream المعلومات إلى مقاصد أخرى مثل الملفات أو إلى حواسيب آخرى عبر شبكة معينة. يُمثَل ذلك ما يُعرَف باسم البرمجة كائنية التوجه (object-oriented programming): عندما يَتَوفَّر لدينا مجموعة من الأشياء المختلفة في العموم والتي لديها شيئًا مشتركًا مثل كَوْنها تَعمَل مقصدًا للخَرْج، فإنه يُمكِن اِستخدَامها بنفس الطريقة من خلال برنامج فرعي مثل println. في هذا المثال، يُعبِر الصَنْف PrintStream عن الأمور المُشتركة بين كل تلك الأصناف (objects).

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

لمّا كانت أسماء البرامج الفرعية (routines) دائمًا متبوعة بقوس أيسر، يَصعُب خَلْطها إذًا مع أسماء المُتْغيِّرات. في المقابل، تُستخدَم أسماء كُلًا من الأصناف (classes) والمُتْغيِّرات (variables) بنفس الطريقة، لذا قد يَكُون من الصعب أحيانًا التَمْييز بينها. في الواقع، تَتَّبِع جميع الأسماء المُعرَّفة مُسْبَقًا بالجافا نَمْط تسمية تبدأ فيه أسماء الأصناف بحروف كبيرة (upper case) بينما تبدأ أسماء المُتْغيِّرات والبرامج الفرعية (subroutines) بحروف صغيرة (lower case). لا يُمثِل ذلك قاعدة صيغة (syntax rule)، ومع ذلك من الأفضل أن تَتَّبِعها أيضًا.

كملاحظة عامة أخيرة، يُستخدَم عادةً مصطلح "التوابع (methods)" للإشارة إلى البرامج الفرعية (subroutines) بالجافا. يَعنِي مصطلح "التابع (method)" أن برنامجًا فرعيًا (subroutine) مُعرَّف ضِمْن صنف (object) أو كائن (object). ولأن ذلك يُعدّ صحيحًا لأي برنامج فرعي بالجافا، فإن أيًا منها يُعدّ تابعًا. سنميل عمومًا إلى اِستخدَام المصطلح الأعم "البرنامج الفرعي (subroutine)"، ولكن كان لابُدّ من إعلامك بأن بعض الأشخاص يُفضِّلون اِستخدَام مصطلح "التابع (method)".

العمليات على السلاسل النصية من النوع String

يُمثِل String صنفًا (class)، وأي قيمة من النوع String هي عبارة عن كائن (object). يَحتوِي أي كائن في العموم على بيانات (data) وبرامج فرعية (subroutines). بالنسبة للنوع String، فإن البيانات مُكوَّنة من متتالية محارف السِلسِلة النصية (string) أما البرامج الفرعية فهي في الواقع مجموعة من الدوال (functions) منها مثلًا الدالة length والتي تَحسِب عدد المحارف (characters) بالسِلسِلة النصية. لنَفْترِض أن advice هو مُتْغيِّر يُشير إلى String، يُمكِننا إذًا أن نُصرِّح عنه ونُسنِد إليه قيمة كالتالي:

String advice;
advice = "Seize the day!";

الآن، يُمثِل التعبير advice.length()‎ استدعاءً لدالة (function call)، والتي ستُعيد في تلك الحالة تحديدًا عدد محارف السِلسِلة النصية "Seize the day!" أي ستُعيد القيمة ١٤. في العموم، لأي مُتْغيِّر str من النوع String، يُمثِل الاستدعاء str.length()‎ قيمة من النوع int تُساوِي عدد المحارف بالسِلسِلة النصية (string). لا تَستقبِل تلك الدالة (function) أي مُعامِلات (parameters) لأن السِلسِلة النصية المطلوب حِسَاب طولها هي بالفعل قيمة المُتْغيِّر str. يُعرِّف الصَنْف String البرنامج الفرعي length ويُمكِن اِستخدَامه مع أي قيمة من النوع String حتى مع أي سِلسِلة نصية مُجرّدة (string literals) فهي في النهاية ليست سوى قيمة ثابتة (constant value) من النوع String. يُمكِنك مثلًا كتابة برنامج يَحسِب عدد محارف السِلسِلة النصية "Hello World" كالتالي:

System.out.print("The number of characters in ");
System.out.print("the string \"Hello World\" is ");
System.out.println( "Hello World".length() );

يُعرِّف الصَنْف String الكثير من الدوال (functions) نَستعرِض بعضًا منها خلال القائمة التالية:

  • s1.equals(s2)‎: تُوازن تلك الدالة بين السِلسِلتين s1 و s2، وتُعيد قيمة من النوع boolean تُساوِي true إذا كانت s1 و s2 مُكوّنتين من نفس متتالية المحارف، وتُساوِي false إن لم تَكُن كذلك.

  • s1.equalsIgnoreCase(s2)‎: هي دالة من النوع المنطقي (boolean-valued function). مثل الدالة السابقة، تَفْحَص تلك الدالة ما إذا كانت السِلسِلتان s1 و s2 مُكوّنتين من نفس السِلسِلة النصية (string)، ولكنها تختلف عن الدالة السابقة في أن الحروف الكبيرة (upper case) والصغيرة (lower case) تُعدّ متكافئة. فمثلًا، إذا كانت s1 تحتوي على السِلسِلة النصية "cat"، فستُعيد الدالة s1.equals("Cat")‎ القيمة false أما الدالة s1.equalsIgnoreCase("Cat")‎ فستُعيد القيمة true.

  • s1.length()‎: هي دالة من النوع الصحيح (integer-valued function)، وتُعيد قيمة تُمثِل عدد محارف السِلسِلة النصية s1.

  • s1.charAt(N)‎: حيث N هو عدد صحيح. تُعيد تلك الدالة قيمة من النوع char تُساوي قيمة محرف السِلسِلة النصية برقم المَوضِع N. يبدأ الترقيم من الصفر، لذا يُمثِل استدعاء الدالة s1.charAt(0)‎ المحرف الأول أما استدعاء الدالة s1.charAt(1)‎ فيُمثِل المحرف الثاني، وهكذا حتى نَصِل إلى المَوضِع الأخير s1.length() - 1. مثلًا، يُعيد ‎"cat".charAt(1)‎ القيمة 'a'. لاحظ أنه إذا كانت قيمة المُعامِل المُمرَّرة أقل من صفر أو أكبر من أو تُساوِي s1.length، فسيَحدُث خطأ.

  • s1.substring(N,M)‎: حيث N و M هي أعداد صحيحة. تُعيد تلك الدالة قيمة من النوع String مُكوَّنة من محارف السلسلة النصية بالمواضع N و N+1 و .. حتى M-1 (المحرف بالمَوضِع M ليس مُضمَّنًا). تُعدّ القيمة المُعادة "سلسلة جزئية (substring)" من السِلسِلة الأصلية s1. يُمكِنك ألا تُمرِّر قيمة للمُعامِل M كالتالي s1.substring(N)‎ وعندها ستُعيد تلك الدالة سِلسِلة جزئية من s1 مُكوَّنة من محارف السلسلة النصية بدايةً من N إلى النهاية.

  • s1.indexOf(s2)‎: تُعيد عددًا صحيحًا. إذا كانت s2 هى سِلسِلة جزئية (substring) من s1، تُعيد تلك الدالة مَوضِع المحرف الأول من السِلسِلة الجزئية s2 بالسِلسِلة الأصلية s1. أما إذا لم تَكُن جزءًا منها، فإنها تُعيد القيمة -١. تَتَوفَّر أيضًا الدالة s1.indexOf(ch)‎ حيث ch عبارة عن محرف من النوع char، وتُستخدَم للبحث عنه بسِلسِلة نصية s1. تُستخدَم أيضًا الدالة s1.indexOf(x,N)‎ لإيجاد أول حُدوث من x بعد موضع N بسِلسِلة نصية، وكذلك الدالة s1.lastIndexOf(x)‎ لإيجاد آخر حُدوث من x بسِلسِلة نصية s1.

  • s1.compareTo(s2)‎: هي دالة من النوع العددي الصحيح (integer-valued function) تُوازن بين سلسلتين نصيتين s1 و s2، فإذا كانت السِلسِلتان متساويتين، تُعيد الدالة القيمة ٠ أما إذا كانت s1 أقل من s2، فإنها تُعيد عددًا أقل من ٠، وأخيرًا إذا كانت s1 أكبر من s2، فإنها تُعيد عددًا أكبر من ٠. إذا كانت السلسلتان s1 و s2 مُكوّنتين من حروف صغيرة (lower case) فقط أو حروف كبيرة (upper case) فقط، فإن الترتيب المُستخدَم بموازنات مثل "أقل من" أو "أكبر من" تُشير إلى الترتيب الأبجدي. أما إذا تَضمَّنتا محارف آخرى فسيَكُون الترتيب أكثر تعقيدًا. تَتَوفَّر دالة آخرى مشابهة هي s1.compareToIgnoreCase(s2)‎.

  • s1.toUpperCase()‎: هي دالة من النوع النصي (String-valued function) تُعيد سِلسِلة نصية جديدة تُساوِي s1 ولكن بَعْد تَحْوِيل أي حرف صغير (lower case) بها إلى حالته الكبيرة (upper case). فمثلًا، تُعيد الدالة ‎"‎Cat".toUpperCase()‎ السِلسِلة النصية "CAT". تَتَوفَّر دالة آخرى مشابهة هي s1.toLowerCase.

  • s1.trim()‎: هي دالة من النوع النصي (String-valued function) تُعيد سِلسِلة نصية جديدة (string) تساوي s1 ولكن بَعْد حَذْف أي محارف غَيْر مطبوعة -كالمسافات الفارغة (spaces)- من بداية السِلسِلة النصية (string) ونهايتها. فمثلًا إذا كانت s1 هي السِلسِلة النصية "fred ‎"، فستُعيد الدالة s1.trim()‎ السِلسِلة "fred" بدون أي مسافات فارغة بالنهاية.

لا تُغيِّر الدوال s1.toUpperCase()‎ و s1.toLowerCase()‎ و s1.trim()‎ قيمة s1، وإنما تُنشِئ سِلسِلة نصية جديدة (string) تُعَاد كقيمة للدالة يُمكِن اِستخدَامها بتَعْليمَة إِسْناد (assignment statement) مثلًا كالتالي smallLetters = s1.toLowerCase();‎. إذا كنت تُريد تَعْدِيل قيمة s1 نفسها، فيُمكِنك ببساطة أن تَستخدِم تَعْليمَة الإِسْناد التالية s1 = s1.toLowerCase();‎.

يُمكِنك أن تَستخدِم عَامِل الزيادة (plus operator) + لضم (concatenate) سِلسِلتين نصيتين (strings)، وينتج عنهما سِلسِلة نصية جديدة مُكوَّنة من كل محارف السِلسِلة النصية الأولى متبوعة بكل محارف السِلسِلة النصية الثانية. فمثلًا، يؤول التعبير "Hello" + "World" إلى السِلسِلة النصية "HelloWorld". إذا كنت تُريد مسافة فارغة (space) بين الكلمات، فعليك أن تُضيفها إلى أي من السِلسِلتين كالتالي Hello " + "World".

لنَفْترِض أن name هو مُتْغيِّر من النوع String يُشير إلى اسم مُستخدِم البرنامج. يُمكِنك إذًا أن تُرحِّب به بتّنْفيذ التَعْليمَة التالية:

System.out.println("Hello, "  +  name  +  ".  Pleased to meet you!");

يُمكِنك حتى أن تَستخدِم نفس العَامِل + لكي تَضُمُّ (concatenate) قيم من أي نوع آخر إلى سِلسِلة نصية (string). ستَتَحوَّل تلك القيم إلى سِلسِلة نصية (string) أولًا كما يَحدُث عندما تَطبَعها إلى الخَرْج القياسي (standard output) ثم ستُضَمّ إلى السِلسِلة النصية الآخرى. فمثلًا، سيؤول التعبير "Number" + 42 إلى السِلسِلة النصية "Number42". اُنظر التعليمات التالية:

System.out.print("After ");
System.out.print(years);
System.out.print(" years, the value is ");
System.out.print(principal);

يُمكِنك اِستخدَام التَعْليمَة المُفردة التالية بدلًا من التَعْليمَات بالأعلى:

System.out.print("After " + years + 
                    " years, the value is " + principal);

تُعدّ النسخة الثانية أفضل بكثير ويُمكِنها أن تَختصِر الكثير من الأمثلة التي عَرَضَناها خلال هذا الفصل.

مقدمة إلى التعدادات (enums)

تُوفِّر الجافا ثمانية أنواع أساسية مَبنية مُسْبَقًا (built-in primitive types) بالإضافة إلى مجموعة ضخمة من الأنواع المُعرَّفة باِستخدَام أصناف (classes) مثل النوع String، ولكنها ما تزال غَيْر كافية لتغطية جميع المواقف المُحتمَلة والتي قد يحتاج المُبرمج إلى التَعامُل معها. لهذا، وبالمثل من غالبية اللغات البرمجية الآخرى، تَمنَحك الجافا القدرة على إنشاء أنواع (types) جديدة، والتي غالبًا ما تَكُون بهيئة أصناف (classes)، وهو ما سنَتَعلَّمه بالفصل الخامس. تَتَوفَّر مع ذلك طريقة آخرى وهي التعدادات (enumerated types - enums) سنناقشها خلال هذا القسم.

تقنيًا، يُعدّ أي تعداد (enum) نوعًا خاصًا من صَنْف (class)، ولكن هذا غَيْر مُهِمّ في الوقت الحالي. سنَفْحَص خلال هذا القسم التعدادات بصيغتها البسيطة (simplified form)، والمُستخدَمة عمليًا في غالبية الحالات.

أي تعداد (enum) هو عبارة عن نوع (type) يَتَضمَّن قائمة ثابتة مُكوَّنة من قيمه المُحتمَلة تُخصَّص عند إِنشاء نوع التعداد. بشكل ما، تُشبه أنواع التعداد (enum) النوع boolean والتي قيمه المحتملة هي true و false فقط. لكن لاحِظ أن النوع boolean هو نوع أساسي (primitive type) أما أنواع التعداد (enums) فليست كذلك.

يُكْتَب تعريف تعداد (enum definition) معين بالصيغة التالية:

enum <enum-type-name> { <list-of-enum-values> }

لا يُمكِنك كتابة ذلك التعريف (definition) داخل أي برنامج فرعي (subroutine)، لذا قد تَضَعه خارج البرنامج main()‎ أو حتى بملف آخر مُنفصِل. تُشير إلى اسم نوع التعداد (enum) بنفس الطريقة التي تُشير بها كلمات مثل "boolean" و "String" إلى النوع boolean والنوع String على الترتيب. يُمكِننا أن نَستخدِم أي مُعرّف بسيط (simple identifier) كاسم لنوع التعداد. أما فتُشير إلى قيم التعداد المُحتمَلة وتَتَكوَّن من قائمة من المُعرّفات (identifiers) يَفصِل بينها فاصلة (comma). يُعرِّف المثال التالي نوع تعداد (enum) اسمه هو Season وقيمه المُحتمَلة هي أسماء فصول السنة الأربعة:

enum Season { SPRING, SUMMER, FALL, WINTER }

عادةً ما تُكْتَب القيم المُحتمَلة لنوع التعداد (enum) بحروف كبيرة (upper case)، لكنه ليس أمرًا ضروريًا فهو ليس ضِمْن قواعد الصيغة (syntax rules). تُعدّ القيم المُحتمَلة لتعداد معين ثوابتًا (constants) لا يُمكِن تعديلها، وعادةً ما يُطلَق عليها اسم ثوابت التعداد (enum constants).

لأن ثوابت التعداد لنوع مثل Season مُعرَّفة داخله، لابُدّ من أن نُشير إليها باستخدام أسماء مركبة مُكوَّنة من اسم النوع المُتْضمِّن ثم نقطة ثم اسم ثابت التعداد كالتالي: Season.SPRING و Season.SUMMER و Season.FALL و Season.WINTER.

بمُجرّد إنشاء نوع تعداد (enum)، تستطيع أن تَستخدِمه للتَصْرِيح (declare) عن مُتْغيِّرات بنفس الطريقة التي تُصرِّح بها عن مُتْغيِّرات من أي أنواع آخرى. يمكنك مثلًا أن تُصرِّح عن مُتْغيِّر اسمه vacation من النوع Season باستخدام التَعْليمَة التالية:

Season vacation;

بعد التَصْرِيح عن مُتْغيِّر، يُمكِنك أن تُسنِد (assign) إليه قيمة باستخدام تَعْليمَة إِسْناد (assignment statement). يُمكن لتلك القيمة -على يمين عامل الإِسْناد- أن تَكُون أي من ثوابت التعداد من النوع Season. تَذَكَّر أنه لابُدّ من اِستخدَام الاسم الكامل لثابت التعداد بما في ذلك اسم النوع Season. اُنظر المثال التالي:

vacation = Season.SUMMER;

يُمكِنك أن تَستخدِم تَعْليمَة طباعة عادية مثل System.out.print(vacation)‎ لطباعة قيمة تعداد، وتَكُون القيمة المطبوعة عندها عبارة عن اسم ثابت التعداد (enum constant) بدون اسم نوع التعداد أي سيَكُون الخَرْج في هذه الحالة هو "SUMMER".

لأن التعداد (enum) هو عبارة عن صَنْف (class) تقنيًا، فلابُدّ إذًا من أن تَكُون قيم التعداد (enum value) عبارة عن كائنات (objects) وبالتالي يُمكِنها أن تَحتوِي على برامج فرعية (subroutines). أحد البرامج الفرعية المُعرَّفة بأي قيمة تعداد من أي نوع هي ordinal()‎ والتي تُعيد العدد الترتيبي (ordinal number) لتلك القيمة بقائمة القيم المُحتمَلة لذلك التعداد. يُشير العدد الترتيبي ببساطة إلى مَوضِع القيمة بالقائمة، فمثلًا، يُعيد Season.SPRING.ordinal()‎ قيمة من النوع int تُساوِي صفر أما Season.SUMMER.ordinal()‎ فيُعيد ١ أما Season.FALL.ordinal()‎ فيُعيد ٢ وأخيرًا Season.WINTER.ordinal()‎ يُعيد ٣. يمكنك بالطبع أن تَستخدِم التابع (method‏) ordinal()‎ مع مُتْغيِّر من النوع Season مثل vacation.ordinal()‎.

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

public class EnumDemo {

       // ‫عرف نوعين تعداد خارج البرنامج main

    enum Day { SUNDAY, MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY }

    enum Month { JAN, FEB, MAR, APR, MAY, JUN, JUL, AUG, SEP, OCT, NOV, DEC }

    public static void main(String[] args) {

         Day tgif;     // صرح عن متغير من النوع‫ Day
         Month libra;  // ‫صرح عن متغير من النوع Month

         tgif = Day.FRIDAY;    // ‫أسند قيمة من النوع Day إلى tgif
         libra = Month.OCT;    // ‫أسند قيمة من النوع Month إلى libra

         System.out.print("My sign is libra, since I was born in ");
        // ‫قيمة الخرج ستكون OCT
         System.out.println(libra);   
         System.out.print("That's the ");
         System.out.print( libra.ordinal() );
         System.out.println("-th month of the year.");
         System.out.println("   (Counting from 0, of course!)");

         System.out.print("Isn't it nice to get to ");
         // ‫قيمة الخرج ستكون:  FRIDAY
         System.out.println(tgif);   

         System.out.println( tgif + " is the " + tgif.ordinal() 
                                            + "-th day of the week.");
    }

}

كما ذَكَرنا مُسْبَقًا، يُمكِنك أن تُعرِّف التعدادات (enum) بملفات مُنفصِلة. لاحظ أن البرنامج SeparateEnumDemo.java هو نفسه البرنامج EnumDemo.java باستثناء أن أنواع التعداد المُستخدَمة قد عُرِّفت بملفات مُنفصلة هي Month.java و Day.java.

ترجمة -بتصرّف- للقسم Section 3: Strings, Classes, Objects, and Subroutines من فصل Chapter 2: Programming in the Small I: Names and Things من كتاب Introduction to Programming Using Java.





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


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



يجب أن تكون عضوًا لدينا لتتمكّن من التعليق

انشاء حساب جديد

يستغرق التسجيل بضع ثوان فقط


سجّل حسابًا جديدًا

تسجيل الدخول

تملك حسابا مسجّلا بالفعل؟


سجّل دخولك الآن