Почему я не могу использовать оператор switch для String?

1004

Эта функциональность будет добавлена ​​в более позднюю версию Java?

Может кто-нибудь объяснить, почему я не могу сделать это, например, в техническом смысле, как switchработает утверждение Java ?

Алекс Бердсли
источник
195
Это в SE 7. 16 лет после его запроса. download.oracle.com/javase/tutorial/java/nutsandbolts/…
angryITguy
81
Sun был честен в своих оценках: "Don't hold your breath."lol, bugs.sun.com/bugdatabase/view_bug.do?bug_id=1223179
raffian
3
@ raffian Я думаю, это потому, что она дважды вздохнула. Они тоже немного опоздали, после почти 10 лет. Возможно, она тогда упаковывала ланч-боксы своим внукам.
WeirdElfB0y

Ответы:

1004

Операторы switch с Stringделами были реализованы в Java SE 7 , по крайней мере, через 16 лет после их первого запроса. Четкой причины задержки предоставлено не было, но, скорее всего, это было связано с производительностью.

Реализация в JDK 7

В настоящее время эта функция реализована в процессе javac «удаления сахара»; чистый высокоуровневый синтаксис с использованием Stringконстант в caseобъявлениях расширяется во время компиляции в более сложный код, следуя шаблону. Полученный код использует инструкции JVM, которые существовали всегда.

А switchс Stringделами во время компиляции переводится в два ключа. Первая отображает каждую строку в уникальное целое число - ее положение в исходном переключателе. Это делается путем первого включения хеш-кода метки. Соответствующий случай - это ifоператор, который проверяет равенство строк; если в хэше есть коллизии, тест является каскадным if-else-if. Второй переключатель отражает это в исходном исходном коде, но заменяет метки регистра соответствующими позициями. Этот двухэтапный процесс позволяет легко сохранить управление потоком исходного переключателя.

Переключатели в JVM

Для большей технической глубины switchвы можете обратиться к Спецификации JVM, где описана компиляция операторов switch . В двух словах, есть две разные инструкции JVM, которые можно использовать для коммутатора, в зависимости от разреженности констант, используемых в случаях. Оба зависят от использования целочисленных констант для эффективного выполнения каждого случая.

Если константы плотные, они используются в качестве индекса (после вычитания наименьшего значения) в таблицу указателей инструкций - tableswitchинструкцию.

Если константы редкие, выполняется бинарный поиск правильного регистра - lookupswitchинструкция.

В де-Обсахаривания switchна Stringобъектах, обе инструкции могут быть использованы. lookupswitchПодходят для первого переключателя на хэш - кодах , чтобы найти исходное положение корпуса. Полученный порядковый номер является естественным соответствием для tableswitch.

Обе инструкции требуют, чтобы целочисленные константы, назначенные каждому случаю, были отсортированы во время компиляции. Во время выполнения, хотя O(1)производительность tableswitchобычно выглядит лучше, чем O(log(n))производительность lookupswitch, требуется некоторый анализ, чтобы определить, является ли таблица достаточно плотной, чтобы оправдать компромисс между пространством и временем. Билл Веннерс (Bill Venners) написал отличную статью, которая более подробно описывает это, а также подробное описание других инструкций по управлению потоком Java.

До 7 JDK

До JDK 7 enumможно было бы приблизиться к Stringпереключателю на основе. При этом используется статическийvalueOf метод, сгенерированный компилятором для каждого enumтипа. Например:

Pill p = Pill.valueOf(str);
switch(p) {
  case RED:  pop();  break;
  case BLUE: push(); break;
}
Эриксон
источник
26
Возможно, быстрее будет просто использовать If-Else-If вместо хеша для строкового переключателя. Я нашел словари довольно дорогими, если хранить только несколько предметов.
Джонатан Аллен
84
If-elseif-elseif-elseif-else может быть быстрее, но я бы выбрал более чистый код 99 раз из 100. Строки, будучи неизменяемыми, кэшируют свой хэш-код, поэтому «вычисление» хеша происходит быстро. Нужно было бы профилировать код, чтобы определить, какая выгода есть.
Эриксон
21
Причина, приведенная против добавления switch (String), заключается в том, что он не соответствует гарантиям производительности, ожидаемым от операторов switch (). Они не хотели «вводить в заблуждение» разработчиков. Честно говоря, я не думаю, что они должны гарантировать производительность switch () для начала.
Гили
2
Если вы просто используете Pillдля выполнения каких-либо действий на основе, strя бы сказал, что если-еще предпочтительнее, так как он позволяет обрабатывать strзначения за пределами диапазона КРАСНЫЙ, СИНИЙ, без необходимости перехватывать исключение valueOfили вручную проверять совпадение с именем каждый тип перечисления, который просто добавляет ненужные издержки. По моему опыту имеет смысл использовать valueOfдля преобразования в перечисление, только если позднее было необходимо типизированное представление значения String.
МайлзХэмпсон
Интересно, прилагают ли компиляторы какие-либо усилия, чтобы проверить, существует ли какая-либо пара чисел (x, y), для которой набор значений (hash >> x) & ((1<<y)-1)будет давать разные значения для каждой строки, hashCodeкоторая отличается и (1<<y)меньше чем вдвое больше строк (или в по крайней мере, не намного больше, чем это).
суперкат
125

