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

الاستثناءات exceptions وتعليمة try..catch في جافا


رضوى العربي

تسهُل كتابة البرامج عادةً بصورةٍ مثاليةٍ عن كتابتها لتكون متينة robust، حيث تتأقلم البرامج المتينة مع أيّ ظروفٍ استثنائيةٍ تواجهها دون أن تنهار crash، كما يمكنك أن تكتبها عن طريق تحديد المشاكل التي يُحتمل أن تقع، وكذلك تضمين الاختبارات الضرورية لكلّ مشكلةٍ منها؛ فمثلًا، إذا كان لدينا مصفوفة A واستخدَم برنامجٌ عنصر المصفوفة A[‎i]‎، فإنه قد ينهار إذا كانت قيمة i خارج النطاق المسموح به لقيم فهارس indices تلك المصفوفة؛ إذًا، ينبغي لأيّ برنامجٍ متينٍ أن يتوقّع احتمالية استخدام فهرسٍ غير صالحٍ، ويوفّر أيضًا الحماية الضرورية؛ ونستطيع أن نكتب برنامجًا يتأكد من أن الفهرس المستخدَم سيقع دائمًا ضِمن النطاق المسموح به بأن يكون الشرْط اللاحق للشيفرة التي تسبِق استخدام عنصر المصفوفة، أو اختبار صلاحية قيمة الفهرس index قبل استخدامها للإشارة إلى عنصرٍ بالمصفوفة كالتالي:

if (i < 0 || i >= A.length) {
   ...  // Do something to handle the out-of-range index, i
}
else {
   ...  // Process the array element, A[i]
}

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

الاستثناءات وأصناف Exception

توفّر جافا تقنيةً أكثر تنظيمًا وأناقةً للتعامل مع الأخطاء التي قد تحدُث أثناء تشغيل البرنامج، وعادةً ما يشار إلى تلك التقنية باسم معالجة الاستثناءات exception handling، إذ يُعَد الاستثناء مفهومًا أشمل من الخطأ، حيث يتضمّن أيّ ظروفٍ قد تحدُث أثناء تنفيذ البرنامج وتحيده عن مساره الطبيعي.

اقتباس

لاحظ أن الاستثناء قد يكون خطأً أو مجرّد حالةٍ خاصةٍ لا ترغب لها أن تعبَث في خوارزميتك.

عمومًا ما يُقال أن البرنامج بلّغ thrown عن استثناء عند حدوث خطأٍ أثناء تنفيذ البرنامج، وفي تلك الحالات يخرُج البرنامج عن مساره الطبيعي وقد ينهار؛ ويمكن تجنُّب ذلك بالتقاط caught الاستثناء ومعالجته بطريقةٍ ما؛ وجرت العادة أن يبلّغ جزءٌ من البرنامج عن الاستثناء فيما يلتقطه جزءٌ آخر، وبينما يتسبّب الاستثناء -إذا لم يُلتقط- في انهيار البرنامج. يمكن للخيوط الأخرى في البرامج متعددة الخيوط multithreaded أن تستمر بالعمل حتى بعد انهيار إحداها؛ فمثلًا، تُعَد برامج واجهة المستخدم الرسومية واحدةٌ من البرامج متعددة الخيوط التي يمكن لبعض أجزائها أن تستمر بالعمل حتى إذا تعطّلت بعض أجزائها الأخرى نتيجةً لحدوث استثناء ما.

نظرًا لأن برامج جافا تنفَّذ بواسطة مفسِّر جافا interpreter، لذلك فإن انهيار برنامجٍ معينٍ لا يعني انهيار المفسِّر، وإنما يعني انتهاء البرنامج قبل أوانه، بصورةٍ أكثر تحديدًا يلتقِط المفسِّر أي استثناء لم يلتقطه البرنامج ثم ينهي ذلك البرنامج؛ أما في لغاتٍ برمجيةٍ أخرى فقد يؤدي انهيار برنامج إلى انهيار النظام بأكمله إلى أن يُعاد تشغيله. الأمر الذي يستحيل حدوثه في لغة جافا.

لقد تعرّضنا لكل من الاستثناءات وتعليمة try..catch المستخدَمة لالتقاط الاستثناءات ومعالجتها، إلا أنه ما تزال هناك بعض قواعد الصيغة syntax الخاصة بتلك التعليمة التي سنناقشها خلال هذا المقال.

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

يجب أن تنتمي الكائنات المبلّغ عنها إلى صنفٍ فرعيٍ subclass من الصنف القياسي java.lang.Throwable، وعمومًا يمثَّل كلّ نوعٍ مختلفٍ من الاستثناءات بواسطة صنفٍ فرعيٍ خاصٍ مُشتقٍ من Throwable، وترتَّب تلك الأصناف الفرعية وفقًا لسلالة أصنافٍ class hierarchy معقدةٍ تُظهر العلاقات بين أنواع الاستثناءات المختلفة، كما يملك الصنف Throwable صنفين فرعيين مباشرين هما Error وException، وبدورهما يملكان الكثير من الأصناف الفرعية الأخرى المعرّفة مسبقًا؛ إذًا يستطيع المبرمِج أن ينشئ أصناف استثناءات جديدةٍ لتمثيل أنواعٍ جديدةٍ من الاستثناءات.

