web-dev-qa-db-fra.com

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

Cette fonctionnalité sera-t-elle intégrée à une version ultérieure Java?

Quelqu'un peut-il expliquer pourquoi je ne peux pas faire cela, comme c'est le cas de la manière technique dont fonctionne l'instruction switch de Java?

972
Alex Beardsley

Les instructions de commutation avec String cas ont été implémentées dans Java SE 7 , au moins 16 ans après leur première demande. Aucune raison claire du retard n'a été fournie. , mais cela a probablement à voir avec la performance.

Mise en œuvre dans JDK 7

La fonctionnalité a maintenant été implémentée dans javacavec un processus "de réduction du sucre"; une syntaxe propre et de haut niveau utilisant des constantes String dans case développé à la compilation en un code plus complexe suivant un motif. Le code résultant utilise des instructions JVM qui ont toujours existé.

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

Commutateurs dans la machine virtuelle Java

Pour plus de détails techniques sur switch, vous pouvez vous reporter à la spécification JVM, où compilation d'instructions de commutateur est décrit. En résumé, il existe deux instructions JVM différentes pouvant être utilisées pour un commutateur, en fonction de la rareté des constantes utilisées par les observations. Les deux dépendent de l'utilisation de constantes entières pour que chaque cas s'exécute efficacement.

Si les constantes sont denses, elles sont utilisées comme index (après avoir soustrait la valeur la plus basse) dans une table de pointeurs d'instruction, l'instruction tableswitch.

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

Pour réduire un objet switch sur String, les deux instructions sont susceptibles d'être utilisées. Le lookupswitch convient au premier commutateur sur les codes de hachage pour trouver la position initiale du boîtier. L'ordinal résultant est un ajustement naturel pour un tableswitch.

Les deux instructions exigent que les constantes de nombre entier assignées à chaque cas soient triées au moment de la compilation. Au moment de l'exécution, bien que la performance O(1) de tableswitch apparaisse généralement meilleure que la O(log(n)) performance de lookupswitch, une analyse est nécessaire pour déterminer si le tableau est suffisamment dense pour justifier la suppression. compromis espace-temps. Bill Venners a écrit n excellent article qui couvre cette question de manière plus détaillée, avec un aperçu complet d'autres instructions de contrôle de flux Java.

Avant le JDK 7

Avant la JDK 7, enum pouvait se rapprocher d'un commutateur basé sur String. Ceci utilise la méthode la méthode statique valueOf générée par le compilateur pour chaque type enum. Par exemple:

Pill p = Pill.valueOf(str);
switch(p) {
  case RED:  pop();  break;
  case BLUE: Push(); break;
}
985
erickson

Si vous avez un emplacement 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 entendu, vous limitez les valeurs potentielles de chaînes que vous pouvez avoir à celles de l'énumération, que vous souhaitiez ou non.

Bien sûr, votre énumération pourrait avoir une entrée pour 'other' 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;
}
121
JeeBee

Ce qui suit est un exemple complet basé sur le message de JeeBee, utilisant Java enum 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.

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
  }
}
89
Thulani Chivandikwa

Les commutateurs basés sur des entiers peuvent être optimisés pour obtenir 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, puisqu'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 ont apparemment pensé comme les concepteurs de C.

25
James Curran

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 un autre type de données ne peuvent être compilés que pour une série d'instructions if (). Pour cette raison, C & C++ n'autorise les commutateurs que sur des types d'entiers. car il était inutile avec d'autres types ".

Mon opinion, et seulement celle-là, est que dès que vous commencez à activer des non-primitives, vous devez commencer à penser à "égal à" par rapport à "==". Premièrement, comparer deux chaînes peut être une procédure assez longue, qui ajoute aux problèmes de performances mentionnés ci-dessus. Deuxièmement, s’il existe une commutation sur des chaînes, il y aura une demande pour une commutation sur des chaînes ignorant la casse, sur des chaînes prenant en compte/ignorant les paramètres régionaux, sur des chaînes basées sur regex .... J'approuverais une décision qui économise beaucoup de temps pour le développeurs de langages au prix d'un peu de temps pour les programmeurs.

