ทำไมหนึ่งควรใช้ Objects.requireNonNull ()


214

ผมได้ตั้งข้อสังเกตว่าหลาย Java 8 วิธีใน Oracle JDK การใช้งานObjects.requireNonNull()ซึ่งภายในพ่นNullPointerExceptionถ้าวัตถุที่กำหนด (อาร์กิวเมนต์) nullเป็น

public static <T> T requireNonNull(T obj) {
    if (obj == null)
        throw new NullPointerException();
    return obj;
}

แต่NullPointerExceptionจะถูกโยนทิ้งหากnullวัตถุถูกยกเลิกการลงทะเบียน ดังนั้นทำไมหนึ่งควรทำนี้ตรวจสอบโมฆะพิเศษและโยน NullPointerException?

หนึ่งคำตอบที่ชัดเจน (หรือประโยชน์) คือมันทำให้โค้ดอ่านง่ายขึ้นและฉันเห็นด้วย ฉันกระตือรือร้นที่จะทราบเหตุผลอื่น ๆ ที่ใช้ Objects.requireNonNull()ในการเริ่มต้นของวิธีการ




1
วิธีการตรวจสอบอาร์กิวเมนต์ช่วยให้คุณโกงเมื่อคุณเขียนการทดสอบหน่วย ฤดูใบไม้ผลิมีสาธารณูปโภคเช่นนั้น (ดู docs.spring.io/spring/docs/current/javadoc-api/org/… ) หากคุณมี "ถ้า" และต้องการให้มีการทดสอบหน่วยสูงคุณจะต้องครอบคลุมทั้งสองสาขา: เมื่อตรงตามเงื่อนไขและเมื่อไม่ตรงตามเงื่อนไข หากคุณใช้Objects.requireNonNullรหัสของคุณไม่มีการแตกแขนงดังนั้นการผ่านการทดสอบหน่วยเดียวจะทำให้คุณครอบคลุม 100% :-)
xorcus

คำตอบ:


214

เพราะคุณสามารถทำสิ่งที่ชัดเจนโดยการทำเช่นนั้น ชอบ:

public class Foo {
  private final Bar bar;

  public Foo(Bar bar) {
    Objects.requireNonNull(bar, "bar must not be null");
    this.bar = bar;
  }

หรือสั้นกว่า:

  this.bar = Objects.requireNonNull(bar, "bar must not be null");

ตอนนี้คุณรู้ว่า :

  • เมื่อวัตถุ Foo สร้างสำเร็จโดยใช้new()
  • แล้วของบาร์ฟิลด์รับประกันไม่เป็นโมฆะ

เปรียบเทียบกับ: คุณสร้างวัตถุ Foo วันนี้และพรุ่งนี้คุณจะเรียกใช้วิธีการที่ใช้เขตข้อมูลนั้นและโยน เป็นไปได้มากที่คุณจะไม่รู้ว่าพรุ่งนี้ทำไมการอ้างอิงนั้นเป็นโมฆะเมื่อวานนี้เมื่อมันถูกส่งผ่านไปยังตัวสร้าง

ในคำอื่น ๆ : โดยใช้วิธีนี้อย่างชัดเจนเพื่อตรวจสอบการอ้างอิงที่เข้ามาคุณสามารถควบคุมจุดในเวลาที่ข้อยกเว้นจะถูกโยน และส่วนใหญ่ของเวลาที่คุณต้องการที่จะล้มเหลวให้เร็วที่สุดเท่าที่เป็นไปได้ !

ข้อดีที่สำคัญคือ:

