¿Por qué la sentencia String Switch no admite un caso nulo?

125

Me pregunto por qué la switchdeclaración de Java 7 no admite un nullcaso y en su lugar arroja NullPointerException. Vea la línea comentada a continuación (ejemplo tomado del artículo de Tutoriales de Java enswitch ):

{
    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;
}

Esto habría evitado una ifcondición de verificación nula antes de cada switchuso.

Prashant Bhate
fuente
12
No hay una respuesta concluyente a esto, ya que no somos las personas que hicieron el idioma. Todas las respuestas serán pura conjetura.
asteri
2
Un intento de encender nullcausará una excepción. Realice una ifverificación para null, luego vaya a la switchdeclaración.
gparyani
28
Desde el JLS : a juicio de los diseñadores del lenguaje de programación Java, [arrojar un NullPointerExceptionsi la expresión se evalúa como nullen tiempo de ejecución] es un mejor resultado que omitir silenciosamente toda la declaración del interruptor o elegir ejecutar las declaraciones (si las hay) después del etiqueta predeterminada (si existe).
gparyani
3
@gparyani: Haz de eso una respuesta. Eso suena muy oficial y definitivo.
Thilo
77
@JeffGohlke: "No hay forma de responder una pregunta de por qué a menos que usted sea la persona que tomó la decisión". ... bueno, el comentario de gparyani demuestra lo contrario
user541686

Respuestas:

145

Como damryfbfnetsi señala en los comentarios, JLS §14.11 tiene la siguiente nota:

La prohibición de usar nullcomo etiqueta de interruptor evita que uno escriba código que nunca se puede ejecutar. Si la switchexpresión es de un tipo de referencia, es decir, Stringo un tipo primitivo en caja o un tipo de enumeración, se producirá un error en tiempo de ejecución si la expresión se evalúa nullen tiempo de ejecución. A juicio de los diseñadores del lenguaje de programación Java, este es un mejor resultado que omitir silenciosamente toda la switchdeclaración o elegir ejecutar las declaraciones (si corresponde) después de la defaultetiqueta (si corresponde).

(énfasis mío)

Si bien la última oración omite la posibilidad de usar case null:, parece razonable y ofrece una visión de las intenciones de los diseñadores de idiomas.

Si preferimos ver los detalles de implementación, esta publicación de blog de Christian Hujer tiene algunas especulaciones perspicaces sobre por qué nullno está permitido en los switches (aunque se centra en el enumswitch en lugar del Stringswitch):

Bajo el capó, la switchdeclaración generalmente se compilará en un código de byte de cambio de tabla. Y el argumento "físico" switchy sus casos son ints. El valor int para encender se determina invocando el método Enum.ordinal(). Los [...] ordinales comienzan en cero.

Eso significa, mapeo nulla 0no sería una buena idea. Un cambio en el primer valor de enumeración sería indistinguible de nulo. Tal vez hubiera sido una buena idea comenzar a contar los ordinales para enumeraciones en 1. Sin embargo, no se ha definido así, y esta definición no se puede cambiar.

Si bien los Stringconmutadores se implementan de manera diferente , el enumconmutador vino primero y sentó el precedente de cómo debe comportarse el encendido de un tipo de referencia cuando la referencia es null.

Paul Bellora
fuente
1
Hubiera sido una gran mejora permitir el manejo nulo como parte de un case null:si se hubiera implementado exclusivamente String. Actualmente, todas las Stringcomprobaciones requieren una comprobación nula de todos modos si queremos hacerlo correctamente, aunque principalmente implícitamente colocando la cadena constante primero como en "123test".equals(value). Ahora nos vemos obligados a escribir nuestra sentencia switch como enif (value != null) switch (value) {...
Yoyo
1
Re "asignar nulo a 0 no sería una buena idea": eso es un eufemismo ya que el valor de "" .hashcode () es 0! Significaría que una cadena nula y una cadena de longitud cero tendrían que tratarse de manera idéntica en una declaración de cambio, que claramente no es viable.
skomisa
Para el caso de enum, ¿qué les impedía mapear nulo a -1?
krispy
31

En general nulles desagradable de manejar; Quizás un mejor idioma pueda vivir sin él null.

Su problema puede ser resuelto por

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

    }
ZhongYu
fuente
No es una buena idea si monthes una cadena vacía: esto la tratará de la misma manera que una cadena nula.
gparyani
13
Para muchos casos, tratar nulo como cadena vacía puede ser perfectamente razonable
Eric Woodruff
23

No es bonito, pero le String.valueOf()permite usar una cadena nula en un interruptor. Si encuentra null, lo convierte "null", de lo contrario, solo devuelve la misma cadena que le pasó. Si no lo maneja "null"explícitamente, irá a default. La única advertencia es que no hay forma de distinguir entre la cadena "null"y una nullvariable real .

    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;
krispy
fuente
2
Creo que hacer algo así en Java es un antipatrón.
Łukasz Rzeszotarski
2
@ ŁukaszRzeszotarski Eso es más o menos lo que quise decir con "no es bonito"
krispy
15

Este es un intento de responder por qué arroja NullPointerException

El resultado del siguiente comando javap revela que casese elige en función del código hash de la switchcadena de argumento y, por lo tanto, arroja NPE cuando .hashCode()se invoca en una cadena nula.

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

