رسائل الأخطاء والاستثناءات والوقاية منها
نقرأ رسالة الخطأ كخريطة تدلّنا على مكانه، ونعالج الاستثناءات بـ try و catch، ونصحّح بمنهجية، ونتبع ممارسات تمنع الأخطاء.
- تحدد رقم السطر ونوع الخطأ ووصفه من رسالة المترجم.
- تقرأ رسالة الاستثناء وتتبّع المكدّس Stack Trace.
- تعالج الاستثناءات باستخدام try و catch و finally.
- تتبع منهجية من خمس خطوات لتصحيح الأخطاء وتعيد الاختبار.
- تطبّق ممارسات برمجة جيدة تقلل حدوث الأخطاء.
١رسالة الخطأ صديقك
عندما يظهر خطأ يرتبك كثير من المبتدئين من النص الإنجليزي الطويل ويتجاهلونه. لكن رسالة الخطأ هي أهم دليل لديك: تخبرك ماذا حدث، وأين حدث، وأحيانًا لماذا. تعلّم قراءتها يوفر عليك معظم وقت التصحيح.
٢تشريح رسالة خطأ الصياغة
رسائل المترجم في NetBeans تأتي بهذا الشكل الثابت:
public class Main { public static void main(String[] args) { int count = 3; String msg = "عدد الطلاب " + count System.out.println(msg); }}
السطر الثاني من الرسالة يعرض السطر نفسه، والعلامة ^ تشير إلى المكان الذي توقف فيه المترجم، وهو هنا نهاية السطر حيث كان ينتظر ;.
٣قاموس أشهر رسائل المترجم
| الرسالة | معناها | الحل المعتاد |
|---|---|---|
';' expected | تتوقع فاصلة منقوطة | أضف ; في نهاية الجملة |
cannot find symbol | اسم غير معروف: متغير أو دالة أو كلاس | تحقق من الإملاء وحالة الأحرف، وأن المتغير معرّف قبل استخدامه |
incompatible types: String cannot be converted to int | وضع قيمة من نوع في متغير من نوع آخر | غيّر نوع المتغير أو حوّل القيمة |
missing return statement | دالة تعيد قيمة لكن لا يوجد return | أضف return بالقيمة المناسبة |
variable k might not have been initialized | استخدام متغير قبل إعطائه قيمة | أعطه قيمة ابتدائية مثل int k = 0; |
reached end of file while parsing | قوس { غير مغلق | أضف } الناقصة، واستخدم Alt+Shift+F لتنسيق الكود |
')' expected | قوس ) ناقص | أكمل الأقواس في الشرط أو الاستدعاء |
class, interface, enum, or record expected | كود خارج الكلاس، غالبًا بسبب } زائدة | احذف القوس الزائد |
مثال: رسالتان في برنامج واحد
public class Main { static int square(int x) { int r = x * x; } public static void main(String[] args) { int k; System.out.println(k); }}
الحل: أضف return r; قبل نهاية الدالة square، واكتب int k = 0;
٤تشريح رسالة خطأ زمن التنفيذ
رسالة الاستثناء أطول، لكنها منظمة. اقرأها من الأعلى:
بعدها تأتي أسطر تبدأ بـ at تُسمّى تتبّع المكدّس Stack Trace: تبيّن سلسلة الدوال التي كانت تعمل لحظة الخطأ. السطر الأول منها هو مكان حدوث الخطأ، والأسطر التالية هي من استدعى هذه الدالة.
public class Main { static double average(int total, int count) { return total / count; } public static void main(String[] args) { System.out.println("بداية البرنامج"); double a = average(250, 0); System.out.println(a); }}
نقرأ: الخطأ قسمة على صفر، حدث في الدالة average في السطر 3، والتي استُدعيت من main في السطر 7. إذن نصلح إما الدالة (نتحقق أن count ليس صفرًا)، أو الاستدعاء (لا نمرّر صفرًا).
عندما يبدأ التتبّع من مكتبة جافا
أحيانًا تظهر أولًا أسطر من داخل مكتبات جافا مثل java.base/java.util.Scanner. هذه ليست أخطاء في جافا، بل في طريقة استخدامنا لها. تجاوزها وابحث عن أول سطر فيه اسم ملفك مثل Main.java.
السطر المهم هو الأخير: Main.main(Main.java:6)، أي سطر nextInt() في برنامجنا.
٥قاموس أشهر الاستثناءات
| الاستثناء | متى يحدث؟ | مثال |
|---|---|---|
ArithmeticException: / by zero | قسمة عدد صحيح على صفر | 10 / 0 |
InputMismatchException | إدخال لا يطابق نوع دالة القراءة | كتابة «abc» عند nextInt() |
NumberFormatException | تحويل نص لا يمثل رقمًا | Integer.parseInt("12a") |
NullPointerException | استخدام كائن لم يُنشأ بعد (قيمته null) | استدعاء دالة من متغير قيمته null |
ArrayIndexOutOfBoundsException | رقم خانة خارج حدود المصفوفة | a[3] ومصفوفة طولها 3 |
StringIndexOutOfBoundsException | رقم حرف خارج طول النص | "Ali".charAt(5) |
٦أقسام الاستثناءات
١) Checked Exception: الاستثناء المتحقق
يتحقق منه المترجِم ويُلزمك بالتعامل معه قبل التشغيل: بـ try-catch، أو بالتصريح عنه بـ throws. أمثلة: IOException و FileNotFoundException.
package exception; import java.io.FileReader; public class Student { public static void main(String[] args) { FileReader file = new FileReader("data.txt"); } }
package exception; import java.io.FileNotFoundException;import java.io.FileReader; public class Student { public static void main(String[] args) throws FileNotFoundException { FileReader file = new FileReader("data.txt"); } }
package exception; import java.io.FileNotFoundException;import java.io.FileReader; public class Student { public static void main(String[] args) { try { FileReader file = new FileReader("data.txt"); } catch (FileNotFoundException ex) { System.out.println(ex.getMessage()); } } }
في NetBeans يقترح المصباح الحلّين: Add throws clause أو Surround Statement with try-catch.
٢) Unchecked Exception: غير المتحقق (Runtime Exception)
لا يتحقق منه المترجِم، بل يظهر وقت التشغيل، والمبرمج غير مُلزم بمعالجته. ينتج عادةً عن أخطاء منطقية:
public class Main { public static void main(String[] args) { int[] myNumbers = {1, 2, 3}; System.out.println(myNumbers[10]); // error! }}
| الجانب | Checked | Unchecked |
|---|---|---|
| وقت الفحص | أثناء الترجمة | أثناء التشغيل |
| إلزام المعالجة | إلزامي | اختياري |
| أمثلة | IOException, SQLException | NullPointerException, ArithmeticException |
٣) Error
خطأ بسبب حدود الجهاز الذي يعمل عليه البرنامج، ولا يرتبط بالبرنامج مباشرة؛ مثل امتلاء الذاكرة: «JVM is out of memory».
٧معالجة الاستثناءات: try و catch و finally
| الكتلة | وظيفتها |
|---|---|
try | نكتب فيها الكود الذي نتوقع أن يسبب خطأ؛ فإذا وقع الخطأ «ترمي» الاستثناء. |
catch | تلتقط الاستثناء الذي رمته try وتعالجه، ثم يستمر التنفيذ بعدها. |
finally | تُنفَّذ دائمًا سواء حدث استثناء أم لا؛ مثل إغلاق ملف أو اتصال بقاعدة بيانات. |
try { // الكود الذي قد يسبب استثناء}catch (Exception e) { // التعامل مع الخطأ}finally { // (اختياري) كود يتم تنفيذه دائمًا}
مثال الدرجات
package exception;import java.util.Scanner;public class Main { public static void main(String[] args) { double marks [] = {90, 88.50, 77, 100, 50}; Scanner scan = new Scanner(System.in); int idx; try { System.out.print("Enter an index number.(0 to 4): "); idx = scan.nextInt(); System.out.println("The mark is: " + marks[idx]); } catch (Exception e) { System.out.println(e); } finally { System.out.println("finally block"); } System.out.println("After try and block"); }}
أدخل 4 فيطبع الدرجة 50.0. جرّب إدخال 5 فيلتقط catch الخطأ ArrayIndexOutOfBoundsException ويكمل البرنامج.
أكثر من catch: لكل خطأ رسالة
package exception;import java.util.InputMismatchException;import java.util.Scanner;public class Main { public static void main(String[] args) { double marks [] = {90, 88.50, 77, 100, 50}; Scanner scan = new Scanner(System.in); int idx = 0; try { System.out.print("Enter an index number.(0 to 4): "); idx = scan.nextInt(); System.out.println("The mark is: " + marks[idx]); } catch (ArrayIndexOutOfBoundsException e) { System.out.println("ERROR: the number " + idx +" is out of range"); } catch(InputMismatchException e){ System.out.println("ERROR: the number you entered is not an integer number"); } catch(Exception e){ System.out.println("ERROR: " + e); } finally { System.out.println("finally block"); } System.out.println("After try and block"); }}
٨منهجية التصحيح: خمس خطوات
التصحيح العشوائي، أي تغيير الكود على أمل أن يعمل، يضيّع الوقت وقد يضيف أخطاء جديدة. المبرمج المنظّم يتبع خطوات ثابتة:
| الخطوة | ماذا تفعل؟ |
|---|---|
| ١. أعِد إنتاج الخطأ | حدّد المدخلات التي تُظهر الخطأ بثبات. خطأ لا تستطيع تكراره لا تستطيع إصلاحه. |
| ٢. حدّد المكان | اقرأ رسالة الخطأ، أو استخدم الطباعة ونقاط التوقف لتضييق المكان إلى سطر أو بضعة أسطر. |
| ٣. افهم السبب | لماذا حدث؟ لا تكتفِ بإخفاء العَرَض. مثلًا: لماذا أصبح المقسوم عليه صفرًا؟ |
| ٤. أصلح | عدّل أقل قدر ممكن من الكود، وتغييرًا واحدًا في كل مرة. |
| ٥. أعِد الاختبار | جرّب المدخلات التي أظهرت الخطأ، ثم بقية الحالات للتأكد أنك لم تُفسد شيئًا آخر. |
٩مثال كامل: تصحيح برنامج الفاتورة
المطلوب: اقرأ السعر والكمية، فإذا كان الإجمالي 1000 فأكثر يحصل العميل على خصم 10%. والسعر والكمية يجب أن يكونا أكبر من صفر.
١. نختبر بجدول حالات
| المدخلات | المتوقع | الناتج الفعلي | الحكم |
|---|---|---|---|
| 100 و 5 | الإجمالي: 500.0 | الإجمالي: 500.0 | ✓ |
| 250 و 4 | الإجمالي: 900.0 | الإجمالي: 1000.0 | ✗ |
| 300 و 4 | الإجمالي: 1080.0 | الإجمالي: 1080.0 | ✓ |
| -50 و 2 | قيمة غير صحيحة | الإجمالي: -100.0 | ✗ |
import java.util.Scanner;public class Main { public static void main(String[] args) { Scanner in = new Scanner(System.in); double price = in.nextDouble(); int qty = in.nextInt(); double total = price * qty; double discount = 0; if (total > 1000) discount = total * 0.1; total = total - discount; System.out.println("الإجمالي: " + total); }}
٢. نحدد المكان ونفهم السبب
- الحالة 250 × 4 = 1000 بالضبط: لم يُطبَّق الخصم. السبب الشرط
total > 1000يستبعد القيمة 1000 نفسها. - الحالة −50: البرنامج قبل سعرًا سالبًا. السبب لا يوجد تحقق من المدخلات.
٣. نصلح ثم نعيد الاختبار
import java.util.Scanner;public class Main { public static void main(String[] args) { Scanner in = new Scanner(System.in); double price = in.nextDouble(); int qty = in.nextInt(); if (price <= 0 || qty <= 0) { System.out.println("قيمة غير صحيحة"); return; } double total = price * qty; double discount = 0; if (total >= 1000) discount = total * 0.1; total = total - discount; System.out.println("الإجمالي: " + total); }}
import java.util.Scanner;public class Main { public static void main(String[] args) { Scanner in = new Scanner(System.in); double price = in.nextDouble(); int qty = in.nextInt(); if (price <= 0 || qty <= 0) { System.out.println("قيمة غير صحيحة"); return; } double total = price * qty; double discount = 0; if (total >= 1000) discount = total * 0.1; total = total - discount; System.out.println("الإجمالي: " + total); }}
أعدنا تشغيل كل حالات الجدول، فأصبحت كلها ✓. الكلمة return داخل main تنهي البرنامج مبكرًا.
١٠الوقاية: ممارسات البرمجة الجيدة
أفضل خطأ هو الذي لا يحدث. هذه الممارسات تقلل الأخطاء وتجعل اكتشافها أسهل:
| الممارسة | بدلًا من | الأفضل |
|---|---|---|
| أسماء واضحة | double x = a * b; | double total = price * qty; |
| تنسيق ومسافات بادئة | كود متلاصق غير مرتب | Alt + Shift + F في NetBeans |
| ثوابت بدل الأرقام السحرية | total * 0.15 | final double VAT = 0.15; |
| دوال صغيرة | main طويلة من 100 سطر | دالة لكل مهمة، كما في التفكيك |
| التحقق من المدخلات | القسمة مباشرة | if (count != 0) قبل القسمة |
| البناء التدريجي | كتابة البرنامج كاملًا ثم تشغيله | اكتب جزءًا صغيرًا وشغّله، ثم أضف |
| تعليقات مفيدة | i++; // زيادة i | تعليق يشرح السبب لا الواضح |
التحقق من المدخلات قبل القراءة
الدالة hasNextInt() تخبرنا هل ما كتبه المستخدم رقم صحيح قبل قراءته، فنتجنب InputMismatchException:
Scanner in = new Scanner(System.in);System.out.println("ادخل عمرك:");while (!in.hasNextInt()) { System.out.println("أدخل رقمًا صحيحًا من فضلك:"); in.next(); // تجاهل الإدخال الخاطئ}int age = in.nextInt();System.out.println("عمرك " + age);
١١قائمة فحص قبل تسليم أي برنامج
- يُترجَم دون أخطاء، وراجعت التحذيرات الصفراء.
- اختبرته بقيم عادية، وبالقيم الحدّية تمامًا، وبقيم غير صحيحة.
- المخرجات تطابق المطلوب في نص المسألة حرفيًّا.
- حذفت عبارات الطباعة التشخيصية DEBUG.
- الأسماء واضحة والكود منسّق Alt + Shift + F.
- كل جزء من التفكيك له مكان واضح في الكود أو دالة خاصة.
توقّع وتحقّق
في الرسالة Main.java:12: error: ';' expected في أي سطر الخطأ؟
ما معنى cannot find symbol؟
في تتبّع المكدّس، أي سطر يحدد مكان الخطأ في برنامجك؟
أي استثناء يظهر عند a[5] ومصفوفة طولها 5؟
ما أول خطوة في منهجية التصحيح؟
برنامج أعطى نتيجة صحيحة مع مدخل واحد. هذا يعني:
ما فائدة hasNextInt()؟
أي اسم متغير أفضل لمجموع الفاتورة؟
الخلاصة
- ابدأ دائمًا بأول رسالة خطأ، وابحث عن رقم السطر ووصف الخطأ.
- في رسالة الاستثناء: النوع والتفاصيل، ثم أسطر at يبيّن أولها في ملفك مكان الخطأ.
- try تحيط بالكود الذي قد يفشل، و catch تعالج الاستثناء بدل توقف البرنامج، و finally تُنفّذ دائمًا.
- أعِد إنتاج الخطأ، ثم حدد مكانه، وافهم سببه، وأصلحه، وأعد الاختبار.
- الأسماء الواضحة والتحقق من المدخلات والبناء التدريجي تمنع كثيرًا من الأخطاء.
تحدٍّ برمجي: أصلح حاسبة المعدل
متقدمالبرنامج يقرأ عدد المواد ثم درجاتها ويطبع المعدل، لكنه يعطي نتيجة خاطئة، ويتوقف إذا كان عدد المواد صفرًا.
- استخدم جدول تتبّع أو الطباعة التشخيصية لإيجاد الخطأ المنطقي.
- أضف تحققًا يمنع القسمة على صفر ويطبع: لا توجد مواد
- مع المدخلات 3 ثم 80 90 70 يجب أن يطبع: المعدل = 80.0
ابدأ من هذا الكود، واضغط ▶ شغّل هنا لتكتب حلّك وتجرّبه مباشرة:
import java.util.Scanner;public class Main { public static void main(String[] args) { Scanner in = new Scanner(System.in); int n = in.nextInt(); int sum = 0; for (int i = 1; i < n; i++) { sum = sum + in.nextInt(); } double avg = sum / n; System.out.println("المعدل = " + avg); }}
💡 تلميح 1
💡 تلميح 2
حاول بنفسك أولًا؛ للمسألة أكثر من حلّ صحيح.
import java.util.Scanner;public class Main { public static void main(String[] args) { Scanner in = new Scanner(System.in); int n = in.nextInt(); if (n == 0) { System.out.println("لا توجد مواد"); return; } int sum = 0; for (int i = 1; i <= n; i++) { sum = sum + in.nextInt(); } double avg = (double) sum / n; System.out.println("المعدل = " + avg); }}