¿Por qué no puedo usar la instrucción switch en una cadena?

1004

¿Esta funcionalidad se va a poner en una versión posterior de Java?

¿Alguien puede explicar por qué no puedo hacer esto, como en la forma técnica en que funciona la switchdeclaración de Java ?

Alex Beardsley
fuente
195
Está en SE 7. 16 años después de su solicitud. download.oracle.com/javase/tutorial/java/nutsandbolts/…
angryITguy
81
Sun fue honesto en su evaluación: "Don't hold your breath."lol, bugs.sun.com/bugdatabase/view_bug.do?bug_id=1223179
raffian
3
@raffian Creo que es porque ella suspiró dos veces. Llegaron un poco tarde para responder también, después de casi 10 años. Ella podría haber estado empacando cajas de almuerzo a sus nietos en ese momento.
WeirdElfB0y

Respuestas:

1004

Las declaraciones de cambio con Stringcasos se han implementado en Java SE 7 , al menos 16 años después de que se solicitaron por primera vez. No se proporcionó una razón clara para la demora, pero probablemente tuvo que ver con el rendimiento.

Implementación en JDK 7

La característica ahora se ha implementado javac con un proceso de "des-azúcar"; una sintaxis limpia y de alto nivel que usa Stringconstantes en las casedeclaraciones se expande en tiempo de compilación en un código más complejo siguiendo un patrón. El código resultante utiliza instrucciones JVM que siempre han existido.

A switchcon Stringcasos se traduce en dos interruptores durante la compilación. El primero asigna cada cadena a un número entero único: su posición en el interruptor original. Esto se hace activando primero el código hash de la etiqueta. El caso correspondiente es una ifdeclaración que prueba la igualdad de cadena; Si hay colisiones en el hash, la prueba es en cascada if-else-if. El segundo conmutador refleja eso en el código fuente original, pero sustituye las etiquetas de los casos con sus posiciones correspondientes. Este proceso de dos pasos facilita la preservación del control de flujo del interruptor original.

Interruptores en la JVM

Para obtener más detalles técnicos sobre switch, puede consultar la Especificación JVM, donde se describe la compilación de las instrucciones del conmutador . En pocas palabras, hay dos instrucciones JVM diferentes que se pueden usar para un conmutador, dependiendo de la escasez de las constantes utilizadas por los casos. Ambos dependen del uso de constantes enteras para que cada caso se ejecute de manera eficiente.

Si las constantes son densas, se usan como un índice (después de restar el valor más bajo) en una tabla de punteros de instrucción: la tableswitchinstrucción.

Si las constantes son escasas, se realiza una búsqueda binaria del caso correcto: la lookupswitchinstrucción.

Al quitar el azúcar switcha los Stringobjetos, es probable que se utilicen ambas instrucciones. El lookupswitches adecuado para el primer interruptor de códigos hash para encontrar la posición original de la caja. El ordinal resultante es un ajuste natural para a tableswitch.

Ambas instrucciones requieren que las constantes enteras asignadas a cada caso se ordenen en tiempo de compilación. En tiempo de ejecución, aunque el O(1)rendimiento de tableswitchgeneralmente parece mejor que el O(log(n))rendimiento de lookupswitch, requiere un análisis para determinar si la tabla es lo suficientemente densa como para justificar la compensación espacio-tiempo. Bill Venners escribió un gran artículo que cubre esto con más detalle, junto con una mirada oculta a otras instrucciones de control de flujo de Java.

Antes de JDK 7

Antes de JDK 7, enumpodría aproximarse a un Stringinterruptor basado en. Esto utiliza elvalueOf método estático generado por el compilador en cada enumtipo. Por ejemplo:

