Pourquoi ne puis-je pas utiliser l'instruction switch sur une chaîne?

1004

Cette fonctionnalité va-t-elle être intégrée dans une version Java ultérieure?

Quelqu'un peut-il expliquer pourquoi je ne peux pas faire cela, comme dans, la façon technique dont fonctionne la switchdéclaration de Java ?

Alex Beardsley
la source
195
C'est dans SE 7. 16 ans après sa demande. download.oracle.com/javase/tutorial/java/nutsandbolts/…
angryITguy
81
Sun a été honnête dans son évaluation: "Don't hold your breath."lol, bugs.sun.com/bugdatabase/view_bug.do?bug_id=1223179
raffian
3
@raffian Je pense que c'est parce qu'elle a soupiré deux fois. Ils ont également répondu un peu tard, après près de 10 ans. Elle aurait peut-être alors emballé des boîtes à lunch pour ses petits-enfants.
WeirdElfB0y

Réponses:

1004

Les instructions de commutation avec des Stringcas ont été implémentées dans Java SE 7 , au moins 16 ans après leur première demande. Une raison claire de ce retard n'a pas été fournie, mais elle était probablement liée à la performance.

Implémentation dans JDK 7

La fonction a maintenant été mise en œuvre javac avec un processus de "désucrage"; une syntaxe propre et de haut niveau utilisant des Stringconstantes dans les casedéclarations est développée au moment de la compilation en un code plus complexe suivant un modèle. Le code résultant utilise des instructions JVM qui ont toujours existé.

Un switchavec Stringcas est traduit en deux commutateurs lors de la compilation. Le premier mappe chaque chaîne à un entier unique — sa position dans le commutateur d'origine. Cela se fait en activant d'abord le code de hachage de l'étiquette. Le cas correspondant est une ifinstruction qui teste l'égalité des chaînes; s'il y a des collisions sur le hachage, le test est en cascade if-else-if. Le deuxième commutateur reflète cela dans le code source d'origine, mais remplace les étiquettes de cas avec leurs positions correspondantes. Ce processus en deux étapes permet de conserver facilement le contrôle de flux du commutateur d'origine.

Commutateurs dans la JVM

Pour plus de détails techniques switch, vous pouvez vous référer à la spécification JVM, où la compilation des instructions de commutateur est décrite. En résumé, deux instructions JVM différentes peuvent être utilisées pour un commutateur, selon la rareté des constantes utilisées par les cas. Les deux dépendent de l'utilisation de constantes entières pour chaque cas pour s'exécuter efficacement.

Si les constantes sont denses, elles sont utilisées comme index (après soustraction de la valeur la plus basse) dans un tableau de pointeurs d'instructions - l' tableswitchinstruction.

Si les constantes sont rares, une recherche binaire du cas correct est effectuée - l' lookupswitchinstruction.

Lors du dépuchage switchd' Stringobjets sur , les deux instructions sont susceptibles d'être utilisées. Le lookupswitchconvient au premier interrupteur sur les codes de hachage pour trouver la position d'origine du boîtier. L'ordinal résultant est un ajustement naturel pour a tableswitch.

Les deux instructions nécessitent que les constantes entières attribuées à chaque cas soient triées au moment de la compilation. Au moment de l'exécution, bien que les O(1)performances de tableswitchgénéralement semblent meilleures que les O(log(n))performances de lookupswitch, elles nécessitent une analyse pour déterminer si la table est suffisamment dense pour justifier le compromis espace-temps. Bill Venners a écrit un excellent article qui couvre cela plus en détail, ainsi qu'un regard sous le capot sur d'autres instructions de contrôle de flux Java.

Avant JDK 7

Avant JDK 7, enumpouvait s'approcher d'un Stringcommutateur basé sur. Cela utilise lavalueOf méthode statique générée par le compilateur sur chaque enumtype. Par exemple:

