เหตุใดคำสั่ง String switch จึงไม่รองรับกรณีว่าง


125

ฉันแค่สงสัยว่าทำไมswitchคำสั่งJava 7 ไม่รองรับnullเคสและโยนแทนNullPointerException? ดูบรรทัดแสดงความคิดเห็นด้านล่าง (ตัวอย่างจากบทความ Java Tutorials บนswitch ):

{
    String month = null;
    switch (month) {
        case "january":
            monthNumber = 1;
            break;
        case "february":
            monthNumber = 2;
            break;
        case "march":
            monthNumber = 3;
            break;
        //case null:
        default: 
            monthNumber = 0;
            break;
    }

    return monthNumber;
}

สิ่งนี้จะหลีกเลี่ยงifเงื่อนไขสำหรับการตรวจสอบค่าว่างก่อนการswitchใช้งานทุกครั้ง


12
ไม่มีคำตอบที่ชัดเจนสำหรับเรื่องนี้เนื่องจากเราไม่ใช่คนที่สร้างภาษา คำตอบทั้งหมดจะเป็นการคาดเดาที่บริสุทธิ์
asteri

2
ความพยายามในการเปิดเครื่องnullจะทำให้เกิดข้อยกเว้น ทำการifตรวจสอบnullจากนั้นเข้าไปในswitchคำสั่ง
gparyani

28
จากJLS : ในการตัดสินของผู้ออกแบบภาษาโปรแกรม Java [การโยนNullPointerExceptionถ้านิพจน์ประเมินเป็นnullรันไทม์] เป็นผลลัพธ์ที่ดีกว่าการข้ามคำสั่ง switch ทั้งหมดแบบเงียบ ๆ หรือเลือกที่จะดำเนินการคำสั่ง (ถ้ามี) หลังจาก ป้ายกำกับเริ่มต้น (ถ้ามี)
gparyani

3
@gparyani: ให้คำตอบนั้น ฟังดูเป็นทางการและชัดเจนมาก
Thilo

7
@JeffGohlke: "ไม่มีทางตอบคำถามว่าทำไมเว้นแต่คุณจะเป็นคนตัดสินใจ" ... ดีความคิดเห็นของ gparyani พิสูจน์ให้เห็นเป็นอย่างอื่น
user541686

คำตอบ:


145

ดังที่ damryfbfnetsi ชี้ให้เห็นในความคิดเห็นJLS §14.11มีหมายเหตุดังต่อไปนี้:

ข้อห้ามในการใช้nullเป็นฉลากสวิตช์จะป้องกันไม่ให้มีการเขียนโค้ดที่ไม่สามารถดำเนินการได้ หากswitchนิพจน์เป็นประเภทการอ้างอิงนั่นคือStringหรือชนิดดั้งเดิมแบบบรรจุกล่องหรือชนิด enum ข้อผิดพลาดขณะทำงานจะเกิดขึ้นหากนิพจน์ประเมินเป็นnullในขณะรันไทม์ ในการตัดสินของผู้ออกแบบภาษาการเขียนโปรแกรม Java นี่เป็นผลลัพธ์ที่ดีกว่าการข้ามข้อความทั้งหมดแบบเงียบ ๆswitchหรือเลือกที่จะดำเนินการคำสั่ง (ถ้ามี) หลังdefaultป้ายกำกับ (ถ้ามี)

(เน้นเหมือง)

แม้ว่าประโยคสุดท้ายจะข้ามความเป็นไปได้ในการใช้งานcase null:แต่ก็ดูสมเหตุสมผลและให้มุมมองเกี่ยวกับความตั้งใจของนักออกแบบภาษา

หากเราต้องการดูรายละเอียดการใช้งานบล็อกโพสต์นี้โดย Christian Hujer มีการคาดเดาเชิงลึกเกี่ยวกับสาเหตุที่nullไม่อนุญาตให้ใช้สวิตช์ (แม้ว่าจะเน้นที่enumสวิตช์มากกว่าสวิตช์ก็ตามString):