Pill p = Pill.valueOf(str);
switch(p) {
  case RED:  pop();  break;
  case BLUE: push(); break;
}
erickson
fuente
26
Puede ser más rápido usar If-Else-If en lugar de un hash para un interruptor basado en cadenas. He encontrado que los diccionarios son bastante caros cuando solo guardo algunos artículos.
Jonathan Allen
84
Un if-elseif-elseif-elseif-else podría ser más rápido, pero tomaría el código más limpio 99 veces de cada 100. Las cadenas, al ser inmutables, almacenan en caché su código hash, por lo que "calcular" el hash es rápido. Uno tendría que perfilar el código para determinar qué beneficio hay.
erickson
21
La razón dada en contra de agregar switch (String) es que no cumpliría con las garantías de rendimiento que se esperan de las declaraciones switch (). No querían "engañar" a los desarrolladores. Francamente, no creo que deberían garantizar el rendimiento de switch () para empezar.
Gili
2
Si solo está utilizando Pillalgunas medidas basadas en str, diría que si no, es preferible ya que le permite manejarstr valores fuera del rango ROJO, AZUL sin necesidad de detectar una excepción valueOfo verificar manualmente una coincidencia con el nombre de cada tipo de enumeración que solo agrega una sobrecarga innecesaria. En mi experiencia, solo tiene sentido usarlo valueOfpara transformarlo en una enumeración si más adelante se necesitaba una representación segura del tipo de cadena.
MilesHampson
Me pregunto si los compiladores hacen algún esfuerzo para probar si hay algún par de números (x, y) para los cuales el conjunto de valores (hash >> x) & ((1<<y)-1)produciría valores distintos para cada cadena que hashCodees diferente y (1<<y)es menos del doble del número de cadenas (o en al menos no mucho mayor que eso).
supercat
125

Si tiene un lugar en su código donde puede activar una Cadena, entonces puede ser mejor refactorizar la Cadena para que sea una enumeración de los posibles valores, que puede activar. Por supuesto, limita los valores potenciales de las cadenas que puede tener a los de la enumeración, que pueden o no ser deseables.

Por supuesto, su enumeración podría tener una entrada para 'otro' y un método fromString (String), entonces podría tener

ValueEnum enumval = ValueEnum.fromString(myString);
switch (enumval) {
   case MILK: lap(); break;
   case WATER: sip(); break;
   case BEER: quaff(); break;
   case OTHER: 
   default: dance(); break;
}
JeeBee
fuente
44
Esta técnica también le permite decidir sobre cuestiones como la insensibilidad a mayúsculas y minúsculas, los alias, etc. En lugar de depender de un diseñador de lenguaje para llegar a la solución "talla única".
Darron
2
De acuerdo con JeeBee, si está activando cadenas probablemente necesite una enumeración. La cadena generalmente representa algo que va a una interfaz (usuario o no) que puede cambiar o no en el futuro, así que mejor reemplácela con enumeraciones
hhafez
18
Consulte xefer.com/2006/12/switchonstring para obtener una buena reseña de este método.
David Schmitt
@DavidSchmitt La redacción tiene una falla importante. Captura todas las excepciones en lugar de las que realmente arroja el método.
M. Mimpen
91

El siguiente es un ejemplo completo basado en la publicación de JeeBee, utilizando la enumeración de Java en lugar de utilizar un método personalizado.

Tenga en cuenta que, en Java SE 7 y versiones posteriores, puede utilizar un objeto String en la expresión de la instrucción switch.

public class Main {

    /**
    * @param args the command line arguments
    */
    public static void main(String[] args) {

      String current = args[0];
      Days currentDay = Days.valueOf(current.toUpperCase());

      switch (currentDay) {
          case MONDAY:
          case TUESDAY:
          case WEDNESDAY:
              System.out.println("boring");
              break;
          case THURSDAY:
              System.out.println("getting better");
          case FRIDAY:
          case SATURDAY:
          case SUNDAY:
              System.out.println("much better");
              break;

      }
  }

  public enum Days {

    MONDAY,
    TUESDAY,
    WEDNESDAY,
    THURSDAY,
    FRIDAY,
    SATURDAY,
    SUNDAY
  }
}
Thulani Chivandikwa
fuente
26

Los conmutadores basados ​​en enteros se pueden optimizar para obtener un código muy eficiente. Los conmutadores basados ​​en otro tipo de datos solo se pueden compilar en una serie de sentencias if ().

Por esa razón, C & C ++ solo permiten cambios en tipos enteros, ya que no tenía sentido con otros tipos.

Los diseñadores de C # decidieron que el estilo era importante, incluso si no había ventaja.

Los diseñadores de Java aparentemente pensaron como los diseñadores de C.

