טיפול יעיל ב- Java NullPointerException

לא נדרש ניסיון רב בפיתוח ג'אווה בכדי ללמוד ממקור ראשון מהו NullPointerException. למעשה, אדם אחד הדגיש את ההתמודדות עם זה כטעות מספר אחת שעושים מפתחי ג'אווה. כתבתי בעבר על שימוש ב- String.value (Object) כדי להפחית את NullPointerExceptions לא רצויים. ישנן מספר טכניקות פשוטות אחרות שאפשר להשתמש בהן כדי להפחית או לחסל את המופעים של סוג RuntimeException שכיח זה שהיה איתנו מאז JDK 1.0. פוסט בבלוג זה אוסף ומסכם כמה מהפופולריות ביותר בטכניקות אלה.

בדוק אם כל אובייקט אפס לפני השימוש

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

final String causeStr = "adding String to Deque that is set to null."; final String elementStr = "Fudd"; Deque deque = null; try { deque.push(elementStr); log("Successful at " + causeStr, System.out); } catch (NullPointerException nullPointer) { log(causeStr, nullPointer, System.out); } try { if (deque == null) { deque = new LinkedList(); } deque.push(elementStr); log( "Successful at " + causeStr + " (by checking first for null and instantiating Deque implementation)", System.out); } catch (NullPointerException nullPointer) { log(causeStr, nullPointer, System.out); } 

בקוד לעיל, Deque המשמש הוא מאותחל בכוונה לאפס כדי להקל על הדוגמה. הקוד tryבבלוק הראשון אינו בודק אם הוא נמצא לפני שהוא מנסה לגשת לשיטת Deque. הקוד tryבבלוק השני אכן בודק Dequeאם הוא null ומיישם יישום של (LinkedList) אם הוא ריק. הפלט משתי הדוגמאות נראה כך:

ERROR: NullPointerException encountered while trying to adding String to Deque that is set to null. java.lang.NullPointerException INFO: Successful at adding String to Deque that is set to null. (by checking first for null and instantiating Deque implementation) 

ההודעה הבאה שגיאה בפלט שלעיל מציינת כי NullPointerExceptionנזרק כאשר מנסים להתקשר לשיטת null Deque. ההודעה הבאה על INFO בפלט שלעיל מציינת כי על ידי בדיקת Dequenull תחילה ולאחר מכן הפעלת יישום חדש עבורו כאשר הוא null, נמנע לחלוטין החריג.

גישה זו משמשת לעיתים קרובות, וכפי שמוצג לעיל, יכולה להיות שימושית מאוד במניעת NullPointerExceptionמקרים לא רצויים (בלתי צפויים) . עם זאת, זה לא ללא עלויות. בדיקת אפס לפני השימוש בכל אובייקט יכולה לנפח את הקוד, יכולה להיות מייגעת לכתוב, ופותחת יותר מקום לבעיות בפיתוח ותחזוקה של הקוד הנוסף. מסיבה זו, דובר על הצגת תמיכה בשפת Java לזיהוי אפס מובנה, הוספה אוטומטית של צ'קים אלה עבור null לאחר הקידוד הראשוני, סוגים בטוחים ללא בטל, שימוש בתכנות מונחה-רוחב (AOP) להוספת בדיקת null לקוד בתים וכלים אחרים לזיהוי אפס.

Groovy כבר מספק מנגנון נוח להתמודדות עם הפניות לאובייקטים שעלולות להיות ריקות. מפעיל הניווט הבטוח של גרובי ( ?.) מחזיר null במקום לזרוק a NullPointerExceptionכאשר ניגשים להפניה לאובייקט null.

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

זהו מצב בו המפעיל השלוש יכול להיות שימושי במיוחד. במקום

// retrieved a BigDecimal called someObject String returnString; if (someObject != null) { returnString = someObject.toEngineeringString(); } else { returnString = ""; } 

המפעיל השלילי תומך בתחביר תמציתי יותר זה

// retrieved a BigDecimal called someObject final String returnString = (someObject != null) ? someObject.toEngineeringString() : ""; } 

בדוק ארגומנטים של שיטת Null

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

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

 /** * Append predefined text String to the provided StringBuilder. * * @param builder The StringBuilder that will have text appended to it; should * be non-null. * @throws IllegalArgumentException Thrown if the provided StringBuilder is * null. */ private void appendPredefinedTextToProvidedBuilderCheckForNull( final StringBuilder builder) { if (builder == null) { throw new IllegalArgumentException( "The provided StringBuilder was null; non-null value must be provided."); } builder.append("Thanks for supplying a StringBuilder."); } /** * Append predefined text String to the provided StringBuilder. * * @param builder The StringBuilder that will have text appended to it; should * be non-null. */ private void appendPredefinedTextToProvidedBuilderNoCheckForNull( final StringBuilder builder) { builder.append("Thanks for supplying a StringBuilder."); } /** * Demonstrate effect of checking parameters for null before trying to use * passed-in parameters that are potentially null. */ public void demonstrateCheckingArgumentsForNull() { final String causeStr = "provide null to method as argument."; logHeader("DEMONSTRATING CHECKING METHOD PARAMETERS FOR NULL", System.out); try { appendPredefinedTextToProvidedBuilderNoCheckForNull(null); } catch (NullPointerException nullPointer) { log(causeStr, nullPointer, System.out); } try { appendPredefinedTextToProvidedBuilderCheckForNull(null); } catch (IllegalArgumentException illegalArgument) { log(causeStr, illegalArgument, System.out); } } 

כאשר הקוד לעיל מבוצע, הפלט מופיע כפי שמוצג להלן.

