بالإضافة إلى بُنَى التحكُّم (control structures) المُستخدَمة لتَحْدِيد مَسار التحكُّم الطبيعي (flow of control) بالبرنامج، تُوفِّر لغة الجافا طريقة للتَعامُل مع الحالات الاِعتراضية (exceptional cases)، والتي يُمكِنها تَغْيِير مَسار التحكُّم الطبيعي.
فمثلًا، يُعدّ إنهاء (terminate) البرنامج وما يَتبعه من طباعة رسالة الخطأ هو مَسار التحكُّم الافتراضي في حالة حُدُوث خطأ أثناء التَّنْفيذ، مع ذلك تَسمَح لغة الجافا بالتقاط (catch) مثل تلك الأخطاء بصورة تَمنع اِنهيار البرنامج (crashing)، وبحيث يَتمكَّن المُبرمِج من الرد بصورة ملائمة على الخطأ. تُستخدَم تَعْليمَة try..catch
لهذا الغرض، والتي سنكتفي بإلقاء نظرة مَبدئية وغَيْر مُكتملة عليها بهذا القسم؛ ففي الواقع يُعدّ موضوع مُعالجة الأخطاء (error handling) موضوعًا مُعقدًا نوعًا ما، ولذلك سنتناوله تفصيليًا بالقسم ٨.٣، وعندها سنَسْتَعْرِض التَعْليمَة try..catch
بمزيد من التفصيل، ونُوضِح قواعد صياغتها (syntax) كاملة.
الاعتراضات
يُشير مصطلح الاِعتراض (exception) إلى تلك النوعية من الأحَدََاث التي قد تَرغَب عادةً بمُعالجتها عن طريق تَعْليمَة try..catch
. يُفضَّل اِستخدَام هذا المصطلح بدلًا من كلمات مثل خطأ (error)؛ وذلك لأن الاِعتراض في بعض الحالات قد لا يُمثِل خَطأ مِنْ الأساس. ربما يُمكِنك التفكير بالاِعتراض (exception) بعدّه اِستثناءًا بمَسار التحكُّم (flow of control) الطبيعي للبرنامج، أيّ أنه مُجرَّد وسيلة أخرى لتنظيم البرنامج.
تُمثَل الاِعتراضات (exceptions) بلغة الجافا باِستخدَام كائنات (objects) من الصَنْف Exception
. تُعرَّف (definition) عادةً أصناف فرعية (subclasses) مُشتقَّة من الصَنْف Exception
؛ لتمثيل الاِعتراضات الفعليّة، بحيث يُمثِل كل صَنْف فرعي (subclass) نوعًا مختلفًا من الاِعتراضات. سنفْحَص بهذا القسم نوعين فقط من الاِعتراضات، هما: NumberFormatException
و IllegalArgumentException
.
يُمكِن أن يَحدُث الاِعتراض NumberFormatException
أثناء محاولة تَحْوِيل سِلسِلة نصية (string) إلى عَدَد (number). تُستخدَم الدالتين Integer.parseInt
و Double.parseDouble
لإِجراء مثل تلك التَحْوِيلات (انُظر القسم الفرعي ٢.٥.٧). فمثلًا، تَستقبِل الدالة Integer.parseInt(str)
مُعامِلًا (parameter) من النوع String
، فإذا كانت قيمة المُتَغيِّر str
-المُمرَّرة كقيمة لهذا المُعامِل- تُساوِي السِلسِلة النصية "٤٢"، فعندها ستتمكن الدالة من تَحْوِيلها إلى القيمة ٤٢ من النوع int
تَحْوِيلًا صحيحًا. في المقابل، إذا كانت قيمة المُتَغيِّر str
تُساوِي السِلسِلة النصية "fred"، فعندها ستَفشَل الدالة؛ لأن تلك السِلسِلة لا تُعدّ تَمثيلًا نصيًا (string representation) صالحًا لأيّ قيمة ممكنة من النوع العددي int
، لذا سيَحدُث اِعتراض من النوع NumberFormatException
في هذه الحالة، وسينهار (crash) البرنامج إذا لم يُعالَج (handle) هذا الاِعتراض.
يُمكِن أن يَحدُث الاِعتراض IllegalArgumentException
عندما تُمرِّر قيمة غَيْر صالحة كمُعامِل (parameter) إلى برنامج فرعي (subroutine). على سبيل المثال، إذا مَرَّرت قيمة سالبة (negative) كمُعامِل إلى برنامج فرعي، وكان هذا البرنامج الفرعي يَتطلَّب أن تكون قيمة ذلك المُعامِل أكبر من أو تُساوِي الصفر، فمِنْ المُحتمَل أن يَحدُث اِعتراض من النوع IllegalArgumentException
. على الرغم من شيوع حُدُوث ذلك الاِعتراض في مثل تلك الحالات، فما يزال لا يُمكننا الجَزْم بحُدُوثه في كل مرة ستُمرِّر فيها قيمة غَيْر صالحة كمُعامِل لبرنامج فرعي؛ ففي الواقع تَتوقَف طريقة التَعامُل مع القيم غَيْر الصالحة على الشخص الذي كَتَب البرنامج الفرعي.
تعليمة try..catch
عندما يَحدُث اِعتراض (exception)، يُقَال أنه قد بُلِّغ (thrown) عنه. فعلى سبيل المثال، يُبلِّغ Integer.parseInt(str)
عن اِعتراض من النوع NumberFormatException
إذا كانت قيمة المُتَغيِّر str
المُمرَّرة إليه غَيْر صالحة. تُستخدَم تَعْليمَة try..catch
لالتقاط (catch) الاِعتراضات، ومَنعها من التَسبُّب بانهيار (crashing) البرنامج. تُكتَب هذه التَعْليمَة بالصياغة (syntax) التالية في أبسط صورها:
try { <statements-1> } catch ( <exception-class-name> <variable-name> ) { <statements-2> }
قد يُشير الصَنْف NumberFormatException
أو IllegalArgumentException
أو أيّ صَنْف (class) آخر طالما كان من النوع Exception
. عندما يُنفِّذ الحاسوب تَعْليمَة try..catch
، فإنه سيبدأ بتَّنْفيذ التَعْليمَات الموجودة داخل الجزء try
-والمُشار إليها بالتعليمة catch
ليَمضِي قُدُمًا لتَّنْفيذ بقية البرنامج. أما إذا حَدَث اِعتراض من النوع المُحدَّد وِفقًا للعبارة catch
ليُنفِّذ العبارة try
. لاحِظ أن تَعْليمَة try..catch
بالأعلى ستَلتقِط نوعًا واحدًا فقط من الاِعتراضات -النوع المُحدَّد وِفقًا للعبارة
تُمثِل عبارة catch
؛ فقد اُلتقَط (catching) الاِعتراض، وعُولَج (handling)، وعليه لم يَتَسبَّب بانهيار البرنامج.
يُعدّ القوسين {
و }
المُستخدَمين ضِمْن تَعْليمَة try..catch
جزءً أساسيًا من صياغة (syntax) التَعْليمَة، حتى في حالة اِحتوائها على تَعْليمَة واحدة، وهو ما يختلف عن بقية التَعْليمَات الآخرى التي مَررنا بها حتى الآن، والتي يكون فيها اِستخدَام الأقواس أمرًا اختياريًا في حالة التَعْليمَات المُفردة (single statement).
بفَرْض أن لدينا مُتَغيِّر str
من النوع String
، يُحتمَل أن تكون قيمته مُمثِلة لعَدَد حقيقي (real) صالح، اُنظر الشيفرة التالية:
(real)double x; try { x = Double.parseDouble(str); System.out.println( "The number is " + x ); } catch ( NumberFormatException e ) { System.out.println( "Not a legal number." ); x = Double.NaN; }
إذا بَلَّغ استدعاء (call) الدالة Double.parseDouble(str)
عن خطأ، فستُنفَّذ التَعْليمَات الموجودة بالجزء catch
مع تَخَطِّي تَعْليمَة الخَرْج (output) بالجزء try
. يُعالَج الاعتراض، في هذا المثال، بإِسْناد القيمة Double.NaN
إلى المُتَغيِّر x
. تُشير هذه القيمة إلى عدم حَمْل مُتَغيِّر من النوع double
لقيمة عَدَدية.
لا تَحتاج دائمًا إلى اِلتقاط الاِعتراضات (catch exceptions)، والاستمرار بتَّنْفيذ البرنامج. على العكس، فقد يُؤدي ذلك أحيانًا إلى حُدُوث فوضى أكبر فيما بَعْد، ولربما يَكُون عندها من الأفضل السماح ببساطة بانهيار (crash) البرنامج. مع ذلك، يَكُون التَعافِي من أخطاء معينة مُمكنًا في أحيان آخرى.
لنفْترِض مثلًا أنك تَرغَب بكتابة برنامج يَحسِب متوسط (average) متتالية من الأعداد الحقيقية (real numbers)، التي يُدْخِلها المُستخدِم، وبحيث يُمكِنه الإشارة إلى نهاية المتتالية عن طريق إِدْخَال سَطْر فارغ. في الواقع، يُشبه هذا البرنامج المثال الذي تَعرَّضنا له بالقسم ٣.٣، مع الفارق في اِستخدَام القيمة صفر للإشارة إلى انتهاء المتتالية. قد تُفكِر باِستخدَام الدالة (function) TextIO.getlnInt()
لقراءة دَخْل المُستخدِم (input). ولكن لمّا كانت تلك الدالة تَتَخَطَّى الأسطر الفارغة، فإننا ببساطة لن نتَمكَّنْ من تَحْدِيد السَطْر الفارغ، ولذلك سنَستخدِم بدلًا منها الدالة TextIO.getln()
، مما سيُمكِّننا من تَحْدِيد السَطْر الفارغ عند إِدْخاله. سنَستخدِم أيضًا الدالة Double.parseDouble
لتَحْوِيل الدَخْل -إذا لم يكن سَطْرًا فارغًا- إلى عَدَد حقيقي، وسنستدعيها ضِمْن تَعْليمَة try..catch
؛ لتَجَنُّب انهيار البرنامج في حالة إِدْخَال المُستخدِِم عددًا غَيْر صالح. اُنظر شيفرة البرنامج:
import textio.TextIO; public class ComputeAverage2 { public static void main(String[] args) { String str; // مدخل المستخدم double number; // مدخل المستخدم بعد تحويله إلى عدد double total; // حاصل مجموع الأعداد المدخلة double avg; // متوسط الأعداد المدخلة int count; // عدد الأعداد المدخلة total = 0; count = 0; System.out.println("Enter your numbers, press return to end."); while (true) { System.out.print("? "); str = TextIO.getln(); if (str.equals("")) { break; // اخرج من الحلقة لأن المستخدم أدخل سطر فارغ } try { number = Double.parseDouble(str); // إذا حدث خطأ، سيتم تخطي السطرين التاليين total = total + number; count = count + 1; } catch (NumberFormatException e) { System.out.println("Not a legal number! Try again."); } } avg = total/count; System.out.printf("The average of %d numbers is %1.6g%n", count, avg); } }
اعتراضات الصنف TextIO
يستطيع الصَنْف TextIO
قراءة البيانات من عدة مصادر. (انظر القسم الفرعي ٢.٤.٤). فمثلًا، عندما يُحاوِل قراءة قيمة عددية مُدْخَلة من قِبَل المُستخدِم، فإنه يتحقَّق من كون دَخْل المُستخدِم صالحًا، وذلك باِستخدَام طريقة شبيهة للمثال السابق، أيّ عن طريق اِستخدَام حَلْقة التَكْرار while
ضِمْن تَعْليمَة try..catch
، وبالتالي لا يُبلِّغ عن اِعتراض. في المقابل، عندما يُحاوِل القراءة من ملف، فلا توجد طريقة واضحة للتَعَافِي من وجود قيمة غَيْر صالحة بالدَخْل (input)، ولهذا فإنه يُبلِّغ عن اِعتراض.
يُبلِّغ الصَنْف TextIO
-بغرض التبسيط- عن اِعتراضات من النوع IllegalArgumentException
فقط، وذلك بِغَضّ النظر عن نوع الخطأ الفعليّ الذي قد وَاجهه الصَنْف. فمثلًا، إذا حَاولت قراءة ملف تمَّت قراءة جميع محتوياته بالفعل، فسيَحدُث اِعتراض من الصَنْف IllegalArgumentException
. إذا كان لديك رؤية أفضل لكيفية معالجة أخطاء الملفات غَيْر السماح للبرنامج بالانهيار (crash)، اِستخدِم تَعْليمَة try..catch
لالتقاط الاِعتراضات من النوع IllegalArgumentException
.
لنفْحَص البرنامج التالي لحِسَاب قيمة متوسط (average) مجموعة من الأعداد. في هذا المثال، وبفَرْض وجود ملف يَحتوِي على أعداد حقيقية (real numbers) فقط، فإن البرنامج سيَقرأ قيم الأعداد من ذلك الملف تباعًا، ويَحسِب كُلًا من حاصل مجموعها ومتوسطها. لمّا كان عَدَد الأعداد الموجودة بالملف غَيْر مَعلوم، فإننا بحاجة إلى معرفة إلى متى سنستمر بالقراءة. أحد الحلول هو أن نستمر حتى نَصِل إلى نهاية الملف، وعندها سيَحدُث اِعتراض، والذي لا يُمكِن عدّه في تلك الحالة خطأً فعليًا، وإنما هو فقط الطريقة المُتَّبَعة للإشارة إلى عدم وجود بيانات آخرى بالملف، وعليه، يُمكِننا التقاط هذا الاِعتراض وإنهاء البرنامج. بعبارة آخرى، سنَقرأ البيانات داخل حَلْقة تَكْرار while
لا نهائية، ونَخْرُج منها فقط عند حُدوث اِعتراض. يُوظِّف هذا المثال الاِعتراضات بعدّها جزءً متوقعًا من مَسار التحكُّم (flow of control) بالبرنامج، وهو ما قد يُمثِل طريقة غَيْر اعتيادية نوعًا ما لاِستخدَام الاِعتراضات.
سنحتاج إلى معرفة اسم الملف حتى نَتَمكَّن من قراءته. سنَسمَح في الواقع للمُستخدِم بإِدْخَال اسم الملف، بدلًا من تَوْفِيره بالشيفرة (hard-coding) كقيمة ثابتة؛ وذلك بهدف تَعميم البرنامج. لكن قد يُدخِل المُستخدِم اسم ملف غَيْر موجود أساسًا. وعليه، سيُبلَّغ عن اِعتراض من النوع IllegalArgumentException
عندما نَستخدِم الدالة TextIO.readfile
لمحاولة فتح ذلك الملف. يُمكِننا أن نَلتقِط (catch) هذا الاِعتراض، ثُمَّ نَطلُب من المُستخدِم إِدْخَال اسم ملف آخر صالح. اُنظر الشيفرة التالية:
import textio.TextIO; public class AverageNumbersFromFile { public static void main(String[] args) { while (true) { String fileName; // اسم الملف المدخل من قبل المستخدم System.out.print("Enter the name of the file: "); fileName = TextIO.getln(); try { TextIO.readFile( fileName ); // حاول فتح الملف break; // إذا نجحت عملية الفتح، أخرج من الحلقة } catch ( IllegalArgumentException e ) { System.out.println("Can't read from the file \"" + fileName + "\"."); System.out.println("Please try again.\n"); } } // الصنف TextIO سيقرأ الملف double number; // عدد مقروء من الملف double sum; // حاصل مجموع الأعداد المقروءة حتى الآن int count; // عدد الأعداد المقروءة حتى الآن sum = 0; count = 0; try { while (true) { // تتوقف الحلقة عند حدوث اعتراض number = TextIO.getDouble(); count++; // يتم تخطي هذه العبارة في حالة حدوث اعتراض sum += number; } } catch ( IllegalArgumentException e ) { // نتوقع حدوث اعتراض عندما نفرغ من قراءة الملف // تم التقاط الاعتراض فقط حتى نمنع انهيار البرنامج // لكن ليس هناك ما نفعله لمعالجة الاعتراض لأنه ليس خطأ أساسا } // تمت قراءة جميع محتويات الملف عند هذه النقطة System.out.println(); System.out.println("Number of data values read: " + count); System.out.println("The sum of the data values: " + sum); if ( count == 0 ) System.out.println("Can't compute an average of 0 values."); else System.out.println("The average of the values: " + (sum/count)); } }
تعليقات
إرسال تعليق