ภายใต้ประทุนswitchโดยทั่วไปคำสั่งจะคอมไพล์เป็น tablesswitch รหัสไบต์ และอาร์กิวเมนต์ "ทางกายภาพ" switchและกรณีของมันคือints ค่า int Enum.ordinal()เพื่อเปิดจะถูกกำหนดโดยวิธีการที่กล่าวอ้าง ลำดับ [... ] เริ่มต้นที่ศูนย์

ซึ่งหมายความว่าการทำแผนที่nullเพื่อ0จะไม่เป็นความคิดที่ดี สวิตช์บนค่า enum แรกจะแยกไม่ออกจาก null อาจเป็นความคิดที่ดีที่จะเริ่มนับลำดับสำหรับ enums ที่ 1 อย่างไรก็ตามมันไม่ได้กำหนดไว้เช่นนั้นและไม่สามารถเปลี่ยนแปลงคำจำกัดความนี้ได้

ในขณะที่Stringสวิทช์จะดำเนินการที่แตกต่างกันที่สวิทช์มาก่อนและเป็นแบบอย่างสำหรับวิธีการเปลี่ยนชนิดการอ้างอิงควรประพฤติเมื่ออ้างอิงenumnull


1
มันจะได้รับการ improvment ใหญ่ที่จะช่วยให้การจัดการ null เป็นส่วนหนึ่งของcase null:ถ้ามันจะได้รับการดำเนิน exclusivily Stringสำหรับ ปัจจุบันทุกStringการตรวจสอบต้องมี null ตรวจสอบ "123test".equals(value)แต่อย่างใดถ้าเราต้องการที่จะทำมันถูกต้องแม้ว่าส่วนใหญ่โดยปริยายโดยใส่อย่างต่อเนื่องสตริงเป็นครั้งแรกใน ตอนนี้เราถูกบังคับให้เขียนคำสั่ง switch ของเราในif (value != null) switch (value) {...
YoYo

1
Re "การแมป null เป็น 0 ไม่ใช่ความคิดที่ดี" นั่นคือการพูดน้อยเกินไปเนื่องจากค่าของ "" .hashcode () คือ 0! หมายความว่าสตริงว่างและสตริงที่มีความยาวเป็นศูนย์จะต้องได้รับการปฏิบัติเหมือนกันในคำสั่งสวิตช์ซึ่งเห็นได้ชัดว่าไม่สามารถทำงานได้
skomisa

สำหรับกรณีของ enum อะไรที่ทำให้พวกเขาหยุดการแมป null เป็น -1
krispy

31

โดยทั่วไปnullเป็นสิ่งที่น่ารังเกียจที่จะจัดการ บางทีภาษาที่ดีกว่าก็อยู่nullได้

ปัญหาของคุณอาจได้รับการแก้ไขโดย

    switch(month==null?"":month)
    {
        ...
        //case "":
        default: 
            monthNumber = 0;

    }

ไม่ใช่ความคิดที่ดีถ้าmonthเป็นสตริงว่างสิ่งนี้จะถือว่าเป็นแบบเดียวกับสตริงว่าง
gparyani

13
สำหรับหลาย ๆ กรณีที่ถือว่า null เป็นสตริงว่างนั้นสมเหตุสมผลอย่างยิ่ง
Eric Woodruff

23

มันไม่สวย แต่String.valueOf()ให้คุณใช้ null String ในสวิตช์ หากพบnullมันจะแปลงเป็น"null"มิฉะนั้นจะส่งคืนสตริงเดียวกับที่คุณส่งผ่านไป หากคุณไม่ได้จัดการอย่างชัดเจนแล้วก็จะไป"null" defaultข้อแม้เดียวคือไม่มีวิธีแยกแยะระหว่าง String "null"กับnullตัวแปรจริง

    String month = null;
    switch (String.valueOf(month)) {
        case "january":
            monthNumber = 1;
            break;
        case "february":
            monthNumber = 2;
            break;
        case "march":
            monthNumber = 3;
            break;
        case "null":
            monthNumber = -1;
            break;
        default: 
            monthNumber = 0;
            break;
    }
    return monthNumber;

2
ฉันเชื่อว่าการทำสิ่งนี้ใน java เป็นการต่อต้าน
Łukasz Rzeszotarski

2
@ ŁukaszRzeszotarskiนั่นคือสิ่งที่ฉันหมายถึง "มันไม่สวย"
krispy

15

นี่คือความพยายามที่จะตอบว่าทำไมมันถึงพ่น NullPointerException

ผลลัพธ์ของคำสั่ง javap ด้านล่างแสดงให้เห็นว่าcaseถูกเลือกตามแฮชโค้ดของswitchสตริงอาร์กิวเมนต์และด้วยเหตุนี้จึงพ่น NPE เมื่อ.hashCode()ถูกเรียกใช้ในสตริงว่าง

6: invokevirtual #18                 // Method java/lang/String.hashCode:()I
9: lookupswitch  { // 3
    -1826660246: 44
     -263893086: 56
      103666243: 68
        default: 95
   }

ซึ่งหมายความว่าตามคำตอบของ hashCode ของ Java สามารถสร้างค่าเดียวกันสำหรับสตริงที่แตกต่างกันได้หรือไม่ แม้ว่าจะหายาก แต่ก็ยังมีความเป็นไปได้ที่จะมีการจับคู่สองกรณี (สองสายที่มีรหัสแฮชเดียวกัน) ดูตัวอย่างด้านล่างนี้

    int monthNumber;
    String month = args[0];

    switch (month) {
    case "Ea":
        monthNumber = 1;
        break;
    case "FB":
        monthNumber = 2;
        break;
    // case null:
    default:
        monthNumber = 0;
        break;
    }
    System.out.println(monthNumber);

javap ที่

  10: lookupswitch  { // 1
              2236: 28
           default: 59
      }
  28: aload_3       
  29: ldc           #22                 // String Ea
  31: invokevirtual #24                 // Method java/lang/String.equals:(Ljava/lang/Object;)Z
  34: ifne          49
  37: aload_3       
  38: ldc           #28                 // String FB
  40: invokevirtual #24                 // Method java/lang/String.equals:(Ljava/lang/Object;)Z
  43: ifne          54
  46: goto          59 //Default

ดังที่คุณเห็นมีเพียงกรณีเดียวเท่านั้นที่ถูกสร้างขึ้นสำหรับ"Ea"และ"FB"แต่มีสองifเงื่อนไขในการตรวจสอบการจับคู่กับสตริงแต่ละกรณี วิธีที่น่าสนใจและซับซ้อนในการใช้งานฟังก์ชันนี้!


5
สิ่งนี้สามารถนำไปใช้ในทางอื่นได้
Thilo

2
ฉันจะบอกว่านี่เป็นข้อบกพร่องในการออกแบบ
Deer Hunter

6
ฉันสงสัยว่าสิ่งนี้เกี่ยวข้องกับเหตุใดบางแง่มุมที่น่าสงสัยของฟังก์ชันแฮชสตริงจึงไม่ได้รับการเปลี่ยนแปลง: โดยทั่วไปโค้ดไม่ควรพึ่งพาการhashCodeส่งคืนค่าเดียวกันในการรันโปรแกรมที่แตกต่างกัน แต่เนื่องจากแฮชสตริงถูกอบให้เป็นไฟล์ปฏิบัติการโดย คอมไพเลอร์วิธีแฮชสตริงจะกลายเป็นส่วนหนึ่งของข้อกำหนดภาษา
supercat

5

เรื่องสั้นสั้น ... (และหวังว่าจะน่าสนใจพอ !!!)

Enum ถูกนำมาใช้ครั้งแรกในJava1.5 ( ก.ย. 2547 ) และข้อผิดพลาดที่ขออนุญาตให้เปิดใช้งาน Stringนั้นถูกส่งกลับมานาน ( Oct'95 ) หากคุณดูความคิดเห็นที่โพสต์ในข้อผิดพลาดนั้นในเดือนมิถุนายน 2547จะมีข้อความว่าDon't hold your breath. Nothing resembling this is in our plans.ดูเหมือนว่าพวกเขาเลื่อน ( เพิกเฉย ) ข้อบกพร่องนี้และในที่สุดก็เปิดตัว Java 1.5 ในปีเดียวกันที่พวกเขาเปิดตัว 'enum' ด้วยลำดับเริ่มต้นที่ 0 และตัดสินใจ ( พลาด ) ไม่สนับสนุน null สำหรับ enum ต่อมาในJava1.7 ( ก.ค. 2554 ) พวกเขาทำตาม ( บังคับ) ปรัชญาเดียวกันกับ String (เช่นในขณะที่สร้าง bytecode ไม่มีการตรวจสอบ null ก่อนที่จะเรียกใช้วิธี hashcode ())

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

TL; DR With String พวกเขาสามารถดูแล NPE ได้ (เกิดจากความพยายามในการสร้าง hashcode สำหรับ null) ในขณะที่ใช้รหัส java เพื่อการแปลงรหัสไบต์ แต่สุดท้ายก็ตัดสินใจไม่ทำ

อ้างอิง: TheBUG , JavaVersionHistory , JavaCodeToByteCode , SO


1

ตาม Java Docs:

สวิตช์ทำงานร่วมกับชนิดข้อมูลไบต์สั้นถ่านและ int primitive นอกจากนี้ยังใช้งานได้กับประเภทที่แจกแจง (กล่าวถึงในประเภท Enum) คลาส String และคลาสพิเศษสองสามคลาสที่รวมประเภทดั้งเดิมบางประเภท: Character, Byte, Short และ Integer (กล่าวถึงใน Numbers และ Strings)

เนื่องจากnullไม่มีประเภทและไม่ใช่อินสแตนซ์ของสิ่งใดจึงจะไม่ทำงานกับคำสั่ง switch


4
และยังnullเป็นค่าที่ถูกต้องสำหรับString, Character, Byte, ShortหรือIntegerการอ้างอิง
asteri

0

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

ดังนั้นกรณีว่าง (ซึ่งผิดกฎหมาย) จึงไม่สามารถดำเนินการได้เลย;)


1
สิ่งนี้สามารถนำไปใช้ในทางอื่นได้
Thilo

ตกลง @ Thilo คนที่ฉลาดกว่าฉันมีส่วนร่วมในการใช้งานนี้ หากคุณรู้วิธีอื่น ๆ ที่สามารถนำไปใช้งานได้ฉันอยากรู้ว่าสิ่งเหล่านี้คืออะไร [และฉันแน่ใจว่ามีคนอื่นอยู่ด้วย] จึงแบ่งปัน ...
amrith

3
สตริงไม่ใช่ชนิดดั้งเดิมแบบบรรจุกล่องและ NPE จะไม่เกิดขึ้นเนื่องจากมีผู้พยายาม "แกะกล่อง" ออก
Thilo

@thilo วิธีอื่น ๆ ในการนำไปใช้คืออะไร?
amrith

3
if (x == null) { // the case: null part }
Thilo

0

ฉันเห็นด้วยกับความคิดเห็นเชิงลึก (ภายใต้ประทุน .... ) ในhttps://stackoverflow.com/a/18263594/1053496ในคำตอบของ @Paul Bellora

ฉันพบอีกเหตุผลหนึ่งจากประสบการณ์ของฉัน

หาก 'case' สามารถเป็น null ได้ซึ่งหมายความว่า switch (ตัวแปร) เป็นโมฆะตราบใดที่ผู้พัฒนาระบุกรณี 'null' ที่ตรงกันเราก็สามารถโต้แย้งได้ แต่จะเกิดอะไรขึ้นหากผู้พัฒนาไม่ระบุกรณี 'null' ที่ตรงกัน จากนั้นเราจะต้องจับคู่กับกรณี 'ค่าเริ่มต้น' ซึ่งอาจไม่ใช่สิ่งที่นักพัฒนาตั้งใจจะจัดการในกรณีเริ่มต้น ดังนั้นการจับคู่ 'null' กับค่าเริ่มต้นอาจทำให้เกิด 'พฤติกรรมที่น่าประหลาดใจ' ดังนั้นการโยน 'NPE' จะทำให้ผู้พัฒนาสามารถจัดการทุกกรณีได้อย่างชัดเจน ฉันพบว่าการขว้าง NPE ในกรณีนี้มีความรอบคอบมาก


0

ใช้คลาส Apache StringUtils

String month = null;
switch (StringUtils.trimToEmpty(month)) {
    case "xyz":
        monthNumber=1;  
    break;
    default:
       monthNumber=0;
    break;
}
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.