บูลีนตัวดำเนินการตามเงื่อนไขและการทำกล่องอัตโนมัติ


132

ทำไมถึงโยน NullPointerException

public static void main(String[] args) throws Exception {
    Boolean b = true ? returnsNull() : false; // NPE on this line.
    System.out.println(b);
}

public static Boolean returnsNull() {
    return null;
}

ในขณะที่สิ่งนี้ไม่

public static void main(String[] args) throws Exception {
    Boolean b = true ? null : false;
    System.out.println(b); // null
}

?

วิธีแก้ปัญหาคือการแทนที่falseโดยBoolean.FALSEเพื่อหลีกเลี่ยงnullการbooleanแกะกล่อง - ซึ่งเป็นไปไม่ได้ แต่นั่นไม่ใช่คำถาม คำถามคือทำไม ? มีการอ้างอิงใน JLS ที่ยืนยันพฤติกรรมนี้โดยเฉพาะในกรณีที่ 2 หรือไม่?


28
ว้าว, autoboxing เป็นแหล่งที่มาของ ... เอ่อ ... เรื่องน่าประหลาดใจสำหรับโปรแกรมเมอร์ java ใช่หรือไม่? :-)
leonbloy

ฉันมีปัญหาที่คล้ายกันและสิ่งที่ทำให้ฉันประหลาดใจคือมันล้มเหลวใน OpenJDK VM แต่ทำงานบน HotSpot VM ... เขียนครั้งเดียวทำงานได้ทุกที่!
kodu

คำตอบ:


92

ความแตกต่างคือประเภทที่ชัดเจนของreturnsNull()วิธีการมีผลต่อการพิมพ์แบบคงที่ของนิพจน์ในเวลาคอมไพล์:

E1: `true ? returnsNull() : false` - boolean (auto-unboxing 2nd operand to boolean)

E2: `true ? null : false` - Boolean (autoboxing of 3rd operand to Boolean)

