טיפול בחריגות ב-Java

הקדמה

חריגה היא אירוע שגיאה שיכול לקרות במהלך ביצוע תכנית ומפריע לזרימתה הרגילה. ב-Java קיימת דרך יציבה ומבוססת עצם לטיפול במצבי חריגה שנקראת טיפול בחריגות ב-Java.

חריגות ב-Java יכולות להתרחש מסוגים שונים של מצבים כמו קלט שגוי של המשתמש, כשיבוש בחומת החומה, כשל בחיבור הרשת או שרת מסד נתונים שאינו פעיל. הקוד שמגדיר מה לעשות במצבי חריגה מסוימים נקרא טיפול בחריגות.

השלכת ותפיסת חריגות

ב-Java, נוצרת אובייקט חריגה כאשר אירעה טעות בביצוע הפקודה. אובייקט החריגה מכיל המון מידע ניפוי כמו היררכיה של המתודות, מספר השורה בה אירעה החריגה וסוג החריגה.

אם יש חריגה בתוך שיטה, התהליך של יצירת אובייקט החריגה והעברתו לסביבת ההרצה נקרא "השלכת החריגה". זרימת התוכנית הרגילה נעצרת וה-Java Runtime Environment (JRE) מנסה למצוא את טפס החריגה. מטפס החריגה הוא קטע קוד שיכול לעבד את אובייקט החריגה.

  • הלוגיקה למציאת טפס החריגה מתחילה עם חיפוש בשיטה בה התרחשה השגיאה.
  • אם לא נמצא טפס מתאים, החיפוש ימשיך לשיטת הקורא.
  • וכך הלאה.

לכן, אם קונקס השיחה של השיטה הוא A->B->C ויש חריגה בשיטה C, אז החיפוש אחר טפס החריגה המתאים יתקדם מ-C->B->A.

אם טפס החריגה המתאים נמצא, אז אובייקט החריגה יועבר לטפס כדי לעבד אותו. הטפס נחשב ל-"תופס את החריגה". אם לא נמצא טפס החריגה המתאים, אז התוכנית תסתיים ותדפיס מידע על החריגה לקונסול.

גרם הטיפול בחריגות ב-Java משמש לטיפול בשגיאות זמן ריצה בלבד. שגיאות זמן קומפילציה חייבות להיתקן על ידי מפתח התוכנה שכותב את הקוד, אחרת התוכנית לא תופעל.

מילות המפתח לטיפול בחריגות ב-Java

Java מספקת מילות מפתח ספציפיות למטרות טיפול בשגיאות.

  1. השלכת – אנו יודעים כי אם חולה טעות, אובייקט חריגה נוצר ואז זירות הריצה של Java מתחילות לעבד אותן. לפעמים ייתכן שנרצה לייצר חריגות מפורשות בקוד שלנו. לדוגמה, בתוכנית אימות משתמש, עלינו לזרוק חריגות ללקוחות אם הסיסמה היא null. מילת המפתח throw משמשת לזריקת חריגות לזירות הריצה כדי לטפל בהן.
  2. הזריקה – כאשר אנו זורקים חריגה בשיטה ולא מטפלים בה, אז עלינו להשתמש במילת המפתח throws בחתימת השיטה כדי להודיע לתכנית הקוראת על החריגות שעשויות להיזרק על ידי השיטה. השיטה הקוראת עשויה לטפל בחריגות אלה או להעביר אותן לשיטת הקורא השלה באמצעות מילת המפתח throws. אנו יכולים לספק מספר חריגות במקטע ה-throws, וניתן להשתמש בו עם השיטה main() גם.
  3. נסה-תפוס – אנו משתמשים בבלוק try-catch לטיפול בחריגות בקוד שלנו. try הוא תחילת הבלוק ו-catch נמצא בסופו של הבלוק try כדי לטפל בחריגות. אנו יכולים לקבוע מספר של בלוקי 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(). כאשר אני לא מטפל בהן, אני מעביר אותן לזמן ריצה בעזרת פסקה המשמשת לכך בשיטת ה- 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.