Если в вашем коде есть место, где вы можете включить строку, то может быть лучше реорганизовать строку, чтобы она представляла собой перечисление возможных значений, которые вы можете включить. Конечно, вы можете ограничить потенциальные значения строк, которые вы можете иметь, перечисленными в перечислении, что может быть или не быть желательным.

Конечно, в вашем перечислении может быть запись для «other» и метод fromString (String), тогда вы можете иметь

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
источник
4
Этот метод также позволяет вам решать такие проблемы, как отсутствие учета регистра, псевдонимы и т. Д. Вместо того, чтобы полагаться на то, что разработчик языка придумает решение «один размер подходит всем».
Даррон
2
Согласитесь с JeeBee, если вы включаете строки, вероятно, нужен enum. Строка обычно представляет что-то, идущее в интерфейс (пользовательский или другой), которое может измениться или не измениться в будущем, поэтому лучше заменить его на
enums
18
См. Xefer.com/2006/12/switchonstring для хорошего описания этого метода.
Дэвид Шмитт
@DavidSchmitt У рецензии есть один существенный недостаток. Он перехватывает все исключения вместо тех, которые на самом деле выдает метод.
М. Мимпен
91

Ниже приведен полный пример, основанный на публикации JeeBee, с использованием перечислений java вместо использования пользовательских методов.

Обратите внимание, что в Java SE 7 и более поздних версиях вы можете использовать объект String в выражении оператора 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
  }
}
Тулани Чивандиква
источник
26

Переключатели на основе целых чисел могут быть оптимизированы для очень эффективного кода. Переключатели, основанные на другом типе данных, могут быть скомпилированы только в серию операторов if ().

По этой причине C & C ++ разрешает переключаться только на целочисленные типы, поскольку с другими типами это было бессмысленно.

Дизайнеры C # решили, что стиль важен, даже если не было никакого преимущества.

Дизайнеры Java, очевидно, думали как дизайнеры C.

Джеймс Керран
источник
26
Переключатели, основанные на любом хешируемом объекте, могут быть реализованы очень эффективно с использованием хеш-таблицы - см. .NET. Так что ваша причина не совсем верна.
Конрад Рудольф
Да, и это то, чего я не понимаю. Они боятся, что хеширование объектов в конечном итоге станет слишком дорогим?
Алекс Бердсли
3
@Nalandial: на самом деле, с небольшим усилием со стороны компилятора, это совсем не дорого, потому что, когда набор строк известен, довольно просто сгенерировать идеальный хеш (хотя это не делается в .NET; наверное, усилий тоже не стоит).
Конрад Рудольф
3
@Nalandial & @Konrad Rudolph - хотя хэширование строки (из-за ее неизменной природы) кажется решением этой проблемы, вы должны помнить, что все неконечные объекты могут иметь переопределенные функции хеширования. Это затрудняет во время компиляции обеспечение согласованности в переключателе.
martinatime
2
Вы также можете создать DFA для соответствия строке (как это делают механизмы регулярных выражений). Возможно, даже более эффективно, чем хеширование.
Nate CK
19

Пример прямого Stringиспользования начиная с версии 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;
    }

}
Гуннар Форсгрен - Mobimation
источник
18

Джеймс Керран кратко говорит: «Переключатели, основанные на целых числах, могут быть оптимизированы до очень эффективного кода. Переключатели, основанные на другом типе данных, могут быть скомпилированы только в серию операторов if (). По этой причине C & C ++ разрешает переключать только целочисленные типы, так как это было бессмысленно с другими типами ".

Мое мнение, и только это, заключается в том, что как только вы начинаете переключаться на не примитивы, вы должны начать думать о «равных» против «==». Во-первых, сравнение двух строк может быть довольно длительной процедурой, добавляя к проблемам производительности, которые упомянуты выше. Во-вторых, если происходит переключение строк, будет требоваться включение строк без учета регистра, включение строк с учетом / игнорирование локали, переключение строк на основе регулярных выражений .... Я бы одобрил решение, которое сэкономило много времени для Языковые разработчики за счет небольшого количества времени для программистов.

