تخطَّ إلى المحتوى

معالجة الأخطاء في Java: try و catch و throw

الدرس 10 من 11 · Java

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

عندما يحدث خطأ وقت التشغيل، تنشئ Java كائناً يصف المشكلة و«ترميه». إذا لم يلتقطه أحد، يتوقف البرنامج ويطبع رسالة مثل:

Exception in thread "main" java.lang.ArithmeticException: / by zero
at Main.main(Main.java:5)

اقرأ الرسالة بهدوء، ففيها كل ما تحتاج: نوع الخطأ (ArithmeticException)، ووصفه (/ by zero)، ورقم السطر الذي حدث فيه.

ضع الكود الخطِر داخل try، واكتب خطة الطوارئ داخل catch:

try {
int result = 10 / 0;
System.out.println("لن يُطبع هذا السطر");
} catch (ArithmeticException e) {
System.out.println("خطأ حسابي: " + e.getMessage());
}
System.out.println("البرنامج مستمر");
الناتج
خطأ حسابي: / by zero
البرنامج مستمر

عند حدوث الخطأ يقفز التنفيذ فوراً إلى catch، وتُهمل بقية أسطر try. بعدها يكمل البرنامج طريقه بشكل طبيعي. والمتغير e هو كائن الاستثناء، و getMessage() تُرجع وصف المشكلة.

الأمر مثل شبكة الأمان تحت لاعب السيرك: إذا سقط، التقطته الشبكة واستمر العرض.

يمكنك التعامل مع كل نوع خطأ بطريقة مختلفة:

String[] inputs = { "25", "abc", null };
for (String input : inputs) {
try {
int number = Integer.parseInt(input.trim());
System.out.println("العدد: " + number);
} catch (NumberFormatException e) {
System.out.println("ليس رقماً: " + input);
} catch (NullPointerException e) {
System.out.println("القيمة فارغة null");
}
}
الناتج
العدد: 25
ليس رقماً: abc
القيمة فارغة null

Java تجرّب أقسام catch بالترتيب وتنفّذ أول قسم يطابق نوع الخطأ. هذه بعض الأنواع التي ستقابلها كثيراً:

الاستثناء متى يحدث
NullPointerException استدعاء دالة على متغير قيمته null
ArrayIndexOutOfBoundsException رقم عنصر خارج حدود المصفوفة
NumberFormatException تحويل نص ليس رقماً إلى عدد
ArithmeticException قسمة عدد صحيح على صفر

قسم finally يعمل في كل الأحوال: سواء نجح الكود أو فشل. نستخدمه لأعمال التنظيف مثل إغلاق ملف أو اتصال:

int[] data = { 1, 2, 3 };
try {
System.out.println(data[5]);
} catch (ArrayIndexOutOfBoundsException e) {
System.out.println("موضع غير موجود");
} finally {
System.out.println("انتهت المحاولة");
}
الناتج
موضع غير موجود
انتهت المحاولة

أنت أيضاً تستطيع رمي استثناء عندما تستقبل دالتك قيمة غير منطقية:

static void setAge(int age) {
if (age < 0) {
throw new IllegalArgumentException("العمر لا يمكن أن يكون سالباً: " + age);
}
System.out.println("تم تسجيل العمر " + age);
}
try {
setAge(15);
setAge(-4);
setAge(20);
} catch (IllegalArgumentException e) {
System.out.println("مرفوض: " + e.getMessage());
}
الناتج
تم تسجيل العمر 15
مرفوض: العمر لا يمكن أن يكون سالباً: -4

throw توقف الدالة فوراً وترسل الخطأ إلى من استدعاها. لاحظ أن setAge(20) لم تُنفَّذ، لأن التنفيذ قفز إلى catch عند أول خطأ.

المفحوصة وغير المفحوصة

Section titled “المفحوصة وغير المفحوصة”