تمثِّل أغلب الأصناف الفرعية المشتقّة من الصنف Error أخطاءً خطيرةً في آلة جافا الافتراضية Java virtual machine، كما تتسبّب عادةً في انتهاء البرنامج لعدم توفُّر أي طريقةٍ لمعالجتها، وعمومًا لا يحبَّذ أن تُلتقط تلك النوعية من الأخطاء، فمثلًا يقع الخطأ ClassFormatError عندما تحاول آلة جافا الافتراضية العثور على بياناتٍ غير صالحةٍ في ملفٍ يُفترض أنه يحتوي على شيفرة صنف مصرّفة، فإذا كان الصنف الجاري تحميله جزءًا من البرنامج، فلن يُستكمل البرنامج.

بينما تمثِّل الأصناف الفرعية المشتقّة من الصنف Exception استثناءات يمكن التقاطها، وفي حالاتٍ كثيرةٍ تُعَد الأخطاء errors استثناءات أيضًا، إلأ أنها تكون أخطاءً ضمن البرنامج أو ضمن البيانات المُدخَلة والتي يتوقّعها المبرمِج ويستجيب لها بالطريقة المناسبة.

اقتباس

لاحظ أنه ليس من الصواب أن تلتقِط جميع أنواع الاستثناءات التي يمكن أن تحدُث لتمنع برنامجك من الانهيار؛ فإذا لم يكن لديك سببًا معقولًا لتستجيب لخطأٍ معينٍ، فمن الأفضل أن تترك البرنامج ينهار بدلًا من استمراره الذي قد يؤدي إلى خسائر أكبر بكثيرٍ.

يملِك الصنف Exception صنفًا فرعيًا اسمه RuntimeException، ويتضمّن ذلك الصنف على الكثير من أصناف الاستثناءات الشائعة بما في ذلك كلّ الأصناف التي تعرّضنا لها بالأقسام السابقة؛ فمثلًا الصنفان الآتيان:

  • IllegalArgumentException
  • NullPointerException

مشتقّان من الصنف RuntimeException، وعمومًا تشير تلك النوعية من أصناف الاستثناءات إلى وجود خطأٍ برمجيٍ bug ينبغي على المبرمِج إصلاحه، بينما يتجاهل البرنامج احتمالية حدوث الاستثناءات من النوعين RuntimeExceptions وError لينهار في حالة وقوعها، الأمر الذي بالفعل في كل مرة يستخدِم فيها البرنامج عنصر مصفوفة A[‎i]‎ بدون أن يستعدّ لالتقاط استثناء من النوع التالي:

 ArrayIndexOutOfBoundsException

 وتُعَد معالجة أصناف الاستثناءات الأخرى من غير الصنفين Error وRuntimeExceptions عمليةً إجباريةً mandatory، الأمر الذي سيتضّح معناه بعد قليل.

تظهِر سلالة الأصناف الصنف Throwable وعددٌ قليلٌ من أصنافه الفرعية، حيث تتطلّب الأصناف الملونة باللون الأحمر معالجة استثناءات اجباريةٍ mandatory exception-handling، كما في الصورة التالية:

001Exception_hierarchy.png

يتضمّن الصنف Throwable مجموعةً من توابع النسَخ instance methods التي تستخدمها كائنات الاستثناء، فإذا كان e ينتمي إلى الصنف Throwable أو إلى أيٍ من أصنافه الفرعية، فإن الدالة e.getMessage()‎ تعيد سلسلةً نصيةً من النوع String لتصِف ذلك الاستثناء، وكذلك تعيد الدالة e.toString()‎ التي يستخدمها النظام للتمثيل النصي string representation للكائنات سلسلةً نصيةً من النوع String تتكون من اسم الصنف الذي ينتمي إليه الاستثناء، بالإضافة إلى نفْس السلسلة النصية التي تعيدها الدالة e.getMessage()‎، كما يطبع التابع e.printStackTrace()‎ استدعاءات المكدّس stack trace إلى الخرْج القياسي، الأمر الذي قد يفيد عند التعرّف على سبب مشكلةٍ ما، وفي حالة لم يلتقط البرنامج استثناءً معينًا، ترسَل قائمة الاستدعاءات إلى الخرْج القياسي بصورةٍ افتراضيةٍ.

تعليمة Try

تلتقِط تعليمة try الاستثناءات في برامج جافا، حيث تشبه تعليمات try التعليمة التالية:

try {
    double determinant = M[0][0]*M[1][1] - M[0][1]*M[1][0];
    System.out.println("The determinant of M is " + determinant);
}
catch ( ArrayIndexOutOfBoundsException e ) {
   System.out.println("M is the wrong size to have a determinant.");
   e.printStackTrace();
}

سيحاول الحاسوب أن ينفّذ كتلة التعليمات التالية لكلمة try، وإذا لم يحدث استثناء أثناء تنفيذها، فإنه سيتجاهل تمامًا عبارة catch ضمن التعليمة؛ أما إذا حدث استثناء من النوع
 

ArrayIndexOutOfBoundsException

 فسيقفز الحاسوب إلى عِبارة catch الخاصة بالتعليمة، حيث تعالِج عِبارة catch في هذه الحالة الاستثناء من النوع
 

