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

الإمساك بعدة استثناءات وإعادة الرمي

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

الإمساك بعدة استثناءات وإعادة الرمي

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

مشكلة تكرار كتل catch

لنفترض أنك تحلّل مدخلات المستخدم وتكتب في ملف. قد تفشل العمليتان بطرق مختلفة، لكن إجراء التعافي واحد: تسجيل المشكلة وإعادة قيمة افتراضية. النهج الساذج يؤدي إلى تكرار:

try { int value = Integer.parseInt(input); Files.writeString(path, String.valueOf(value)); } catch (NumberFormatException e) { logger.error("Bad input", e); return DEFAULT; } catch (IOException e) { logger.error("Bad input", e); // جسم متطابق — رائحة كود سيئة! return DEFAULT; }

قدّمت Java 7 تعدد الإمساك multi-catch للقضاء على هذا التكرار.

صيغة multi-catch

افصل أنواع الاستثناءات بشارة الأنبوب (|) داخل جملة catch واحدة. المعامل يصبح ضمنيًا final.

try { int value = Integer.parseInt(input); Files.writeString(path, String.valueOf(value)); } catch (NumberFormatException | IOException e) { logger.error("Operation failed", e); return DEFAULT; }

النوعان غير مترابطَين في التسلسل الهرمي، ومع ذلك يشتركان في معالج واحد. يستنتج Java النوع المشترك الأكثر خصوصية — هنا Exception — ولهذا يكون المعامل ضمنيًا final: لا يمكنك إعادة تعيين مرجع لا يمكن للمترجم التنبؤ بنوعه الدقيق في وقت التشغيل.

الترتيب لا يهم في multi-catch — خلافًا لكتل catch المتسلسلة، الأنواع في قائمة الأنبوب لا تُقيَّم من الأعلى للأسفل. جميعها تُعامَل على قدم المساواة. لا يمكنك إدراج نوع أب مع أحد أنواعه الفرعية (مثل Exception | IOException) لأن ذلك يجعل النوع الفرعي غير قابل للوصول؛ سيرفضها المترجم بخطأ في وقت الترجمة.

متى يكون multi-catch الأداة المناسبة

  • منطق التعافي متطابق تمامًا لجميع الأنواع المدرجة.
  • الأنواع غير مترابطة (لا تربطها علاقة أب-ابن).
  • لا تزال تريد الحفاظ على الاستثناء الأصلي في سجل أو سلسلة.

إذا اختلفت المعالجة — مثلًا تريد إعادة المحاولة عند IOException لكن ليس عند NumberFormatException — فاستخدم كتلًا منفصلة.

إعادة رمي الاستثناء نفسه

أحيانًا تمسك بالاستثناء فقط لإضافة سطر سجل أو تحرير مورد، ثم تتركه يتصاعد دون تغيير. تفعل ذلك بـ throw e; عادي:

public void processFile(Path path) throws IOException { try { String content = Files.readString(path); parse(content); } catch (IOException e) { logger.error("Failed to read {}", path, e); throw e; // إعادة رمي الأصلي، لا يزال من نوع IOException } }

منذ Java 7، يكون المترجم ذكيًا بما يكفي ليعرف أن IOException فقط هي التي يمكن رميها من كتلة try، لذا حتى لو كان نوع معامل الإمساك Exception، يُسمح بإعادة رميه في دالة مُعلنة بـ throws IOException. يُسمى هذا إعادة الرمي الدقيق.

تغليف الاستثناءات (التسلسل)

حين يتسرب استثناء منخفض المستوى من دالة عالية المستوى، يضطر المستدعي لمعرفة تفصيلة تنفيذية لا ينبغي أن يعرفها. الحل هو تغليف الاستثناء الأصلي داخل استثناء جديد يناسب مستوى التجريد، مع الحفاظ على السبب لأغراض التنقيح.

public class UserRepository { public User findById(int id) { try { return database.query("SELECT * FROM users WHERE id = " + id); } catch (SQLException e) { // المستدعي لا يحتاج لمعرفة شيء عن SQL. // غلّفها في استثناء على مستوى النطاق واحفظ السبب. throw new RepositoryException("Could not load user " + id, e); } } }

صيغة البنّاء المستخدمة أعلاه هي الصيغة القياسية للاستثناءات القابلة للتغليف:

public class RepositoryException extends RuntimeException { public RepositoryException(String message, Throwable cause) { super(message, cause); } }

الآن تتضمن تتبع المكدس المطبوع بـ e.printStackTrace() كلًا من RepositoryException عالية المستوى والـ SQLException الأصلية تحت عنوان "Caused by:"، مما يمنحك كل ما تحتاجه للتنقيح دون كشف طبقة SQL للمستدعين.

أمرّ دائمًا الاستثناء الأصلي كسبب عند التغليف. كتابة throw new RepositoryException("Could not load user") — دون e — تُهدر السبب الجذري بصمت وتجعل تتبع الأخطاء في الإنتاج صعبًا جدًا.

إعادة الرمي داخل multi-catch

يمكن دمج الأسلوبين معًا. في المثال التالي، تمسك دالة مساعدة بنوعَين غير مترابطَين، تسجّلهما بمعالج واحد، ثم تعيد رمي كل منهما مغلَّفًا كاستثناء نطاق:

public Config loadConfig(String fileName) { try { Path p = Paths.get(fileName); String json = Files.readString(p); return parseJson(json); } catch (IOException | JsonParseException e) { // جملة سجل واحدة تغطي كلا نوعَي الخطأ logger.error("Config load failed for {}", fileName, e); // غلّف وأعد الرمي — المستدعون لا يرون إلا ConfigException throw new ConfigException("Unable to load config: " + fileName, e); } }
لا تمسك وتُعيد الرمي فقط لإضافة ضجيج. إن لم تكن تسجّل، ولا تغلّف، ولا تحرر موردًا — مجرد كتابة catch (Exception e) { throw e; } — فاحذف الكتلة كليًا. إعادة الرمي بلا فائدة تُشوّش الشيفرة وقد تغيّر التزامات الاستثناءات المحددة/غير المحددة بطرق تفاجئ المستدعين.

الخلاصة

  • Multi-catch (catch (A | B e)) يُزيل المعالجات المكررة حين يكون التعافي متطابقًا؛ يجب أن تكون الأنواع غير مترابطة في التسلسل الهرمي.
  • إعادة الرمي تتيح لطبقة ما تسجيل أو تحرير موارد ثم تمرير الاستثناء للأعلى دون تغيير.
  • تغليف الاستثناءات يترجم استثناءً منخفض المستوى إلى استثناء يناسب طبقة التجريد الحالية، مع استخدام معامل cause في البنّاء للحفاظ على الأصل لأغراض التنقيح.
  • أدرج دائمًا الاستثناء الأصلي كسبب عند التغليف — لا تُهدره بصمت أبدًا.