معالجة الاستثناءات

هرمية الاستثناءات

15 دقيقة الدرس 3 من 14

هرمية الاستثناءات

كل إشارة خطأ في Java — سواء كانت خللًا برمجيًا أو ملفًا مفقودًا أو نفادًا للذاكرة — تُمثَّل كـكائن. وتنتمي جميع هذه الكائنات إلى شجرة وراثة واحدة جذرها الفئة Throwable. فهم هذه الشجرة يخبرك أي المشكلات يُفترض بك معالجتها، وأيها يدل على أخطاء في كودك، وأيها خطير لدرجة يصعب معها الاسترداد.

الصورة الكاملة

إليك الهرمية التي تحتاج إلى حفظها:

java.lang.Object └── java.lang.Throwable ├── java.lang.Error │ ├── OutOfMemoryError │ ├── StackOverflowError │ └── AssertionError (وغيرها) └── java.lang.Exception ├── IOException ├── SQLException ├── ParseException └── java.lang.RuntimeException ├── NullPointerException ├── ArrayIndexOutOfBoundsException ├── IllegalArgumentException ├── NumberFormatException └── ClassCastException (وغيرها)

كل فئة تراها في تتبّع المكدس موجودة في مكان ما من هذه الشجرة. لنستعرض كل مستوى.

Throwable — جذر كل شيء

Throwable هي الفئة الأم لجميع الكائنات القابلة للرمي. فقط نسخ هذه الفئة (أو فئاتها الفرعية) يمكن استخدامها مع throw وcatch وthrows. وهي تعرّف الوظائف الأساسية التي تستخدمها يوميًا:

  • getMessage() — تُعيد رسالة الخطأ المقروءة للإنسان.
  • getCause() — تُعيد الاستثناء الذي تسبب في هذا الاستثناء (تُستخدم للاستثناءات المتسلسلة).
  • printStackTrace() — تطبع مكدس الاستدعاء كاملًا على stderr.
  • getStackTrace() — تُعيد مكدس الاستدعاء كمصفوفة يمكنك فحصها برمجيًا.
نادرًا ما تمتد Throwable مباشرة. تمتد إما Exception (للحالات القابلة للاسترداد) أو RuntimeException (لأخطاء البرمجة). امتداد Throwable مباشرة يتجاوز التمييز بين الاستثناءات المدققة وغير المدققة ويُفاجئ المطورين الآخرين.

Error — لا تلتقطها

Error تمثّل مشكلة خطيرة لا ينبغي للتطبيق المكتوب جيدًا أن يحاول التعافي منها. هذه عادةً أعطال على مستوى JVM لا يستطيع كودك التعامل معها بشكل واقعي.

  • OutOfMemoryError — نفدت ذاكرة الكومة لدى JVM. لا شيء مفيد يمكنك فعله حين يحدث هذا.
  • StackOverflowError — تكرار لا نهاية له أو تداخل عميق جدًا أفرغ مكدس الاستدعاء.
  • AssertionError — فشل تعليمة assert أثناء الاختبار.
// هذه الطريقة ستُطلق StackOverflowError — لا تفعل هذا public static int infiniteRecursion(int n) { return infiniteRecursion(n + 1); // لا حالة أساسية — المكدس يمتلئ وينهار }
لا تلتقط Error أو فئاتها الفرعية في كود الإنتاج. خطأ شائع للمبتدئين هو كتابة catch (Exception e) ظنًا أنه يلتقط كل شيء — لكنه لا يلتقط Error. حتى catch (Throwable t) سيلتقط Errors، لكن ذلك خاطئ في الغالب. سجّل الموقف ودع JVM (أو الحاوية) تُغلق بشكل نظيف.

Exception — الفرع القابل للاسترداد

Exception هي الفئة للحالات التي قد يرغب برنامج معقول في التقاطها ومعالجتها. معظم الفئات التي تكتب لها كتل try/catch موجودة هنا. الفئات الفرعية المباشرة لـException (باستثناء RuntimeException) تُسمى استثناءات مدققة — يُجبرك المصرّف على التقاطها أو التصريح بها مع throws.