ArrayIndexOutOfBoundsException

 إذ ستمنع معالجة الاستثناء بهذه الطريقة البرنامج من الانهيار.

اقتباس

لاحظ أن الكائن الممثِّل للاستثناء يسنَد إلى المتغيّر e قبل تنفيذ عِبارة catch التي تستخدمه لطباعة استدعاءات المكدّس.

تتيح قواعد صيغة تعليمة try الكثير من الخيارات الأخرى، فمثلًا يمكن لتعليمة try..catch أن تتضمّن أكثر من عِبارة catch واحدةٍ، الأمر الذي ما يسمَح بالتقاط أنواعٍ مختلفةٍ من الاستثناءات ضِمن تعليمةٍ try واحدةٍ، ويمكن أن يحدَث استثناء من النوع NullPointerException إذا كانت قيمة M فارغةٌ بالإضافة إلى الاستثناء من النوع
 

ArrayIndexOutOfBoundsException

الذي حدَث في المثال السابق، ونعالِج لاستثناءين بإضافة عبارة catch أخرى إلى تعليمة try، هكذا:

try {
    double determinant = M[0][0]*M[1][1] - M[0][1]*M[1][0];
    System.out.println("The determinant of M is " + determinant);
}
catch ( ArrayIndexOutOfBoundsException e ) {
   System.out.println("M is the wrong size to have a determinant.");
}
catch ( NullPointerException e ) {
   System.out.print("Programming error!  M doesn't exist." + );
}

سيحاول الحاسوب أن ينفّذ التعليمات ضِمن عِبارة try، فإذا لم يقع أيّ خطأٍ، سيتخطّى الحاسوب عِبارتي catch في المثال السابق؛ أما إذا حدث استثناء من النوع
 

ArrayIndexOutOfBoundsException

 فسينفّذ الحاسوب عِبارة catch الأولى ويتخطّى الثانية، بينما إذا حدث استثناء من النوع NullPointerException، فسينفّذ الحاسوب عِبارة catch الثانية ويتجاهل عِبارة catch الأولى.

يُشتّق الصنفان التاليان من الصنف  RuntimeException:

  • ArrayIndexOutOfBoundsException
  • NullPointerException

ولذلك يسهُل أن تلتقِط جميع الاستثناءات من النوع RuntimeException بعبارة catch واحدةٍ كما في المثال التالي:

try {
    double determinant = M[0][0]*M[1][1] - M[0][1]*M[1][0];
    System.out.println("The determinant of M is " + determinant);
}
catch ( RuntimeException err ) {
   System.out.println("Sorry, an error has occurred.");
   System.out.println("The error was: " + err);
}

ستَلتقط عبارة catch في تعليمة try السابقة أيّ استثناء مشتقٍ من الصنف RuntimeException أو من أصنافه الفرعية؛ ويوضِّح ذلك هدف تنظيم الأصناف الممثِّلة للاستثناءات في هيئة سلالة أصنافٍ، حيث يمكنك أن تضيّق نطاق الالتقاط على نوع استثناء محدّدٍ أو توسّعه ليلتقط نطاقًا أوسع من أنواع الاستثناءات.

اقتباس

لاحظ أنه في حالة وجود أكثر من عِبارة catch ضمن تعليمة try، قد يتوافق استثناء معينٌ مع أكثر من عِبارة catch واحدةٍ، فمثلًا سيتوافق استثناء من النوع NullPointerException مع عبارات catch المخصّصة لالتقاط NullPointerException أو RuntimeException أو Exception أو Throwable؛ وفي تلك الحالات، ينفّذ الحاسوب أول عبارةٍ catch تتوافق مع نوع الاستثناء المبلّغ عنه.

يتوافق ملتقِط الاستثناءات من النوع RuntimeException مع أنواعٍ كثيرةٍ أخرى غير تلك التي تكون موضع الاهتمام، ولذلك يمكنك أن تدمج أنواع الاستثناءات المتوقّعة ضِمن عِبارة catch واحدةٍ كالتالي:

try {
    double determinant = M[0][0]*M[1][1] - M[0][1]*M[1][0];
    System.out.println("The determinant of M is " + determinant);
}
catch ( NullPointerException | ArrayIndexOutOfBoundsException err ) {
   System.out.println("Sorry, an error has occurred.");
   System.out.println("The error was: " + err);
}

دمجنا في المثال السابق الاستثناءين بواسطة محرِّف الخط العمودي "|" الذي يُستخدم أيضًا لتمثيل العامل المنطقي or، حيث تلتقط عبارة catch في المثال السابق الأخطاء من النوعين الآتيين فقط:

  •  NullPointerException
  • ArrayIndexOutOfBoundsException