היררכיה של יוצאי חריגות ב-Java

כפי שנאמר מראש, כאשר חריגה נגרמת, נוצרת עצם חריגה. יוצאי החריגות ב-Java הם היררכיים ונעשה שימוש בהורשה כדי לקטגורז את סוגי החריגות השונים. Throwable הוא המחלקה האב של היררכיית יוצאי החריגות ב-Java ויש לה שני אובייקטי ילד – Error ו־Exception. Exception מחולקות נוספת ל-Checked Exceptions ול-Runtime Exceptions.

  1. שגיאות: Errors הן תרחישים חריגים שאינם בתחום היישום, ואי אפשר לצפות ולהתאושש מהם. לדוגמה, כשיש תקלה בחומרה, התמוטטות של מכונת ה-Java (JVM), או שגיאת אי-זיכרון. זה הסיבה שיש לנו היררכיה נפרדת של Errors ואנו לא צריכים לנסות להתמודד עם מצבים אלה. כמה מה-Errors הנפוצים הם OutOfMemoryError ו־StackOverflowError.
  2. שגיאות מאומתות: שגיאות מאומתות הן תרחישים חריגים שאנו יכולים להניח בתוכנית ולנסות להתאושש מהם. לדוגמה, FileNotFoundException. עלינו לתפוס את החריג הזה ולספק הודעה שימושית למשתמש ולרשום אותה בצורה תקינה לצורך ניפוי שגיאות. ה-Exception הוא המחלקה האב של כל ה-Exception מאומתות. אם אנו מזינים Exception מאומתת, עלינו לתפוס אותה באותו השיטה, או שעלינו להעביר אותה לקורא באמצעות המילת מפתח throws.
  3. שגיאות זמן ריצה: שגיאות זמן ריצה נגרמות על ידי תכנות רע. לדוגמה, ניסיון לאחזר איבר ממערך. עלינו לבדוק תחילה את אורך המערך לפני שננסה לאחזר את האיבר, אחרת זה עשוי לגרום ל-ArrayIndexOutOfBoundException בזמן ריצה. RuntimeException הוא המחלקה האב של כל שגיאות הזמן הריצה. אם אנו מזינים שגיאת זמן ריצה בשיטה, אין צורך לציין אותן בעצם החתימה של השיטה באמצעות הביטוי throws. ניתן למנוע שגיאות זמן ריצה עם תכנות יותר טוב.

כמה שיטות שימושיות של מחלקות החריגות

Java Exception וכל תתי-מחלקותיה אינן מספקות שום שיטות ספציפיות, וכל השיטות מוגדרות במחלקת הבסיס – Throwable. מחלקות ה-Exception נוצרו כדי לציין סוגים שונים של תרחות Exception כך שנוכל לזהות בקלות את הסיבה המרכזית ולטפל ב-Exception לפי סוגו. מחלקת ה-Throwable מיישמת את הממשק Serializable לאינטרואופרביליות.

כמה מהשיטות השימושיות של מחלקת ה-Throwable הן:

  1. public String getMessage() – שיטה זו מחזירה את ההודעה מסוג String של Throwable וההודעה יכולה להיספק בעת יצירת החריגה דרך בנאי החריגה שלה.
  2. public String getLocalizedMessage() – שיטה זו ניתנת כך שתתת-מחלקות יכולות לדרוס אותה כדי לספק הודעה התואמת למקום נקרא. יישום המחלקה Throwable משתמש בשיטת getMessage() כדי להחזיר את ההודעה של החריגה.
  3. public synchronized Throwable getCause() – שיטה זו מחזירה את הסיבה לחריגה או null אם הסיבה אינה ידועה.
  4. public String toString() – שיטה זו מחזירה את המידע על Throwable בפורמט של String, ה-String שמוחזר מכיל את שם מחלקת ה-Throwable וההודעה המקומית.
  5. public void printStackTrace()–‏ מתודה זו מדפיסה את מידע מעקב המחסנית לזרם השגיאה התקני, מתודה זו מוטעית, וניתן להעביר PrintStream או PrintWriter כארגומנט כדי לכתוב את מידע מעקב המחסנית לקובץ או לזרם.

