השגיאה הנפוצה ביותר ב-Java
NullPointerException (כולם קוראים לה NPE) קורית כשמנסים להשתמש בהפניה שמצביעה על כלום, null, כאילו היא מצביעה על אובייקט אמיתי. משתנים ב-Java מטיפוס אובייקט מחזיקים או אובייקט או null, הערך שאומר "אין כאן אובייקט". ברגע שמבקשים מ-null לעשות משהו, לקרוא עליו למתודה, לקרוא אחד מהשדות שלו, לגשת אליו לפי אינדקס, אין שם על מה לפעול, וה-JVM זורקת חריגה.
בניגוד ל-try-catch, שבו השתמשתם בעמוד הקודם כדי לטפל בכשלים צפויים, NPE היא כמעט תמיד באג פשוט. המטרה של העמוד הזה היא לא לתפוס אותן, אלא להבין למה הן קורות ולכתוב קוד שלא מייצר אותן.
אין String ש-length() יכולה לרוץ עליו, ולכן התוכנית נעצרת עם NullPointerException.
קריאת ההודעה
מאז Java 14, ההודעה אומרת לכם בדיוק מה היה null, וזה נקרא Helpful NullPointerException. קראו אותה לפני שאתם משנים משהו:
Exception in thread "main" java.lang.NullPointerException: Cannot invoke "String.length()" because "name" is null
at Main.main(Main.java:4)
שני חלקים חשובים. Cannot invoke "String.length()" היא הפעולה שנכשלה, ו-because "name" is null מצביע על האשם. השורה at Main.main(Main.java:4) היא ה-stack trace שמצביע על השורה המדויקת. לכן התיקון הוא לא "לעטוף את שורה 4 ב-try", אלא "להבין למה name הוא null בשורה 4". כמעט תמיד הבאג נמצא מוקדם יותר, במקום שבו הערך היה צריך להיות מושם ולא הושם.
הדרכים לגרום לה
NPE מגיעות מקומץ פעולות, וכולן וריאציות על "נגיעה ב-null":
חיפושים במפה הם מקור קלאסי: get מחזירה null כשהמפתח לא קיים, וה-NPE צצה לעיתים קרובות כמה שורות אחר כך, כשסוף סוף משתמשים בערך. Unboxing הוא המקרה הערמומי: השמה של Integer שהוא null ל-int זורקת חריגה, כי אין מספר להעתיק.
הגנה עם בדיקת null
ההגנה הפשוטה ביותר היא if שמוודא שהפניה אינה null לפני שמשתמשים בה:
כשמשווים משתנה ל-String קבוע, שימו את הקבוע ראשון: "yes".equals(answer) במקום answer.equals("yes"). אם answer הוא null, הצורה הראשונה מחזירה false בשקט, והשנייה זורקת חריגה.
להיכשל מהר עם Objects.requireNonNull
פיזור בדיקות null בכל מקום נהיה רועש. כשערך אף פעם לא אמור להיות null, למשל ארגומנט של בנאי, בודקים אותו בגבול עם Objects.requireNonNull. היא זורקת חריגה מיד, עם הודעה ברורה, בנקודה שבה הערך הבעייתי מגיע, במקום עמוק בתוך הקוד מאוחר יותר:
ההרגל הזה של "להיכשל מהר" הופך NPE מעורפלת 200 שורות משם לתלונה מדויקת במקור. תפיסת החריגה כאן נועדה רק להראות את ההודעה: בקוד אמיתי נותנים לה לצוף כדי שהבאג יתוקן.
להימנע מ-null מלכתחילה
ה-NPE הטובה ביותר היא זו שלא יכולה לקרות, כי אין null להיתקל בו. כמה הרגלים עושים הבדל גדול:
- החזירו אוסף ריק או מחרוזת ריקה, אף פעם לא
null. עלCollections.emptyList()ועל""בטוח לעבור בלולאה ולקרוא למתודות. - השתמשו ב-
getOrDefaultבמפות, כדי שחיפוש שלא מצא יחזיר ערך אמיתי במקוםnull. - אתחלו שדות כשמצהירים עליהם, במקום להשאיר אותם
nullעד "אחר כך".
כשערך הוא אופציונלי באמת, חיפוש שבאופן לגיטימי עשוי לא למצוא כלום, Java מציעה את Optional, מיכל שמכריח את הקורא לטפל במקרה של "אין ערך" במקום להחזיר null בשקט. זה הנושא הקשור שכדאי לקרוא הבא אם רוצים לתכנן את החורים האלה החוצה מה-API שלכם לגמרי.
לסיכום
NullPointerException היא הדרך של Java לומר לכם שהשתמשתם בהפניה שמחזיקה null כאילו היא מחזיקה אובייקט. התיקון הוא לעיתים רחוקות לתפוס אותה: קוראים את ההודעה המפורטת, עוקבים אחורה למקום שבו הערך היה צריך להיקבע, ואז מבטיחים שהוא לא null או מגינים על המקום שבו משתמשים בו. היעזרו ב-Objects.requireNonNull כדי להיכשל מהר בגבולות, העדיפו ערכים ריקים ו-getOrDefault על פני null, והשתמשו ב-Optional כשהיעדר ערך הוא תוצאה אמיתית וצפויה. אמצו את צורת החשיבה הזו, והשגיאה הנפוצה ביותר ב-Java תהפוך לאחת הנדירות ביותר בקוד שלכם.
שאלות נפוצות
מה גורם ל-NullPointerException ב-Java?
היא קורית כשמשתמשים בהפניה שמצביעה על null כאילו היא מצביעה על אובייקט אמיתי: למשל קוראים למתודה (name.length()), קוראים שדה, ניגשים לאיבר במערך, או מבצעים unboxing ל-Integer שהוא null. המשתנה לא מחזיק אובייקט, אין על מה לפעול, וה-JVM זורקת NullPointerException.
איך מתקנים NullPointerException ב-Java?
קוראים את ההודעה: מאז Java 14 היא אומרת בדיוק מה היה null (למשל "Cannot invoke "String.length()" because "name" is null"). אחר כך מבררים למה המשתנה הזה null: אתחול חסר, מתודה שהחזירה null, או חיפוש במפה שלא מצא. מתקנים את המקור כך שהערך לעולם לא יהיה null, או מגינים על השימוש בבדיקת null, ב-Objects.requireNonNull או ב-Optional.
עדיף לבדוק null או לתפוס NullPointerException?
לבדוק null. NullPointerException מסמנת באג בלוגיקה, לא מצב צפוי, ולכן צריך למנוע אותה ולא לתפוס אותה. תפיסה שלה מסתירה איפה הבעיה האמיתית. שמרו את try/catch למצבים חריגים באמת, כמו כשלים בקלט ופלט.