James Curran
fuente
26
Los conmutadores basados ​​en cualquier objeto hashable se pueden implementar de manera muy eficiente utilizando una tabla hash; consulte .NET. Entonces tu razón no es completamente correcta.
Konrad Rudolph
Sí, y esto es lo que no entiendo. ¿Temen que los objetos de hash se vuelvan demasiado caros a la larga?
Alex Beardsley
3
@Nalandial: en realidad, con un poco de esfuerzo por parte del compilador, no es costoso porque cuando se conoce el conjunto de cadenas, es bastante fácil generar un hash perfecto (sin embargo, esto no lo hace .NET; probablemente tampoco valga la pena el esfuerzo).
Konrad Rudolph
3
@Nalandial y @Konrad Rudolph: aunque el hashing de una cadena (debido a su naturaleza inmutable) parece una solución a este problema, debe recordar que todos los objetos no finales pueden tener sus funciones de hash anuladas. Esto hace que sea difícil en el momento de la compilación garantizar la coherencia en un cambio.
Martinatime
2
También puede construir un DFA para que coincida con la cadena (como lo hacen los motores de expresión regular). Posiblemente incluso más eficiente que el hash.
Nate CK
19

También Stringse puede mostrar un ejemplo de uso directo desde 1.7:

public static void main(String[] args) {

    switch (args[0]) {
        case "Monday":
        case "Tuesday":
        case "Wednesday":
            System.out.println("boring");
            break;
        case "Thursday":
            System.out.println("getting better");
        case "Friday":
        case "Saturday":
        case "Sunday":
            System.out.println("much better");
            break;
    }

}
Gunnar Forsgren - Mobimation
fuente
18

James Curran dice sucintamente: "Los interruptores basados ​​en números enteros pueden optimizarse a un código muy eficiente. Los interruptores basados ​​en otro tipo de datos solo pueden compilarse en una serie de declaraciones if (). Por esa razón, C & C ++ solo permiten cambios en tipos enteros, ya que no tenía sentido con otros tipos ".

Mi opinión, y es solo eso, es que tan pronto como comience a encender los no primitivos, debe comenzar a pensar en "igual" versus "==". En primer lugar, comparar dos cadenas puede ser un procedimiento bastante largo, que se suma a los problemas de rendimiento que se mencionan anteriormente. En segundo lugar, si se activan las cadenas, habrá demanda para activar las cadenas ignorando mayúsculas y minúsculas, activando las cadenas considerando / ignorando el entorno local, activando las cadenas basadas en expresiones regulares ... aprobaría una decisión que ahorró mucho tiempo para el desarrolladores de lenguaje a costa de una pequeña cantidad de tiempo para los programadores.

DJClayworth
fuente
Técnicamente, las expresiones regulares ya "cambian", ya que son básicamente máquinas de estado; simplemente tienen dos "casos", matchedy not matched. (Sin embargo, sin tener en cuenta cosas como [nombre] grupos / etc.)
JAB
1
docs.oracle.com/javase/7/docs/technotes/guides/language/… afirma: El compilador de Java genera generalmente un
Wim Deblauwe
12

Además de los buenos argumentos anteriores, agregaré que mucha gente ve hoy switch como un resto obsoleto del pasado procesal de Java (de vuelta a C veces).

No comparto completamente esta opinión, creo que switchpuede tener su utilidad en algunos casos, al menos debido a su velocidad, y de todos modos es mejor que algunas series de números en cascada else ifque vi en algún código ...

Pero, de hecho, vale la pena mirar el caso en el que necesita un interruptor, y ver si no puede ser reemplazado por algo más OO. Por ejemplo, enumeraciones en Java 1.5+, tal vez HashTable o alguna otra colección (a veces lamento no tener funciones (anónimas) como ciudadanos de primera clase, como en Lua, que no tiene interruptor, o JavaScript) o incluso polimorfismo.

PhiLho
fuente
"a veces lamento no tener funciones (anónimas) como ciudadanos de primera clase" Eso ya no es cierto.
user8397947
@dorukayhan Sí, por supuesto. Pero, ¿desea agregar un comentario en todas las respuestas de los últimos diez años para decirle al mundo que podemos tenerlas si actualizamos a versiones más recientes de Java? :-D
PhiLho
8

Si no está utilizando JDK7 o superior, puede usarlo hashCode()para simularlo. Debido a que String.hashCode()generalmente devuelve valores diferentes para cadenas diferentes y siempre devuelve valores iguales para cadenas iguales, es bastante confiable (Las cadenas diferentes pueden producir el mismo código hash que @Lii mencionado en un comentario, como "FB"y "Ea") Consulte la documentación .

Entonces, el código se vería así:

String s = "<Your String>";

switch(s.hashCode()) {
case "Hello".hashCode(): break;
case "Goodbye".hashCode(): break;
}

