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

المقدمة

الاستثناء هو حدث خطأ يمكن أن يحدث أثناء تنفيذ برنامج ويعطل تدفقه الطبيعي. توفر لغة الجافا طريقة قوية وموجهة نحو الكائنات لمعالجة سيناريوهات الاستثناء المعروفة باسم معالجة الاستثناء في الجافا.

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

رمي والتقاط الاستثناءات

تنشئ الجافا كائن استثناء عند حدوث خطأ أثناء تنفيذ بيان. يحتوي كائن الاستثناء على الكثير من معلومات التصحيح مثل تسلسل الطرق، ورقم الخط الذي حدث فيه الاستثناء، ونوع الاستثناء.

إذا حدث استثناء في الطريقة، يُطلق على عملية إنشاء كائن الاستثناء وتسليمه إلى بيئة التشغيل اسم “رمي الاستثناء“. يتوقف التدفق العادي للبرنامج وتحاول بيئة تشغيل جافا (JRE) البحث عن معالج للاستثناء. المعالج للاستثناء هو كتلة الشيفرة التي يمكنها معالجة كائن الاستثناء.

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

لذا، إذا كانت سلسلة استدعاء الطريقة هي A->B->C وتم رفع استثناء في الطريقة C، فإن عملية البحث عن معالج مناسب ستنتقل من C->B->A.

إذا تم العثور على معالج استثناء مناسب، يتم تمرير كائن الاستثناء إلى المعالج لمعالجته. يُقال أن المعالج يقوم بـ “اصطياد الاستثناء“. إذا لم يكن هناك معالج استثناء مناسب، يتم إنهاء البرنامج وطباعة معلومات حول الاستثناء في وحدة التحكم.

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

كلمات رئيسية لمعالجة الاستثناء في جافا

توفر جافا كلمات مفتاحية محددة لأغراض التعامل مع الاستثناءات.

  1. throw – نحن نعلم أنه إذا حدث خطأ، يتم إنشاء كائن استثناء وبعد ذلك يبدأ تشغيل جافا في معالجتها للتعامل معها. في بعض الأحيان قد نرغب في إثارة الاستثناءات بوضوح في كودنا. على سبيل المثال، في برنامج المصادقة للمستخدم، يجب أن نثير استثناءات إلى العملاء إذا كانت كلمة المرور null. تستخدم الكلمة المفتاحية throw لإثارة الاستثناءات إلى تشغيل جافا للتعامل معها.
  2. throws – عندما نقوم بإثارة استثناء في طريقة ولا نتعامل معه، فإنه يجب علينا استخدام الكلمة المفتاحية throws في توقيع الطريقة لإعلام برنامج الاستدعاء بالاستثناءات التي قد تثار بواسطة الطريقة. يمكن للطريقة اللاحقة التعامل مع هذه الاستثناءات أو نقلها إلى طريقة المستدعي الخاصة بها باستخدام الكلمة المفتاحية throws. يمكننا توفير استثناءات متعددة في شريحة throws، ويمكن استخدامها مع طريقة main() أيضًا.
  3. try-catch – نستخدم كتلة try-catch للتعامل مع الاستثناءات في كودنا. try هو بداية الكتلة وcatch هو في نهاية كتلة try للتعامل مع الاستثناءات. يمكن أن تحتوي كتلة try-catch على عدة كتل catch مع كتلة try. يمكن تضمين كتلة try-catch أيضًا. تتطلب كتلة catch معلمة يجب أن تكون من نوع Exception.
  4. أخيرًا – الكتلة finally اختيارية ويمكن استخدامها فقط مع كتلة try-catch. نظرًا لأن الاستثناء يوقف عملية التنفيذ، قد نكون لدينا بعض الموارد مفتوحة التي لن تُغلق، لذا يمكننا استخدام كتلة finally. تُنفذ الكتلة finally دائمًا، سواء حدث استثناء أم لا.

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

ExceptionHandling.java
package com.journaldev.exceptions;

import java.io.FileNotFoundException;
import java.io.IOException;

public class ExceptionHandling {

	public static void main(String[] args) throws FileNotFoundException, IOException {
		try {
			testException(-5);
			testException(-10);
		} catch(FileNotFoundException e) {
			e.printStackTrace();
		} catch(IOException e) {
			e.printStackTrace();
		} finally {
			System.out.println("Releasing resources");
		}
		testException(15);
	}

