הימנע מבדיקת הצהרת אפס ב- Java

1. סקירה כללית

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

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

במדריך זה נבדוק את הצורך לבדוק ריק בג'אווה ובחלופות שונות שעוזרות לנו להימנע ריק בודק בקוד שלנו.

2. מה זה NullPointerException?

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

  • קורא לשיטת מופע של ריק לְהִתְנַגֵד
  • גישה או שינוי של שדה של ריק לְהִתְנַגֵד
  • לוקח את אורכו של ריק כאילו זה מערך
  • גישה או שינוי החריצים של ריק כאילו זה מערך
  • זְרִיקָה ריק כאילו היה א זורק ערך

בואו נראה במהירות כמה דוגמאות של קוד Java הגורמות לחריג זה:

חלל ציבורי doSomething () {תוצאה מחרוזת = doSomethingElse (); אם (result.equalsIgnoreCase ("הצלחה")) // הצלחה}} מחרוזת פרטית doSomethingElse () {return null; }

פה, ניסינו להפעיל קריאת שיטה עבור ריק התייחסות. זה יביא ל- NullPointerException.

דוגמה נפוצה נוספת היא אם אנו מנסים לגשת ל- a ריק מַעֲרָך:

סטטי ציבורי ריק ריק (String [] args) {findMax (null); } ריק סטטי פרטי findMax (int [] arr) {int max = arr [0]; // בדוק אלמנטים אחרים בלולאה}

זה גורם ל- a NullPointerException בשורה 6.

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

דרך נפוצה להימנע מ- NullPointerException זה לבדוק ריק:

חלל ציבורי doSomething () {תוצאה מחרוזת = doSomethingElse (); if (result! = null && result.equalsIgnoreCase ("Success")) {// success} else // failure} מחרוזת פרטית doSomethingElse () {return null; }

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

בחלקים הבאים נעבור על חלק מהחלופות בג'אווה המונעות יתירות כזו.

3. טיפול ריק באמצעות חוזה ה- API

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

עם זאת, לעתים קרובות ישנם ממשקי API שיכולים לטפל בהם ריק ערכים. לדוגמה:

הדפסת חלל ציבורית (Object param) {System.out.println ("הדפסה" + param); } תהליך אובייקט ציבורי () זורק Exception {Object result = doSomething (); if (result == null) {throw Exception new ("העיבוד נכשל. קיבלת תגובה null"); } אחר {תוצאה חוזרת; }}

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

אז עבור קוד לקוח שניגש לממשקי ה- API הנ"ל, אין צורך ב- ריק חשבון.

עם זאת, ממשקי API כאלה חייבים להבהיר זאת במפורש בחוזה שלהם. מקום נפוץ עבור ממשקי API לפרסום חוזה כזה הוא JavaDoc.

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

בחלק הבא נראה כיצד מספר IDE וכלי פיתוח אחרים מסייעים למפתחים בכך.

4. אוטומציה של חוזי API

4.1. שימוש בניתוח קוד סטטי

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

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

קבל חלל ציבורי (@Nonnull Object param) {System.out.println (param.toString ()); }

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

4.2. שימוש בתמיכה ב- IDE

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

חלק מה- IDE מאפשרים למפתחים לנהל חוזי API ובכך לבטל את הצורך בכלי ניתוח קוד סטטי. IntelliJ IDEA מספקת את @NonNull ו @מאפשרת ערכי null ביאורים. כדי להוסיף את התמיכה להערות אלה ב- IntelliJ, עלינו להוסיף את התלות הבאה של Maven:

 ביאורי org.jetbrains 16.0.2 

עַכשָׁיו, IntelliJ תיצור אזהרה אם ה- ריק המחאה חסרה, כמו בדוגמה האחרונה שלנו.

IntelliJ מספקת גם א חוֹזֶה ביאור לטיפול בחוזי API מורכבים.

5. טענות

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

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

כאן נוכל להשתמש ב- Java Assertions במקום במסורתית ריק בדוק הצהרה מותנית:

קבל חלל ציבורי (Param param) {assert param! = null; doSomething (פרמטר); }

בשורה 2 אנו בודקים אם ריק פָּרָמֶטֶר. אם הקבוצות מופעלות, הדבר יביא ל- קביעה שגיאה.

למרות שזו דרך טובה לקבוע תנאים מקדימים כמו לאריק פרמטרים, לגישה זו שתי בעיות עיקריות:

  1. קביעות בדרך כלל מושבתות ב- JVM
  2. א שֶׁקֶר קביעה מביאה לשגיאה בלתי מסומנת שאינה ניתנת לשחזור

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

6. הימנעות ריק בדיקות באמצעות שיטות קידוד

6.1. תנאים מוקדמים

בדרך כלל מקובל לכתוב קוד שנכשל מוקדם. לכן, אם API מקבל מספר פרמטרים שאינם מורשים להיות ריק, עדיף לבדוק את כל הלאריק פרמטר כתנאי מוקדם של ה- API.

לדוגמה, בואו נסתכל על שתי שיטות - אחת שנכשלת מוקדם ואחת שלא:

public void goodAccept (מחרוזת אחת, מחרוזת שתיים, מחרוזת שלוש) {if (one == null || two == null || three == null) {throw new IllegalArgumentException (); } תהליך (אחד); תהליך (שניים); תהליך (שלוש); } public void badAccept (String one, String two, String three) {if (one == null) {throw new IllegalArgumentException (); } אחר {תהליך (אחד); } if (two == null) {throw new IllegalArgumentException (); } אחר {תהליך (שניים); } אם (שלוש == null) {זרוק IllegalArgumentException () חדש; } אחר {תהליך (שלוש); }}

ברור שאנחנו צריכים להעדיף goodAccept () על badAccept ().

כחלופה, אנו יכולים גם להשתמש בתנאים המוקדמים של גויאבה לאימות פרמטרי API.

6.2. שימוש בפרימיטיבים במקום בשיעורי עטיפה

מאז ריק אינו ערך מקובל עבור פרימיטיבים כמו int, עלינו להעדיף אותם על פני עמיתיהם לעטיפה כמו מספר שלם בכל מקום אפשרי.

שקול שתי יישומים של שיטה המסכמת שני מספרים שלמים:

סטטי ציבורי int primitiveSum (int a, int b) {return a + b; } עטיפת ציבור שלם סטטי ציבורי Sum (Integer a, Integer b) {return a + b; }

עכשיו, בואו נקרא APIs אלה בקוד הלקוח שלנו:

סכום int = primitiveSum (null, 2);

זה יביא לשגיאת זמן הידור מאז ריק אינו ערך חוקי עבור int.

וכאשר משתמשים ב- API עם שיעורי עטיפה, אנו מקבלים NullPointerException:

assertThrows (NullPointerException.class, () -> wrapperSum (null, 2));

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

6.3. אוספים ריקים

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

שמות רשימות ציבוריים () {if (userExists ()) {return Stream.of (readName ()). collect (Collectors.toList ()); } אחר {להחזיר Collections.emptyList (); }}

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

7. שימוש חפצים

ג'אווה 7 הציגה את החדש חפצים ממשק API. ל- API זה מספר סטָטִי שיטות שירות המסלקות הרבה קוד מיותר. בואו נסתכל על שיטה אחת כזו, requireNonNull ():

קבל חלל ציבורי (Object param) {Objects.requireNonNull (param); // עשה משהו() }

עכשיו, בואו נבדוק את לְקַבֵּל() שיטה:

assertThrows (NullPointerException.class, () -> accept (null));

אז אם ריק מועבר כוויכוח, לְקַבֵּל() זורק א NullPointerException.

בכיתה זו יש גם isNull () ו nonNull () שיטות שניתן להשתמש בהן כפרדיקות לבדיקת אובייקט ריק.

8. שימוש אופציונאלי

8.1. באמצעות orElseTrow

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

ציבורי תהליך אופציונלי (מעובד בוליאני) {מחרוזת תגובה = doSomething (מעובד); if (response == null) {return Optional.empty (); } להחזיר Optional.of (תגובה); } מחרוזת פרטית doSomething (מעובד בוליאני) {if (מעובד) {return "עבר"; } אחר {להחזיר null; }}

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

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

assertThrows (Exception.class, () -> process (false) .orElseThrow (() -> Exception new ()));

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

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

ציבור תהליך אופציונלי (מעובד בוליאני) {תגובת מחרוזת = doSomething (מעובד); החזרה Optional.ofNullable (תגובה); }

8.2. באמצעות אופציונאלי עם אוספים

תוך כדי התמודדות עם אוספים ריקים, אופציונאלי מגיע שימושי:

מחרוזת ציבורית findFirst () {return getList (). stream () .findFirst () .orElse (DEFAULT_VALUE); }

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

זה מאפשר לנו לטפל ברשימות ריקות או ברשימות שאחרי שהשתמשנו ב זרם ספריות לְסַנֵן בשיטה, אין פריטים לספק.

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

public findOptionalFirst () אופציונלי {return getList (). stream () .findFirst (); }

לכן, אם התוצאה של getList ריק, שיטה זו תחזיר ריק אופציונאלי ללקוח.

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

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

8.3. שילוב אופציות

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

שֶׁלָנוּ findFirst שיטה רוצה להחזיר אופציונאלי היסוד הראשון של אופציונאלי רשימה:

public OptionalListFirst () אופציונלי {return getOptionalList () .flatMap (רשימה -> list.stream (). findFirst ()); }

באמצעות flatMap פונקציה ב- אופציונאלי חזר מ getOptional אנו יכולים לפרק את התוצאה של ביטוי פנימי שחוזר אופציונאלי. לְלֹא flatMap, התוצאה תהיה אופציונאלי. ה flatMap הפעולה מתבצעת רק כאשר ה- אופציונאלי זה לא ריק.

9. ספריות

9.1. שימוש בלומבוק

Lombok היא ספרייה נהדרת המפחיתה את כמות קוד הדוד בפרויקטים שלנו. זה מגיע עם קבוצה של ביאורים שתופסים את מקומם של חלקי קוד משותפים שלעתים קרובות אנו כותבים בעצמנו ביישומי ג'אווה, כגון גטרים, מגדירים ו toString (), עד כמה שם.

הערה נוספת שלה היא @NonNull. לכן, אם פרויקט כבר משתמש ב- Lombok כדי לחסל את קוד הדוד, @NonNull יכול להחליף את הצורך ב ריק צ'קים.

לפני שנמשיך לראות כמה דוגמאות, בואו נוסיף תלות ב- Maven עבור Lombok:

 org.projectlombok lombok 1.18.6 

עכשיו, אנחנו יכולים להשתמש @NonNull בכל מקום בו ריק יש צורך בבדיקה:

קבל חלל ציבורי (@NonNull Object param) {System.out.println (param); }

אז פשוט הערנו את האובייקט שעבורו ריק בדיקה הייתה נדרשת, ולומבוק יוצר את הכיתה המהוללת:

קבל חלל ציבורי (@NonNull Object param) {if (param == null) {זרוק NullPointerException חדש ("param"); } אחר {System.out.println (param); }}

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

9.2. באמצעות StringUtils

בדרך כלל, חוּט אימות כולל בדיקת ערך ריק בנוסף ל ריק ערך. לכן, הצהרת אימות נפוצה תהיה:

קבל חלל ציבורי (פרמטר מחרוזת) {if (null! = param &&! param.isEmpty ()) System.out.println (param); }

זה הופך במהירות למיותר אם נצטרך להתמודד עם הרבה חוּט סוגים. זה איפה StringUtils מגיע שימושי. לפני שנראה זאת בפעולה, בואו להוסיף תלות ב- Maven עבור commons-lang3:

 org.apache.commons commons-lang3 3.8.1 

בואו כעת לשקף את הקוד לעיל עם StringUtils:

קבל חלל ציבורי (Parring String) {if (StringUtils.isNotEmpty (param)) System.out.println (param); }

אז החלפנו את שלנו ריק או המחאה ריקה עם סטָטִי שיטת השירות זה לא ריק(). ממשק API זה מציע שיטות עוצמה אחרות לטיפול נפוץ חוּט פונקציות.

10. מסקנה

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

כל הדוגמאות זמינות ב- GitHub.