في الحقيقة، المثال السابق غير واقعيٍ؛ لأنه من يستبعَد استخدام معالجة الاستثناءات لتخطّي أخطاءٍ مثل استخدام مؤشرٍ فارغٍ null pointer أو استخدام فهرسٍ غير صالحٍ لمصفوفة، ولذلك عليك أن تتأكّد من أن البرنامج قد أسنَد assign قيمةً غير فارغةٍ للمصفوفة M، فلو كانت لغة جافا ستجبرك على كتابة تعليمة try..catch في كل مرةٍ تستخدم فيها مصفوفة، لكنت ستشعر بالاستياء بلا شك، ولهذا السبب لا تُعَد معالجة الاستثناءات من النوع RuntimeException عمليةً ضروريةً أو إلزاميةً؛ فهناك أمورً كثيرةً قد تقع على نحوٍ خاطئٍ، وعمومًا نستنتج من ذلك أن معالجة الاستثناءات لا تمثِّل حلًا لمشكلة متانة البرامج، وإنما هي مجرّد أداةٍ تساعدك على حلّ المشكلة بطريقة أكثر تنظيمًا في معظم الحالات.

نستكمل حديثنا عن قواعد الصيغة الخاصة بتعليمة try، فبالإضافة إلى ما سبق، يمكنك أن تضيف عِبارة finally في النهاية على النحو التالي:

try {
   statements
}
optional-catch-clauses
optional-finally-clause
اقتباس

لاحظ أن عِبارات catch في المثال السابق اختياريةٌ، بمعنى أن تعليمة try قد تتضمن عبارة catch واحدةٍ أو أكثر إلى جانب عبارة finally محتملة بالنهاية، وقد لا تتضمنها كذلك؛ لذا من المهم أن تخصِّص واحدةً منهما على الأقل، أي قد تحتوي تعليمة try على عِبارة finally واحدةٍ أو أكثر، وكذلك قد تحتوي على واحدةٍ أو أكثر من عِبارات catch.

سنكتب عبارة catch بالصيغة التالية:

catch ( exception-class-names variable-name ) {
   statements
}

قد تمثِّل exception-class-names صنف استثناء واحدٍ أو عدّة أصنافٍ يفصِل بينها المحرِّف "|"، وسنكتب عِبارة finally بالصيغة التالية:

finally {
   statements
}

تنفَّذ عِبارة finally دائمًا مثل خطوةٍ أخيرةٍ ضِمن تعليمة try، وسواءٌ إذا حدَث استثناء وعولِج أولم يعالَج، أو إذا لم يحدُث من الأساس، ستتضمّن عبارة finally بعض الشيفرة المسئولة عن عمليات التنظيف cleanup التي ستنفَّذ في جميع الأحوال، وأحد الأمثلة على شيفرة التنظيف هو غلْق الاتصال الشبكي المفتوح انظر مثلًا إلى خوارزمية الشيفرة الوهمية (استخدمنا شيفرةً وهميةً لأننا لم نناقش الشبكات بعد) التالية:

try {
    // افتح اتصال شبكي
   open a network connection
    // تفاعَل عبر الاتصال
   communicate over the connection
}
catch ( IOException e ) {
    // بلِّغ عن خطأ
   report the error
}
finally {
    // إذا كان الاتصال قد فُتح بنجاح
   if the connection was successfully opened
       // أغلِق الاتصال
      close the connection
}

تتأكّد عِبارة finally من أن الاتصال الشبكي قد أُغلق بصوةٍ مؤكَّدةٍ سواءٌ حدَث خطأٌ أو لم يحدُث أثناء الاتصال، حيث تتبّع الشيفرة الوهمية في المثال السابق نمطًا pattern شائعًا عندما تتعامل مع الموارد resources، فتحصل أولًا على المورِد ثم استخدامه وبعد ذلك تحرِّره release في نهاية الأمر.

اقتباس

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

يشتَرط لنفعّل ذلك الخيار أن يمثَّل المورد بواسطة كائنٍ ينفِّذ implement واجهة interface تُعرف بواجهة AutoCloseable، حيث تعرِّف تابعًا اسمه close()‎ دون أية معامِلات parameters؛ وعمومًا تنفِّذ أصناف جافا القياسية الممثِّلة لأشياء مثل الملفات والاتصالات الشبكية تلك الواجهة بالفعل، كما ينفِّذ الصنف Scanner تلك الواجهة، وستستخدِم الشيفرة التالية النمط السابق لتتأكّد من غلْق كائنٍ من الصنف Scanner بصورةٍ تلقائيةٍ:

try( Scanner in = new Scanner(System.in) ) {
    // استخدم الصنف لتقرأ الدخل القياسي
}
catch (Exception e) {
    // حدثت بعض الأخطاء أثناء استخدام الصنف 
}

يُشترط للتعليمة التي تنشِئ كائنًا من الصنف Scanner أن توضع ضِمن قوسين بعد كلمة try، وأن يعرّف المتغيّر variable declaration الموجود بها وأن تتضمّن تهيئةً initialization مبدئيةً لقيمة المتغيّر، بمعنى أن يكون المتغيّر محليًا local، كما يمكنك أن تعرِّف عِدة متغيّراتٍ يُفصل بينها بفاصلةٍ منقوطةٍ ضِمن هذين القوسين؛ وتضمن الطريقة السابقة نجاح استدعاء النظام للدالة in.close()‎ في نهاية تعليمة try بشرْط أن يُهيأ الكائن الممثِّل للصنف Scanner بنجاح.