	public static void testException(int i) throws FileNotFoundException, IOException {
		if (i < 0) {
			FileNotFoundException myException = new FileNotFoundException("Negative Integer " + i);
			throw myException;
		} else if (i > 10) {
			throw new IOException("Only supported for index 0 to 10");
		}
	}
}
  • تُلقي الطريقة testException() استثناءات باستخدام الكلمة الأساسية throw. تستخدم توقيع الطريقة كلمة الأساسية throws لإعلام المُستدعي بنوع الاستثناءات التي قد تُلقيها.
  • في الطريقة main()، أقوم بمعالجة الاستثناءات باستخدام كتلة try-catch في الطريقة main(). عندما لا أقوم بمعالجته، أقوم بنقله إلى الوقت التنفيذي باستخدام جملة throws في الطريقة main().
  • لا تُنفذ الطريقة testException(-10) أبدًا بسبب الاستثناء، ثم تُنفذ الكتلة finally.

تعتبر printStackTrace() واحدة من الطرق المفيدة في فئة Exception لأغراض تصحيح الأخطاء.

سينتج هذا الكود التالي:

Output
java.io.FileNotFoundException: Negative Integer -5 at com.journaldev.exceptions.ExceptionHandling.testException(ExceptionHandling.java:24) at com.journaldev.exceptions.ExceptionHandling.main(ExceptionHandling.java:10) Releasing resources Exception in thread "main" java.io.IOException: Only supported for index 0 to 10 at com.journaldev.exceptions.ExceptionHandling.testException(ExceptionHandling.java:27) at com.journaldev.exceptions.ExceptionHandling.main(ExceptionHandling.java:19)

بعض النقاط المهمة للملاحظة:

  • لا يمكن أن يكون لدينا جملة catch أو finally بدون بيان try.
  • A try statement should have either catch block or finally block, it can have both blocks.
  • لا يمكننا كتابة أي كود بين كتل try-catch-finally.
  • يمكننا أن نمتلك عدة كتل catch مع عبارة try واحدة.
  • يمكن تداخل كتل try-catch مشابه لعبارات if-else.
  • يمكننا أن نمتلك فقط كتلة finally واحدة مع عبارة try-catch.

تسلسل استثناءات جافا

كما ذكر سابقًا، عند رفع استثناء يتم إنشاء كائن استثناء. تكون استثناءات جافا هرمية ويتم استخدام التوريث لتصنيف أنواع مختلفة من الاستثناءات. Throwable هو الفئة الأم لتسلسل استثناءات جافا ولديها كائنان فرعيان – Error و Exception. يتم تقسيم استثناءات Exception إلى استثناءات تم التحقق منها واستثناءات تشغيلية.

  1. الأخطاء: الأخطاء هي سيناريوهات استثنائية خارج نطاق التطبيق، ولا يمكن التنبؤ بها أو استردادها. على سبيل المثال، فشل الأجهزة، تعطل جهاز الجافا الظاهري (JVM)، أو خطأ في الذاكرة. لهذا السبب لدينا تسلسل منفصل للأخطاء ويجب ألا نحاول التعامل مع هذه الحالات. بعض الأخطاء الشائعة هي OutOfMemoryError و StackOverflowError.
  2. الاستثناءات المدققة: الاستثناءات المدققة هي السيناريوهات الاستثنائية التي يمكننا توقعها في البرنامج ونحاول التعافي منها. على سبيل المثال، FileNotFoundException. يجب أن نقوم بالتقاط هذا الاستثناء وتقديم رسالة مفيدة للمستخدم وتسجيله بشكل مناسب لأغراض التصحيح. الاستثناء هو الفئة الأساسية لجميع الاستثناءات المدققة. إذا كنا نقوم برمي استثناء مدقق، يجب أن نلتقطه في نفس الطريقة، أو علينا أن ننقله إلى المستدعي باستخدام كلمة المفتاح throws.
  3. استثناء تشغيلي: الاستثناءات التشغيلية تحدث بسبب سوء البرمجة. على سبيل المثال، محاولة استرجاع عنصر من مصفوفة. يجب أن نتحقق من طول المصفوفة أولاً قبل محاولة استرجاع العنصر، وإلا فقد يقوم برمي ArrayIndexOutOfBoundException أثناء التشغيل. الفئة RuntimeException هي الفئة الأساسية لجميع استثناءات التشغيل. إذا كنا نقوم بـ throw أي استثناء تشغيلي في طريقة، فليس من الضروري تحديده في توقيع الطريقة بجملة throws. يمكن تجنب الاستثناءات التشغيلية ببرمجة أفضل.