DJClayworth
источник
Технически, регулярные выражения уже "переключаются", поскольку они в основном просто конечные автоматы; у них просто есть только два «дела», matchedи not matched. (Тем не менее, не принимая во внимание такие вещи, как [именованные] группы / и т.д.).
JAB
1
docs.oracle.com/javase/7/docs/technotes/guides/language/… состояния: компилятор Java обычно генерирует более эффективный байт-код из операторов switch, которые используют объекты String, чем из цепочечных операторов if-then-else.
Вим Deblauwe
12

Помимо приведенных выше хороших аргументов, я добавлю, что многие люди сегодня считают switchустаревшим остатком процедурного прошлого Java (назад в C раз).

Я не полностью разделяю это мнение, я думаю, switchчто в некоторых случаях он может быть полезен, по крайней мере, из-за его скорости, и в любом случае он лучше, чем некоторые серии каскадных чисел, которые else ifя видел в некотором коде ...

Но действительно, стоит взглянуть на случай, когда вам нужен переключатель, и посмотреть, не может ли он быть заменен чем-то более оригинальным. Например, перечисления в Java 1.5+, возможно, HashTable или какая-то другая коллекция (иногда я сожалею, что у нас нет (анонимных) функций в качестве первоклассного гражданина, как в Lua - у которого нет переключателя - или JavaScript) или даже полиморфизма.

PhiLho
источник
«Иногда я сожалею, что у нас нет (анонимных) функций как гражданина первого класса» Это больше не правда.
user8397947
@dorukayhan Да, конечно. Но хотите ли вы добавить комментарий ко всем ответам за последние десять лет, чтобы сообщить миру о том, что мы можем получить их, если мы обновимся до более новых версий Java? :-D
PhiLho
8

Если вы не используете JDK7 или выше, вы можете использовать его hashCode()для имитации. Поскольку String.hashCode()обычно возвращает разные значения для разных строк и всегда возвращает одинаковые значения для одинаковых строк, это довольно надежно (разные строки могут создавать тот же хеш-код, что и @Lii, упомянутый в комментарии, например, "FB"и "Ea"). См. Документацию .

Итак, код будет выглядеть так:

String s = "<Your String>";

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

Таким образом, вы технически включаете int.

В качестве альтернативы вы можете использовать следующий код:

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
источник
5
Две разные строки могут иметь один и тот же хеш-код, поэтому, если вы включите хеш-коды, может быть взята неверная ветвь регистра.
Лии
@Lii Спасибо за указание на это! Хотя это маловероятно, но я бы не поверил, что это сработает. «FB» и «Ea» имеют одинаковый хэш-код, поэтому найти коллизию невозможно. Второй код, вероятно, более надежный.
HyperNeutrino
Я удивлен, что это компилируется, поскольку, как caseя думал, операторы всегда должны быть постоянными значениями, и String.hashCode()это не так (даже если на практике вычисления между JVM никогда не менялись).
StaxMan
@ StaxMan Хм интересно, я никогда не переставал наблюдать это. Но да, caseзначения операторов не должны быть определяемыми во время компиляции, поэтому они работают нормально.
HyperNeutrino
4

В течение многих лет мы использовали препроцессор (с открытым исходным кодом) для этого.

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

Предварительно обработанные файлы называются Foo.jpp и обрабатываются в Foo.java с помощью скрипта ant.

Преимущество заключается в том, что он перерабатывается в Java, работающую на версии 1.0 (хотя обычно мы поддерживали только версию 1.4). Кроме того, это было намного проще сделать (много строковых переключателей) по сравнению с вымышлением с помощью перечислений или других обходных путей - код было намного легче читать, поддерживать и понимать. IIRC (на данный момент не может предоставить статистику или технические обоснования) также был быстрее, чем естественные эквиваленты Java.

Недостатки в том, что вы не редактируете Java, так что это немного больше рабочего процесса (редактировать, обрабатывать, компилировать / тестировать), плюс IDE свяжет обратно с Java, которая немного запутана (переключатель становится серией логических шагов if / else) и порядок переключения не поддерживается.

Я бы не рекомендовал его для версии 1.7+, но это полезно, если вы хотите программировать Java, которая предназначена для более ранних версий JVM (поскольку в Joe public редко устанавливаются последние версии).

