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

throw و throws

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

throw و throws

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

throw — إطلاق استثناء بنفسك

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

public class BankAccount { private double balance; public BankAccount(double initialBalance) { if (initialBalance < 0) { throw new IllegalArgumentException( "Initial balance cannot be negative: " + initialBalance ); } this.balance = initialBalance; } public void withdraw(double amount) { if (amount <= 0) { throw new IllegalArgumentException("Amount must be positive."); } if (amount > balance) { throw new IllegalStateException( "Insufficient funds. Balance: " + balance + ", Requested: " + amount ); } balance -= amount; } }

لاحظ عدّة أمور:

  • throw تأخذ كائنًا — دائمًا تكتب throw new SomeException("message").
  • تختار أكثر فئة استثناء تحديدًا تتناسب مع المشكلة. IllegalArgumentException مناسبة لمُدخلات خاطئة؛ أما IllegalStateException فمناسبة حين يكون الكائن نفسه في حالة خاطئة.
  • يجب أن تساعد سلسلة الرسالة المستدعيَ على تشخيص المشكلة — أضف القيمة الخاطئة فيها كلّما أمكن.
لماذا throw بدلًا من إعادة قيمة خاصة؟ الدالة التي تُعيد -1 أو null للإشارة إلى الفشل تُلزم كل مستدعٍ بتذكّر التحقّق — وغالبًا ما ينسون. الاستثناء لا يمكن تجاهله بصمت: إذا لم يلتقطه أحد، ينهار البرنامج مع تتبّع مكدّس واضح.

throws — الإعلان عن الاستثناءات المتحقّق منها

تنتمي الكلمة المفتاحية throws إلى توقيع الدالة، ليس داخل جسمها. إنّها عقد: تُخبر المُترجم وكل مستدعٍ "هذه الدالة قد تُطلق الاستثناء المُتحقَّق منه المُدرج، وعليك التعامل معه."

import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; public class FileProcessor { // الدالة تُعلن أنّها قد تُطلق IOException public String readFile(Path path) throws IOException { return Files.readString(path); // Files.readString نفسها تُطلق IOException } }

بما أن IOException استثناء متحقَّق منه، يُجبر المُترجم كل مستدعٍ لـreadFile على إمّا:

  1. إحاطة الاستدعاء بكتلة try-catch، أو
  2. إضافة نفس الإعلان throws IOException إلى دالتهم الخاصة، ما يُحوّل المسؤولية نحو الأعلى.
الاستثناءات غير المتحقَّق منها لا تحتاج إلى throws. يمكن إطلاق RuntimeException وفروعها (مثل IllegalArgumentException وNullPointerException) في أي مكان دون إعلانها. فقط الاستثناءات المتحقَّق منها (تلك التي تمتد من Exception لكن ليس من RuntimeException) يجب إعلانها.

كيف ينسحب المكدّس — الانتشار

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

public class PropagationDemo { public static void main(String[] args) { try { level1(); } catch (IllegalStateException e) { System.out.println("Caught in main: " + e.getMessage()); } } static void level1() { level2(); // لا try-catch هنا — الاستثناء ينتشر نحو الأعلى } static void level2() { level3(); // لا try-catch هنا — الاستثناء ينتشر نحو الأعلى } static void level3() { throw new IllegalStateException("Something went wrong deep down"); } }

المُخرج:

Caught in main: Something went wrong deep down

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

الجمع بين throw و throws

الدالة التي تُعلن استثناءً متحقَّقًا منه (بـthrows) وتُطلقه يدويًا (بـthrow) أمر شائع جدًا:

public class UserService { public String findUser(int id) throws IllegalArgumentException { if (id <= 0) { throw new IllegalArgumentException("User ID must be positive, got: " + id); } // ... ابحث عن المستخدم في قاعدة البيانات return "Alice"; } }
لا تُطلق Exception أو Throwable مباشرةً. كتابة throw new Exception("something broke") تُجبر المستدعين على التقاط أوسع نوع ممكن، ممّا يُخفي ما حدث فعليًا. دائمًا افضّل فئة استثناء محدّدة وذات معنى — استخدم الفئات القياسية من JDK أو أنشئ فئاتك الخاصة (ستتعلّمها في الدرس 7).

مثال عملي كامل

import java.io.IOException; import java.nio.file.Path; public class ReportGenerator { // يُعلن IOException المتحقَّق منه لذا يجب على المستدعين معالجته public void generate(int reportId, Path outputDir) throws IOException { if (reportId <= 0) { // غير متحقَّق منه — لا حاجة لإعلانه في throws throw new IllegalArgumentException("reportId must be positive"); } if (!outputDir.toFile().isDirectory()) { throw new IOException("Output path is not a directory: " + outputDir); } // ... إنشاء التقرير System.out.println("Report " + reportId + " written to " + outputDir); } public static void main(String[] args) { ReportGenerator gen = new ReportGenerator(); try { gen.generate(42, Path.of("/tmp/reports")); } catch (IOException e) { System.err.println("IO problem: " + e.getMessage()); } // IllegalArgumentException ستنتشر دون التقاط لو مرّرنا -1 } }

الخلاصة

  • throw تُكتب داخل جسم الدالة — تُطلق كائن استثناء الآن فورًا.
  • throws تُكتب على توقيع الدالة — تُعلن أن الدالة قد تُطلق استثناءً متحقَّقًا منه وتُحمّل مسؤولية المعالجة للمستدعي.
  • الاستثناءات غير المتحقَّق منها (فروع RuntimeException) يمكن إطلاقها بحريّة دون إعلان.
  • حين لا يُلتقط الاستثناء محليًا، ينتشر نحو الأعلى في مكدّس الاستدعاء تلقائيًا حتى يجد من يلتقطه أو ينهار البرنامج.