بعض الطرق المفيدة لفئات الاستثناء

الاستثناءات في جافا وجميع فصائلها لا توفر أي طرق محددة، وتم تعريف جميع الطرق في الفئة الأساسية – Throwable. يتم إنشاء فئات الاستثناء لتحديد أنواع مختلفة من سيناريوهات الاستثناء بحيث يمكننا تحديد سبب الجذر والتعامل مع الاستثناء وفقًا لنوعه. تنفذ فئة Throwable واجهة Serializable للتوافق بين البرامج.

بعض الطرق المفيدة في فئة Throwable هي:

  1. public String getMessage() – تُرجع هذه الطريقة سلسلة الرسائل لـ Throwable ويمكن توفير الرسالة أثناء إنشاء الاستثناء من خلال مُنشئه.
  2. public String getLocalizedMessage() – يتم توفير هذه الطريقة بحيث يمكن للفصائل تجاوزها لتوفير رسالة محددة للموقع لبرنامج الاستدعاء. تستخدم تنفيذ فئة Throwable لهذه الطريقة طريقة getMessage() لإرجاع رسالة الاستثناء.
  3. public synchronized Throwable getCause() – تُرجع هذه الطريقة سبب الاستثناء أو قيمة null إذا كان السبب غير معروف.
  4. public String toString() – تُرجع هذه الطريقة معلومات حول Throwable في تنسيق String، يحتوي السلسلة المُرجعة اسم فئة Throwable ورسالة موقع.
  5. public void printStackTrace() – تقوم هذه الطريقة بطباعة معلومات تتبع الكومة إلى تيار الخطأ القياسي، وهذه الطريقة متعددة الحملة، ويمكننا تمرير PrintStream أو PrintWriter كمعلمة لكتابة معلومات تتبع الكومة إلى الملف أو التيار.

تحسينات إدارة الموارد التلقائية وكتلة الالتقاط في جافا 7

إذا كنت تقوم بـ catch للكثير من الاستثناءات في كتلة try واحدة، فستلاحظ أن كود كتلة الـ catch يتكون في الغالب من كود متكرر لتسجيل الخطأ. في جافا 7، كانت إحدى الميزات تحسين كتلة الـ catch حيث يمكننا التقاط العديد من الاستثناءات في كتلة catch واحدة. فيما يلي مثال على كتلة الـ catch بهذه الميزة:

catch (IOException | SQLException ex) {
    logger.error(ex);
    throw new MyException(ex.getMessage());
}

هناك بعض القيود مثل أن كائن الاستثناء نهائي ولا يمكننا تعديله داخل كتلة catch، اقرأ التحليل الكامل في تحسينات كتلة الـ catch في جافا 7.

معظم الوقت، نستخدم كتلة finally فقط لإغلاق الموارد. أحيانًا ننسى إغلاقها ونحصل على استثناءات تشغيل عندما تستنفد الموارد. هذه الاستثناءات صعبة التصحيح، وقد نحتاج إلى النظر في كل مكان نستخدم فيه تلك المورد للتأكد من إغلاقه. في جافا 7، كانت إحدى التحسينات try-with-resources حيث يمكننا إنشاء مورد في تعليمة try نفسها واستخدامه داخل كتلة try-catch. عندما تخرج التنفيذ من كتلة try-catch، تقوم البيئة التشغيلية تلقائيًا بإغلاق هذه الموارد. إليك مثال على كتلة try-catch مع هذا التحسين:

try (MyResource mr = new MyResource()) {
	System.out.println("MyResource created in try-with-resources");
} catch (Exception e) {
	e.printStackTrace();
}

A Custom Exception Class Example

توفر جافا العديد من فئات الاستثناءات لنا للاستخدام، ولكن في بعض الأحيان قد نحتاج إلى إنشاء فئات استثناء مخصصة خاصة بنا. على سبيل المثال، لإخطار المتصل بنوع معين من الاستثناء مع الرسالة المناسبة. يمكننا أيضًا الحصول على حقول مخصصة لتتبع مثل رموز الخطأ. على سبيل المثال، دعونا نفترض أننا كتبنا طريقة لمعالجة ملفات النص فقط، حتى نتمكن من توفير رمز الخطأ المناسب للمتصل عند إرسال نوع ملف آخر كمدخل.

أولاً، أنشئ MyException:

MyException.java
package com.journaldev.exceptions;

public class MyException extends Exception {