Вы можете получить его из SVN или просмотреть код онлайн . Вам понадобится EBuild, чтобы построить его как есть.

Чарльз Гудвин
источник
6
Вам не нужна 1.7 JVM для запуска кода с переключателем String. Компилятор 1.7 превращает переключатель String в нечто, использующее ранее существующий байт-код.
Дауд ибн Карим
4

В других ответах говорилось, что это было добавлено в Java 7 и даны обходные пути для более ранних версий. Этот ответ пытается ответить на вопрос «почему»

Java была реакцией на чрезмерные сложности C ++. Он был разработан, чтобы быть простым чистым языком.

String получил немного особой обработки в этом языке, но мне кажется, что дизайнеры пытались свести к минимуму количество специальных оболочек и синтаксического сахара.

переключение струн довольно сложно под капотом, так как струны не являются простыми примитивными типами. Это не было обычной чертой в то время, когда была разработана Java, и она не очень хорошо вписывалась в минималистский дизайн. Тем более, что они решили не использовать специальный регистр == для строк, было бы (и бывает) немного странно, когда case работает, а == нет.

Между 1.0 и 1.4 сам язык оставался почти таким же. Большинство улучшений Java были на стороне библиотеки.

Все изменилось с Java 5, язык был значительно расширен. Дальнейшие расширения следовали в версиях 7 и 8. Я ожидаю, что это изменение отношения было вызвано ростом C #

plugwash
источник
Повествование о switch (String) соответствует истории, временной шкале, контексту cpp / cs.
Эспрессо
Не реализовывать эту функцию было большой ошибкой, все остальное - дешевое оправдание. За эти годы Java потеряла многих пользователей из-за отсутствия прогресса и упорства дизайнеров не развивать язык. К счастью, они полностью изменили направление и отношение после JDK7
firephil
0

JEP 354. Выражения-переключатели (предварительный просмотр) в JDK-13 и JEP-361. Выражения-переключатели (стандартный) в JDK-14 расширяют оператор switch, чтобы его можно было использовать в качестве выражения .

Теперь вы можете:

  • напрямую назначить переменную из выражения switch ,
  • используйте новую форму метки переключателя ( case L ->):

    Код справа от метки переключателя «case L ->» ограничен выражением, блоком или (для удобства) оператором throw.

  • использовать несколько констант для каждого случая, разделенных запятыми,
  • а также больше нет разрывов значений :

    Чтобы получить значение из выражения switch, breakоператор with value отбрасывается в пользу yieldоператора.

Таким образом, демо из ответов ( 1 , 2 ) может выглядеть так:

  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");
    }
Искусков Александр
источник
-2

Не очень красиво, но вот другой способ для Java 6 и ниже:

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);
Конете Кристиан
источник
-3

Это легкий ветерок в Groovy; Я встраиваю groovy jar и создаю groovyслужебный класс, чтобы делать все это и многое другое, что я считаю раздражающим в Java (поскольку я застрял на Java 6 на предприятии).

it.'p'.each{
switch (it.@name.text()){
   case "choclate":
     myholder.myval=(it.text());
     break;
     }}...
Алекс Пуннен
источник
9
@SSpoke Потому что это вопрос Java, а ответ на Groovy не по теме и бесполезный плагин.
Мартин,
12
Даже в больших консервативных домах SW Groovy используется вместе с Java. JVM предоставляет среду, не зависящую от языка, больше, чем язык, чтобы смешивать и использовать наиболее подходящую парадигму программирования для решения. Так что, может быть, теперь я должен добавить фрагмент в Clojure, чтобы собрать больше голосов :) ...
Алекс Пуннен
1
Кроме того, как работает синтаксис? Я предполагаю, что Groovy - это другой язык программирования ...? Сожалею. Я ничего не знаю о Groovy.
HyperNeutrino
-4

Когда вы используете intellij также посмотрите на:

Файл -> Структура проекта -> Проект

Файл -> Структура проекта -> Модули

Если у вас есть несколько модулей, убедитесь, что вы установили правильный уровень языка на вкладке модуля.

botenvouwer
источник
1
Не уверен, насколько ваш ответ имеет отношение к вопросу. Он спросил, почему операторы строкового переключателя, подобные следующим, недоступны: String mystring = "кое-что"; switch (mystring) {case "кое-что" sysout ("попал сюда"); , , }
Дипак Агарвал
-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; 
         } 
    } 

} 
Иссак Баладжи
источник
8
ОП не спрашивает, как включить строку. Он / она спрашивает, почему он / она не может этого сделать из-за ограничений синтаксиса до JDK7.
HyperNeutrino