ما هو الاستثناء
الاستثناء هو الطريقة التي تقول بها جافا «حدث خطأ ما ولا يمكنني المتابعة بشكل طبيعي». فبدلًا من إرجاع قيمة خاطئة أو إفساد بياناتك بصمت، يُنشئ زمن التشغيل كائن استثناء يصف المشكلة ويرميه. يتوقف التنفيذ الطبيعي عند تلك النقطة، وتبدأ جافا في البحث عن شيفرة تعرف كيف تتعامل مع الموقف.
وإذا لم يعالجه أحد، يصل الاستثناء إلى قمة برنامجك، فتطبع آلة جافا الافتراضية (JVM) تتبّع المكدّس، وتنتهي العملية بحالة غير صفرية.
لاحظ أنّ "after" لا يُطبع أبدًا. ففي اللحظة التي يُقيَّم فيها numbers[5]، يُرمى استثناء ArrayIndexOutOfBoundsException ويُهمَل ما تبقّى من main.
قراءة تتبّع المكدّس
عندما يبقى الاستثناء دون معالجة، تحصل على مخرجات كهذه. تبدو مخيفة، لكنّها مجرد قائمة:
Exception in thread "main" java.lang.ArithmeticException: / by zero
at Main.divide(Main.java:8)
at Main.main(Main.java:4)
اقرأها من الأعلى إلى الأسفل:
- السطر الأول هو النوع (
ArithmeticException) ورسالة (/ by zero). - وكل سطر
at ...هو إطار مكدّس. الإطار العلوي هو المكان الذي رُمي فيه الاستثناء فعليًا، أيdivideفي السطر 8. وتحته يأتي المُستدعِي،mainفي السطر 4.
سطر at العلوي هو حيث تنظر أولًا. والإطارات التي تحته تجيب عن سؤال «كيف وصلنا إلى هنا؟».
شغّل هذا فيشير التتبّع مباشرةً إلى سطر a / b، ثم يعرض main بوصفه المُستدعِي. سلسلة الاستدعاءات هذه هي أنفع أداة تنقيح تمنحك إيّاها جافا مجانًا.
هرمية الاستثناءات
كل استثناء هو كائن، وكلها تنحدر من Throwable. والفرعان المهمّان هما:
Error— مشكلات خطيرة تثيرها آلة جافا الافتراضية، مثلOutOfMemoryErrorأوStackOverflowError. وعمومًا لا تلتقطها.Exception— مشكلات يمكن لبرنامجك أن يتوقّعها ويعالجها بشكل معقول. وفي داخلها يقعRuntimeException، وهو أصل الأخطاء اليومية مثلNullPointerException.
Throwable
├── Error (don't catch: OutOfMemoryError, StackOverflowError)
└── Exception
├── IOException (checked)
├── SQLException (checked)
└── RuntimeException (unchecked)
├── NullPointerException
├── ArithmeticException
└── ArrayIndexOutOfBoundsException
ولأنّها أصناف حقيقية، يمكن للاستثناء أن يحمل رسالة، ويمكنك أن تستفسر منه عن نفسه:
تُعيد getMessage() النص الذي يلي النقطتين الرأسيتين في تتبّع المكدّس؛ بينما تمنحك getClass().getSimpleName() نوع الاستثناء باسمه.
المُتحقَّق منه مقابل غير المُتحقَّق منه
هذا هو التمييز الذي يربك المبتدئين أكثر من غيره.
- الاستثناءات غير المُتحقَّق منها ترث
RuntimeException. وهي تعني عادةً خطأً في شيفرتك، مثلnullلم تتوقّعه، أو فهرس خاطئ، أو قسمة على صفر. والمترجم لا يجبرك على معالجتها. - الاستثناءات المُتحقَّق منها ترث
Exceptionلكنّها لا ترثRuntimeException(مثلIOException). وهي تمثّل ظروفًا خارجة عن سيطرتك، مثل ملف مفقود، أو اتصال شبكي انقطع. والمترجم يجبرك إمّا على التقاطها أو الإعلان عنها.
إذا كان بإمكان دالة أن ترمي استثناءً مُتحقَّقًا منه، فعليها أن تصرّح بذلك باستخدام throws، وعلى كل مُستدعٍ أن يتعامل معه:
أزِل throws Exception من main، ولن تُترجَم الشيفرة أصلًا، وهذا هو المترجم وهو يفرض عقد الاستثناءات المُتحقَّق منها. أمّا الاستثناءات غير المُتحقَّق منها فلا تتطلّب ذلك أبدًا.
رمي استثناءاتك الخاصة
أنت لا تتفاعل مع الاستثناءات فحسب، بل يمكنك أيضًا أن تثيرها. استخدم throw مع كائن استثناء جديد للإشارة إلى أنّ وسيطًا أو حالةً غير صالحة. وهذا أفضل بكثير من إرجاع قيمة سحرية مثل -1 والأمل في أن يتحقّق المُستدعِي منها.
ضمِّن دائمًا رسالة تشرح ما الذي كان خاطئًا، ويُفضَّل القيمة المخالفة أيضًا، فنسختك المستقبلية وهي تقرأ تتبّع المكدّس ستشكرك. وIllegalArgumentException وIllegalStateException هما الاثنان اللذان ستلجأ إليهما أكثر عند التحقّق من المدخلات.
مزالق شائعة
- ابتلاع الاستثناءات. التقاط استثناء وعدم فعل أي شيء (كتلة فارغة) يُخفي العلل. سجّله على الأقل؛ وعادةً ينبغي أن تعالجه أو تعيد رميه.
- التقاط
Exceptionعلى نطاق واسع جدًّا. التقاطExceptionالأساس (أو الأسوأ،Throwable) قد يحجب مشكلات لم تقصد معالجتها. التقط النوع المحدّد الذي تتوقّعه. - قراءة التتبّع من الأسفل إلى الأعلى. الإطار الأكثر صلة هو سطر
atالعلوي، لا السفلي. ابدأ من هناك. - الخلط بين
ErrorوException. لا تحاول التعافي منOutOfMemoryErrorأوStackOverflowError؛ بل أصلِح السبب الجذري بدلًا من ذلك.
التالي: try-catch
أصبحت الآن تعرف ما الاستثناءات وكيف تقرأها. تتناول الصفحة التالية كيفية معالجتها فعليًا: تغليف الشيفرة الخطِرة في كتلة try، والتعافي في catch، وتشغيل شيفرة التنظيف في finally كي يستمرّ برنامجك في العمل بدلًا من أن ينهار.
الأسئلة الشائعة
ما هو الاستثناء في جافا؟
الاستثناء كائن يمثّل مشكلة يجري اكتشافها أثناء تشغيل البرنامج، مثل القسمة على صفر، أو الوصول إلى فهرس يتجاوز نهاية مصفوفة، أو استدعاء دالة على null. عند حدوث المشكلة، تقوم جافا برمي الاستثناء: فتوقف المسار الطبيعي وتبحث عن شيفرة قادرة على معالجته. وإذا لم تعالجه أي شيفرة، يطبع البرنامج تتبّع المكدّس ثم ينتهي.
ما الفرق بين الاستثناء المُتحقَّق منه وغير المُتحقَّق منه في جافا؟
الاستثناءات المُتحقَّق منها (الأصناف الفرعية من Exception وليست من RuntimeException، مثل IOException) يجب إمّا التقاطها أو الإعلان عنها باستخدام throws، والمترجم يفرض ذلك. أمّا الاستثناءات غير المُتحقَّق منها (الأصناف الفرعية من RuntimeException، مثل NullPointerException وArrayIndexOutOfBoundsException) فتشير عادةً إلى أخطاء برمجية ولا تحتاج إلى إعلان. كما أنّ Error (مثل OutOfMemoryError) غير مُتحقَّق منه أيضًا، وعمومًا ليس مقصودًا التقاطه.
كيف أقرأ تتبّع المكدّس في جافا؟
اقرأه من الأعلى إلى الأسفل. السطر الأول يذكر نوع الاستثناء والرسالة (مثل java.lang.ArithmeticException: / by zero). وكل سطر at ... بعده هو إطار مكدّس يعرض الدالة والملف ورقم السطر، بدءًا من المكان الذي رُمي فيه الاستثناء وعودةً عبر المُستدعِين. وسطر at العلوي هو دائمًا تقريبًا المكان الذي تنظر فيه أولًا.