18
DJClayworth

Un exemple d'utilisation directe String depuis 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;
    }

}

A côté des bons arguments ci-dessus, j'ajouterai que beaucoup de gens voient aujourd'hui switch comme un reste obsolète du passé procédural de Java (retour à l'époque C).

Je ne partage pas tout à fait cette opinion, je pense que switch peut avoir son utilité dans certains cas, du moins en raison de sa rapidité, et de toute façon, il vaut mieux qu'une série de cascadages numériques else if que j'ai vus dans du code ...

Mais en effet, il est utile de regarder le cas où vous avez besoin d’un commutateur et voyez s’il ne peut pas être remplacé par quelque chose de plus OO. Par exemple, enums dans Java 1.5+, peut-être HashTable ou une autre collection (je regrette parfois de ne pas avoir (anonymes) des fonctions de citoyen de première classe, comme dans Lua - qui n'a pas de commutateur - ou JavaScript) ou même polymorphisme.

12
PhiLho

Si vous n'utilisez pas JDK7 ou une version ultérieure, vous pouvez utiliser hashCode() pour le simuler. Comme String.hashCode() renvoie habituellement des valeurs différentes pour différentes chaînes et renvoie toujours des valeurs égales pour des chaînes égales, il est relativement fiable (Chaînes différentes). pouvez produit le même code de hachage que @Lii mentionné dans un commentaire, tel que "FB" et "Ea") Voir documentation .

Donc, le code ressemblerait à ceci:

String s = "<Your String>";

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

De cette façon, vous activez 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;
            }
        }
    }
}
8
HyperNeutrino

Cela fait des années que nous utilisons un préprocesseur (n open source).

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

Les fichiers prétraités sont nommés Foo.jpp et sont transformés en Foo.Java avec un script ant.

L'avantage est qu'il est traité dans Java qui s'exécute sur la version 1.0 (bien que nous n'ayons généralement pris en charge que la version 1.4). En outre, il était beaucoup plus facile de le faire (beaucoup de commutateurs de chaîne) que de le falsifier avec des énumérations ou d’autres solutions de contournement - le code était beaucoup plus facile à lire, à maintenir et à comprendre. Le IIRC (pour l’instant, ne peut fournir aucune statistique ni aucun raisonnement technique), il était également plus rapide que les équivalents naturels Java.

Les inconvénients sont que vous n'éditez pas Java, donc il nécessite un peu plus de flux de travail (édition, traitement, compilation/test) plus un IDE renverra à la Java ce qui est un peu compliqué (le commutateur devient une série d'étapes logiques if/else) et l'ordre du boîtier de commutateur n'est pas maintenu.

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

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

4
Charles Goodwin

D'autres réponses ont indiqué que cela avait été ajouté dans Java 7 et que des solutions de contournement avaient été apportées 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 un peu manié la casse spéciale dans la langue, mais il me semble évident que les concepteurs essayaient de réduire au minimum la quantité de douille spéciale et de sucre syntaxique.

activer 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 à l'époque Java a été conçu et ne correspond pas vraiment à la conception minimaliste. D'autant plus qu'ils avaient décidé de ne pas utiliser le cas spécial == pour les chaînes, il serait (et est) un peu étrange que le cas fonctionne si == ne le fait pas.

Entre 1.0 et 1.4, le langage lui-même est resté à peu près le même. La plupart des améliorations apportées à Java se trouvaient du côté de la bibliothèque.

Tout cela a changé avec Java 5, le langage a été considérablement étendu. Des extensions supplémentaires ont suivi dans les versions 7 et 8. Je pense que ce changement d’attitude a été motivé par la montée en puissance de C #.

4
plugwash