เหตุใด findFirst () จึงโยน NullPointerException หากองค์ประกอบแรกที่พบเป็นโมฆะ


90

ทำไมถึงโยนjava.lang.NullPointerException?

List<String> strings = new ArrayList<>();
        strings.add(null);
        strings.add("test");

        String firstString = strings.stream()
                .findFirst()      // Exception thrown here
                .orElse("StringWhenListIsEmpty");
                //.orElse(null);  // Changing the `orElse()` to avoid ambiguity

รายการแรกstringsคือnullซึ่งเป็นมูลค่าที่ยอมรับได้อย่างสมบูรณ์ นอกจากนี้ยังfindFirst()ส่งคืนตัวเลือกซึ่งเหมาะสมยิ่งกว่าfindFirst()ที่จะสามารถจัดการกับnulls ได้

แก้ไข: อัปเดตorElse()ให้มีความคลุมเครือน้อยลง


4
ค่า null ไม่ใช่ค่าที่ยอมรับได้อย่างสมบูรณ์ ... ใช้ "" แทน
Michele Lacorte

1
@MicheleLacorte แม้ว่าฉันจะใช้Stringที่นี่ แต่ถ้าเป็นรายการที่แสดงคอลัมน์ในฐานข้อมูลล่ะ ค่าของแถวแรกสำหรับคอลัมน์นั้นสามารถเป็นnullได้
neverendingqs

ใช่ แต่ใน java null ไม่เป็นที่ยอมรับ. ใช้แบบสอบถามเพื่อตั้งค่า null เป็น db
Michele Lacorte

10
@MicheleLacorte nullเป็นค่าที่ยอมรับได้อย่างสมบูรณ์ใน Java โดยทั่วไปจะพูด โดยเฉพาะอย่างยิ่งมันเป็นองค์ประกอบที่ถูกต้องสำหรับArrayList<String>ไฟล์. อย่างไรก็ตามเช่นเดียวกับค่าอื่น ๆ มีข้อ จำกัด เกี่ยวกับสิ่งที่สามารถทำได้ "ไม่เคยใช้null" ไม่ใช่คำแนะนำที่เป็นประโยชน์เนื่องจากคุณไม่สามารถหลีกเลี่ยงได้
John Bollinger

@NathanHughes - ฉันสงสัยว่าเมื่อถึงเวลาที่คุณโทรfindFirst()ไปไม่มีอะไรอื่นที่คุณต้องการทำ
neverendingqs

คำตอบ:


72

เหตุผลนี้คือการใช้Optional<T>ในทางกลับกัน ไม่อนุญาตให้มีทางnullเลือก โดยพื้นฐานแล้วมันไม่มีวิธีแยกแยะสถานการณ์ "มันไม่ได้อยู่ที่นั่น" และ "อยู่ที่นั่น แต่ถูกตั้งค่าเป็นnull"

นั่นเป็นเหตุผลว่าทำไมเอกสารจึงห้ามสถานการณ์อย่างชัดเจนเมื่อnullถูกเลือกในfindFirst():

พ่น:

NullPointerException - หากองค์ประกอบที่เลือกคือ null


3
มันจะง่ายมากในการติดตามว่าค่ามีอยู่ด้วยบูลีนส่วนตัวภายในอินสแตนซ์ของOptional. อย่างไรก็ตามฉันคิดว่าฉันพูดจาโผงผาง - ถ้าภาษาไม่รองรับก็ไม่รองรับ
neverendingqs

2
@neverendingqs แน่นอนว่าการใช้ a booleanเพื่อแยกความแตกต่างของสถานการณ์ทั้งสองนี้จะสมเหตุสมผล สำหรับฉันดูเหมือนว่าการใช้Optional<T>ที่นี่เป็นตัวเลือกที่น่าสงสัย
Sergey Kalinichenko

1
@neverendingqs ฉันไม่สามารถคิดถึงทางเลือกที่ดูดีได้นอกจากการหมุนค่าว่างของคุณเองซึ่งไม่เหมาะเช่นกัน
Sergey Kalinichenko