ما تزال هناك خياراتٌ أخرى توفّرها تعليمة try، ويمكنك أن تطّلع على برنامج TryStatementDemo.java، حيث يتضمّن أمثلةً على جميع تلك الخيارات،كما ستجد الكثير من التعليقات التي قد تساعدك على فهْم ما قد يحدُث عند تنفيذ البرنامج.

التبليغ عن الاستثناءات Throwing Exceptions

قد يبلِّغ البرنامج نفْسه في بعض الأحيان عن اسستثناء بصورةٍ متعمَّدة، فمثًا قد يكتشِف البرنامج نوعًا معينًا من الأخطاء التي لا يجد طريقةً معقولةً ليعالجها ضِمن تلك النقطة من البرنامج التي حدثت فيها المشكلة، وفي تلك الحالة، يستطيع البرنامج أن يبلِّغ عن استثناء على أمل أن يلتقطه جزءٌ آخرٌ من البرنامج ويعالجه؛ وسنستخدِم تعليمة throw لذلك، حيث تُكتب على النحو التالي:

throw  exception-object ;

يجب أن ينتمي exception-object إلى أحد الأصناف الفرعية المشتقّة من Throwable، وعادةً ما ينتمي إلى صنفٍ فرعيٍ من الصنف Exception على وجه التحديد، ويُنشَأ باستخدام العامل new كالتالي:

throw new ArithmeticException("Division by zero");

يمثِّل المُعامل الممرِّر للمنشِأ رسالة الخطأ ضِمن كائن استثناء، فمثلًا إذا كان e يشير إلى ذلك الكائن، فتُستعاد نفْس رسالة الخطأ باستدعاء e.getMessage()‎.

قد تجد أن المثال السابق سخيفًا نوعًا ما؛ لأنك قد تتوقّع أن النظام سيبلّغ عن استثناء من النوع ArithmeticException عندما يَقسم عددًا على 0، فإذا كانت الأعداد المقسومة من النوع int، إذًا ستؤدي القسمة على 0 إلى حدوث استثناء من النوع المذكور، بينما إذا تضمنت العمليات الحسابية أعدادًا حقيقيةً، فلن تقع أيّ استثناءاتٍ نهائيًا، وإنما ستُستخدم القيمة الخاصة Double.NaN لتمثِّل عمليةً غير صالحةٍ؛ ومع ذلك قد يُبلّغ أحيانًا عن استثناء من النوع ArithmeticException عند قسمة عددٍ حقيقيٍ على 0.

تعالَج الاستثناءات المبلّغ عنها سواءٌ عن طريق النظام أو تعليمة throw بنفْس الطريقة، سنفترض أن لدينا استثناءً بُلغ عنه ضِمن تعليمة try، فإذا احتوت تلك التعليمة على عِبارة catch تتوافق مع نوع الاستثناء المبلّغ عنه، فسيقفز الحاسوب إلى تلك العِبارة وينفّذها، وحينها يعالَج الاستثناء؛ ثم سينفّذ الحاسوب عِبارة finally إذا ضمِّنت في تعليمة try وسيستمر الحاسوب في تنفيذ شيفرة البرنامج بصورةٍ طبيعيةٍ، أي سينتقل إلى التعليمات التالية لتعليمة try، بينما إذا لم يُلتقط الاستثناء ويعالَج على الفور، فسيستمر الحاسوب بعملية معالجة الاستثناء.

ينتهي البرنامج بعد تنفيذ عِبارة finally ويحصُل البرنامج routine المستدعي لذاك البرنامج الفرعي على فرصةٍ ليعالِج الاستثناء إذا حدث استثناء أثناء تنفيذ برنامجٍ فرعيٍ، ولم يعالَج بواسطة البرنامج الفرعي؛ فمثلًا، إذا وقَعت عملية استدعاء البرنامج الفرعي ضِمن تعليمة try تتوافق عِبارة catch الموجودة بها مع نوع الاستثناء المبلّغ عنه، فسينفِّذ الحاسوب تلك العِبارة وسيستمر بعدها في تنفيذ البرنامج بصورةٍ طبيعيةٍ، بينما إذا لم يعالِج البرنامج الثاني الاستثناء أيضًا فسينتهي، وحينها سيحصل البرنامج المُستدعي له على فرصته ليعالج الاستثناء وهكذا، وفي حال مرّ الاستثناء بكامل سلسلة استدعاءات البرامج الفرعية دون أن يعالَج، فسينهار البرنامج بالكامل.

يُحتمل لأي برنامجٍ فرعيٍ أن يولّد استثناء، ويعلِن عن تلك الحقيقة صراحةً بإضافة العبارة throws exception-class-name إلى تعريفه، انظر المثال التالي:

// [1]
static public double root( double A, double B, double C ) 
                              throws IllegalArgumentException {
    if (A == 0) {
      throw new IllegalArgumentException("A can't be zero.");
    }
    else {
       double disc = B*B - 4*A*C;
       if (disc < 0)
          throw new IllegalArgumentException("Discriminant < zero.");
       return  (-B + Math.sqrt(disc)) / (2*A);
    }
}

[1] يعيد قيمة الجَذر الأكبر للمعادلة التربيعية Axx + Bx + C = 0، فإذا كان A == 0 أو BB - 4AC قيمةً سالبةً، حينها سيبلَّغ عن استثناء من النوع IllegalArgumentException.