  • ดังกล่าวพฤติกรรมการควบคุม
  • แก้จุดบกพร่องได้ง่ายขึ้น - เนื่องจากคุณสร้างในบริบทของการสร้างวัตถุ ในช่วงเวลาที่คุณมีโอกาสแน่นอนที่บันทึก / ร่องรอยของคุณจะบอกคุณว่าเกิดอะไรขึ้น!
  • และดังที่แสดงด้านบน: พลังที่แท้จริงของความคิดนี้แผ่ออกไปควบคู่กับเขตข้อมูลสุดท้าย เพราะตอนนี้รหัสอื่น ๆในชั้นเรียนของคุณสามารถสันนิษฐานได้ว่าbarไม่เป็นโมฆะและคุณไม่จำเป็นต้องมีการif (bar == null)ตรวจสอบในสถานที่อื่น!

23
คุณสามารถทำให้โค้ดของคุณมีขนาดเล็กลงได้โดยการเขียนthis.bar = Objects.requireNonNull(bar, "bar must not be null");
Kirill Rakhman

3
@ KirillRakhman การเรียนการสอน GhostCat บางสิ่งบางอย่างที่ฉันไม่รู้ -> ชนะตั๋วใน GhostCat ที่มีลอตเตอรี่
GhostCat

1
ผมคิดว่าการแสดงความคิดเห็น # 1 Objects.requireNonNull(bar, "bar must not be null");ที่นี่เป็นประโยชน์ที่แท้จริงของ ขอบคุณสำหรับสิ่งนี้
Kawu

1
อันไหนเป็นอันแรกและทำไมคน "ภาษา" ควรทำตามผู้แต่งห้องสมุดบางคน ! ทำไมฝรั่งไม่ทำตามพฤติกรรม Java lang!
GhostCat

1
this.bar = Objects.requireNonNull(bar, "bar must not be null");ตกลงในการก่อสร้าง แต่อาจเป็นอันตรายในวิธีการอื่น ๆ ถ้าสองคนหรือมากกว่าตัวแปรที่ตั้งอยู่ในวิธีการเดียวกัน ตัวอย่าง: talkwards.com/2018/11/03/...
Erk

100

ล้มเหลวอย่างรวดเร็ว

รหัสควรล้มเหลวโดยเร็วที่สุด ไม่ควรทำครึ่งหนึ่งของงานและจากนั้นทำการยกเลิกค่า Null และ crash เพียงปล่อยครึ่งหนึ่งของงานที่ทำไปทำให้ระบบอยู่ในสถานะที่ไม่ถูกต้อง

นี้เป็นที่นิยมเรียกว่า "ล้มเหลวในช่วงต้น" หรือ"ล้มเหลวอย่างรวดเร็ว"


40

แต่ NullPointerException จะถูกโยนทิ้งหากวัตถุ null ถูกยกเลิกการลงทะเบียน ดังนั้นทำไมหนึ่งควรทำนี้ตรวจสอบโมฆะพิเศษและโยน NullPointerException

มันหมายความว่าคุณตรวจสอบปัญหาที่เกิดขึ้นทันทีและน่าเชื่อถือ

พิจารณา:

  • การอ้างอิงอาจไม่สามารถใช้งานได้จนกว่าจะภายหลังในวิธีการหลังจากรหัสของคุณได้ดำเนินการผลข้างเคียงบางอย่างแล้ว
  • การอ้างอิงอาจไม่ถูกอ้างถึงในวิธีนี้เลย
    • มันอาจถูกส่งผ่านไปยังรหัสที่แตกต่างกันโดยสิ้นเชิง (เช่นสาเหตุและข้อผิดพลาดอยู่ห่างกันในพื้นที่โค้ด)
    • สามารถใช้งานได้ในภายหลัง (เช่นสาเหตุและข้อผิดพลาดอยู่ห่างกัน)
  • อาจใช้บางแห่งที่การอ้างอิงที่เป็นโมฆะนั้นถูกต้อง แต่มีผลที่ไม่ตั้งใจ

.NET ทำให้สิ่งนี้ดีขึ้นโดยการแยกNullReferenceException("คุณอ้างถึงค่า Null") จากArgumentNullException("คุณไม่ควรส่งผ่านค่า null เป็นอาร์กิวเมนต์ - และสำหรับพารามิเตอร์นี้ ) ฉันหวังว่า Java ทำสิ่งเดียวกัน เพียงแค่NullPointerExceptionก็ยังคงมากง่ายต่อการแก้ไขรหัสว่าข้อผิดพลาดจะถูกโยนทิ้งที่จุดที่เก่าแก่ที่สุดที่มันสามารถตรวจพบได้


12

การใช้requireNonNull()เป็นคำสั่งแรกในวิธีการอนุญาตให้ระบุได้ทันที / รวดเร็วสาเหตุของข้อยกเว้น
stacktrace บ่งชี้อย่างชัดเจนว่ามีการโยนข้อยกเว้นทันทีที่รายการของวิธีการเนื่องจากผู้โทรไม่เคารพข้อกำหนด / สัญญา การส่งnullวัตถุไปยังวิธีอื่นอาจกระตุ้นให้เกิดข้อยกเว้นในแต่ละครั้ง แต่สาเหตุของปัญหาอาจซับซ้อนกว่าที่จะเข้าใจเนื่องจากข้อยกเว้นจะถูกส่งออกไปในการร้องขอเฉพาะบนnullวัตถุที่อาจเพิ่มขึ้นอีกมาก


นี่คือรูปธรรมและเป็นจริงตัวอย่างที่แสดงให้เห็นว่าทำไมเรามีเพื่อให้ประโยชน์แก่ล้มเหลวอย่างรวดเร็วโดยทั่วไปและอื่น ๆ โดยเฉพาะอย่างยิ่งการใช้Object.requireNonNull()หรือวิธีการใด ๆ ที่จะดำเนินการตรวจสอบไม่มี null nullพารามิเตอร์ที่ออกแบบมาให้ไม่ได้

สมมติว่าDictionaryคลาสที่ประกอบด้วย a LookupServiceและ a ListของStringคำที่มีอยู่ในฟิลด์เหล่านี้ถูกออกแบบมาให้ไม่nullและหนึ่งในนั้นจะถูกส่งผ่านในตัว Dictionary สร้าง

ทีนี้สมมติว่าการใช้งาน "ไม่ดี" Dictionaryโดยไม่มีการnullตรวจสอบในรายการวิธีการ (นี่คือตัวสร้าง)

public class Dictionary {

    private final List<String> words;
    private final LookupService lookupService;

    public Dictionary(List<String> words) {
        this.words = this.words;
        this.lookupService = new LookupService(words);
    }

    public boolean isFirstElement(String userData) {
        return lookupService.isFirstElement(userData);
    }        
}


public class LookupService {

    List<String> words;

    public LookupService(List<String> words) {
        this.words = words;
    }

    public boolean isFirstElement(String userData) {
        return words.get(0).contains(userData);
    }
}

ตอนนี้ลองเรียกใช้ตัวDictionaryสร้างพร้อมการnullอ้างอิงwordsพารามิเตอร์:

Dictionary dictionary = new Dictionary(null); 

// exception thrown lately : only in the next statement
boolean isFirstElement = dictionary.isFirstElement("anyThing");

JVM จะพ่น NPE ที่คำสั่งนี้

return words.get(0).contains(userData); 
ข้อยกเว้นในเธรด "main" java.lang.NullPointerException
    ที่ LookupService.isFirstElement (LookupService.java,5)
    ที่ Dictionary.isFirstElement (Dictionary.java:15)
    ที่ Dictionary.main (Dictionary.java:22)

ข้อยกเว้นจะเกิดขึ้นในLookupServiceชั้นเรียนในขณะที่ต้นกำเนิดของมันเป็นอย่างดีก่อนหน้านี้ ( Dictionaryนวกรรมิก) มันทำให้การวิเคราะห์ปัญหาโดยรวมชัดเจนน้อยลง
คือwords nullอะไร คือwords.get(0) nullอะไร ทั้งคู่ ทำไมคนอื่น ๆ หรืออาจจะทั้งสองnull? มันเป็นข้อผิดพลาดในการเขียนในDictionary(นวกรรมิก? วิธีการเรียก?)? มันเป็นข้อผิดพลาดในการเขียนโปรแกรมLookupServiceหรือไม่? (วิธีการเรียกใช้ตัวสร้าง?)?
ในที่สุดเราจะต้องตรวจสอบรหัสเพิ่มเติมเพื่อค้นหาต้นกำเนิดข้อผิดพลาดและในชั้นเรียนที่ซับซ้อนยิ่งขึ้นอาจใช้ตัวดีบักเพื่อทำความเข้าใจได้ง่ายขึ้นว่าเกิดอะไรขึ้น
แต่ทำไมเรื่องง่าย ๆ (การขาดเช็คว่าง) กลายเป็นปัญหาที่ซับซ้อน?
เนื่องจากเราอนุญาตให้ข้อผิดพลาดเริ่มต้น / การขาดหายไปที่ระบุในการรั่วไหลของส่วนประกอบเฉพาะในส่วนประกอบที่ต่ำกว่า
ลองนึกภาพที่LookupServiceไม่ได้เป็นบริการในท้องถิ่น แต่เป็นบริการระยะไกลหรือห้องสมุดของบุคคลที่สามที่มีข้อมูลการแก้ไขข้อบกพร่องเล็กน้อยหรือจินตนาการว่าคุณไม่มี 2 ชั้น แต่มีการเรียกใช้วัตถุ 4 หรือ 5 ชั้นก่อนที่nullจะถูกตรวจพบ ปัญหาจะยังคงซับซ้อนในการวิเคราะห์

ดังนั้นวิธีที่จะชอบคือ:

public Dictionary(List<String> words) {
    this.words = Objects.requireNonNull(words);
    this.lookupService = new LookupService(words);
}

ด้วยวิธีนี้ไม่มีอาการปวดหัว: เราได้รับข้อผิดพลาดทันทีที่ได้รับ:

// exception thrown early : in the constructor 
Dictionary dictionary = new Dictionary(null);

// we never arrive here
boolean isFirstElement = dictionary.isFirstElement("anyThing");
ข้อยกเว้นในเธรด "main" java.lang.NullPointerException
    ที่ java.util.Objects.requireNonNull (Objects.java:203)
    ที่ com.Dictionary. (Dictionary.java:15)
    at com.Dictionary.main (Dictionary.java:24)

โปรดทราบว่าที่นี่ฉันแสดงให้เห็นปัญหากับนวกรรมิก แต่การเรียกใช้เมธอดอาจมีข้อ จำกัด การตรวจสอบที่ไม่ใช่ค่า null เดียวกัน


ว้าว! คำอธิบายที่ดี
Jonathas Nascimento

2

อย่างที่ทราบกันแล้วสิ่งนี้ล้มเหลวอย่างรวดเร็วก่อนที่จะObject#requireNotNullถูกนำมาใช้แตกต่างกันเล็กน้อยก่อน java-9 ภายในคลาส jre บางตัว สมมติว่ากรณี:

 Consumer<String> consumer = System.out::println;

ใน java-8 คอมไพล์เป็น (เฉพาะส่วนที่เกี่ยวข้อง)

getstatic Field java/lang/System.out
invokevirtual java/lang/Object.getClass

โดยทั่วไปการดำเนินการดังนี้yourReference.getClass- ซึ่งจะล้มเหลวถ้า yourRefercence nullคือ

สิ่งต่าง ๆ มีการเปลี่ยนแปลงใน jdk-9 ที่รหัสเดียวกันรวบรวมเป็น

getstatic Field java/lang/System.out
invokestatic java/util/Objects.requireNonNull

หรือโดยทั่วไป Objects.requireNotNull (yourReference)


1

การใช้งานขั้นพื้นฐานคือการตรวจสอบและโยนNullPointerExceptionทันที

ทางเลือกที่ดีกว่า (ทางลัด) เพื่อตอบสนองความต้องการเดียวกันคือ@NonNullคำอธิบายประกอบโดย lombok


1
คำอธิบายประกอบไม่ใช่การแทนที่สำหรับวิธีวัตถุพวกเขาทำงานร่วมกัน คุณไม่สามารถพูด @ NonNull x = mightBeNull ได้คุณจะพูดว่า @ NonNull x = Objects.requireNonNull (mightBeNull, "ไม่น่าเชื่อ!");
Bill K

@BillK ขออภัยฉันไม่ได้รับคุณ
Supun Wijerathne

ฉันแค่บอกว่าคำอธิบายประกอบ Nonnull ทำงานร่วมกับ requireNonNull ไม่ใช่ทางเลือก แต่พวกเขาทำงานร่วมกันได้ค่อนข้างดี
Bill K

การใช้ธรรมชาติที่ล้มเหลวอย่างรวดเร็วเป็นทางเลือกใช่ไหม? ใช่ฉันเห็นด้วยไม่ใช่ทางเลือกสำหรับการมอบหมาย
Supun Wijerathne

ฉันเดาว่าฉันจะเรียก requireNonNull () "การแปลง" จาก @ Nullable เป็น @ NonNull หากคุณไม่ได้ใช้คำอธิบายประกอบวิธีการนั้นไม่น่าสนใจมากนัก (เนื่องจากทั้งหมดนี้คือการโยน NPE เช่นเดียวกับรหัสที่ใช้ป้องกัน) - แม้ว่ามันจะแสดงเจตนาของคุณอย่างชัดเจน
Bill K

1

ข้อยกเว้นตัวชี้ Null ถูกส่งออกมาเมื่อคุณเข้าถึงสมาชิกของวัตถุซึ่งอยู่nullในจุดต่อมา Objects.requireNonNull()ตรวจสอบค่าทันทีและโยนข้อยกเว้นทันทีโดยไม่ต้องเดินหน้า


0

ฉันคิดว่ามันควรจะใช้ในการสร้างสำเนาและกรณีอื่น ๆ เช่น DI ซึ่งพารามิเตอร์อินพุตเป็นวัตถุคุณควรตรวจสอบว่าพารามิเตอร์เป็นโมฆะหรือไม่ ในสถานการณ์เช่นนี้คุณสามารถใช้วิธีนี้ได้อย่างสะดวก


0

ในบริบทของส่วนขยายคอมไพเลอร์ที่ใช้การตรวจสอบ Nullability (เช่น: uber / NullAway ) Objects.requireNonNullควรใช้ในสถานการณ์ที่ค่อนข้าง จำกัด เมื่อคุณมีเขตข้อมูล null ที่คุณรู้ว่าไม่เป็นโมฆะในบางจุดในรหัสของคุณ

ด้วยวิธีนี้มีสองวิธีหลัก:

  • การตรวจสอบ

    • ครอบคลุมอยู่แล้วโดยคำตอบอื่น ๆ ที่นี่
    • การตรวจสอบรันไทม์พร้อมโอเวอร์เฮดและ NPE ที่เป็นไปได้
  • การทำเครื่องหมาย Nullability (เปลี่ยนจาก @Nullable เป็น @Nonnull)

    • ใช้งานน้อยที่สุดของการตรวจสอบรันไทม์ในความโปรดปรานของการตรวจสอบเวลารวบรวม
    • ใช้งานได้เมื่อคำอธิบายประกอบถูกต้องเท่านั้น (บังคับใช้โดยคอมไพเลอร์)

ตัวอย่างการใช้ Nullability Marking:

@Nullable
Foo getFoo(boolean getNull) { return getNull ? null : new Foo(); }

// Changes contract from Nullable to Nonnull without compiler error
@Nonnull Foo myFoo = Objects.requireNonNull(getFoo(false));

0

นอกจากคำตอบที่ถูกต้องทั้งหมด:

เราใช้มันในลำธารที่มีปฏิกิริยา โดยปกติแล้วผลลัพธ์ที่NullpointerExceptionได้จะถูกรวมอยู่ในข้อยกเว้นอื่น ๆ ทั้งนี้ขึ้นอยู่กับการเกิดขึ้นในกระแส ดังนั้นเราสามารถตัดสินใจได้อย่างง่ายดายในภายหลังว่าจะจัดการกับข้อผิดพลาดได้อย่างไร

เป็นเพียงตัวอย่าง: ลองนึกภาพคุณมี

<T> T parseAndValidate(String payload) throws ParsingException { ... };
<T> T save(T t) throws DBAccessException { ... };

ที่parseAndValidatewrapps NullPointerExceptionจากในrequireNonNullParsingException

ตอนนี้คุณสามารถตัดสินใจเช่นเมื่อจะลองอีกครั้งหรือไม่:

...
.map(this::parseAndValidate)
.map(this::save)
.retry(Retry.<T>allBut(ParsingException.class))

หากไม่มีการตรวจสอบ NullPointerException จะเกิดขึ้นในsaveวิธีการซึ่งจะส่งผลให้ลองใหม่ไม่รู้จบ ลองนึกภาพการสมัครสมาชิกที่ยาวนานและเพิ่ม

.onErrorContinue(
    throwable -> throwable.getClass().equals(ParsingException.class),
    parsingExceptionConsumer()
)

ตอนนี้RetryExhaustExceptionจะฆ่าการสมัครของคุณ

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.