Pill p = Pill.valueOf(str);
switch(p) {
  case RED:  pop();  break;
  case BLUE: push(); break;
}
erickson
la source
26
Il pourrait être plus rapide d'utiliser simplement If-Else-If au lieu d'un hachage pour un commutateur basé sur une chaîne. J'ai trouvé que les dictionnaires coûtaient assez cher lorsqu'ils ne stockaient que quelques éléments.
Jonathan Allen
84
Un if-elseif-elseif-elseif-else pourrait être plus rapide, mais je prendrais le code plus propre 99 fois sur 100. Les chaînes, étant immuables, mettent en cache leur code de hachage, donc "calculer" le hachage est rapide. Il faudrait profiler le code pour déterminer quel avantage il y a.
erickson
21
La raison donnée contre l'ajout de switch (String) est qu'il ne répondrait pas aux garanties de performances attendues des instructions switch (). Ils ne voulaient pas "induire en erreur" les développeurs. Franchement, je ne pense pas qu'ils devraient garantir les performances de switch () pour commencer.
Gili
2
Si vous utilisez simplement Pillune action basée sur strJe dirais que si autre chose est préférable car cela vous permet de gérer des strvaleurs en dehors de la plage ROUGE, BLEU sans avoir besoin d'attraper une exception valueOfou de vérifier manuellement une correspondance avec le nom de chaque type d'énumération qui ajoute simplement une surcharge inutile. D'après mon expérience, cela n'a de sens que de valueOfse transformer en énumération si une représentation sécurisée de type de la valeur String était nécessaire plus tard.
MilesHampson
Je me demande si les compilateurs font un effort pour tester s'il existe une paire de nombres (x, y) pour laquelle l'ensemble de valeurs (hash >> x) & ((1<<y)-1)donnerait des valeurs distinctes pour chaque chaîne dont la hashCodedifférence est, et (1<<y)est inférieure au double du nombre de chaînes (ou à pas beaucoup plus que cela).
supercat
125

Si vous avez un endroit dans votre code où vous pouvez activer une chaîne, il peut être préférable de refactoriser la chaîne pour qu'elle soit une énumération des valeurs possibles, que vous pouvez activer. Bien sûr, vous limitez les valeurs potentielles des chaînes que vous pouvez avoir à celles de l'énumération, qui peuvent ou non être souhaitées.

Bien sûr, votre énumération pourrait avoir une entrée pour «autre» et une méthode fromString (String), alors vous pourriez avoir

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
la source
4
Cette technique vous permet également de décider de problèmes tels qu'une insensibilité à la casse, des alias, etc. Au lieu de dépendre d'un concepteur de langage pour trouver la solution "taille unique".
Darron
2
D'accord avec JeeBee, si vous activez des chaînes, vous aurez probablement besoin d'une énumération. La chaîne représente généralement quelque chose qui va à une interface (utilisateur ou autre) qui peut ou non changer à l'avenir, alors mieux vaut la remplacer par des
énumérations
18
Voir xefer.com/2006/12/switchonstring pour une belle rédaction de cette méthode.
David Schmitt
@DavidSchmitt L'écriture a un défaut majeur. Il intercepte toutes les exceptions au lieu de celles qui sont réellement levées par la méthode.
M. Mimpen
91

Ce qui suit est un exemple complet basé sur la publication de JeeBee, utilisant les énumérations java au lieu d'utiliser une méthode personnalisée.

Notez que dans Java SE 7 et versions ultérieures, vous pouvez utiliser un objet String dans l'expression de l'instruction switch à la place.

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
la source
26

Les commutateurs basés sur des entiers peuvent être optimisés pour un code très efficace. Les commutateurs basés sur un autre type de données ne peuvent être compilés qu'en une série d'instructions if ().

Pour cette raison, C & C ++ n'autorise que les commutateurs sur les types entiers, car il était inutile avec d'autres types.

Les concepteurs de C # ont décidé que le style était important, même s'il n'y avait aucun avantage.

Les concepteurs de Java pensaient apparemment comme les concepteurs de C.