ดูข้อกำหนดภาษา Java ส่วน15.25 ตัวดำเนินการตามเงื่อนไข? :

  • สำหรับ E1, ประเภทของตัวถูกดำเนินการ 2 และ 3 ที่มีBooleanและbooleanตามลำดับดังนั้นข้อนี้มีผลบังคับใช้:

    หากตัวถูกดำเนินการที่สองและสามเป็นประเภทบูลีนและอีกประเภทหนึ่งเป็นประเภทบูลีนประเภทของนิพจน์เงื่อนไขจะเป็นบูลีน

    เนื่องจากประเภทของนิพจน์คือbooleanตัวถูกดำเนินการที่ 2 จึงต้องถูกบังคับให้booleanเป็น คอมไพเลอร์แทรกรหัสอัตโนมัติ unboxing เพื่อตัวถูกดำเนินการ 2 (ค่าตอบแทนของreturnsNull()) booleanที่จะทำให้มันพิมพ์ แน่นอนว่านี่เป็นสาเหตุของ NPE จากการnullส่งคืนเมื่อรันไทม์

  • สำหรับ E2 ประเภทของตัวถูกดำเนินการที่ 2 และ 3 คือ<special null type>(ไม่ใช่Booleanใน E1!) และbooleanตามลำดับดังนั้นจึงไม่มีการใช้ประโยคการพิมพ์ที่เฉพาะเจาะจง ( go read 'em! ) ดังนั้นจึงใช้อนุประโยคสุดท้าย "อย่างอื่น":

    มิฉะนั้นตัวถูกดำเนินการที่สองและสามจะเป็นประเภท S1 และ S2 ตามลำดับ ให้ T1 เป็นประเภทที่เกิดจากการใช้การแปลงมวยเป็น S1 และให้ T2 เป็นประเภทที่เป็นผลมาจากการใช้การแปลงมวยเป็น S2 ประเภทของนิพจน์เงื่อนไขเป็นผลมาจากการใช้การแปลงการดักจับ (§5.1.10) กับ lub (T1, T2) (§15.12.2.7)

    • S1 == <special null type>(ดู§4.1 )
    • S2 == boolean
    • T1 == box (S1) == <special null type>(ดูรายการสุดท้ายในรายการการแปลงชกมวยใน §5.1.7 )
    • T2 == กล่อง (S2) == `บูลีน
    • หลับ (T1, T2) == Boolean

    ดังนั้นประเภทของการแสดงออกเงื่อนไขเป็นBooleanและถูกดำเนินการที่ 3 Booleanจะต้องถูกบังคับให้ คอมไพเลอร์แทรกรหัสมวยอัตโนมัติสำหรับตัวถูกดำเนินการที่ 3 ( false) ตัวถูกดำเนินการที่ 2 ไม่จำเป็นต้องมีการแกะกล่องอัตโนมัติเช่นเดียวกับในE1ดังนั้นจึงไม่มีการยกเลิกการทำกล่อง NPE โดยอัตโนมัติเมื่อnullถูกส่งคืน


คำถามนี้ต้องการการวิเคราะห์ประเภทเดียวกัน:

ตัวดำเนินการเงื่อนไข Java?: ชนิดผลลัพธ์


4
ฉันคิดว่า §15.12.2.7เป็นความเจ็บปวด
BalusC

เป็นเรื่องง่าย ... แต่ในการมองย้อนกลับเท่านั้น :-)
Bert F

@BertF อะไรฟังก์ชั่นlubในการlub(T1,T2)ยืนหรือไม่?
Geek

1
@Geek - lub () - ขอบเขตบนน้อยที่สุด - โดยพื้นฐานแล้วเป็นซูเปอร์คลาสที่ใกล้เคียงที่สุดที่พวกเขามีเหมือนกัน เนื่องจาก null (ประเภท "ประเภทค่าว่างพิเศษ") สามารถแปลงโดยปริยาย (กว้างขึ้น) เป็นประเภทใดก็ได้คุณสามารถพิจารณาว่าประเภท null พิเศษเป็น "superclass" ประเภทใดก็ได้ (คลาส) สำหรับวัตถุประสงค์ของ lub ()
Bert F

25

เส้น:

    Boolean b = true ? returnsNull() : false;

ถูกเปลี่ยนภายในเป็น:

    Boolean b = true ? returnsNull().booleanValue() : false; 

เพื่อทำการแกะกล่อง ดังนั้น: null.booleanValue()จะให้ NPE

นี่เป็นหนึ่งในข้อผิดพลาดที่สำคัญเมื่อใช้ autoboxing พฤติกรรมนี้ได้รับการบันทึกไว้ใน5.1.8 JLS

แก้ไข: ฉันเชื่อว่าการแกะกล่องเกิดจากตัวดำเนินการที่สามเป็นประเภทบูลีนเช่น (เพิ่มการแสดงโดยนัย):

   Boolean b = (Boolean) true ? true : false; 

2
ทำไมมันถึงพยายามแกะกล่องแบบนั้นในเมื่อค่าสูงสุดเป็นวัตถุบูลีน
Erick Robertson

16

จากข้อกำหนดภาษา Java ส่วน 15.25 :

  • หากตัวถูกดำเนินการที่สองและสามเป็นประเภทบูลีนและอีกประเภทหนึ่งเป็นประเภทบูลีนประเภทของนิพจน์เงื่อนไขจะเป็นบูลีน

ดังนั้นตัวอย่างแรกพยายามที่จะเรียกBoolean.booleanValue()เพื่อแปลงBooleanไปbooleanตามกฎข้อแรก

ในกรณีที่สองตัวถูกดำเนินการตัวแรกเป็นประเภท null เมื่อตัวดำเนินการที่สองไม่ใช่ประเภทการอ้างอิงดังนั้นจึงใช้การแปลงกล่องอัตโนมัติ:

  • มิฉะนั้นตัวถูกดำเนินการที่สองและสามจะเป็นประเภท S1 และ S2 ตามลำดับ ให้ T1 เป็นประเภทที่เกิดจากการใช้การแปลงมวยเป็น S1 และให้ T2 เป็นประเภทที่เป็นผลมาจากการใช้การแปลงมวยเป็น S2 ประเภทของนิพจน์เงื่อนไขเป็นผลมาจากการใช้การแปลงการดักจับ (§5.1.10) กับ lub (T1, T2) (§15.12.2.7)

สิ่งนี้ตอบโจทย์กรณีแรก แต่ไม่ใช่กรณีที่สอง
BalusC

nullอาจจะมีข้อยกเว้นสำหรับกรณีที่ค่าใดค่าหนึ่งคือ
Erick Robertson

@Erick: JLS ยืนยันสิ่งนี้หรือไม่?
BalusC

1
@Erick: ฉันไม่คิดว่ามันใช้ได้เพราะbooleanไม่ใช่ประเภทอ้างอิง
axtavt

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

0

เราสามารถเห็นปัญหานี้ได้จากรหัสไบต์ ที่บรรทัดที่ 3 ของรหัสไบต์หลักของ3: invokevirtual #3 // Method java/lang/Boolean.booleanValue:()Zมวยบูลีนของค่าว่างinvokevirtualเมธอดjava.lang.Boolean.booleanValueมันจะโยน NPE แน่นอน

    public static void main(java.lang.String[]) throws java.lang.Exception;
      descriptor: ([Ljava/lang/String;)V
      flags: ACC_PUBLIC, ACC_STATIC
      Code:
        stack=2, locals=2, args_size=1
           0: invokestatic  #2                  // Method returnsNull:()Ljava/lang/Boolean;
           3: invokevirtual #3                  // Method java/lang/Boolean.booleanValue:()Z
           6: invokestatic  #4                  // Method java/lang/Boolean.valueOf:(Z)Ljava/lang/Boolean;
           9: astore_1
          10: getstatic     #5                  // Field java/lang/System.out:Ljava/io/PrintStream;
          13: aload_1
          14: invokevirtual #6                  // Method java/io/PrintStream.println:(Ljava/lang/Object;)V
          17: return
        LineNumberTable:
          line 3: 0
          line 4: 10
          line 5: 17
      Exceptions:
        throws java.lang.Exception

    public static java.lang.Boolean returnsNull();
      descriptor: ()Ljava/lang/Boolean;
      flags: ACC_PUBLIC, ACC_STATIC
      Code:
        stack=1, locals=0, args_size=0
           0: aconst_null
           1: areturn
        LineNumberTable:
          line 8: 0
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.