ERROR: NullPointerException encountered while trying to provide null to method as argument. java.lang.NullPointerException ERROR: IllegalArgumentException encountered while trying to provide null to method as argument. java.lang.IllegalArgumentException: The provided StringBuilder was null; non-null value must be provided. 

בשני המקרים נרשמה הודעת שגיאה. עם זאת, המקרה שבו נבדק אפס השליך IllegalArgumentException שפורסם שכלל מידע נוסף על הקשר לגבי מתי נתקל באפס. לחלופין, ניתן היה לטפל בפרמטר null זה במגוון דרכים. במקרה בו לא טופל פרמטר null, לא היו אפשרויות כיצד לטפל בו. אנשים רבים מעדיפים לזרוק א ' NullPolinterExceptionעם מידע ההקשר הנוסף כאשר מתגלה null במפורש (ראה פריט מס' 60 במהדורה השנייה של Java יעילה או פריט מס '42 במהדורה הראשונה), אך יש לי העדפה קלה IllegalArgumentExceptionמתי הוא במפורש טיעון שיטה שהוא אפס מכיוון שלדעתי החריג מאוד מוסיף פרטי הקשר וקל לכלול את "אפס" בנושא.

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

בדיקת פרמטרים של שיטות עבור null היא גם קבוצת משנה של הנוהג הכללי יותר בבדיקת פרמטרים של שיטות לתקפות כללית כפי שנדון בפריט מס '38 במהדורה השנייה של Java יעילה (פריט 23 במהדורה הראשונה).

שקול פרימיטיבים במקום אובייקטים

אני לא חושב שזה רעיון טוב לבחור סוג נתונים פרימיטיבי (כגון int) על פני סוג ההתייחסות האובייקט המתאים לו (כגון מספר שלם) פשוט כדי למנוע אפשרות של NullPointerException, אבל אין להכחיש שאחד היתרונות של סוגים פרימיטיביים הם שהם לא מובילים ל- NullPointerExceptions. עם זאת, עדיין יש לבדוק תקפות (ראשית לא יכול להיות מספר שלם שלילי) ולכן היתרון הזה עשוי להיות קטן. מצד שני, לא ניתן להשתמש בפרימיטיבים באוספי Java ויש פעמים שרוצים את היכולת להגדיר ערך null.

הדבר החשוב ביותר הוא להיות זהיר מאוד בשילוב של פרימיטיבים, סוגי התייחסות ותיבות אוטומטיות. קיימת אזהרה ב- Java Effective (מהדורה שנייה, פריט מס '49) בנוגע לסכנות, כולל השלכה NullPointerException, הקשורות לערבוב רשלני של סוגים פרימיטיביים וסימוכין.

שקול בזהירות שיחות בשיטה משורשרת

A NullPointerExceptionיכול להיות קל מאוד למצוא כי מספר שורה יציין היכן הוא התרחש. לדוגמה, מעקב אחר ערימה עשוי להיראות כמו זה שמוצג בהמשך:

java.lang.NullPointerException at dustin.examples.AvoidingNullPointerExamples.demonstrateNullPointerExceptionStackTrace(AvoidingNullPointerExamples.java:222) at dustin.examples.AvoidingNullPointerExamples.main(AvoidingNullPointerExamples.java:247) 

עקבות הערימה מבהירה כי NullPointerExceptionהושלך כתוצאה מקוד שבוצע בקו 222 של AvoidingNullPointerExamples.java. גם עם מספר השורה המסופק, עדיין יכול להיות קשה לצמצם איזה אובייקט הוא ריק אם ישנם אובייקטים מרובים עם שיטות או שדות שניגשים באותה שורה.

לדוגמה, הצהרה כמו someObject.getObjectA().getObjectB().getObjectC().toString();כוללת ארבע שיחות אפשריות שעשויות לזרוק את NullPointerExceptionהמיוחס לאותה שורת קוד. שימוש בבאגים יכול לעזור בכך, אך יתכנו מצבים שעדיף פשוט לפרק את הקוד הנ"ל כך שכל שיחה תתבצע בקו נפרד. זה מאפשר למספר השורה הכלול במעקב מחסנית לציין בקלות איזו שיחה מדויקת הייתה הבעיה. יתר על כן, זה מאפשר בדיקה מפורשת של כל אובייקט לאפס. עם זאת, בצד החיסרון, פירוק הקוד מגדיל את שורת ספירת הקודים (לחלקם זה חיובי!) ולא יכול להיות תמיד רצוי, במיוחד אם בטוחים שאף אחת מהשיטות המדוברות לעולם לא תהיה בטלה.

הפוך את NullPointerExceptions לאינפורמטיבי יותר

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

java.lang.NullPointerException at dustin.examples.AvoidingNullPointerExamples.demonstrateNullPointerExceptionStackTrace(Unknown Source) at dustin.examples.AvoidingNullPointerExamples.main(Unknown Source) 

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

הדוגמה הבאה מדגימה עיקרון זה.

final Calendar nullCalendar = null; try { final Date date = nullCalendar.getTime(); } catch (NullPointerException nullPointer) { log("NullPointerException with useful data", nullPointer, System.out); } try { if (nullCalendar == null) { throw new NullPointerException("Could not extract Date from provided Calendar"); } final Date date = nullCalendar.getTime(); } catch (NullPointerException nullPointer) { log("NullPointerException with useful data", nullPointer, System.out); } 

הפלט מהפעלת הקוד לעיל נראה כך.

ERROR: NullPointerException encountered while trying to NullPointerException with useful data java.lang.NullPointerException ERROR: NullPointerException encountered while trying to NullPointerException with useful data java.lang.NullPointerException: Could not extract Date from provided Calendar