1
ฉันลงเอยด้วยการเขียนเมธอดส่วนตัวที่รับตัววนซ้ำจากIterableประเภทใดก็ได้ตรวจสอบhasNext()และส่งคืนค่าที่เหมาะสม
neverendingqs

1
ฉันคิดว่ามันสมเหตุสมผลกว่าถ้าfindFirstส่งคืนค่าตัวเลือกว่างในกรณีของ PO
danny

48

ตามที่ได้กล่าวไปแล้วนักออกแบบ API ไม่ถือว่าผู้พัฒนาต้องการรักษาnullค่าและค่าที่ขาดไปในลักษณะเดียวกัน

หากคุณยังต้องการทำเช่นนั้นคุณสามารถทำได้อย่างชัดเจนโดยใช้ลำดับ

.map(Optional::ofNullable).findFirst().flatMap(Function.identity())

ไปที่สตรีม nullผลที่ตามมาจะเป็นตัวเลือกที่ว่างเปล่าในทั้งสองกรณีถ้ามีองค์ประกอบที่ไม่มีแรกหรือถ้าองค์ประกอบแรกคือ ดังนั้นในกรณีของคุณคุณอาจใช้

String firstString = strings.stream()
    .map(Optional::ofNullable).findFirst().flatMap(Function.identity())
    .orElse(null);

เพื่อรับnullค่าหากไม่มีองค์ประกอบแรกหรือnull.

หากคุณต้องการแยกความแตกต่างระหว่างกรณีเหล่านี้คุณสามารถข้ามflatMapขั้นตอน:

Optional<String> firstString = strings.stream()
    .map(Optional::ofNullable).findFirst().orElse(null);
System.out.println(firstString==null? "no such element":
                   firstString.orElse("first element is null"));

คำถามนี้ไม่แตกต่างจากคำถามที่อัปเดตของคุณมากนัก คุณก็ต้องเปลี่ยน"no such element"ด้วย"StringWhenListIsEmpty"และมี"first element is null" nullแต่ถ้าคุณไม่ชอบเงื่อนไขคุณสามารถทำได้เช่น:

String firstString = strings.stream().skip(0)
    .map(Optional::ofNullable).findFirst()
    .orElseGet(()->Optional.of("StringWhenListIsEmpty"))
    .orElse(null);

ตอนนี้firstStringจะเป็นnullถ้าองค์ประกอบมีอยู่ แต่เป็นnullและจะเป็น"StringWhenListIsEmpty"เมื่อไม่มีองค์ประกอบ


ขออภัยฉันตระหนักว่าคำถามของฉันอาจส่อว่าฉันต้องการส่งคืนnullสำหรับ 1) องค์ประกอบแรกคือnullหรือ 2) ไม่มีองค์ประกอบอยู่ในรายการ ฉันได้อัปเดตคำถามเพื่อลบความคลุมเครือ
neverendingqs

1
ในข้อมูลโค้ดที่ 3 การอาจได้รับมอบหมายให้Optional nullเนื่องจากOptionalควรเป็น "ประเภทค่า" จึงไม่ควรเป็นค่าว่าง ==และตัวเลือกไม่ควรนำมาเปรียบเทียบโดย โค้ดอาจล้มเหลวใน Java 10 :) หรือเมื่อใดก็ตามที่ค่าถูกนำมาใช้กับ Java
ZhongYu

1
@ bayou.io: เอกสารไม่ได้บอกว่าการอ้างอิงไปยังประเภทค่าไม่สามารถnullและในขณะที่กรณีที่ไม่ควรนำมาเปรียบเทียบโดย==การอ้างอิงอาจได้รับการทดสอบสำหรับnullใช้==เป็นนั่นเป็นเพียงnullวิธีการทดสอบมัน ฉันไม่เห็นว่าการเปลี่ยนไปเป็น“ ไม่เคยnull” ควรจะทำงานกับโค้ดที่มีอยู่ได้อย่างไรแม้กระทั่งค่าเริ่มต้นสำหรับตัวแปรอินสแตนซ์และองค์ประกอบอาร์เรย์nullทั้งหมด ตัวอย่างข้อมูลไม่ใช่รหัสที่ดีที่สุด แต่ก็ไม่ใช่หน้าที่ในการถือว่าnulls เป็นค่าปัจจุบัน
Holger

