بالإضافة إلى بُنَى التحكُّم 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 الطبيعي للبرنامج، أيّ أنه مُجرَّد وسيلة أخرى لتنظيم البرنامج.
تُمثَل الاستثناءات بلغة الجافا باِستخدَام كائنات 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)); } }
ترجمة -بتصرّف- للقسم Section 7: Introduction to Exceptions and try..catch من فصل Chapter 3: Programming in the Small II: Control من كتاب Introduction to Programming Using Java.
أفضل التعليقات
لا توجد أية تعليقات بعد
انضم إلى النقاش
يمكنك أن تنشر الآن وتسجل لاحقًا. إذا كان لديك حساب، فسجل الدخول الآن لتنشر باسم حسابك.