De esa manera, técnicamente está activando un int.

Alternativamente, puede usar el siguiente código:

public final class Switch<T> {
    private final HashMap<T, Runnable> cases = new HashMap<T, Runnable>(0);

    public void addCase(T object, Runnable action) {
        this.cases.put(object, action);
    }

    public void SWITCH(T object) {
        for (T t : this.cases.keySet()) {
            if (object.equals(t)) { // This means that the class works with any object!
                this.cases.get(t).run();
                break;
            }
        }
    }
}
Hiperneutrino
fuente
55
Dos cadenas diferentes pueden tener el mismo código hash, por lo que si activa los códigos hash, se podría tomar la rama de caso incorrecta.
Lii
@Lii ¡Gracias por señalar esto! Sin embargo, es poco probable, pero no confiaría en que funcione. "FB" y "Ea" tienen el mismo código hash, por lo que no es imposible encontrar una colisión. El segundo código es probablemente más confiable.
HyperNeutrino
Me sorprende que esto se compile, ya que las casedeclaraciones tenían que, siempre pensé, ser valores constantes, y String.hashCode()no lo son (incluso si en la práctica el cálculo nunca ha cambiado entre JVM).
StaxMan
@StaxMan Hm interesante, nunca me detuve a observar eso. Pero sí, los casevalores de las declaraciones no tienen que ser determinables en tiempo de compilación, por lo que funciona finamente.
HyperNeutrino
4

Durante años hemos estado utilizando un preprocesador (n de código abierto) para esto.

//#switch(target)
case "foo": code;
//#end

Los archivos preprocesados ​​se denominan Foo.jpp y se procesan en Foo.java con un script de hormiga.

La ventaja es que se procesa en Java que se ejecuta en 1.0 (aunque normalmente solo admitimos 1.4). También fue mucho más fácil hacer esto (muchos modificadores de cadena) en comparación con falsificarlo con enumeraciones u otras soluciones: el código era mucho más fácil de leer, mantener y comprender. IIRC (no puede proporcionar estadísticas o razonamiento técnico en este momento) también fue más rápido que los equivalentes naturales de Java.

Las desventajas son que no está editando Java, por lo que es un poco más de flujo de trabajo (editar, procesar, compilar / probar) además de que un IDE se vinculará de nuevo con Java, que es un poco complicado (el interruptor se convierte en una serie de pasos lógicos if / else) y el orden de cambio de caja no se mantiene.

No lo recomendaría para 1.7+, pero es útil si desea programar Java que apunte a JVM anteriores (ya que Joe public rara vez tiene la última instalada).

Puede obtenerlo de SVN o buscar el código en línea . Necesitará EBuild para construirlo tal cual.

Charles Goodwin
fuente
66
No necesita la JVM 1.7 para ejecutar código con un modificador de cadena. El compilador 1.7 convierte el modificador de cadena en algo que utiliza código de byte previamente existente.
Dawood ibn Kareem
4

Otras respuestas han dicho que esto se agregó en Java 7 y dio soluciones para versiones anteriores. Esta respuesta trata de responder el "por qué"

Java fue una reacción a las sobrecomplejidades de C ++. Fue diseñado para ser un lenguaje simple y limpio.

String recibió un poco de manejo especial de mayúsculas y minúsculas en el lenguaje, pero me parece claro que los diseñadores estaban tratando de mantener al mínimo la cantidad de tripas especiales y azúcar sintáctica.

encender cadenas es bastante complejo bajo el capó ya que las cadenas no son simples tipos primitivos. No era una característica común en el momento en que Java fue diseñado y realmente no encaja bien con el diseño minimalista. Especialmente porque habían decidido no usar mayúsculas y minúsculas == para cadenas, sería (y es) un poco extraño que las mayúsculas y minúsculas funcionen donde == no lo hace.

Entre 1.0 y 1.4, el lenguaje en sí se mantuvo prácticamente igual. La mayoría de las mejoras a Java estaban en el lado de la biblioteca.

Todo eso cambió con Java 5, el lenguaje se amplió sustancialmente. Otras extensiones siguieron en las versiones 7 y 8. Espero que este cambio de actitud haya sido impulsado por el aumento de C #