	private static final long serialVersionUID = 4664456874499611218L;

	private String errorCode = "Unknown_Exception";

	public MyException(String message, String errorCode) {
		super(message);
		this.errorCode=errorCode;
	}

	public String getErrorCode() {
		return this.errorCode;
	}
}

ثم، أنشئ CustomExceptionExample:

CustomExceptionExample.java
package com.journaldev.exceptions;

import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;

public class CustomExceptionExample {

	public static void main(String[] args) throws MyException {
		try {
			processFile("file.txt");
		} catch (MyException e) {
			processErrorCodes(e);
		}
	}

	private static void processErrorCodes(MyException e) throws MyException {
		switch (e.getErrorCode()) {
			case "BAD_FILE_TYPE":
				System.out.println("Bad File Type, notify user");
				throw e;
			case "FILE_NOT_FOUND_EXCEPTION":
				System.out.println("File Not Found, notify user");
				throw e;
			case "FILE_CLOSE_EXCEPTION":
				System.out.println("File Close failed, just log it.");
				break;
			default:
				System.out.println("Unknown exception occured, lets log it for further debugging." + e.getMessage());
				e.printStackTrace();
		}
	}

	private static void processFile(String file) throws MyException {
		InputStream fis = null;

		try {
			fis = new FileInputStream(file);
		} catch (FileNotFoundException e) {
			throw new MyException(e.getMessage(), "FILE_NOT_FOUND_EXCEPTION");
		} finally {
			try {
				if (fis != null) fis.close();
			} catch (IOException e) {
				throw new MyException(e.getMessage(), "FILE_CLOSE_EXCEPTION");
			}
		}
	}
}

يمكننا أيضًا أن نملك طريقة منفصلة لمعالجة أنواع مختلفة من رموز الأخطاء التي نحصل عليها من طرق مختلفة. يتم استهلاك بعضها لأنه قد لا نرغب في إخطار المستخدم بها، أو قد نعيدها لإخطار المستخدم بالمشكلة.

ها أنا أوسع Exception بحيث يتعين التعامل مع هذا الاستثناء عند إنتاجه في الطريقة أو إعادته إلى برنامج النداء. إذا قمنا بتوسيع RuntimeException، لا داعي لتحديده في عبارة throws.

كانت هذه قرارات التصميم. يتيح استخدام Exception المدروسة ميزة مساعدة المطورين على فهم الاستثناءات التي يمكن توقعها واتخاذ الإجراءات اللازمة للتعامل معها.

أفضل الممارسات لمعالجة الاستثناءات في جافا

  • استخدام استثناءات محددة – لا تقدم فئات الأساس في هرم الاستثناء أي معلومات مفيدة، ولهذا السبب لدينا العديد من فئات الاستثناء في جافا، مثل IOException مع فئات فرعية أخرى مثل FileNotFoundException، EOFException، إلخ. يجب علينا دائمًا أن نقوم بـ throw و catch لفئات الاستثناء المحددة حتى يعرف المتصل سبب الاستثناء بسهولة ويتمكن من معالجته. يجعل هذا التصحيح أسهل ويساعد تطبيقات العميل على التعامل مع الاستثناءات بشكل مناسب.
  • إلقاء الاستثناء في وقت مبكر أو الفشل السريع – يجب أن نحاول أن نلقي الاستثناءات في وقت مبكر إذا كان ذلك ممكنًا. اعتبر الطريقة processFile() أعلاه، إذا قمنا بتمرير وسيط null إلى هذه الطريقة، سنحصل على الاستثناء التالي:
Output
Exception in thread "main" java.lang.NullPointerException at java.io.FileInputStream.<init>(FileInputStream.java:134) at java.io.FileInputStream.<init>(FileInputStream.java:97) at com.journaldev.exceptions.CustomExceptionExample.processFile(CustomExceptionExample.java:42) at com.journaldev.exceptions.CustomExceptionExample.main(CustomExceptionExample.java:12)

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

private static void processFile(String file) throws MyException {
	if (file == null) throw new MyException("File name can't be null", "NULL_FILE_NAME");

	// ... معالجة إضافية
}

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