تفترض الشيفرة السابقة أن الشرْطين المُسبَقين هما A != 0 وBB-4AC >= 0، وسيبلّغ البرنامج عن استثناء من النوع IllegalArgumentException إذا لم يتحقّق أي من الشرطين السابقين، وعمومًا، عندما يجد برنامجٌ فرعيٌ شرطًا غير صالحٍ فإنّه يبلِّغ عن الاستثناء بصورةٍ مناسبةٍ، فإذا امتلك البرنامج المستدعي للبرنامج الفرعي طريقةً مناسبةً لمعالجة الخطأ، فسيَلتقط الاستثناء ويعالجه، بينما سينهار البرنامج إذا لم يعالَج الاستثناء، وسيكون على المبرمج إصلاح برنامجه.

تتضمّن عبارة throw الموجودة تعريف definition البرنامج الفرعي أنواعًا مختلفةً من الاستثناءات، يمكن أن يُفصل بينها بفاصلةٍ منقوطةٍ كالتالي:

void processArray(int[] A) throws NullPointerException, 
                                         ArrayIndexOutOfBoundsException { ...

معالجة الأحداث الإجبارية Mandatory Exception Handling

وضّحنا في المثال السابق إمكانية تبليغ البرنامج الفرعي root()‎ عن استثناء من النوع IllegalArgumentException، الأمر الذي يُعَد مجرد مجاملةً للقارئين المحتملين للشيفرة، وذلك لأن الاستثناءات من النوع IllegalArgumentException ليست إجباريةً، إذ يمكن لأي برنامجٍ أن يبلّغ عن استثناء من النوع IllegalArgumentException دون أن يعلِن عن ذلك صراحةً، وعليه قد يَلتقط البرنامج المستدعي الاستثناء أو يتجاهله بنفس الكيفية التي يمكن لمبرمجٍ أن يختار التقاط استثناء من النوع NullPointerException أو يتجاهله.

يختلف الأمر بالنسبة لأصناف الاستثناءات التي تتطلّب ما يعرَف باسم المعالجة الإجبارية mandatory handling، فإذا أمكن لبرنامجٍ فرعيٍ أن يبلّغ عن استثناء من ذلك النوع، فيجب أن يعلَن عن ذلك صراحةً ضِمن تعريف البرنامج routine definition باستخدام عِبارة throws، حيث يُعَد عدم الإعلان في تلك الحالة خطأً في قواعد بناء الجملة syntax error، ويطلَق عليها اسم الاستثناءات المتحقَّق منها checked exceptions، أي أن المصرِّف سيفحص إذا ما كانت تلك الاستثناءات قد عولجت أم لا.

سنفترض أنه من الممكن لتعليمةٍ ضِمن برنامجٍ فرعيٍ أن تولّد استثناءً متحقَّقًا منه، بمعنى أنه استثناء يتطلّب معالجةً إجباريةً، فمثلًا قد تكون تعليمة throw إذا كان التبليغ مباشرًا، أو تعليمة استدعاءٍ لبرنامجٍ فرعيٍ آخرٍ قد أعلَن عن قدرته على التبليغ عن الاستثناء؛ وفي جميع الحالات، يجب أن يعالَج الاستثناء، وهو ما يتحقَّق بطريقةٍ من اثنتين، الأولى عن طريق وضع تلك التعليمة داخل تعليمة try تتضمّن عِبارة catch متلائمةً مع نوع الاستثناء، في تلك الحالة يعالِج البرنامج الفرعي الاستثناء، وبذلك لن يرى أي مستدعي له ذلك الاستثناء أبدًا، بينما يعلِن البرنامج الفرعي عن قدرته على التبليغ عن الاستثناء من خلال إضافة عِبارة throws إلى تعريفه لتنبيه أي مستدعي له من احتمالية حدوث استثناء أثناء الاستدعاء، وبذلك يَلزم ذلك المستدعي الجديد أن يعالِج الاستثناء ضِمن تعليمة try أو أن يعلِن بدوره عن إمكانية حدوث الاستثناء ضِمن تعريفه.

تُعَد معالجة الاستثناء إجباريةً لأي صنفٍ غير مشتقٍ من الصنف الفرعي RuntimeException أو الصنف Error، حيث تمثِّل الاستثناءات المتحقّق منها شروطًا لا يتحكّم بها المبرمج مثل قيمةٍ مُدخَلةٍ غير صالحةٍ أو فعلٍ غير صالحٍ من قبل المستخدم، ولأنه لا توجد طريقةٌ لتتجنّب وقوع مثل تلك الأخطاء، يَلزم أي برنامجٍ متينٍ أن يستعد لمعالجتها، ولهذا لا تسمح جافا للمبرمجين بتجاهُل معالجة تلك الأخطاء.

تحتوي برامج جافا للدخْل والخرْج على جزءٍ كبيرٍ من الاستثتاء المتحقَّق منها، ويعني ذلك أنه يصعُب استخدام تلك البرامج دون درايةٍ كافيةٍ بمفهوم معالجة الاستثناءات، وسنناقش عمليات الدخْل والخرْج وكيفية استخدام الاستثناءات المتحقَّق منها على بصورةٍ مكثّفةٍ في جزئية لاحقة من هذه السلسلة.

البرمجة باستخدام الاستثناءات

يساعدك استخدام الاستثناءات على كتابة برامجٍ متينةٍ لأنها توفّر أسلوبًا مُنظمًا ومُهيئًا للتعامل مع المتانة، إذ تحتاج عادةً عندما تكتب برنامجٍ متينٍ إلى كتابة الكثير من تعليمات if، الأمر الذي عادةً ما يشوّش على الشيفرة الأساسية، ولحسن الحظ، تمكّنك الاستثناءات من كتابة تنفيذٍ نظيفٍ يشمل جميع الحالات الطبيعية لخوارزميةٍ معينةٍ، بحيث تعالَج الحالات الاستثنائية بعبارة catch ضِمن تعليمة try.

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

اقتباس

لاحظ أن أصناف الاستثناء التي تنتمي للصنف RuntimeException أو أيًا من أصنافه الفرعية لا تتطلّب ما يعرَف باسم المعالجة الإجبارية للاستثناء، بينما سيُشتق صنف الاستثناء من الأصناف الفرعية الأخرى المشتقة من الصنف Exception أو أيًا من أصنافه الفرعية إذا تطلّب الاستثناء معالجةً إجباريةً.

فمثلًا، يُشتق الصنف التالي من الصنف Exception، وذلك لأنه يتطلّب معالجةً اجباريةً عند استخدامه:

public class ParseError extends Exception {
   public ParseError(String message) {
       // ‫أنشيء كائنًا من النوع ParseError بحيث يحتوي على رسالة الخطأ الممرَّرة
      super(message);
   }
}

يبني المُنشِئ constructor المُعرَّف سابقًا بإنشاء كائناتٍ من النوع ParseError، ويرث الصنف البرامج getMessage()‎ وprintStackTrace()‎ من الصنف الأعلى، كما تَستدعي التعليمة super(message)‎ المُنشِئ المُعرَّف في الصنف الأعلى superclass، فإذا كان e كائنًا من النوع ParseError، فسيسترجع استدعاء الدالة e.getMessage()‎ رسالة الخطأ المخصّصة بالمُنشِئ.

اقتباس

لاحظ أن الغرض الأساسي من الصنف ParseError هو وجوده، إذ يمثِّل التبليغ عنه وقوع نوعٍ معينٍ من الأخطاء.

سنستخدم تعليمة throw التالية للتبليغ عن استثناء من النوع ParseError، إذ يجب أن تمرّر رسالة خطأٍ إلى منشِئ الكائن، كما يلي:

throw new ParseError("Encountered an illegal negative number.");

أو هكذا:

throw new ParseError("The word '" + word 
                               + "' is not a valid file name.");

يُعَدParseError صنفًا فرعيًا من الصنف Exception، ولذلك فإنه يمثِّل استثناءً متحقَّقًا منه، بمعني أنه إذا لم تقع تعليمة throw المبلّغة عنه ضِمن تعليمة try لالتقاطه، فيجب أن يعلِن البرنامج الفرعي المتضمِّن للتعليمة عن قدرته على التبليغ عن استثناء من النوع ParseError بإضافة عبارة throws ParseError إلى تعريفه، هكذا:

void getUserData() throws ParseError {
   . . .
}

في المقابل، إذا اشتُق ParseError من الصنف الفرعي RuntimeException بدلًا من الصنف Exception، فلن يكون من الضروري إضافة العبارة السابقة؛ إذ يُعَد الاستثناء ParseError في هذه الحالة استثناءً متحقَّقًا منه.

تُستخدم تعليمة try مع عِبارة catch خاصة بالصنف ParseError إذا أراد برنامجٌ معينٌ أن يعالِج استثناء من النوع ParseError، هكذا:

try {
   getUserData();
   processUserData();
}
catch (ParseError pe) {
   . . .  // Handle the error
}

تَلتقط عبارةً مثل catch (Exception e)‎ الاستثناءات من النوع ParseError إلى جانب أيّ كائنٍ آخرٍ من النوع Exception، حيث يُعَد ParseError صنفًا فرعيًا من الصنف Exception.

يفيد عادةً تخزين بعض البيانات الإضافية ضمن الكائنات الممثِّلة للاستثناءات كالتالي:

class ShipDestroyed extends RuntimeException {
   Ship ship;  // Which ship was destroyed.
   int where_x, where_y;  // Location where ship was destroyed.
   ShipDestroyed(String message, Ship s, int x, int y) {
       super(message);
       ship = s;
       where_x = x;
       where_y = y;
   }
}

يتضمّن كائن ShipDestroyed المعرَّف في المثال السابق رسالة خطأٍ إلى جانب بعض المعلومات الإضافية عن السفينة المدمَّرة، انظر المثال التالي:

if ( userShip.isHit() )
   throw new ShipDestroyed("You've been hit!", userShip, xPos, yPos);

قد لا يمثِّل الشرط المستخدَم خطأً فعليًا، وإنما قد يكون مجرد انحرافٍ عن المسار الطبيعي للعبة، حيث تُستخدم الاستثناءات في بعض الأحيان لتعالِج مثل تلك الانحرافات بدقةٍ.

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

يحتاج البرنامج المستدعي للدالة أو المستخدِم للصنف إلى معرفة أن خطأً معينًا قد حدث بالفعل، وفي اللغات التي لا تدعم الاستثناءات، يكون البديل الوحيد هو إعادة قيمةٍ خاصةٍ أو ضبْط قيمة متغيّرٍ عامٍ global variable ليشير إلى حدوث الخطأ، فمثلًا، تعيد الدالة readMeasurement()‎ قيمة "1-" إذا كانت قيمة مُدخَل المستخدم غير صالحةٍ، ومع ذلك قد لا يَفحص البرنامج القيمة المعادة من الدالة، كما أنه لا يستخدِم القيمة "1-" للإشارة على حدوث خطأٍ من قراءة القياسات السالبة؛ ولهذه الأسباب تُعَدّ الاستثناءات الطريقة الأمثل التي ينبغي للبرامج الفرعية أن تَلجأ إليها عندما تواجه خطأً معينًا.

يَسهُل أن نعدّل الدالة readMeasurement()‎ لتبلّغ عن استثناء بدلًا من أن تعيد قيمةً خاصةً تمثِّل حدوث خطأٍ، فقد تبلّغ النسخة المعدّلة من البرنامج الفرعي عن استثناء من النوع ParseError المشتق من الصنف Exception إذا أدخل المستخدم قيمةً غير صالحةٍ، وقد يكون من الأنسب في تلك الحالة أن يبلَّغ عن استثناء من الصنف القياسي IllegalArgumentException بدلًا من اللجوء إلى تعريف صنفٍ جديدٍ، انظر النسخة المعدّلة من الدالة:

// [2]
static double readMeasurement() throws ParseError {

    double inches;  // حاصل مجموع البوصات الكلية

    double measurement;  // قيمة القياس المُدخَلة

    String units;        // وحدة القياس المُدخَلة

    char ch;  // لفحْص الحرف التالي من مُدخَل المستخدم


   inches = 0;  // No inches have yet been read.

   skipBlanks();
   ch = TextIO.peek();

    // [1]
   while (ch != '\n') {

       /* Get the next measurement and the units.  Before reading
          anything, make sure that a legal value is there to read. */

       if ( ! Character.isDigit(ch) ) {
           throw new ParseError("Expected to find a number, but found " + ch);
       }
       measurement = TextIO.getDouble();

       skipBlanks();
       if (TextIO.peek() == '\n') {
          throw new ParseError("Missing unit of measure at end of line.");
       }
       units = TextIO.getWord();
       units = units.toLowerCase();


       /* حوّل قيمة القياس إلى البوصة وأضفها إلى inches */

       if (units.equals("inch") 
               || units.equals("inches") || units.equals("in")) {
           inches += measurement;
       }
       else if (units.equals("foot") 
                  || units.equals("feet") || units.equals("ft")) {
           inches += measurement * 12;
       }
       else if (units.equals("yard") 
                  || units.equals("yards") || units.equals("yd")) {
           inches += measurement * 36;
       }
       else if (units.equals("mile") 
                  || units.equals("miles") || units.equals("mi")) {
           inches += measurement * 12 * 5280;
       }
       else {
           throw new ParseError("\"" + units 
                                + "\" is not a legal unit of measure.");
       }


       // اختبر إذا ما كان المحرِّف التالي هو محرِّف نهاية السطر أم لا

       skipBlanks();
       ch = TextIO.peek();

   }  // end while

   return inches;

} // end readMeasurement()

[1] إذا كان هناك مُدخَلات أخرى في السطر، اقرأ قيمة القياس وأضف مكافئها من البوصات إلى المتغيّر inches، وإذا وقع خطأٌ أثناء تنفيذ الحلقة، أنهِ البرنامج الفرعي فورًا وأعد القيمة "1-".

[2] اقرأ سطرًا واحدًأ من مُدخَلات المستخدِم، بحيث يكون الشرط المُسبَق: السطر المُدخَل غير فارغ، ويكون الشرط اللاحق: إذا كان مُدخَل المستخدم صالحًا، حوّل قيمة القياس إلى وحدة البوصة وأعدها مثل قيمةٍ للدالة، بينما إذا كانت قيمة المُدخَل غير صالحةٍ، ستبلّغ الدالة عن استثناء من النوع ParseError.

سنستدعي البرنامج الفرعي المعرَّف في المثال السابق داخل تعليمة try، هكذا:

try {
   inches = readMeasurement();
}
catch (ParseError e) {
   . . .  // Handle the error.
}

يمكنك الاطلاع على شيفرة البرنامج بالكامل في الملف LengthConverter3.java.

اقتباس

لاحظ أن مستخدِم البرنامج لن يلاحظ أي اختلافٍ بين نسخة البرنامج السابقة والبرنامج LengthConverter2 رغم أنهما مختلفان تمامًا، حيث يعتمد البرنامج LengthConverter3 على الاستثناءات لمعالجة الأخطاء.

ترجمة -بتصرّف- للمقال Section 3: Exceptions and try..catch من فصل Chapter 8: Correctness, Robustness, Efficiency من كتاب 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.


×
×
  • أضف...