الدرس الثالث: المتغيرات في لغة جافا
يشرح هذا الدرس طريقة كتابة تعليمات تحوي متغيرات تخزن فيها قيمًا مختلفة، كالأرقام والكلمات، وتحوي أيضًا عوامل حسابية (operators)، وهي رموز تمثل عمليات حسابية.
كما نتحدث عن ثلاثة أنواع من الأخطاء البرمجية ونذكر فيه المزيد من النصائح عن تنقيح البرامج من الأخطاء.
في لغة الجافا لكل متغير نوع معين ، عند تعريف المتغير يتم وضع نوعه قبل اسم المتغير ، وهذه بعض الأمثلة على ذلك : ;double salary ;int vacationDays ;long earthPopulation ;boolean done
مع ملاحظة الفاصلة المنقطة (semicolon) عند نهاية كل تعريف ، و هي ضرورية لإنهاء كل جملة يُراد تنفيذها .
المتغير يجب أن يبدأ بحرف و أن يتكون من سلسلة من الأحرف (letter) أو الأرقام (digit) بدون مسافات فيما بينها. مصطلح الأحرف (letter) و الأرقام (digit) في لغه الجافا يتضمن معنى أوسع من باقي لغات البرمجة. فالمقصود بالأحرف (letter) في الجافا: ( ‘A’ إلى ‘Z’) و( ‘a’ إلى ‘z’) و ‘_’ وأي رمز آخر مخصص يمثل حرفاً في اللغات الأخرى ، وهنا بعض الأمثلة لتوضيح هذه الفكرة بشكل أفضل : بإمكان مستخدمي اللغة الألمانية استعمال حرف ‘ ä ‘ كمتغير داخل البرنامج ، وعلى سبيل المثال أيضاً اليونانيون يستطيعون استعمال’ π ‘. و كذلك الأرقام (digit) فالمقصود منها في الجافا : (0 – 9) أو أي رمز مخصص يمثل رقماً في أي لغة أخرى مثل (+) و (@) فهذه الرموز بالإضافة إلى المسافة (space) لا يُمكن استعمالها لتمثيل اسم متغير . جميع الحروف أو الرموز التي تكوّن اسم المتغير تعتبر عنصراً هامّاً في تمييزه من قبل الجافا و كذلك حالة الأحرف (upper case) أو (lower case).
مع العلم بأن هناك كلمات محجوزة لعمليات خاصة من قبل الجافا لا يجوز للمبرمج استعمالها كإسم لمتغير. ويمكنك تعريف أكثر من متغير في سطر واحد ثم وضع فاصلة منقطة في نهاية السطر ، مثال :
; int i , j ولكن هذه الطريقة غير محبذة لأن تعريف كل متغير في سطر مخصص له يجعل البرنامج أسهل للقراءة.
بعد الانتهاء من تعريف اسم المتغير و نوعه يجب أن يتم تهيئته للاستخدام وذلك عن طريق ربط هذا المتغير بقيمة معينة ، و يتم ذلك من خلال كتابة الجملة التي تربط هذا المتغير بالقيمة المخصصة له و التي تسمى بالـ (assignment statement). لا تنسى أنه من غير الممكن أن يتم استخدام متغير لم يتم ربطه بأي قيمة. في المثال التالي سيُظهر لك الجافا أن خطأً ما قد تم ارتكابه في هذه السطور (error message):
;int vacationDays ;(System.out.println(vacationDays
يمكنك أن تربط قيمة معينة بمتغير تم تعريفه مسبقاً في البرنامج وذلك عن طريق وضع اسم المتغير على اليسار ومن ثم تضع علامة (=) و من ثم تضع القيمة المراد استخدامها على يمين علامة الـ (=) ، مثال على ذلك :
;int vacationDays ;vacationDays = 12
ويمكنك أيضاً أن تقوم بتعريف متغير و ربطه بقيمة معينة في نفس الوقت ، مثال :
;int vacationDays = 12
ومن الجدير بالذكر أنه بإستطاعتك أن تقوم بتعريف متغير في أي مكان تريده من البرنامج الذي تقوم بكتابته ، مثال :
;double salary = 65000.0 ;System.out.println(salary) ;int vacationDays = 12
يعتبر من الأفضل في الجافا أن تقوم بتعريف المتغير بالقرب من المكان الذي يتم استخدامه فيه.
في الجافا تُستخدم كلمة (final) للدلالة على ثابت ، على سبيل المثال :
Public class Constants
}
public static void main (String[] args)
} ;final double CM_PER_INCH = 2.54 ;double paperWidth = 8.5 ;double paperHeight = 11 { ; System.out.println("paper size in centimeters:" + paperWidth * CM_PER_INCH + "by" + paperHeight * CM_PER_INCH )
{
استخدام كلمة final يمكننا من أن نعرف متغيراً و نوضح القيمة التي يحملها مرة واحدة فقط و ستبقى هذه القيمة ثابتة ولن تتغير حتى نهاية البرنامج. ومن العادات المتبعة غالباً في الجافا أن يتم تسمية الثوابت بحروف حالتها (upper case). غالباً في الجافا نجد بأن الأكثر شيوعاً هو أن يتم تعريف ثوابت يمكن استخدامها في أكثر من (method) والتي تنتمي إلى نفس الـ (class)و عادة ما تسمي هذه الثوابت بـ ( class constants). عليك أن تعرف الـ (class constant) بـكتابة هذه الكلمتين static final . هنا مثال يوضح استخدام الـ (class constants):
public class Constants2
}
public static void main (String[] args)
{ ;double paperWidth = 8.5 ;double paperHeight = 11 ; (System.out.println("paper size in centimeters:" + paperWidth * CM_PER_INCH + "by" + paperHeight * CM_PER_INCH } ;public static final double CM_PER_INCH = 2.54 {
لاحظ أن تعريف الـ (class constant) يظهر خارج الـ (main method)، وذلك لكي نمَكنّ من استخدام هذا الثابت من قِبَل (methods) أخرى تنتمي إلى هذا الـ (class) نفسه. هناك معلومة أخرى يجب توضيحها وهي أنه إذا تم كتابة كلمة (public) من ضمن تعريف الـ(class constant) فإن ذلك سيمَكِّننا من استخدام هذا الثابت للـ (methods) التي تنتمي إلى (classes) أخرى، كما هو الحال في المثال الذي ذكرناه في الأعلى Constants2.CM_PER_INCH
التصريح عن المتغيرات
أحد أقوى المزايا لأي لغة برمجة هي القدرة على تعريف ومعالجة المتغيرات (variables). المتغير هو منطقة ذات اسم تخزّن قيمة (value). قد تكون القيم أرقامًا، أو نصوصًا، أو صورًا، أو مقاطع صوتية، وغيرها من أنواع البيانات. عليك أولًا التصريح عن متغير ثم تخزين القيم فيه.
String message;
هذا النوع من التعليمات يدعى تصريح (declaration)، لأنها تصرح أن نوع المتغير المدعو message هو String. لكل متغير نوع (type) يحدد نوع القيم التي يمكنه تخزينها. مثلًا، النوع int يخزن الأعداد الصحيحة، والنوع String يخزن السلاسل المحرفية.
بعض الأنواع تبدأ بحرف كبير وبعضها الآخر يبدأ بحرف صغير. سنعرف معنى هذا التمييز لاحقًا، لكن الآن عليك الانتباه لكتابتها بشكل صحيح. ليس هناك نوع Int ولا string.
لإنشاء متغير من النوع الصحيح، التعليمة هي:
int x;
حيث x هو اسم كيفي اخترناه للمتغير. بشكل عام، عليك اختيار أسماء المتغيرات بحيث تدل على دور المتغير في البرنامج. مثلًا، عندما ترى التصريحات التالية عن المتغيرات:
String firstName;
String lastName;
int hour, minute;
هذا المثال يصرح عن متغيرين من نوع String ومتغيرين من نوع int. عندما يتكون اسم المتغير من كلمتين أو أكثر، مثل المتغير firstName، جرت العادة على جعل الحرف الأول من كل كلمة حرفًا كبيرًا عدا الكلمة الأولى. أسماء المتغيرات حساسة لحالة الأحرف، ولذلك فالمتغير firstName مختلف عن المتغيرfirstName أو FirstName.
يوضح هذا المثال أيضًا صيغة التصريح عن عدة متغيرات من نفس النوع في سطر واحد: كلًا من hour و minute هو عدد صحيح (متغير من النوع int). لاحظ أن كل تعليمة تصريح تنتهي بفاصلة منقوطة.
يمكنك تسمية متغيراتك كما تشاء. لكن هناك حوالي 50 كلمة محجوزة، تدعى الكلمات المفتاحية (keywords)، ولا يسمح لك باستخدامها كأسماء لمتغيراتك. هذه الكلمات تشمل public، وclass، وstatic، وvoid، وint، التي يستخدمها المترجم لتحليل بنية البرنامج.
هناك قائمة كاملة بالكلمات المفتاحية موجودة، لكن لا حاجة لحفظها. معظم المحررات المستخدمة في البرمجة توفر ميزة “تلوين الشفرة” (syntax highlighting)، التي تجعل الأجزاء المختلفة من البرنامج تظهر بألوان مختلفة.
الإسناد
بعد أن صرحنا عن بعض المتغيرات، سنستخدمها لتخزين بعض القيم فيها. يمكننا عمل ذلك باستخدام تعليمة الإسناد (assignment).
message = “Hello!”; // give message the value “Hello!”
hour = 11; // assign the value 11 to hour
minute = 59; // set minute to 59
يبين هذا المثال ثلاث تعليمات إسناد، والتعليقات تظهر ثلاثة أساليب يستخدمها الناس أحيانًا عندما يقرؤون تعليمات الإسناد. قد تكون المفردات مربكة هنا، لكن الفكرة واضحة :
- عندما تصرح عن متغير، أنت تنشئ منطقة تخزينية لها اسم.
- عندما تطبق تعليمة الإسناد على متغير، فأنت تغير القيمة التي يحويها.
كقاعدة عامة، يجب أن يكون نوع المتغير من نفس نوع القيمة التي تسندها إليه. مثلًا، لا يمكنك تخزين سلسلة محرفية في المتغير minute أو عددًا صحيحًا في message.
من ناحية أخرى، هذه القاعدة قد تكون مصدرًا للإرباك أحيانًا، بسبب وجود العديد من الطرق التي تسمح لك بتحويل القيم من نوع لآخر، وأحيانًا تحول Java الأشياء تلقائيًا. لكن الآن عليك فقط أن تتذكر القاعدة العامة بأن المتغيرات والقيم يجب أن تكون من نفس النوع، وسنتحدث عن الحالات الخاصة لاحقًا.
أحد مصادر الإرباك هو أن بعض السلاسل المحرفية تبدو مثل الأرقام، لكنها ليست كذلك. مثلًا، يمكن أن يخزن المتغير message السلسلة المحرفية “123”، المكونة من المحارف ‘1’ و ‘2’ و ‘3’، لكنها ليست مثل العدد الصحيح 123.
message = “123”; // legal
message = 123; // not legal
يجب تهيئة المتغيرات (initialize)، أي إسناد قيمة لها أول مرة، قبل أن تستخدمها. يمكنك التصريح عن متغير ثم إسناد قيمة له لاحقًا، كما في المثال السابق. كما يمكنك أيضًا التصريح عن المتغير وتهيئته بسطر واحد:
String message = “Hello!”;
int hour = 11;
int minute = 59;
مخططات الحالة
قد تظن أن تعليمة a = b هي تعليمة مساواة لأن Java تستخدم الرمز = لعملية الإسناد. لكنها ليست مساواة!
المساواة عملية تبديلية، أما الإسناد فلا. على سبيل المثال، في الرياضيات إذا كان a = 7 إذًا 7 = a. لكن في Javaa = 7; تعليمة إسناد مشروعة، لكن 7 = a غير مشروعة. يجب أن يكون الطرف الأيسر من تعليمة الإسناد متغيرًا (اسمًا لموقع تخزيني).
في الرياضيات أيضًا، جملة المساواة صحيحة دائمًا. إذا كان a = b الآن، فإن a سيبقى مساويًا لـ b دائمًا. أما في Java، فتعليمة الإسناد قد تجعل قيمتي متغيرين متساويتان، لكن قد لا يستمران على هذه الحال.
int a = 5;
int b = a; // a and b are now equal
a = 3; // a and b are no longer equal
في السطر الثالث تغيرت قيمة a، لكن قيمة b لم تتغير، وبالتالي لم يبق المتغيران متساويان.
المتغيرات في البرنامج مع قيمها الحالية تشكل حالة البرنامج (state). يُظهِر الشكل 2.1 حالة البرنامج بعد تنفيذ هذه التعليمات.
تدعى هذه المخططات التي تظهر حالة البرنامج بمخططات الحالة (state diagrams). يُمثَّل كل متغير بصندوق يظهر اسم المتغير خارجه وقيمة المتغير داخله. أثناء تنفيذ البرنامج تتغير الحالة، لذلك عليك اعتبار مخططات الحالة كتمثيل لحظي لنقطة محددة في مسار التنفيذ.
طباعة المتغيرات
يمكنك عرض قيمة متغير باستخدام println أو print. في التعليمات التالية صرحنا عن متغير اسمه firstLine، وأسندنا له القيمة “!Hello, again”، وعرضنا تلك القيمة.
String firstLine = “Hello, again!”;
System.out.println(firstLine);
عندما نتحدث عن عرض متغير فنحن نقصد قيمة المتغير عمومًا. أما لعرض اسمالمتغير، فعليك أن تضعه بين علامتي اقتباس. مثلًا:
System.out.print(“The value of firstLine is “);
System.out.println(firstLine);
خرج هذا البرنامج هو:
The value of firstLine is Hello, again!
بنية تعليمة عرض المتغير هي نفسها بغض النظر عن نوع المتغير. مثلًا:
int hour = 11;
int minute = 59;
System.out.print(“The current time is “);
System.out.print(hour);
System.out.print(“:”);
System.out.print(minute);
System.out.println(“.”);
خرج هذا البرنامج هو:
The current time is 11:59.
لوضع عدة قيم على نفس السطر، من الشائع استخدام عدة تعليمات print ثم تتبعها تعليمة println في النهاية. لكن لا تنسَ تعليمة println على العديد من الحواسيب، يتم تخزين خرج تعليمات print دون عرضه على الشاشة حتى استدعاء println؛ وعندها يظهر السطر كله دفعة واحدة. إذا أغفلت تعليمة println، فقد يعرض البرنامج الخرج المخزن في أوقات غير متوقعة أو ربما انتهى البرنامج دون طباعة أي شيء.
العوامل الحسابية
العوامل (operators) هي رموز تمثل حسابات بسيطة. مثلًا، عامل الجمع هو +، وعامل لطرح -، والضرب *، والقسمة /.
يحول البرنامج التالي الوقت إلى دقائق:
int hour = 11;
int minute = 59;
System.out.print(“Number of minutes since midnight: “);
System.out.println(hour * 60 + minute);
في هذا المثال، لدينا التعبير (expression) التالي: hour * 60 + minute، الذي يمثل قيمة وحيدة بعد الحساب. عند تنفيذ البرنامج، يستبدل كل متغير بقيمته الحالية، ثم تطبق العوامل عليها. تدعى القيم التي تعمل العوامل عليها بالمعاملات (operands).
نتيجة المثال السابق هي:
Number of minutes since midnight: 719
التعابير هي تراكيب تتألف بشكل عام من أرقام، ومتغيرات، وعوامل. عند ترجمة وتنفيذ التعابير ستنتج لدينا قيمة وحيدة.
مثلًا، التعبير 1 + 1 قيمته 2. وفي التعبير hour – 1 تستبدل Java المتغير بقيمته، وبذلك ينتج 11 – 1، الذي قيمته 1. أما في التعبير hour * 60 + minute فيستبدل المتغيران بقيمتيهما، وهذا يعطي 11 * 60 + 59. تنفذ عملية الضرب أولًا، معطية التعبير 660 + 59. بعد ذلك تجرى عملية الجمع التي تنتج 719.
عمليات الجمع والطرح والضرب كلها تعمل كما تتوقع منها تمامًا، لكن عملية القسمة قد تفاجئك. مثلًا، نحاول في التعليمتين التاليتين حساب الجزء الذي مضى من الساعة:
System.out.print(“Fraction of the hour that has passed: “);
System.out.println(minute / 60);
الخرج هو:
Fraction of the hour that has passed: 0
هذه النتيجة تحير الناس عادة. قيمة minute هي 59، وناتج قسمة 59 على 60 هو 0.98333، وليس 0. المشكلة هي أن Java تنفذ عملية “القسمة الصحيحة” عندما يكون المعاملين عددين صحيحين. عملية القسمة الصحيحة تقرب الناتج دومًا إلى العدد الصحيح السابق، حتى في الحالات التي يكون العدد الصحيح التالي أقرب مثل حالتنا هذه. يمكننا كحل بديل حساب النسبة المئوية بدلًا من العدد العشري:
System.out.print(“Percent of the hour that has passed: “);
System.out.println(minute * 100 / 60);
الخرج الجديد هو:
Percent of the hour that has passed: 98
لقد قربت النتيجة للأسفل هنا أيضًا، لكن النتيجة الآن صحيحة تقريبًا على الأقل.
النقطة العائمة
هناك حل مناسب أكثر وهو استخدام أعداد النقطة العائمة (floating-point)، التي تمثل الأعداد العشرية كما تمثل الأعداد الصحيحة أيضًا. في Java، يستخدم النوع double (اختصارًا لعبارة double-precision) افتراضيًا لأعداد النقطة العائمة. يمكنك إنشاء المتغيرات من نوع double وإسناد القيم لها باستخدام نفس الصيغ التي استخدمناها للأنواع الأخرى:
double pi;
pi = 3.14159;
تنفذ Java عملية “قسمة النقطة العائمة” (floating-point division) إذا كان أحد المعاملات أو كلاهما من النوعdouble. وهكذا يمكننا حل المشكلة التي واجهتنا في القسم السابق:
double minute = 59.0;
System.out.print(“Fraction of the hour that has passed: “);
System.out.println(minute / 60.0);
الخرج هو:
Fraction of the hour that has passed: 0.9833333333333333
ورغم فائدة أعداد النقطة العائمة، إلا أنها قد تسبب الإرباك. مثلًا، Java تفرّق بين القيمة الصحيحة 1 وبين القيمة العشرية 1.0، حتى لو بدا أنهما نفس العدد، فهما يختلفان بالنوع، وعلى وجه الدقة، لا يسمح لك بتنفيذ عمليات إسناد بين النوعين. مثلًا، ما يلي ليس مسموحًا لأن المتغير على الطرف الأيسر من النوع int أما القيمة المسندة له على الطرف الأيمن هي double:
int x = 1.1; // compiler error
من السهل نسيان هذه القاعدة لأن هناك حالات عديدة تحول فيها Java أحد الأنواع إلى النوع الآخر تلقائيًا. مثلًا:
double y = 1; // legal, but bad style
من المفترض ألا تكون التعليمة السابقة مشروعة، لكن Java تسمح بها عن طريق التحويل القيمة الصحيحة 1 إلى القيمة العشرية 1.0 تلقائيًا. هذا التساهل مريح، لكنه يسبب المشاكل للمبتدئين غالبًا. مثلًا:
double y = 1 / 3; // common mistake
قد تتوقع أن يعطى المتغير y القيمة 0.333333، وهي قيمة عشرية مشروعة، لكنه في الواقع سيعطى القيمة 0.0. السبب هو أن العبارة على اليمين هي نسبة بين عددين صحيحين، لذلك تجري Java عملية قسمة صحيحة، والتي تنتج القيمة الصحيحة 0. ثم يتم تحويلها إلى قيمة عشرية، الناتج هو 0.0. إحدى الطرق لحل هذه المشكلة (بعد أن تكتشف أن هذه هي المشكلة) هو جعل الطرف الأيمن عبارة عشرية. التعليمة التالية ستعطي y القيمة 0.333333، كما هو متوقع.
double y = 1.0 / 3.0;
عليك دائمًا إسناد قيم عشرية لمتغيرات النقطة العائمة في كتابتك. لن يجبرك المترجم على ذلك، لكنك لا تعرف أبدًا متى تظهر لك غلطة بسيطة وتعود عليك وبالًا.
أخطاء التقريب
معظم أرقام النقطة العائمة صحيحة تقريبيًا. يمكن تمثيل بعض الأرقام بدقة، مثل القيم الصحيحة ذات الأحجام المعقولة. أما الكسور الدورية، مثل 1/3، أو الأرقام غير النسبية، مثل π، فلا يمكن تمثيلها بدقة. الفرق بين العدد الذي نريد والعدد الذي نحصل عليه يدعى خطأ التقريب (rounding error). مثلًا، يجب أن تكون التعليمتان التاليتان متكافئتين:
System.out.println(0.1 * 10);
System.out.println(0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1);
لكن الخرج سيكون كما يلي على معظم الحواسيب:
1.0
0.9999999999999999
المشكلة هي أن 0.1، وهو عدد عشري منته في الأساس 10، هو كسر دوري في الأساس 2، ولذلك يكون تمثيله في النقطة العائمة تقريبي حتمًا. وعندما نجمع الأعداد التقريبية معًا تتراكم أخطاء التقريب. الحساب بالنقطة العائمة له مزايا تفوق عيوبه في العديد من التطبيقات، كالرسوميات الحاسوبية، والتشفير، والتحليل الإحصائي، وإظهار (rendering) الوسائط المتعددة. لكن إذا أردت دقة مطلقة، عليك استخدام الأعداد الصحيحة بدلًا منها. خذ على سبيل المثال حساب بنك فيه رصيد قيمته 123.45$:
double balance = 123.45; // potential rounding error
في هذا المثال، ستصبح الأرصدة غير دقيقة مع الوقت واستخدام المتغير في العمليات الحسابية كالسحب والإيداع. ستكون النتيجة سخط العملاء أو دعاوى قضائية. يمكنك تفادي المشكلة بتمثيل الرصيد كعدد صحيح:
int balance = 12345; // total number of cents
هذا الحل صحيح طالما أن عدد السنتات لا يتجاوز أكبر قيمة صحيحة يمكن تخزينها، وهي حوالي 2 مليار.
العمليات على السلاسل المحرفية
بشكل عام، لا يمكنك تطبيق العمليات الرياضية على السلاسل المحرفية، حتى لو كانت السلاسل المحرفية تبدو وكأنها أرقام. التعابير التالية غير مشروعة:
“Hello” – 1
“World” / 123
“Hello” * “World”
العامل + يعمل مع السلاسل المحرفية، لكنه قد لا ينتج ما تتوقعه. يجري عامل + عملية ربط السلاسل (concatenation) أي دمج المعاملين بوصلهما معًا.
وهكذا فإن “Hello” + “World” ستعطي السلسلة “Hello World”. أو إذا كان لديك متغير اسمهname من النوع String، فسوف يدمج التعبير “Hello” + “Name” قيمة name مع كلمة الترحيب. بما أن عملية الجمع معرفة للأرقام والسلاسل المحرفية أيضًا، فإن Java تجري عمليات تحويل تلقائية قد لا تتوقعها:
System.out.println(1 + 2 + “Hello”);
// the output is 3Hello
System.out.println(“Hello” + 1 + 2);
// the output is Hello12
تنفذ Java هذه العمليات من اليسار إلى اليمين. في السطر الأول، 1 + 2 يساوي 3، و 3 + “Hello” يساوي “3Hello”.
أما في السطر الثاني، Hello” + 1″ يساوي “Hello1″، و Hello1″ + 2” يعطي “Hello12”.
ترتيب الحساب
عندما يظهر أكثر من عامل في تعبير حسابي فسوف تنفذ حسب ترتيب العمليات (order of operation). بشكل عام، تنفذ Java العمليات حسب ترتيب ورودها من اليسار إلى اليمين (كما رأينا في القسم السابق). لكن Java تتبع قواعد الرياضيات في العمليات الحسابية:
- عمليتي الضرب والقسمة لهما ”أولوية“ (precedence) على الجمع والطرح. لذا فإن 3 * 2 + 1 سيعطي 7، وليس 9، كما أن 2 / 4 + 2 تعطي 4، وليس 3.
- إذا كان للعوامل نفس الأولوية فسوف تنفذ بالترتيب من اليسار إلى اليمين. ففي التعبير الحسابي minute * 100 / 60، يتم تنفيذ عملية الضرب أولًا، وإذا كانت قيمة minute هي 59 فسوف ينتج لدينا 60 / 5900، والذي بدوره يعطي 98. لو أن تنفيذ العملية الحسابية جرى من اليمين لليسار، ستكون النتيجة 1 * 59 والذي هو 59، وهو جواب خاطئ.
- في أي وقت ترغب فيه بتجاوز قواعد الأولوية (أو أنك لم تكن واثقًا من تلك القواعد) يمكنك استعمال الأقواس. يتم تنفيذ العمليات ضمن الأقواس أولًا، لهذا فإن 3 * (2 + 1) يعطي 9. يمكنك استعمال الأقواس أيضًا لجعل العبارات الحسابية أسهل للقراءة، كما في 60 / (minute * 100)، مع أنها لا تغير النتيجة. لا تجهد نفسك في حفظ ترتيب تنفيذ العمليات، خصوصًا مع العوامل الأخرى. إذا لم يكن ترتيب التنفيذ واضحًا عند النظر إلى التعبير، فاستخدم الأقواس لجعله واضحًا.
التركيب
في الأجزاء السابقة كنا نتعرف على مكونات لغة البرمجة: المتغيرات، والتعابير، والتعليمات بشكل مستقل، دون أن نناقش طريقة استخدامها معاً. أحد أهم ميزات لغات البرمجة هي قدرتها على تركيب (compose) الأجزاء الصغيرة مع بعضها. مثلاً، نحن نعرف كيف نضرب الأرقام ونعرف كيف نعرض القيم. يمكننا دمج هاتين العمليتين في تعليمة واحدة:
System.out.println(17 * 3);
يمكن استخدام أي تعبير حسابي داخل تعليمات الطباعة. لقد شاهدنا مثالاً على هذا من قبل:
System.out.println(hour * 60 + minute);
يمكنك أيضاً وضع تعابير حسابية متنوعة على الطرف الأيمن لعملية الإسناد:
int percentage;
percentage = (minute * 100) / 60;
لكن الطرف الأيسر لا بد أن يكون اسم متغير، وليس تعبيراً. ذلك لأن الطرف الأيسر يدل على موقع تخزين النتيجة، والتعابير لا تمثل مواقع تخزينية.
hour = minute + 1; // correct
minute + 1 = hour; // compiler error
قد لا تبهرك القدرة على تركيب العمليات الآن، لكننا سنرى لاحقاً أمثلة تسمح لنا بكتابة حسابات معقدة بشكل مرتب وأنيق. لكن لا تبالغ كثيراً، فالتعابير الكبيرة المعقدة قد تصعب قراءتها وتنقيحها من الأخطاء.
أنواع الأخطاء
هناك ثلاثة أنواع يحتمل أن تحدث في البرنامج:
- أخطاء الترجمة،
- أخطاء التنفيذ،
- الأخطاء المنطقية
من المفيد التمييز بينها في سبيل تتبعها بشكل أسرع.
تحدث أخطاء الترجمة (compile-time errors) عندما تخالف التراكيب النحوية (syntax) للغة Java. مثلاً، يجب أن تكون أزواج الأقواس متناظرة. ولذلك فإن (2 + 1) صيغة مقبولة أما (8 ليست كذلك. في الحالة الثانية، لن تتمكن من ترجمة البرنامج، وسيعرض المترجم رسالة خطأ. تدل رسائل الخطأ التي يعرضها المترجم على موقع حدوث الخطأ في البرنامج عادة، وأحياناً تخبرك بطبيعة الخطأ بدقة. على سبيل المثال، لنعد إلى برنامج hello world:
public class Hello {
public static void main(String[] args) {
// generate some simple output
System.out.println(“Hello, World!”);
}
}
إذا نسيت الفاصلة المنقوطة في نهاية تعليمة الطباعة، فقد تظهر لك رسالة خطأ كما يلي:
File: Hello.java [line: 5]
Error: ‘;’ expected
هذا جيد جداً: موقع الخطأ صحيح، ورسالة الخطأ تخبرك بالمشكلة. لكن رسائل الأخطاء ليست يسيرة الفهم دوماً. أحياناً يعطي المترجم مكان اكتشاف الخطأ في البرنامج، وليس مكان حدوثه حقاً. وأحياناً يكون وصف المشكلة محيراً أكثر مما هو مفيد. مثلاً، إذا نسيت قوس الإغلاق المعقوف في نهاية main (سطر 6)، قد تحصل على رسالة تشبة الرسالة التالية:
File: Hello.java [line: 7]
Error: reached end of file while parsing
هناك مشكلتان هنا. أولاً، رسالة الخطأ مكتوبة من وجهة نظر المترجم، وليس وجهة نظرك أنت. عملية الإعراب (parsing) هي عملية قراءة البرنامج قبل الترجمة؛ فإذا وصل المترجم لنهاية الملف قبل انتهاء الإعراب، فهذا يدل على نقصان شيء ما. لكن المترجم لا يعرف ما هو. كما أنه لا يعرف أين. يكتشف المترجم الخطأ عند نهاية البرنامج (سطر 7)، لكن القوس الناقص يجب أن يكون على السطر السابق. تحوي رسائل الأخطاء معلومات مفيدة، لذلك عليك محاولة قراءتها وفهمها. لكن لا تأخذها بشكل حرفي تماماً. ستمضي غالباً وقتاً طويلاً خلال الأسابيع الأولى في سيرتك البرمجية وأنت تتابع أخطاء الترجمة. لكن مع زيادة خبرتك، سوف ترتكب أخطاءً أقل وستعثر عليها أسرع. النوع الثاني من الأخطاء هي أخطاء التنفيذ (run-time errors)، وقد سمّيت كذلك لأنها لا تظهر قبل تنفيذ البرنامج.
في Java، تظهر هذه الأخطاء عندما ينفذ المفسر شفرة بايت ويحدث خطأ ما. تدعى هذه الأخطاء “استثناءات” (exceptions) لأنها تدل عادة على حدوث شيء استثنائي (وسيء). أخطاء التنفيذ نادرة في البرامج البسيطة التي ستراها في الفصول القليلة الأولى، لذلك قد لا ترى واحداً إلا بعد حين. عند حدوث خطأ تنفيذي، يعرض المفسر رسالة خطأ تحوي معلومات تشرح ما حدث وتحدد مكان حدوثه. مثلاً، إذا أجريت عملية قسمة على صفر عن غير قصد فسوف تظهر رسالة تشبه ما يلي:
Exception in thread “main” java.lang.ArithmeticException: / by zero
at Hello.main(Hello.java:5)
بعض هذه المعلومات مفيد في تنقيح البرنامج من الأخطاء. يتضمن السطر الأول اسم الاستثناء java.lang.ArithmeticException ورسالة تبين ما حدث بدقة أكبر “by zero /“. يظهر السطر التالي العملية التي حدث فيها الخطأ؛ حيث يقصد بعبارة Hello.main العملية main في الصنف Hello. كما أنه يذكر أيضاً اسم الملف الذي عرفت فيه العملية (Hello.java) ورقم السطر الذي حدث فيه الخطأ (5).
أحياناً تحوي رسائل الأخطاء معلومات إضافية لن تفهم معناها الآن. لذلك سيكون أحد التحديات معرفة الأجزاء المفيدة دون أن تغرق بالمعلومات الإضافية. وعليك أن تنتبه أيضاً أن السطر الذي سبب انهيار تنفيذ البرنامج قد لا يكون السطر الذي يحتاج للتصحيح. النوع الثالث من الأخطاء هو الأخطاء المنطقية (logic error). إذا كان هناك خطأ منطقي في برنامجك، فستتم ترجمته وتشغيله دون تولد أي رسالة خطأ، لكنه لن يعطي الناتج الصحيح المطلوب، بل سينفذ ما طلبته منه حرفياً. مثلاً، هذا برنامج hello world فيه خطأ منطقي:
public class Hello {
public static void main(String[] args) {
System.out.println(“Hello, “);
System.out.println(“World!”);
}
}
هذا البرنامج يترجم وينفذ بشكل سليم، لكن الخرج هو:
Hello,
World!
على فرض أننا أردنا أن يكون الخرج على سطر واحد، فهذا الناتج غير صحيح. المشكلة هي أن السطر الأول يستخدم println، بينما نحن أرنا غالباً استخدام print.
قد يصعب التعرف على الأخطاء المنطقية لأنك ستضطر للعمل بالمقلوب: تنظر إلى مخرجات البرنامج وتحاول استنتاج السبب الذي يجعله يعطي هذه النتائج الخاطئة، وكيف تجعله يعطي النتائج الصحيحة. عادة لا يستطيع المترجم ولا المفسر مساعدتك هنا، لأنهما لا يعرفان ما هو الشيء الصحيح المطلوب.
من الجميل وضع بصمتك الخاصة اترك لنا رد...