ดูจอห์นเพิ่มขึ้น - และเทียบไม่ได้กับโอเปอเรเตอร์“ ==” แม้แต่กับโมฆะ
ZhongYu

1
เนื่องจากโค้ดนี้ใช้ Generic API จึงเป็นสิ่งที่แนวคิดนี้เรียกว่าการแสดงแบบกล่องซึ่งสามารถเป็นnullได้ อย่างไรก็ตามเนื่องจากการเปลี่ยนภาษาสมมุติเช่นนี้จะทำให้คอมไพเลอร์แสดงข้อผิดพลาดที่นี่ (ไม่ทำลายโค้ดโดยไม่โต้ตอบ) ฉันสามารถใช้ชีวิตได้ด้วยความจริงที่ว่ามันอาจจะต้องปรับให้เข้ากับ Java 10 ฉันคิดว่าStreamAPI จะ ดูแตกต่างกันมากเช่นกัน…
Holger

20

คุณสามารถใช้java.util.Objects.nonNullเพื่อกรองรายการก่อนค้นหา

สิ่งที่ต้องการ

list.stream().filter(Objects::nonNull).findFirst();

1
ฉันอยากfirstStringจะเป็นnullถ้ารายการแรกในstringsคือnull.
neverendingqs

3
น่าเสียดายที่มันใช้Optional.ofสิ่งที่ไม่ปลอดภัย คุณสามารถmapที่จะOptional.ofNullable แล้วใช้findFirstแต่คุณจะจบลงด้วยตัวของตัวเลือก
Mattos

15

รหัสต่อไปนี้แทนที่findFirst()ด้วยlimit(1)และแทนที่orElse()ด้วยreduce():

String firstString = strings.
   stream().
   limit(1).
   reduce("StringWhenListIsEmpty", (first, second) -> second);

limit()อนุญาตให้เข้าถึง 1 องค์ประกอบreduceเท่านั้น BinaryOperatorผ่านไปreduceผลตอบแทนที่ 1 องค์ประกอบหรืออื่น ๆหากองค์ประกอบไม่ถึง"StringWhenListIsEmpty"reduce

ความสวยงามของโซลูชันนี้Optionalคือไม่ได้รับการจัดสรรและBinaryOperatorแลมด้าจะไม่จัดสรรอะไรเลย


1

ทางเลือกควรเป็นประเภท "ค่า" (อ่านแบบละเอียดในjavadoc :) JVM สามารถแทนที่ทั้งหมดได้Optional<Foo>ด้วยFooการลบค่าใช้จ่ายในการชกมวยและการแกะกล่องทั้งหมด ฟูหมายถึงที่ว่างเปล่าnullOptional<Foo>

เป็นการออกแบบที่เป็นไปได้ที่จะอนุญาตให้เป็นทางเลือกที่มีค่าว่างโดยไม่ต้องเพิ่มแฟล็กบูลีนเพียงแค่เพิ่มวัตถุที่เฝ้าระวัง (สามารถใช้thisเป็นแมวมองได้ดู Throwable.cause)

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

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

วิธีแก้ปัญหาคือการทำกล่องnullเช่น

class Box<T>
    static Box<T> of(T value){ .. }

Optional<Box<String>> first = stream.map(Box::of).findFirst();

(พวกเขากล่าวว่าวิธีแก้ปัญหา OOP ทุกข้อคือการแนะนำประเภทอื่น :)


1
ไม่มีความจำเป็นต้องสร้างอีกเป็นBoxประเภท Optionalประเภทตัวเองสามารถตอบสนองวัตถุประสงค์นี้ ดูคำตอบของฉันสำหรับตัวอย่าง
Holger

@ Holger - ใช่ แต่อาจทำให้สับสนได้เนื่องจากไม่ใช่จุดประสงค์ของตัวเลือก ในกรณีของ OP nullเป็นค่าที่ถูกต้องเช่นเดียวกับค่าอื่น ๆ ไม่มีการดูแลเป็นพิเศษ (จนกว่าจะถึงเวลาต่อมา :)
ZhongYu
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.