Output
com.journaldev.exceptions.MyException: File name can't be null at com.journaldev.exceptions.CustomExceptionExample.processFile(CustomExceptionExample.java:37) at com.journaldev.exceptions.CustomExceptionExample.main(CustomExceptionExample.java:12)
  • Catch Late – حيث يفرض لغة الجافا التعامل سواءً بالتعامل مع الاستثناء المفحوص أو بإعلانه في توقيع الطريقة، في بعض الأحيان يميل المطورون إلى catch الاستثناء وتسجيل الخطأ. ولكن هذه الممارسة ضارة لأن برنامج الاتصال لا يحصل على أي إشعار بالاستثناء. يجب أن نقوم بـ catch الاستثناءات فقط عندما نكون قادرين على التعامل معها بشكل مناسب. على سبيل المثال، في الطريقة أعلاه، أنا أقوم بـ throw الاستثناءات مرة أخرى إلى الطريقة المتصلة للتعامل معها. يمكن استخدام نفس الطريقة من قبل تطبيقات أخرى قد ترغب في معالجة الاستثناء بطريقة مختلفة. أثناء تنفيذ أي ميزة، يجب علينا دائمًا أن نقوم بـ throw الاستثناءات مرة أخرى إلى المتصل ونترك لهم أن يقرروا كيفية التعامل معها.
  • إغلاق الموارد – نظرًا لأن الاستثناءات توقف معالجة البرنامج، يجب علينا إغلاق جميع الموارد في كتلة finally أو استخدام تحسين Java 7 try-with-resources للسماح لمحرك Java بإغلاقها بالنيابة عنك.
  • تسجيل الاستثناءات – يجب علينا دائمًا تسجيل رسائل الاستثناء وأثناء رمي الاستثناءات تقديم رسالة واضحة بحيث يعرف المتصل بسهولة سبب حدوث الاستثناء. يجب علينا دائمًا تجنب كتلة الـ catch الفارغة التي تستهلك الاستثناء فقط ولا توفر أي تفاصيل معنوية عن الاستثناء للتصحيح.
  • كتلة واحدة للتقاط الاستثناءات المتعددة – في معظم الأحيان نقوم بتسجيل تفاصيل الاستثناء وتقديم رسالة للمستخدم ، في هذه الحالة ، يجب علينا استخدام ميزة Java 7 لمعالجة الاستثناءات المتعددة في كتلة catch واحدة. سيؤدي هذا النهج إلى تقليل حجم الكود لدينا ، وسيبدو أكثر نظافة أيضًا.
  • استخدام الاستثناءات المخصصة – دائمًا ما يكون من الأفضل تحديد استراتيجية للتعامل مع الاستثناءات في وقت التصميم بدلاً من رمي والتقاط الاستثناءات المتعددة ، يمكننا إنشاء استثناء مخصص مع رمز خطأ ، ويمكن لبرنامج المتصل التعامل مع هذه الرموز الخطأ. من الفكرة الجيدة أيضًا إنشاء طريقة أداة لمعالجة رموز الأخطاء المختلفة واستخدامها.
  • تعاريف الأسماء والتعبئة – عند إنشاء استثناء مخصص ، تأكد من أنه ينتهي بـ Exception حتى يكون واضحًا من الاسم نفسه أنه فئة استثناء. كما تأكد من تعبئتها مثلما هو مفعل في مجموعة أدوات تطوير Java (JDK). على سبيل المثال ، IOException هو الاستثناء الأساسي لجميع عمليات IO.
  • استخدم الاستثناءات بحذر – الاستثناءات تكلف الكثير أحيانًا، وفي بعض الأحيان ليس من الضروري أن نقوم برمي الاستثناءات على الإطلاق، يمكننا بدلاً من ذلك إرجاع متغير بولياني لبرنامج النداء للإشارة إلى ما إذا كانت العملية ناجحة أم لا. يكون ذلك مفيدًا عندما تكون العملية اختيارية، ولا ترغب في أن يتعلق برنامجك بسبب فشله. على سبيل المثال، عند تحديث اقتباسات الأسهم في قاعدة البيانات من خدمة ويب من جهة ثالثة، قد نرغب في تجنب رمي الاستثناءات إذا فشل الاتصال.
  • وثق الاستثناءات المرماة – استخدم Javadoc @throws لتحديد بوضوح الاستثناءات التي يتم رميها بواسطة الطريقة. يكون ذلك مفيدًا جدًا عندما تقدم واجهة لتستخدمها تطبيقات أخرى.

الختام

في هذا المقال، تعلمت حول التعامل مع الاستثناءات في لغة البرمجة جافا. تعلمت حول throw و throws. كما تعلمت حول كتل trytry-with-resourcescatch، و finally.

Source:
https://www.digitalocean.com/community/tutorials/exception-handling-in-java