הרצאה 12: קבצים וחריגות (Exceptions) מבוא למדעי המחשב הרצאה 12: קבצים וחריגות (Exceptions)
זיכרון משני לעומת זיכרון ראשי מתקן לאכסון מידע במחשב – דיסק קשיח, Disk on Key, CD, DVD וכולי... איטי יותר מהזיכרון הראשי זול יותר מהזיכרון הראשי גדול יותר מהזיכרון הראשי אינו נמחק כאשר המחשב מכובה 2
קובץ יחידת המידע הנשמרת בזיכרון המשני קבצי טקסט Word, java, txt קבצי מידע, סרטים, מוזיקה ועוד...
למה צריך קבצים הזיכרון לא מספיק קלט גדול שימוש חוזר של מידע גיבוי של תוצאות ביניים העברת מידע (בין אנשים או בין תוכניות) קובץ קונפיגורציה רוצים לחשב את הגיל הממוצע של תושבי העולם. לא נרצה להכניס את הנתונים ידנית. אולי נרצה להשתמש שוב באותם נתונים בתכנית אחרת. סיפור מהחיים: היתה לי תכנית שהיתה אמורה לרוץ במשך 10 שנים. אחרי 5 שנים היתה הפסקת חשמל. סיפור עוד יותר מהחיים: תכנה של בנק רצה כל הזמן. חייבים שהנתונים יישמרו סיפור מהקורס: שומרים ציונים בקובץ (למשל Excel). מאוחר יותר אולי נוסיף פקטור... ??? מה הכוונה בקובץ קונפיגורציה? !!! כשיש המון פרמטרים שצריך להשתמש בהם אז יש 3 אפשרויות: לקבל מהמשתמש את הנתונים בכל פעם באמצעות Scanner – אבל אמרנו שיש המון פרמטרים – דיכאון... להחזיק את הנתונים הנדרשים בתור שדות במחלקה – אבל אז כל שינוי באחד הפרמטרים מחייב קומפילציה מחדש – דיכאון שתיים... שמירת הפרמטרים בקובץ קונפיגורציה, ושינוי הקובץ לפי הצורך – אפשר להיפטר מהפרוזאק 4
כתיבה לקובץ Import java.io.*; FileWriter fw = new FileWriter("out.txt"); PrintWriter pw = new PrintWriter(fw); pw.println("First line to write!"); pw.println("Second line!"); pw.close(); fw.close(); ראשית נציין שבעבודה עם קבצים יש להוסיף import java.io.* בתחילת הקובץ ניתן להתייחס ל-fw כאל הדף עליו אנו כותבים (קובץ היעד) ניתן להתייחס ל-pw בתור העט שאיתו אנו כותבים כרגע זה לא מתקמפל בגלל חוסר טיפול ב-IOException שיוסבר בהמשך השיעור
קריאה מקובץ Import java.io.*; FileReader fr = new FileReader("out.txt"); BufferedReader br = new BufferedReader(fr); String s1 = br.readLine(); while(s1!=null) { System.out.println(s1); s1 = br.readLine(); } br.close(); fr.close(); ניתן להתייחס ל-fr כאל הדף ממנו אנו קוראים (קובץ המקור) ניתן להתייחס ל-br בתור המשקפיים בהם אנו נעזרים לקריאה readLine של BufferedReader מחזיר null כשמגיעים לסוף הקובץ כרגע זה לא מתקמפל בגלל חוסר טיפול ב-IOException שיוסבר בהמשך השיעור 6
תוכנית שכותבת תוכנית Scanner sc = new Scanner(System.in); FileWriter fw = new FileWriter("MyName.java"); PrintWriter pw = new PrintWriter(fw); System.out.print("Please enter name: "); String name = sc.next(); pw.println("public class MyName {"); pw.println("\t"+"public static void main(String args[]) {"); pw.println("\t\t"+"System.out.println(\""+name+"\");"); pw.println("\t"+"}"); pw.println("}"); pw.close(); fw.close(); שימו לב שתוכנית java היא בסך הכל קובץ טקסט מאחר ולמדנו היום לכתוב טקסט לקובץ, כעת אנו יכולים לכתוב תוכנית שבעצמה כותבת תוכנית java אחרת נסו בבית לקמפל ולהריץ את התוכנית שנוצרה... כרגע זה לא מתקמפל בגלל חוסר טיפול ב-IOException שיוסבר בהמשך השיעור
חריגות (Exceptions) חריגה היא אירוע המתרחש במהלך תוכנית המפר את תהליך הריצה הנורמאלי של פקודות התוכנית לעיתים חריגות מתרחשות בגלל תקלות בלתי צפויות, כגון בעיה בקובץ אליו כותבים (למשל אין הרשאות כתיבה), ולעיתים בגלל תקלות תוכנה, כגון שליחת פרמטר לא מתאים לפונקציה עד עכשיו תארנו את מנגנון הקריאה לפונקציה כמעין חוזה. כותב הפונקציה מגדיר תנאי קדם שעבורו הפונקציה עובדת. אם המשתמש לא מקיים את תנאי הקדם, אין כל התחייבות מצד הפונקציה לבצע פעולה תקינה. אם הפונקציה נקראה ותנאי הקדם מתקיים, יש התחייבות להתנהגות מסויימת. בצורה כזו מחולקת האחריות בין "ספק השרות" ל"לקוח". נראה היום מצבים בהם אע"פ שהלקוח מקיים את תנאי הקדם אין לספק אפשרות לקיים את חלקו בחוזה, וזאת משום תקלות חריגות. 8
בעצם כבר נתקלנו ב-Exceptions ArithmeticException: ניסיון חלוקה באפס IndexOutOfBoundsException: חריגה ממערך NullPointerException: ניסיון לפעול על מערך בעל ערך null המשותף לכל ה-Exceptions האלו הוא שהן נובעות מטעויות תכנות והפתרון להן הוא שינוי של הקוד המוטעה. מאידך, נראה מצבים בהם החריגה נובעת מבעיות שאינן תלויות במתכנת. במקרים כאלה יתכן כי נרצה לתת פתרון לבעיה בגוף התוכנית. 9
הודעה על חריגה הדוגמאות ל-Exceptions שראינו מודיעות שהייתה חריגה בתוכנית גם אנחנו רוצים לפעמים להודיע על חריגה לדוגמא בפונקציה (משיעורים ,87): public static int intValue(String s) נרצה להודיע על חריגה במקרה והמחרוזת s המתקבלת כפרמטר לא מייצגת מספר (למשל, המחרוזת (“12intro091” 10
אלטרנטיבה לחריגה נשתמש בערך המוחזר של הפונקציה פתרון זה הופך את הקוד למסורבל לא תמיד ניתן לבצע זאת Exceptions מאפשרות הפרדה חדה בין קוד הריצה השגרתית של התכנית לבין קוד הרץ במקרה של תקלות טיפול במקרים חריגים במסגרת הקוד עלול להפוך אותו למסורבל לא תמיד ניתן לבצע זאת: לדוגמא הפונקציה intValue שמקבלת String ומחזירה את ערכו המספרי. אם הפונקציה מקבלת מחרוזת שאינה מייצגת מספר? לא ניתן להחזיר ערך המייצג שגיאה, שכן כל הערכים המוחזרים "תפוסים" (כל המספרים השליליים, כל המספרים החיוביים ואפס). 11
מיתוס: תמיד ניתן להימנע מ-Exceptions נניח שרוצים לקרוא מקובץ נניח שבאותו הזמן, משתמש אחר בא ומוחק את הקובץ האם אפשר לדעת בוודאות שהקריאה תתבצע ללא תקלות? האם ניתן לבצע בדיקה שתמנע תקלה? ש 12
מיתוס: תמיד כדאי להימנע מ-Exceptions בהרצת קטע הקוד הבא: Scanner sc = new Scanner(System.in); int a = sc.nextInt(); יכול היה המשתמש להקליד "abc" במקום מספר ואז הפונקציה nextInt היתה מודיעה על חריגה באמצעות זריקת Exception האם nextInt יכלה להימנע מזריקת Exception ע"י החזרת ערך ברירת מחדל?
IndexOutOfBoundsException סוגי Exceptions IOException IOException IOException IOException יש סוגים רבים של Exceptions. אחד החשובים שבהם הוא RuntimeException. אין חובה לטפל ב-Exceptions אלה בקוד, ורוב ה-Exceptions שראינו עד היום הן סוגים של RuntimeException. לדוגמא, IndexOutOfBoundsException, ArithmeticException וכו'. סוגים נוספים של Exceptions אשר לא נמצאים תחת RuntimeException מחייבים טיפול בקוד. לדוגמא, IOException הקשורה לעבודה עם קבצים. נדון בה בהרחבה בהמשך. IOException IndexOutOfBoundsException 14
זריקת Exception public static int intValue(char c) { final String conversion = "0123456789ABCDEF"; int ans = conversion.indexOf(c); if (ans == -1) throw new RuntimeException(“intValue got an incorrect character: ”+c); return ans; } בהתחלה לא לרשום באדום לפניכם הפונקציה intValue שמקבלת תו ומחשבת את ערכו המספרי. ראינו את הפונקציה לפני מספר שיעורים כחלק מהתוכנית Literal ??? מה אם התו שהפונקציה מקבלת בתור פרמטר אינו ספרה או אות אנגלית עד F? !!! יוחזר -1 ??? למי זה יוחזר? !!! לפונקציה שקראה לנו, וזו intValue שמקבלת String ??? האם אנו רוצים להחזיר לה תשובה -1? !!! לא! זה ישבש את החישוב ובעצם קיבלנו שגיאה לוגית אנחנו רוצים להודיע שהקלט לא היה תקין ולכן לא ניתן לבצע את החישוב להוסיף את הטקסט באדום ניצור RuntimeException חדש ונתאר בצורה הברורה ביותר את מקור השגיאה היינו רוצים לייצר סוג חדש ופרטני של Exception (כגון IndexOutOfBoundsException) ולא לזרוק RuntimeException שהוא יותר מדי כללי על יצירת סוג חדש של Exception נלמד בהמשך הקורס 15
איך מתייחסים ל-Exception? ישנן שלוש דרכים לעשות זאת: להתעלם – אפשרי רק עבור RuntimeException לתפוס את ה-Exception על ידי שימוש במילים השמורות try ו-catch להעביר את ה-Exception הלאה על ידי שימוש במילה השמורה throws בכותרת הפונקציה שאנו כותבים התעלמות מ-Exception זה מה שעשינו עד כה. למשל, כשנזרקה IndexOutOfBoundsException לא טיפלנו בזה בקוד, ולכן התוכנית "עפה" והודיעה שהיתה חריגה לפעמים ניתן בגוף התוכנית לטפל בצורה חכמה בחריגה, ובכך להמשיך את ריצת התוכנית בצורה תקינה על האפשרות השלישית נדבר בהמשך
תפיסת Exception public static int intValue(String s) { . for (int i = s.length()-1; i >= first; i = i-1) { try { value = value + intValue(s.charAt(i)) * power; power = power * base; } catch (RuntimeException e) { System.out.println(e.getMessage()+" - skipping the character"); בהתחלה לא לרשום באדום לפניכם הפונקציה intValue שמקבלת מחרוזת הקוראת לפונקציה שעלולה לזרוק RuntimeExeption. ללא שימוש בכלל ב-Exception החישוב היה יוצא שגוי במקרה של תו שאינו חוקי – היה מחושב -1.... כעת תהיה הודעת שגיאה. אבל למה לא פשוט להודיע שהיתה שגיאה, להתעלם מהתו המיותר ולהמשיך בחישוב כרגיל? להוסיף את הטקסט באדום נעטוף את הקוד הבעייתי ב-try. אם בבלוק זה תיזרק RuntimeException יורץ הבלוק של ה-catch במקום. כך נוכל לדלג על העדכון של value ו-power (כלומר להתעלם מהתו הבעייתי) וגם להודיע למשתמש על הבעיה לאחר מכן תמשיך לולאת ה-for לאיבר הבא והחישוב לא ייעצר ??? ומה היה קורה לו power = power * base; היה מופיע לאחר ה-try-catch? !!! Power היה גדל בכל מקרה, כלומר היה מופיע 0 במקום התו הבעייתי וזאת לעומת התעלמות מהתו 17
מבנה try ו-catch את כל הקוד ה"רגיל" של התכנית אנו כוללים בתוך הבלוק של ה-try אם לא ייזרק Exception, הקוד שבתוך הבלוק של ה-catch לעולם לא ירוץ אם ייזרק Exception הקוד הראשון שירוץ מייד עם זריקת ה-Exception הוא הקוד של בלוק ה-catch סוג ה-Exception שמופיע ב-catch חייב להתאים ל-Exception שנזרק, אחרת לא ייתבצע ה-catch 18
הקוד הבא לא מתקמפל FileWriter fw = new FileWriter("out.txt"); PrintWriter pw = new PrintWriter(fw); pw.println("First line to write!"); pw.println("Second line!"); pw.close(); fw.close(); קוד של IO מחייב התייחסות ל-Exceptions הקוד לא מתקמפל, ולא בגלל טיפול שגוי בקבצים. אז מה הבעיה? נסתכל על שגיאת הקומפילציה: TextFile.java:7: unreported exception java.io.IOException; must be caught or declared to be thrown FileWriter fw = new FileWriter("out.txt"); ^ TextFile.java:12: unreported exception java.io.IOException; must be caught or declared to be thrown fw.close(); במהלך פתיחה של קובץ ובמהלך סגירתו עלולות לצוץ בעיות. לדוגמא, אין לנו הרשאת כתיבה בספריה בה אנו עובדים ולכן איננו יכולים לפתוח קובץ. לפיכך עלול להיזרק Exception. אנו חייבים לכלול התייחסות כלשהי ל-Exception בקוד שלנו. 19
לפעמים חייבים לטפל ב-Exceptions עד היום ראינו בעיקר Exceptions מסוג RuntimeException הנובעות לרוב משגיאות תכנות. שגיאות כאלו צריכות להיות מנופות מהקוד. לפיכך הקומפיילר לא מכריח אותנו לתפוס Exceptions מהסוג הזה לעומת זאת, ישנן Exceptions שקשורות לסביבה בה אנו מחשבים (מערכת הקבצים, תקשורת בין מחשבים, עבודה מול מסד נתונים) ואליהן אנו חייבים להתייחס
שימוש ב-try ו-catch public static void writeToFile() { try { FileWriter fw = new FileWriter("out.txt"); PrintWriter pw = new PrintWriter(fw); pw.println("First line to write!"); pw.println("Second line!"); pw.close(); fw.close(); } catch(IOException e) { System.out.print("Error while writing: " + e); דוגמא זו תלווה את ההסבר על טיפול ב Exception ??? מה אני אמור לעשות בתוך הבלוק של ה-catch? !!! כל מקרה לגופו! לעיתים נרצה רק לכתוב הודעת שגיאה למשתמש. אם קיבלנו Exception כי קובץ שרצינו לקרוא ממנו לא היה קיים, ייתכן כי במקרים מסוימים נרצה לנסות לפתוח את הקובץ לאחר שינוי של סיומת הקובץ. 21
העברת Exception הלאה public static void writeToFile() throws IOException { FileWriter fw = new FileWriter("out.txt"); PrintWriter pw = new PrintWriter(fw); pw.println("First line to write!"); pw.println("Second line!"); pw.close(); fw.close(); } במקרים רבים איננו יכולים או רוצים לתת טיפול ל-Exception (אפילו לא הדפסת הודעת שגיאה!). ??? איך עושים את זה טכנית? !!! מוסיפים את המילים throws IOException לכותרת השיטה. (או כל Exception אחרת שהשיטה עלולה לזרוק(. עתה הקוד יתקמפל ללא צורך ב-try ו-catch אך כל פונקציה שקוראת לפונקציה writeToFile חייבת לטפל בעובדה שהפונקציה עלולה לזרוק Exception. 22
חישוב ממוצע ציונים מקובץ public static int calcAverage(String fileName) throws IOException { int numOfGrades = 0, sum = 0; FileReader fr = new FileReader(fileName); BufferedReader br = new BufferedReader(fr); String s1 = br.readLine(); while(s1!=null) { sum = sum + intValue(s1); numOfGrades = numOfGrades + 1; s1 = br.readLine(); } br.close(); fr.close(); return sum/numOfGrades; 23
יותר מחריגה אפשרית אחת בפונקציה הקודמת ישנה אפשרות ל- 2 חריגות שונות: IOException בשל פתיחת קובץ לקריאה לחריגה זו חובה להתייחס. במקרה זה הכרזנו על העברתה הלאה ArithmeticException בשל חלוקה אפשרית באפס זהו סוג של RuntimeException, ולכן לא מחייב התייחסות 24
תפיסת חריגות בפונקציה הקוראת public static void main(String[] args) { int average; try { average = calcAverage("grades2.txt"); System.out.println("The average is "+average); } catch (IOException e1){ System.out.println("Problem with the file"); catch (ArithmeticException e2){ System.out.println("The average is 0"); ירוץ רק catch אחד, וזה ה-catch הראשון שמתאים 25