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

مقدمة إلى الاستثناءت exceptions ومعالجتها في جافا


رضوى العربي

بالإضافة إلى بُنَى التحكُّم 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 بالأعلى ستَلتقِط نوعًا واحدًا فقط من الاستثناءات -النوع المُحدَّد وِفقًا للعبارة -، أما إذا حَدَث استثناء من أي نوع آخر، فسينهار crash البرنامج كالعادة.

تُمثِل عبارة كائنًا من نوع الاستثناء المُلتقَط exception object، والذي يَتضمَّن معلومات عن سبب حُدُوث الاستثناء بما في ذلك رسالة الخطأ error message. تستطيع طباعة قيمة هذا الكائن object، على سبيل المثال، أثناء تَّنْفيذ العبارة ، مما سيترتب عليه طباعة رسالة الخطأ. سيَمضِي الحاسوب لتَّنْفيذ بقية البرنامج بَعْد انتهاءه من تَّنْفيذ الجزء 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.


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

أفضل التعليقات

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



انضم إلى النقاش

يمكنك أن تنشر الآن وتسجل لاحقًا. إذا كان لديك حساب، فسجل الدخول الآن لتنشر باسم حسابك.

زائر
أضف تعليق

×   لقد أضفت محتوى بخط أو تنسيق مختلف.   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.


×
×
  • أضف...