שיפורים בניהול משאבים אוטומטי ב-Java 7 ובלוק catch

אם אתה מתפס הרבה חריגות בבלוק try אחד, תשים לב שקוד הבלוק catch בדרך כלל מורכב מקוד חוזר לרישום השגיאה. ב-Java 7, אחת התכונות הייתה שיפור בבלוק catch שבו אפשר לתפוס מספר חריגות בבלוק catch אחד. הנה דוגמה לבלוק catch עם תכונה זו:

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

קיימות מספר מגבלות כגון זה שאובייקט החריגה הוא סופי ואין אפשרות לשנות אותו בתוך בלוק catch, קרא את הניתוח המלא ב שיפורים בבלוק ה-Catch של Java 7.

רוב הזמן, אנו משתמשים בבלוק finally רק כדי לסגור את המשאבים. לפעמים אנו שוכחים לסגור אותם ומקבלים חריגות זמן ריצה כאשר המשאבים מוגמרים. חריגות אלו קשות לאיתור בדיקה, וייתכן כי נצטרך לחפש בכל מקום בו אנו משתמשים במשאב הזה כדי לוודא שסגרנו אותו. ב-Java 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

Java מספקת הרבה מחלקות חריגות שניתן להשתמש בהן, אך לפעמים עשוי להיות עלינו ליצור את המחלקות המותאמות שלנו. לדוגמה, כדי להודיע לקורא על סוג ספציפי של חריגה עם הודעה מתאימה. אנו יכולים לקבוע שדות מותאמים אישית למעקב, כגון קודי שגיאה. לדוגמה, נניח שאנו כותבים שיטה לעיבוד קבצי טקסט בלבד, כך שנוכל לספק לקורא קוד שגיאה מתאים כאשר סוג אחר של קובץ נשלח כקלט.

ראשית, יש ליצור 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, אין צורך לציין זאת ב-Clause throws.

זו הייתה החלטת עיצוב. השימוש ב-Checked Exceptions מספק יתרון בסיוע למפתחים להבין אילו חרגים יש לצפות ולקבוע פעולות מתאימות לטיפול בהם.

Best Practices for Exception Handling in Java

  • Use Specific Exceptions – במחלקות בסיס של ההיררכיה של Exception איןן ספק מידע שימושי, זו הסיבה שב-Java יש כל כך הרבה מחלקות חריגה, כמו למשל IOException עם מחלקות משנה כמו FileNotFoundException, EOFException, וכו'. תמיד עלינו להשתמש ב-throw וב-catch במחלקות חריגה ספציפיות כך שהקורא ידע בקלות את הסיבה הבסיסית של החרגה ויכול לעבד אותן. זה יעשה את תהליך התיקונים קל יותר ויסייע ליישמר את החרגות במקום.
  • Throw Early or Fail-Fast – עלינו לנסות להשליך חרגות בכמה שיותר מוקדם. נשמע, לדוגמה, בשיטת 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)

בזמן האיתור ותיקון שגיאות, יש להסתכל בקריאה לעקוב אחרי עקבות ה-Stack בקפידה כדי לזהות את המיקום המדויק של החריגה. אם נשנה את הלוגיקה של המימוש שלנו כך שנבדוק מוקדם על ידי הוספת קטעי קוד כמו שלמטה:

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

	// ... further processing
}