James Curran
la source
26
Les commutateurs basés sur n'importe quel objet lavable peuvent être implémentés très efficacement à l'aide d'une table de hachage - voir .NET. Votre raison n'est donc pas tout à fait correcte.
Konrad Rudolph
Ouais, et c'est la chose que je ne comprends pas. Craignent-ils que les objets de hachage deviennent, à long terme, trop chers?
Alex Beardsley
3
@Nalandial: en fait, avec un peu d'effort de la part du compilateur, ce n'est pas cher du tout parce que lorsque l'ensemble de chaînes est connu, il est assez facile de générer un hachage parfait (cela n'est pas fait par .NET, cependant; probablement pas la peine non plus).
Konrad Rudolph
3
@Nalandial & @Konrad Rudolph - Bien que le hachage d'une chaîne (en raison de sa nature immuable) semble être une solution à ce problème, vous devez vous rappeler que tous les objets non finaux peuvent avoir leurs fonctions de hachage remplacées. Cela rend difficile au moment de la compilation d'assurer la cohérence dans un commutateur.
martinatime
2
Vous pouvez également construire un DFA pour correspondre à la chaîne (comme le font les moteurs d'expression régulière). Peut-être encore plus efficace que le hachage.
Nate CK
19

Un exemple d' Stringutilisation directe depuis la version 1.7 peut également être montré:

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
la source
18

James Curran dit succinctement: "Les commutateurs basés sur des entiers peuvent être optimisés pour un code très efficace. Les commutateurs basés sur d'autres types de données ne peuvent être compilés qu'en une série d'instructions if (). Pour cette raison, C & C ++ n'autorise que les commutateurs sur les types entiers, car il était inutile avec d'autres types. "

Mon avis, et ce n'est que cela, est que dès que vous commencez à activer des non-primitives, vous devez commencer à penser à "égal" par rapport à "==". Tout d'abord, la comparaison de deux chaînes peut être une procédure assez longue, s'ajoutant aux problèmes de performances mentionnés ci-dessus. Deuxièmement, s'il y a commutation de chaînes, il y aura une demande pour activer des chaînes en ignorant la casse, en activant les chaînes en tenant compte / en ignorant les paramètres régionaux, en activant les chaînes basées sur l'expression régulière ... J'approuverais une décision qui a fait gagner beaucoup de temps au les développeurs de langage au prix d'un peu de temps pour les programmeurs.

DJClayworth
la source
Techniquement, les regex "commutent" déjà, car ce ne sont essentiellement que des machines à états; ils n'ont que deux "cas", matchedet not matched. (Sans prendre en compte des choses comme les groupes [nommés] / etc., Cependant.)
JAB
1
docs.oracle.com/javase/7/docs/technotes/guides/language/… déclare: Le compilateur Java génère un bytecode généralement plus efficace à partir des instructions switch qui utilisent des objets String que des instructions enchaînées if-then-else.
Wim Deblauwe
12

En plus des bons arguments ci-dessus, j'ajouterai que beaucoup de gens considèrent aujourd'hui switchcomme un reste obsolète du passé procédural de Java (retour à l'époque C).

Je ne partage pas entièrement cette opinion, je pense que cela switchpeut avoir son utilité dans certains cas, du moins à cause de sa vitesse, et de toute façon c'est mieux que certaines séries de cascading numériques que else ifj'ai vues dans certains codes ...

Mais en effet, il vaut la peine d'examiner le cas où vous avez besoin d'un interrupteur et de voir s'il ne peut pas être remplacé par quelque chose de plus OO. Par exemple, les énumérations dans Java 1.5+, peut-être HashTable ou une autre collection (parfois je regrette que nous n'ayons pas de fonctions (anonymes) en tant que citoyen de première classe, comme dans Lua - qui n'a pas de commutateur - ou JavaScript) ou même le polymorphisme.

PhiLho
la source
"Parfois, je regrette que nous n'ayons pas de fonctions (anonymes) en tant que citoyen de première classe" Ce n'est plus vrai.
user8397947
@dorukayhan Oui, bien sûr. Mais voulez-vous ajouter un commentaire à toutes les réponses des dix dernières années pour dire au monde que nous pouvons les avoir si nous mettons à jour des versions plus récentes de Java? :-D
PhiLho
8

Si vous n'utilisez pas JDK7 ou supérieur, vous pouvez l'utiliser hashCode()pour le simuler. Étant donné que String.hashCode()renvoie généralement des valeurs différentes pour différentes chaînes et renvoie toujours des valeurs égales pour des chaînes égales, il est assez fiable (différentes chaînes peuvent produire le même code de hachage que @Lii mentionné dans un commentaire, tel que "FB"et "Ea") Voir la documentation .

Ainsi, le code ressemblerait à ceci:

String s = "<Your String>";

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

De cette façon, vous allumez techniquement un int.

Vous pouvez également utiliser le code suivant:

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;
            }
        }
    }
}
HyperNeutrino
la source
5
Deux chaînes différentes peuvent avoir le même code de hachage, donc si vous activez les codes de hachage, la mauvaise branche-case peut être prise.
Lii
@Lii Merci de l'avoir signalé! C'est peu probable, cependant, mais je ne ferais pas confiance à son fonctionnement. "FB" et "Ea" ont le même code de hachage, il n'est donc pas impossible de trouver une collision. Le deuxième code est probablement plus fiable.
HyperNeutrino
Je suis surpris que cela compile, car les caseinstructions devaient, je pensais, toujours être des valeurs constantes, et ce String.hashCode()n'est pas le cas (même si dans la pratique le calcul n'a jamais changé entre les machines virtuelles Java).
StaxMan
@StaxMan Hm intéressant, je n'ai jamais cessé de l'observer. Mais oui, les casevaleurs des instructions ne doivent pas être déterminables au moment de la compilation, donc cela fonctionne très bien.
HyperNeutrino
4

