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

try و catch و finally

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

try و catch و finally

في الدرس السابق تعلّمت ما هي الاستثناءات ولماذا توجد. حان الآن وقت تعلّم الكلمات المفتاحية الثلاث التي تسمح لك بـمعالجة هذه الاستثناءات: try وcatch وfinally. معًا توفّر لك طريقة منظّمة لتغليف الكود الخطر والتعافي من المشكلات وضمان التنظيف — بغضّ النظر عمّا يحدث.

لماذا نغلّف الكود أصلًا؟

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

كتلة try

تحتوي كتلة try على التعليمات التي قد تُطلق استثناءً. لا يمكنها الوقوف وحدها — يجب أن يتبعها على الأقل كتلة catch واحدة أو كتلة finally أو كلتاهما.

// الهيكل الأساسي try { // كود قد يُطلق استثناءً int result = 10 / 0; } catch (ArithmeticException e) { // معالجة المشكلة System.out.println("لا يمكن القسمة على صفر: " + e.getMessage()); }

هنا 10 / 0 يُطلق استثناء ArithmeticException. لأن التعليمة داخل كتلة try، ينقل Java التحكّم إلى كتلة catch المناسبة بدلًا من تعطيل التطبيق.

كتلة catch

تُعلن كتلة catch عن نوع الاستثناء الذي تعالجه وتمنحه اسمًا محليًا (باتّفاق e أو ex). يمكنك فقط اصطياد الاستثناءات التي تطابق النوع المُطلَق تمامًا أو نوعًا أب في التسلسل الهرمي (المزيد في الدرس الثالث).

String input = "hello"; try { int number = Integer.parseInt(input); // يُطلق NumberFormatException System.out.println("تمّ التحليل: " + number); } catch (NumberFormatException e) { System.out.println("إدخال خاطئ — ليس رقمًا: " + e.getMessage()); } // يستمرّ التنفيذ بشكل طبيعي من هنا System.out.println("البرنامج لا يزال يعمل.");

لاحظ أن السطر System.out.println("تمّ التحليل: " + number) لن يُنفَّذ أبدًا عند فشل التحليل. ينتقل Java مباشرةً من نقطة الفشل إلى كتلة catch، ثم يكمل بعد منشأة try-catch بأكملها.

المتغيّر e كائن. يحمل معلومات عمّا حدث. أكثر الدوال فائدة هي e.getMessage() (وصف مختصر) وe.toString() (النوع + الرسالة). لأغراض التصحيح أثناء التطوير، تطبع e.printStackTrace() سلسلة الاستدعاء الكاملة.

كتل catch متعدّدة

يمكن أن تتبع كتلة try واحدة عدّة كتل catch، كلٌّ منها يعالج نوعًا مختلفًا من الاستثناءات. يتحقّق Java منها بالترتيب من الأعلى ويُنفّذ أول واحدة تتطابق.

import java.io.*; public class ReadDemo { public static void main(String[] args) { try { FileReader reader = new FileReader("data.txt"); // قد يُطلق FileNotFoundException int ch = reader.read(); // قد يُطلق IOException reader.close(); } catch (FileNotFoundException e) { System.out.println("الملف غير موجود: " + e.getMessage()); } catch (IOException e) { System.out.println("خطأ في القراءة: " + e.getMessage()); } } }
ضع الاستثناءات الأكثر تحديدًا أولًا. FileNotFoundException هي فئة ابن من IOException. إذا وضعت كتلة IOException أولًا ستستوعب FileNotFoundException أيضًا، وسيُشير المُصرِّف إلى أن الكتلة الثانية لن تُنفَّذ أبدًا. رتّب دائمًا كتل catch من الأكثر تحديدًا إلى الأكثر عمومية.

كتلة finally

تعمل كتلة finally بغضّ النظر عمّا يحدث — سواء نجحت كتلة try، أو اصطِيد استثناء، أو حتى إذا أُطلق استثناء ولم يُصطَد. إنها المكان المناسب لكود التنظيف: إغلاق اتصال، تحرير قفل، مسح مخزن مؤقت.

import java.io.*; public class FinallyDemo { public static void main(String[] args) { FileReader reader = null; try { reader = new FileReader("config.txt"); // ... قراءة البيانات ... System.out.println("تمّت قراءة الملف بنجاح."); } catch (IOException e) { System.out.println("خطأ: " + e.getMessage()); } finally { // هذه الكتلة تعمل دائمًا if (reader != null) { try { reader.close(); } catch (IOException closeEx) { System.out.println("تعذّر إغلاق الملف: " + closeEx.getMessage()); } } System.out.println("اكتمل التنظيف."); } } }

حتى لو أطلق new FileReader("config.txt") استثناءً وعملت كتلة catch، تُنفَّذ كتلة finally بعدها. هذا الضمان هو ما يجعل finally موثوقة لإدارة الموارد.

إغلاق الموارد في finally هو النمط الكلاسيكي في Java. قدّم Java 7 try-with-resources كبديل أنظف لكائنات AutoCloseable (مشمول في الدرس 8). في الوقت الحالي، فهم النهج اليدوي بـ finally يعلّمك تمامًا ما يفعله try-with-resources تلقائيًا.

تدفّق التحكّم باختصار

  • إذا لم يُطلَق استثناء: تعمل كتلة try كاملةً، وتُتجاهل catch، وتعمل finally.
  • إذا أُطلق استثناء اصطِيد: تتوقّف try عند نقطة الإطلاق، وتعمل كتلة catch المناسبة، وتعمل finally.
  • إذا أُطلق استثناء لم يُصطَد: تتوقّف try، وتُتجاوَز جميع كتل catch، وتعمل finally مع ذلك، ثم يتصاعد الاستثناء في مكدّس الاستدعاء.
تجنّب الإرجاع من داخل finally. إذا احتوت كتلة finally على تعليمة return، فإنها تتجاهل بصمت أي استثناء كان في طور التصاعد. هذه واحدة من أكثر الأخطاء إرباكًا في Java. أبقِ كتل finally مركّزة على التنظيف لا على إرجاع القيم.

مثال متكامل

إليك دالة صغيرة تقرأ عددًا صحيحًا من نص وتجمع الكتل الثلاث وتُظهر تدفّق التحكّم بوضوح:

public class TryCatchDemo { public static int parsePositive(String text) { try { int value = Integer.parseInt(text); // قد يُطلق NumberFormatException if (value <= 0) { throw new IllegalArgumentException("يجب أن تكون القيمة موجبة، وردت: " + value); } return value; // يُصل إليه فقط عند النجاح } catch (NumberFormatException e) { System.out.println("ليس عددًا صحيحًا صالحًا: " + e.getMessage()); return -1; } catch (IllegalArgumentException e) { System.out.println("قيمة غير صالحة: " + e.getMessage()); return -1; } finally { System.out.println("انتهت parsePositive()."); // تطبع دائمًا } } public static void main(String[] args) { System.out.println(parsePositive("42")); // 42 System.out.println(parsePositive("-5")); // -1 System.out.println(parsePositive("abc")); // -1 } }

الخلاصة

استخدم try لتمييز الكود الذي قد يفشل، وcatch لمعالجة المشكلات المحدّدة بأناقة، وfinally لضمان التنظيف بصرف النظر عن النتيجة. رتّب كتل catch المتعدّدة من الأكثر تحديدًا إلى الأكثر عمومية. في الدرس التالي ستستكشف التسلسل الهرمي الكامل للاستثناءات لتفهم تمامًا أي الأنواع تصطاد ومتى.