, אז יתכן שסטאק החריגה יצביע על המקום המדויק של החריגה עם הודעה ברורה:

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 – מאחר ששפת Java דורשת מאיתנו לטפל בחריגות בדרך שהיא או להכריז עליהן בחתימת המתודה, לפעמים מפתחים יוכלו להשתמש במימוש כמו שמתואר כאן, בו נעטיף ב-catch חריגה ונרשום את השגיאה ללוג. אך הפרקטיקה הזו יכולה לגרום לנזק משום שהתכנית הקוראת אינה מקבלת הודעה אודות החריגה. עלינו להשתמש ב-catch רק כאשר יש לנו את היכולת לטפל בחריגה באופן המתאים. לדוגמה, במתודה למעלה, אני משתמש ב-throw כדי להעביר את החריגות חזרה למתודת הקורא על מנת שתטפל בהן. אפשר גם שתוכל להשתמש במתודה זו באפליקציות אחרות שיכולות לרצות לעבד את החריגה בדרך שונה.
  • Closing Resources – מאחר שחריגות משהות את העיבוד של התוכנית, עלינו לסגור את כל המשאבים בבלוק ה-finally או להשתמש בשדרוג Java 7 של try-with-resources כדי שהריצה של Java תסגור אותם עבורנו.
  • רישום חריגות – תמיד כדאי לרשום את הודעות החריגה ובעת הזרקת חריגות לספק הודעה ברורה כך שהקורא יוכל לדעת בקלות למה התרחשה החריגה. כדאי גם להימנע מבלוק catch ריק שמטפל רק בחריגה ואינו מספק פרטים משמעותיים על החריגה למטרות דיבוג.
  • בלוק תפיסת חריגה אחד למספר חריגות – רוב הזמן אנו רושמים את פרטי החריגה ומספקים הודעה למשתמש, במקרה זה כדאי להשתמש בתכונת Java 7 לטיפול במספר חריגות בבלוק catch יחיד. גישה זו תפחית את גודל הקוד שלנו, וגם ייראה נקי יותר.
  • שימוש בחריגות מותאמות אישית – תמיד עדיף להגדיר אסטרטגיית טיפול בחריגות בזמן העיצוב ולאחר מכן, במקום לזרוק ולתפוס מספר חריגות, נוכל ליצור חריגה מותאמת אישית עם קוד שגיאה, והתוכנית הקוראת תוכל לטפל בקודי שגיאה אלה. גם רעיון טוב ליצור שיטת יוליטי לעיבוד קודי שגיאה שונים ולהשתמש בהם.
  • תקני שמות ואריזה – כאשר אתה יוצר חריגה מותאמת אישית שלך, וודא שהיא מסתיימת ב- Exception כך שיהיה ברור מהשם עצמו שזו מחלקת חריגה. וגם, וודא שהם מארוזים כמו שנעשה בכיתת פיתוח Java (JDK). לדוגמה, IOException היא החריגה הבסיסית לכל פעולות הקובץ.
  • השתמש בחריטות בזהירות – חריטות הן יקרות, ולפעמים אין צורך לזרוק חריטות כלל, וניתן להחזיר משתנה בוליאני לתכנית הקוראת כדי לציין האם הפעולה הייתה מוצלחת או לא. זה מועיל במקרים בהם הפעולה אופציונלית, ואין רצונך שהתוכנית שלך תתעקש כי היא נכשלה. לדוגמה, בעת עדכון מחירי המניות במסד הנתונים משירות אינטרנט של צד שלישי, ייתכן שתרצה להימנע מזריקת חריטות אם החיבור נכשל.
  • תיעוד החריטות שנזרקות – השתמש ב-Javadoc @throws כדי לציין בבהירות את החריטות שנזרקות על ידי השיטה. זה מאוד מועיל כאשר אתה מספק ממשק ליישומים אחרים לשימוש.

מסקנה

במאמר זה, למדת על טיפול בחריטות ב-Java. למדת על throw ו-throws. גם למדת על בלוקי try (ובלוקי try-with-resources), catch, ו־finally.

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