import java.io.FileReader; import java.io.IOException; public class ReadDemo { public static void main(String[] args) throws IOException { // IOException استثناء مدقق — المصرّف يصرّ على تصريحه FileReader reader = new FileReader("data.txt"); System.out.println("تم فتح الملف بنجاح"); reader.close(); } }

إذا حذفت throws IOException ولم تغلّف الاستدعاء بـtry/catch، فلن يُصرَّف الكود. هذا المصرّف يحميك: فهو يعلم أن هذه العملية قد تفشل ويجبرك على التفكير في الأمر.

RuntimeException — أخطاء البرمجة

RuntimeException هي فئة فرعية خاصة من Exception. فئاتها الفرعية تُسمى استثناءات غير مدققة — المصرّف لا يُلزمك بمعالجتها. وهي عادةً تدل على أخطاء في كودك: مررت null حيث كان كائن متوقعًا، أو وصلت إلى فهرس مصفوفة غير موجود، أو حاولت تحويل كائن إلى نوع خاطئ.

public class HierarchyDemo { public static void main(String[] args) { // NullPointerException — غير مدقق، لا تحذير من المصرّف String text = null; System.out.println(text.length()); // يُطلق NullPointerException وقت التشغيل // ArrayIndexOutOfBoundsException — غير مدقق int[] numbers = {10, 20, 30}; System.out.println(numbers[5]); // يُطلق ArrayIndexOutOfBoundsException // NumberFormatException — غير مدقق int value = Integer.parseInt("abc"); // يُطلق NumberFormatException } }
أصلح الخطأ، لا تلتقطه. حين ترى NullPointerException أو ArrayIndexOutOfBoundsException يجب أن تُصلح الكود المسبب له — أضف فحصًا للقيمة الفارغة، أو تحقق من الفهرس — بدلًا من ابتلاعه في كتلة catch وإخفاء الخطأ.

فحص الهرمية وقت التشغيل باستخدام instanceof

لأن الهرمية تستخدم الوراثة العادية، يمكنك استخدام instanceof لفحص مكان كائن مُطلق في الشجرة:

import java.io.IOException; public class InstanceofDemo { public static void main(String[] args) { Throwable ex = new IOException("الملف غير موجود"); System.out.println(ex instanceof Throwable); // true System.out.println(ex instanceof Exception); // true System.out.println(ex instanceof IOException); // true System.out.println(ex instanceof RuntimeException); // false System.out.println(ex instanceof Error); // false } }

هذا يُفسّر أيضًا لماذا تلتقط كتلة catch (Exception e) واحدة كلًا من الاستثناءات المدققة كـIOException وغير المدققة كـNullPointerException — كلاهما فئات فرعية من Exception. لكنها لن تلتقط OutOfMemoryError لأن Error فرع شقيق، وليست فئة فرعية من Exception.

لماذا الهرمية مهمة عمليًا

  • التقاط الفئة الأم يلتقط جميع الفئات الفرعية. catch (Exception e) ستلتقط IOException وSQLException وNullPointerException وآلاف غيرها. كن محددًا قدر الإمكان لمعالجة مشكلات مختلفة بطرق مختلفة.
  • المصرّف يُطبّق الاستثناءات المدققة. أي فئة هي فئة فرعية مباشرة من Exception (لكن ليست من RuntimeException) يجب التصريح بها أو التقاطها. الهرمية هي ما يجعل هذه القاعدة ممكنة.
  • الاستثناءات المخصصة تجد مكانها الصحيح. حين تنشئ فئة استثناء خاصة بك (يُغطى ذلك في درس لاحق)، تختار أي فئة تمتدها — وهذا الاختيار يحدد ما إذا كان استثناؤك مدققًا أم غير مدقق.

الخلاصة

Throwable هو الجذر. Error يُشير إلى أعطال JVM لا ينبغي التقاطها. Exception يُشير إلى مشكلات قابلة للاسترداد — فئاتها الفرعية المباشرة مدققة (يُطبّقها المصرّف)، في حين أن فئات RuntimeException الفرعية غير مدققة (تدل على أخطاء برمجية). كل فئة في تتبع المكدس موجودة في هذه الشجرة، وموقعها يحدد كيف تتعامل معها Java والمصرّف.