lavado
fuente
La narrativa sobre el interruptor (String) se ajusta a la historia, la línea de tiempo, el contexto cpp / cs.
Espresso
Fue un gran error no implementar esta función, todo lo demás es una excusa barata. Java perdió muchos usuarios a lo largo de los años debido a la falta de progreso y la terquedad de los diseñadores para no evolucionar el lenguaje. Afortunadamente, cambiaron completamente de dirección y actitud después de JDK7
firephil
0

JEP 354: Expresiones de cambio (Vista previa) en JDK-13 y JEP 361: Expresiones de cambio (Estándar) en JDK-14 extenderá la declaración de cambio para que pueda usarse como una expresión .

Ahora usted puede:

  • asignar directamente la variable desde la expresión del interruptor ,
  • use una nueva forma de etiqueta de interruptor (case L -> ):

    El código a la derecha de una etiqueta de interruptor "caso L ->" está restringido a ser una expresión, un bloque o (por conveniencia) una instrucción throw.

  • use múltiples constantes por caso, separadas por comas,
  • y tampoco hay más quiebres de valor :

    Para obtener un valor de una expresión de cambio, la breakdeclaración with value se descarta a favor de una yielddeclaración.

Entonces, la demostración de las respuestas ( 1 , 2 ) podría verse así:

  public static void main(String[] args) {
    switch (args[0]) {
      case "Monday", "Tuesday", "Wednesday" ->  System.out.println("boring");
      case "Thursday" -> System.out.println("getting better");
      case "Friday", "Saturday", "Sunday" -> System.out.println("much better");
    }
Iskuskov Alexander
fuente
-2

No es muy bonito, pero aquí hay otra forma para Java 6 y más abajo:

String runFct = 
        queryType.equals("eq") ? "method1":
        queryType.equals("L_L")? "method2":
        queryType.equals("L_R")? "method3":
        queryType.equals("L_LR")? "method4":
            "method5";
Method m = this.getClass().getMethod(runFct);
m.invoke(this);
Conete Cristian
fuente
-3

Es una brisa en Groovy; Incrusto el jarro maravilloso y creo una groovyclase de utilidad para hacer todas estas cosas y más que encuentro exasperante en Java (ya que estoy atascado usando Java 6 en la empresa).

it.'p'.each{
switch (it.@name.text()){
   case "choclate":
     myholder.myval=(it.text());
     break;
     }}...
Alex Punnen
fuente
99
@SSpoke Debido a que esta es una pregunta de Java y una respuesta Groovy está fuera de tema y es un complemento inútil.
Martin
12
Incluso en casas conservadoras de SW grandes, Groovy se usa junto con Java. JVM ofrece un entorno agnóstico de lenguaje más que un lenguaje ahora, para mezclar y usar el paradigma de programación más relevante para la solución. Entonces quizás ahora debería agregar un fragmento en Clojure para reunir más votos a favor :) ...
Alex Punnen
1
Además, ¿cómo funciona la sintaxis? Supongo que Groovy es un lenguaje de programación diferente ... Lo siento. No sé nada de Groovy.
HyperNeutrino
-4

Cuando uses intellij también mira:

Archivo -> Estructura del proyecto -> Proyecto

Archivo -> Estructura del proyecto -> Módulos

Cuando tenga varios módulos, asegúrese de establecer el nivel de idioma correcto en la pestaña del módulo.

botenvouwer
fuente
1
No estoy seguro de cómo su respuesta es relevante para la pregunta. Preguntó por qué las declaraciones de cambio de cadena como las siguientes no están disponibles: String mystring = "something"; switch (mystring) {case "something" sysout ("tengo aquí"); . . }
Deepak Agarwal
-8
public class StringSwitchCase { 

    public static void main(String args[]) {

        visitIsland("Santorini"); 
        visitIsland("Crete"); 
        visitIsland("Paros"); 

    } 

    public static void visitIsland(String island) {
         switch(island) {
          case "Corfu": 
               System.out.println("User wants to visit Corfu");
               break; 
          case "Crete": 
               System.out.println("User wants to visit Crete");
               break; 
          case "Santorini": 
               System.out.println("User wants to visit Santorini");
               break; 
          case "Mykonos": 
               System.out.println("User wants to visit Mykonos");
               break; 
         default: 
               System.out.println("Unknown Island");
               break; 
         } 
    } 

} 
Issac Balaji
fuente
8
El OP no pregunta cómo activar una cadena. Él / Ella pregunta por qué él / ella no puede hacerlo, debido a restricciones en la sintaxis antes de JDK7.
HyperNeutrino