Esto significa, en base a las respuestas a ¿Puede el código hash de Java producir el mismo valor para diferentes cadenas? , aunque es raro, todavía existe la posibilidad de que coincidan dos casos (dos cadenas con el mismo código hash). Vea este ejemplo a continuación

    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 para el cual

  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

Como puede ver, solo se genera un caso para "Ea"y "FB"con dos ifcondiciones para verificar una coincidencia con cada cadena de casos. ¡Una forma muy interesante y complicada de implementar esta funcionalidad!

Prashant Bhate
fuente
55
Sin embargo, eso podría haberse implementado de otra manera.
Thilo
2
Yo diría que esto es un error de diseño.
Deer Hunter
66
Me pregunto si esto se relaciona con por qué algunos aspectos dudosos de la función de hash de cadena no se han cambiado: en general, el código no debe basarse en hashCodedevolver los mismos valores en diferentes ejecuciones de un programa, pero dado que los hash de cadena se convierten en ejecutables por el compilador, el método de hash de cadenas termina siendo parte de la especificación del lenguaje.
supercat
5

Larga historia corta ... (¡y espero que sea lo suficientemente interesante!)

Enum se introdujo por primera vez en Java1.5 ( septiembre de 2004 ) y el error que solicitaba permitir el cambio de String se archivó hace mucho tiempo ( octubre de 1995 ). Si mira el comentario publicado sobre ese error en junio de 2004 , dice que Don't hold your breath. Nothing resembling this is in our plans.parece que aplazaron ( ignoraron ) este error y finalmente lanzaron Java 1.5 en el mismo año en que introdujeron 'enum' con ordinal comenzando en 0 y decidieron ( perdido ) para no admitir nulo para enumeración. Más tarde en Java1.7 ( julio de 2011 ) lo siguieron ( forzado) la misma filosofía con String (es decir, al generar el bytecode no se realizó ninguna comprobación nula antes de llamar al método hashcode ()).

Así que creo que se reduce al hecho de que enum entró primero y se implementó con su ordinal comenzando en 0 debido a que no podían admitir el valor nulo en el bloque de interruptores y luego con String decidieron forzar la misma filosofía, es decir, el valor nulo no permitido en el bloque de interruptores.

TL; DR With String podrían haberse ocupado de NPE (causado por el intento de generar código hash para nulo) mientras implementaban la conversión de código java a byte pero finalmente decidieron no hacerlo.

Ref: TheBUG , JavaVersionHistory , JavaCodeToByteCode , SO

sactiw
fuente
1

De acuerdo con Java Docs:

Un conmutador funciona con los tipos de datos primitivos byte, short, char e int. También funciona con tipos enumerados (discutidos en Tipos de enumeración), la clase String y algunas clases especiales que envuelven ciertos tipos primitivos: Carácter, Byte, Short e Integer (discutido en Numbers and Strings).

Como nullno tiene ningún tipo y no es una instancia de nada, no funcionará con una instrucción switch.

BlackHatSamurai
fuente
44
Y sin embargo, nulles un valor válido para un String, Character, Byte, Short, o Integerde referencia.
asteri
0

La respuesta es simplemente que si usa un interruptor con un tipo de referencia (como un tipo primitivo en caja), el error en tiempo de ejecución se producirá si la expresión es nula porque al desempaquetarla arrojaría el NPE.

entonces el caso nulo (que es ilegal) nunca podría ejecutarse de todos modos;)

amrith
fuente
1
Sin embargo, eso podría haberse implementado de otra manera.
Thilo
OK @Thilo, personas más inteligentes que yo participaron en esta implementación. Si conoce otras formas en que esto podría haberse implementado, me encantaría saber cuáles son [y estoy seguro de que hay otras], así que comparta ...
amrith
3
Las cadenas no son tipos primitivos encuadrados, y el NPE no ocurre porque alguien está tratando de "desempaquetarlos".
Thilo
@thilo, las otras formas de implementar esto son, ¿qué?
amrith
3
if (x == null) { // the case: null part }
Thilo
0

Estoy de acuerdo con los comentarios perspicaces (Under the hood ...) en https://stackoverflow.com/a/18263594/1053496 en la respuesta de @Paul Bellora.

Encontré una razón más de mi experiencia.

Si 'case' puede ser nulo, eso significa que switch (variable) es nulo, siempre que el desarrollador proporcione un caso 'nulo' coincidente, entonces podemos argumentar que está bien. Pero, ¿qué sucederá si el desarrollador no proporciona ningún caso 'nulo' coincidente? Luego tenemos que hacer coincidirlo con un caso 'predeterminado' que puede no ser lo que el desarrollador ha intentado manejar en el caso predeterminado. Por lo tanto, hacer coincidir 'nulo' con un valor predeterminado podría causar un 'comportamiento sorprendente'. Por lo tanto, lanzar 'NPE' hará que el desarrollador maneje todos los casos explícitamente. Encontré lanzar NPE en este caso muy reflexivo.

nantitv
fuente
0

Utilice la clase Apache StringUtils

String month = null;
switch (StringUtils.trimToEmpty(month)) {
    case "xyz":
        monthNumber=1;  
    break;
    default:
       monthNumber=0;
    break;
}
Bhagat
fuente