الاستثناءات في Java عائلتان:

  • غير المفحوصة (Unchecked): ترث من RuntimeException، مثل كل ما رأيته حتى الآن. سببها غالباً خطأ في الكود نفسه، ولا يجبرك المترجم على معالجتها.
  • المفحوصة (Checked): مثل IOException عند قراءة الملفات. سببها ظروف خارج سيطرتك، ولذلك يرفض المترجم الكود إذا تجاهلتها.

أمامك مع النوع المفحوص حلّان: إما try/catch، أو الإعلان بكلمة throws أن دالتك قد ترمي هذا الخطأ فيتحمل المسؤولية من يستدعيها:

static String firstLine(String path) throws IOException {
return Files.readAllLines(Path.of(path)).get(0);
}
try {
System.out.println(firstLine("missing.txt"));
} catch (IOException e) {
System.out.println("تعذّر فتح الملف: " + e.getMessage());
}
الناتج
تعذّر فتح الملف: missing.txt

يحتاج هذا المثال إلى import java.io.IOException; و import java.nio.file.*; في أول الملف.

للأخطاء الخاصة ببرنامجك، أنشئ كلاساً يرث من Exception (مفحوص) أو RuntimeException (غير مفحوص):

class InsufficientFundsException extends Exception {
public InsufficientFundsException(double missing) {
super("الرصيد لا يكفي، ينقصك " + missing);
}
}
class Account {
private double balance = 100;
public void withdraw(double amount) throws InsufficientFundsException {
if (amount > balance) {
throw new InsufficientFundsException(amount - balance);
}
balance -= amount;
System.out.println("تم السحب، الرصيد الآن " + balance);
}
}
Account account = new Account();
try {
account.withdraw(30);
account.withdraw(200);
} catch (InsufficientFundsException e) {
System.out.println(e.getMessage());
}
الناتج
تم السحب، الرصيد الآن 70.0
الرصيد لا يكفي، ينقصك 130.0

اسم الاستثناء الواضح يشرح المشكلة لمن يقرأ الكود قبل أن يقرأ الرسالة نفسها.

  1. اكتب دالة divide(int a, int b) ترمي IllegalArgumentException برسالة عربية إذا كان b صفراً.
  2. استدعها داخل try/catch مرتين: مرة بقيم سليمة ومرة بصفر.
  3. أضف finally يطبع «انتهت العملية».
  4. تحدٍّ: اكتب دالة parseAge(String text) تحوّل النص إلى عدد، وترمي استثناءً خاصاً اسمه InvalidAgeException إذا كان النص ليس رقماً أو كان العمر خارج المدى من 0 إلى 120.
  • catch فارغ: كتابة catch (Exception e) {} تبتلع الخطأ بصمت، فتبحث ساعات عن مشكلة لا أثر لها. اطبع رسالة على الأقل.
  • التقاط Exception العامة دائماً: تخفي أخطاء لم تتوقعها. التقط النوع المحدد الذي تعرف كيف تعالجه.
  • ترتيب خاطئ: وضع catch (Exception e) قبل نوع أخص منه خطأ ترجمة، لأن القسم الثاني لن يصل إليه أحد.
  • استخدام الاستثناءات بدل الشروط: إذا استطعت فحص القيمة بـ if بسيطة فافعل، فالاستثناءات للحالات غير المعتادة.

سؤال

متى يُنفَّذ الكود داخل finally؟

سؤال

ما الفرق بين throw و throws؟

  • الاستثناء كائن يصف خطأ وقت التشغيل، وإذا لم يُلتقط توقف البرنامج.
  • try للكود الخطِر، و catch لخطة الطوارئ، و finally لما يجب تنفيذه دائماً.
  • throw new ... لرمي خطأ بنفسك عند استقبال قيمة غير منطقية.
  • الاستثناءات المفحوصة يجبرك المترجم على معالجتها أو إعلانها بـ throws.
  • أنشئ استثناءك الخاص بوراثة Exception أو RuntimeException.