Nous utilisons depuis des années un préprocesseur (n open source).

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

Les fichiers prétraités sont nommés Foo.jpp et sont traités dans Foo.java avec un script ant.

L'avantage est qu'il est transformé en Java qui fonctionne sur 1.0 (bien que nous ne prenions généralement en charge que 1.4). De plus, il était beaucoup plus facile de le faire (beaucoup de commutateurs de chaîne) que de le truquer avec des énumérations ou d'autres solutions de contournement - le code était beaucoup plus facile à lire, à maintenir et à comprendre. IIRC (ne peut pas fournir de statistiques ou de raisonnement technique à ce stade), il était également plus rapide que les équivalents Java naturels.

Les inconvénients sont que vous n'éditez pas Java, c'est donc un peu plus de flux de travail (éditer, traiter, compiler / tester), plus un IDE sera lié à Java qui est un peu compliqué (le commutateur devient une série d'étapes logiques if / else) et l'ordre du boîtier de commutation n'est pas maintenu.

Je ne le recommanderais pas pour 1.7+, mais c'est utile si vous voulez programmer Java qui cible les JVM antérieures (puisque Joe public a rarement la dernière installée).

Vous pouvez l'obtenir auprès de SVN ou parcourir le code en ligne . Vous aurez besoin d' EBuild pour le construire tel quel.

Charles Goodwin
la source
6
Vous n'avez pas besoin de la JVM 1.7 pour exécuter du code avec un commutateur String. Le compilateur 1.7 transforme le commutateur String en quelque chose qui utilise du code d'octet déjà existant.
Dawood ibn Kareem
4

D'autres réponses ont indiqué que cela a été ajouté dans Java 7 et donné des solutions de contournement pour les versions antérieures. Cette réponse tente de répondre au "pourquoi"

Java était une réaction à la complexité excessive du C ++. Il a été conçu pour être un langage simple et propre.

String a obtenu un peu de traitement de cas spécial dans la langue, mais il me semble clair que les concepteurs essayaient de réduire au minimum la quantité de boîtier spécial et de sucre syntaxique.

l'activation des chaînes est assez complexe sous le capot car les chaînes ne sont pas de simples types primitifs. Ce n'était pas une caractéristique commune au moment où Java a été conçu et ne correspond pas vraiment au design minimaliste. D'autant plus qu'ils avaient décidé de ne pas mettre de cas spécial == pour les chaînes, il serait (et est) un peu étrange que la casse fonctionne où == ne fonctionne pas.

Entre 1.0 et 1.4, la langue elle-même est restée à peu près la même. La plupart des améliorations apportées à Java étaient du côté de la bibliothèque.

Tout cela a changé avec Java 5, le langage a été considérablement étendu. D'autres extensions ont suivi dans les versions 7 et 8. Je m'attends à ce que ce changement d'attitude soit provoqué par la montée de C #

plugwash
la source
Le récit sur le commutateur (chaîne) correspond à l'historique, à la chronologie et au contexte cpp / cs.
Espresso
C'était une grosse erreur de ne pas implémenter cette fonctionnalité, tout le reste est une excuse bon marché Java a perdu de nombreux utilisateurs au fil des ans à cause du manque de progrès et de l'entêtement des concepteurs à ne pas faire évoluer le langage. Heureusement, ils ont complètement changé de direction et d'attitude après JDK7
firephil
0

JEP 354: Switch Expressions (Preview) dans JDK-13 et JEP 361: Switch Expressions (Standard) dans JDK-14 étendra l' instruction switch afin qu'elle puisse être utilisée comme expression .

Maintenant vous pouvez:

  • attribuer directement une variable à partir de l' expression du commutateur ,
  • utiliser une nouvelle forme d'étiquette de commutateur ( case L ->):

    Le code à droite d'une étiquette de commutateur "case L ->" est limité à une expression, un bloc ou (pour plus de commodité) une instruction throw.

  • utiliser plusieurs constantes par cas, séparées par des virgules,
  • et il n'y a plus de rupture de valeur :

    Pour générer une valeur à partir d'une expression de commutateur, l' breakinstruction with value est supprimée au profit d'une yieldinstruction.

La démonstration des réponses ( 1 , 2 ) pourrait donc ressembler à ceci:

  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
la source
-2

Pas très joli, mais voici une autre façon pour Java 6 et ci-dessous:

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
la source
-3

C'est un jeu d'enfant à Groovy; J'inclus le pot groovy et crée une groovyclasse utilitaire pour faire toutes ces choses et plus que je trouve exaspérant à faire en Java (car je suis bloqué en utilisant Java 6 dans l'entreprise.)

