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

ما هي الاستثناءات؟

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

ما هي الاستثناءات؟

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

الأخطاء مقابل الاستثناءات

تقسّم Java الحالات غير الطبيعية إلى عائلتين، كلتاهما تنحدر من أصل مشترك يسمى Throwable:

  • Error (الخطأ) — مشكلة جسيمة لا تستطيع JVM التعافي منها، مثل نفاد ذاكرة الكومة (OutOfMemoryError) أو تلف ملف class (ClassFormatError). يجب ألّا تحاول شفرتك التقاط هذه الأخطاء؛ إذ تشير إلى أن البيئة نفسها معطوبة.
  • Exception (الاستثناء) — حالة يمكن لشفرتك توقّعها والتعامل معها بشكل معقول، مثل عدم العثور على ملف (FileNotFoundException)، أو تمرير وسيط غير مقبول (IllegalArgumentException)، أو القسمة على صفر (ArithmeticException).
فكرة أساسية: تكتب شفرتك لمعالجة Exception. وتكاد لا تكتب شفرة تعالج Error — فإذا نفدت ذاكرة JVM، فلن يُنقذك أي كود ذكي؛ بل يجب إصلاح البيئة عوضًا عن ذلك.

ما الذي يحدث عند وقوع استثناء؟

تأمّل هذا البرنامج الصغير:

public class Demo { public static void main(String[] args) { int result = divide(10, 0); System.out.println("Result: " + result); } static int divide(int a, int b) { return a / b; // القسمة على صفر تُلقي ArithmeticException } }

حين تُقيّم Java العبارة a / b وقيمة b = 0، فهي لا تستطيع إنتاج عدد صحيح ذي معنى. في تلك اللحظة تُلقي JVM استثناء ArithmeticException. إذا لم تلتقط أي شفرة هذا الاستثناء، تُنهي JVM البرنامج وتطبع تتبّع المكدس — تقريرًا يوضّح بالضبط أين وكيف وقعت المشكلة.

كيف تُفكّك الاستثناءات مكدس الاستدعاء؟

تحتفظ Java بسجل كل استدعاء دالة في بنية بيانات تُسمى مكدس الاستدعاء. حين تستدعي main التي تستدعي divide، يبدو المكدس كالتالي (الأعلى = ينفَّذ حاليًا):

divide <-- ينفَّذ حاليًا main

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

لماذا يهمّنا التفكيك؟ أي شفرة تأتي بعد السطر الذي يُلقي الاستثناء يتم تخطّيها. في المثال أعلاه، لن يُنفَّذ السطر System.out.println("Result: " + result) أبدًا. فهم هذا يمنع خطأً شائعًا لدى المبتدئين: الظن بأن الأسطر اللاحقة تُنفَّذ حتى حين يُلقي سطر سابق استثناءً.

قراءة تتبّع المكدس

تشغيل الصنف Demo أعلاه يُنتج مخرجات كالتالي:

Exception in thread "main" java.lang.ArithmeticException: / by zero at Demo.divide(Demo.java:8) at Demo.main(Demo.java:3)

حلّل كل سطر على حدة:

  • Exception in thread "main" — الخيط الذي كان يعمل حين أفلت الاستثناء. معظم البرامج المبتدئة لها خيط وحيد هو main.
  • java.lang.ArithmeticException: / by zero — اسم صنف الاستثناء الكامل، متبوعًا برسالة مقروءة تصف ما حدث.
  • at Demo.divide(Demo.java:8) — الإطار الداخلي: أُلقي الاستثناء في السطر 8 من Demo.java، داخل الدالة divide.
  • at Demo.main(Demo.java:3) — المُستدعي: كانت main في السطر 3 حين استدعت divide. كل إطار لاحق هو الدالة التي استدعت التي فوقها.

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

خطأ شائع — تجاهل أول سطر. كثيرًا ما يتجه المبتدئون إلى أسفل تتبّع المكدس. أسفله يظهر الدالة main التي نادرًا ما تكون المشكلة الفعلية. المعلومة المفيدة — نوع الاستثناء والرسالة والسطر الدقيق — تقع في الأعلى.

مثال أعمق قليلًا

إليك سلسلة استدعاء من ثلاثة مستويات لترى تتبّعًا أطول:

public class StackDemo { public static void main(String[] args) { level1(); } static void level1() { level2(); } static void level2() { String text = null; System.out.println(text.length()); // NullPointerException هنا } }

سيكون تتبّع المكدس كالتالي:

Exception in thread "main" java.lang.NullPointerException: Cannot invoke "String.length()" because "text" is null at StackDemo.level2(StackDemo.java:13) at StackDemo.level1(StackDemo.java:8) at StackDemo.main(StackDemo.java:4)

قراءة من الأعلى إلى الأسفل: وقعت الكارثة في level2 في السطر 13. استُدعيت هذه الدالة من level1 في السطر 8، التي استُدعيت من main في السطر 4. تضيف Java 17 حتى تلميحًا باللغة الطبيعية: "because 'text' is null" — مما يجعل تشخيص NullPointerException أسهل بكثير مقارنةً بالإصدارات القديمة.

الخلاصة

الاستثناءات هي طريقة Java للإشارة إلى أن شيئًا غير متوقع حدث أثناء التنفيذ. الـ Errors مشكلات على مستوى JVM تتركها دون تدخّل؛ والـ Exceptions مشكلات على مستوى التطبيق تتعامل معها. حين يُلقى استثناء ولا يُلتقط، تُفكّك Java مكدس الاستدعاء إطارًا تلو إطار بحثًا عن معالج، أو تُنهي البرنامج. تتبّع المكدس الذي تطبعه هو أداتك التشخيصية الأولى — اقرأه دائمًا من الأعلى. في الدرس التالي ستتعلّم كيفية كتابة كتل try وcatch وfinally لاعتراض الاستثناءات قبل أن تُفكّك المكدس بالكامل.