it.'p'.each{
switch (it.@name.text()){
   case "choclate":
     myholder.myval=(it.text());
     break;
     }}...
Alex Punnen
la source
9
@SSpoke Parce que c'est une question java et une réponse Groovy est hors sujet et une prise inutile.
Martin
12
Même dans les grandes maisons SW conservatrices, Groovy est utilisé avec Java. JVM offre un environnement indépendant du langage plus qu'un langage actuellement, pour mélanger et utiliser le paradigme de programmation le plus pertinent pour la solution. Alors peut-être que maintenant je devrais ajouter un extrait dans Clojure pour rassembler plus de downvotes :) ...
Alex Punnen
1
De plus, comment fonctionne la syntaxe? Je suppose que Groovy est un langage de programmation différent ...? Désolé. Je ne sais rien de Groovy.
HyperNeutrino
-4

Lorsque vous utilisez intellij, regardez également:

Fichier -> Structure du projet -> Projet

Fichier -> Structure du projet -> Modules

Lorsque vous avez plusieurs modules, assurez-vous de définir le niveau de langue correct dans l'onglet module.

botenvouwer
la source
1
Je ne sais pas en quoi votre réponse est pertinente pour la question. Il a demandé pourquoi une instruction de changement de chaîne comme celle-ci n'est pas disponible: String mystring = "something"; switch (mystring) {case "quelque chose" sysout ("arrivé ici"); . . }
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
la source
8
L'OP ne demande pas comment activer une chaîne. Il / Elle demande pourquoi il / elle ne peut pas, à cause des restrictions de syntaxe avant JDK7.
HyperNeutrino