Можно ли использовать оператор instanceof в операторе switch?


267

У меня есть вопрос об использовании регистра переключателя для instanceofобъекта:

Например: моя проблема может быть воспроизведена на Java:

if(this instanceof A)
    doA();
else if(this instanceof B)
    doB();
else if(this instanceof C)
    doC():

Как бы это было реализовано с помощью switch...case?


6
Если вы действительно чувствуете, что нуждаетесь в переключателе, вы можете хэшировать имя класса в int и использовать его, хотя и следите за возможными конфликтами. Добавление в качестве комментария, а не ответа, поскольку мне не нравится, что идея фактически использовалась. Возможно, что вам действительно нужно, это шаблон посетителей.
Викирк

1
Начиная с Java 7, вы можете даже включить полностью определенное имя класса, чтобы избежать таких хэш-столкновений, как указывал @vickirk, но это все еще безобразно.
Митя

Ответы:


225

Это типичный сценарий, в котором помогает полиморфизм подтипа. Сделайте следующее

interface I {
  void do();
}

class A implements I { void do() { doA() } ... }
class B implements I { void do() { doB() } ... }
class C implements I { void do() { doC() } ... }

Тогда вы можете просто позвонить do()по this.

Если вы не можете свободно изменять A, Bи C, вы можете применить шаблон посетителя для достижения того же.


33
Шаблон посетителя означает, что A, B и C должны реализовать интерфейс с абстрактным методом, который принимает Visitor в качестве входного параметра, что если вы не можете изменить A, B, C и никто из них не реализует этот интерфейс?
Therm

21
Последний комментарий о шаблоне посетителя неверен. Вам все равно нужно заставить A, B и C реализовать интерфейс.
Бен Терли

10
К сожалению, это не работает, если для кода do () требуется среда хоста (т. Е. Доступ к переменным, отсутствующим в самом do ()).
Мафу

2
Вопрос @mafu OP касался диспетчеризации на основе типов. Если ваш метод do () нуждается в большем количестве входных данных для отправки, чем ваша проблема, IMHO выходит за рамки обсуждаемого здесь вопроса.
JMG

3
этот ответ предполагает, что вы можете изменить классы A, B, C, хотя я думаю, что смысл в том, как это сделать без изменения A, B, C, поскольку они могут быть в библиотеке третьей части
cloudy_weather

96

если вы абсолютно не можете кодировать интерфейс, вы можете использовать enum в качестве посредника:

public A() {

    CLAZZ z = CLAZZ.valueOf(this.getClass().getSimpleName());
    switch (z) {
    case A:
        doA();
        break;
    case B:
        doB();
        break;
    case C:
        doC();
        break;
    }
}


enum CLAZZ {
    A,B,C;

}

спасибо, мне также пришлось внести некоторые изменения: 1) инициализировать каждый идентификатор перечисления ссылкой на класс; 2) присвоить простое имя класса с помощью перечислимого идентификатора .toString (); 3) найти перечисление по сохраненной ссылке на класс по идентификатору перечисления. Я думаю, что это также безопасно для затемнения.
Водолей Сила

если this.getClass (). getSimpleName () не совпадает со значением CLAZZ, он выбрасывает исключение ... лучше окружить блоком try catch, и исключение будет рассматриваться как опция «по умолчанию» или «еще» выключатель
тетри

40

Просто создайте карту, где класс является ключом, а функциональность, то есть лямбда или аналогичная, является значением.

Map<Class,Runnable> doByClass = new HashMap<>();
doByClass.put(Foo.class, () -> doAClosure(this));
doByClass.put(Bar.class, this::doBMethod);
doByClass.put(Baz.class, new MyCRunnable());

// конечно, рефакторинг это только один раз инициализировать

doByClass.get(getClass()).run();

Если вам нужно проверить Исключения, тогда реализуйте FunctionalInterface, который выдает Исключение и использует его вместо Runnable.


3
Лучшее решение imho, особенно потому что оно позволяет легко рефакторинг.
Фейтейра

2
Единственным недостатком этого решения является то, что оно не может использовать локальные (методические) переменные в лямбдах (если, конечно, это может понадобиться).
Сапатеро

1
@zapatero Вы можете просто изменить на Function вместо Runnable, передать экземпляр в качестве параметра, а затем
вызвать

Upvoted; это один из немногих ответов, который на самом деле помогает ОП делать то, что он просит (и да, часто можно реорганизовать код, чтобы не делать этого instanceof, и нет, мой сценарий, к сожалению, не один из тех, которые легко вписываются в та коробка ...)
Пер Лундберг

@ SergioGutiérrez Спасибо. Ну, этот шаблон должен быть нужен только с внешней библиотекой. И даже тогда вы можете просто создать интерфейс с реализацией адаптера, но это полезно, когда вы хотите, чтобы поведенческий DIFF, так сказать, был более очевидным. Полагаю, что это похоже на API-интерфейс маршрутизации Fluent vs.
Новатерата

36

На всякий случай, если кто-то прочтет это:

Лучшее решение в Java это:

public enum Action { 
    a{
        void doAction(...){
            // some code
        }

    }, 
    b{
        void doAction(...){
            // some code
        }

    }, 
    c{
        void doAction(...){
            // some code
        }

    };

    abstract void doAction (...);
}

БОЛЬШИМИ преимуществами такой модели являются:

  1. Вы просто делаете это как (никаких переключателей вообще):

    void someFunction ( Action action ) {
        action.doAction(...);   
    }
  2. В случае, если вы добавляете новое действие под названием «d», вы ДОЛЖНЫ реализовать метод doAction (...)

ПРИМЕЧАНИЕ. Этот шаблон описан в блоке Джошуа «Эффективная Java (2-е издание)».


1
отлично! Является ли @Overrideпредписанная выше каждой реализации doAction()?
mateuscb

9
Как это «лучшее» решение? Как бы вы решили, какой actionиспользовать? По внешнему экземпляру каскада, который вызывает someFunction()с правильным action? Это просто добавляет еще один уровень косвенности.
PureSpider

1
Нет, это будет сделано автоматически во время выполнения. Если вы вызываете someFunction (Action.a), то вызывается a.doAction.
с.соловьев

11
Я не понимаю этого. Как бы вы узнали, какой enum использовать? Как сказал @PureSpider, это похоже на очередной уровень работы.
Джеймс Мейн

2
Очень грустно, что вы не предложили полный пример , например, как сопоставить любой экземпляр класса a, b или C с этим перечислением. Я попытаюсь разыграть Экземпляр к этому Enum.
Том

21

Ты не можешь switchЗаявление может содержать только caseутверждение , которые компиляция константы времени и которые являются целым числом (до Java 6 и строки в Java 7).

То, что вы ищете, называется «сопоставление с образцом» в функциональном программировании.

Смотрите также Как избежать instanceof в Java


1
Нет, в большинстве функциональных языков вы не можете сопоставлять шаблоны по типам, только по конструкторам. По крайней мере, это верно в отношении ML и Haskell. В Scala и OCaml это возможно, но не является типичным применением сопоставления с образцом.
JMG

Конечно, но проверка на конструкторы будет «эквивалентна» описанному выше сценарию.
Карло В. Данго

1
В некоторых случаях, но не в целом.
JMG

Коммутаторы также могут поддерживать перечисления.
Соломон Уцко

«Вы не можете» смотреть на другой язык, редко бывает полезным ответом.
Л. Блан

17

Как обсуждалось в верхних ответах, традиционный подход ООП заключается в использовании полиморфизма вместо переключения. Для этого трюка есть даже хорошо документированный шаблон рефакторинга: Заменить Условный на Полиморфизм . Всякий раз, когда я достигаю этого подхода, мне нравится также реализовывать объект Null для обеспечения поведения по умолчанию.

Начиная с Java 8, мы можем использовать лямбды и дженерики, чтобы дать нам кое-что, с чем функциональные программисты хорошо знакомы: сопоставление с образцом. Это не базовая языковая функция, но библиотека Javaslang предоставляет одну реализацию. Пример из Javadoc :

Match.ofType(Number.class)
    .caze((Integer i) -> i)
    .caze((String s) -> new BigDecimal(s))
    .orElse(() -> -1)
    .apply(1.0d); // result: -1

Это не самая естественная парадигма в мире Java, поэтому используйте ее с осторожностью. Хотя универсальные методы избавят вас от необходимости типизировать совпадающее значение, мы упускаем стандартный способ декомпозиции сопоставляемого объекта, как , например, в классах case Scala .


9

Я знаю, что это очень поздно, но для будущих читателей ...

Остерегайтесь вышеприведенных подходов, основанных только на названии класса A , B , C ...:

Если вы не можете гарантировать, что A , B , C ... (все подклассы или реализации Base ) являются окончательными, то подклассы A , B , C ... не будут рассматриваться.

Хотя подход if, elseif, elseif .. медленнее для большого числа подклассов / реализаторов, он более точен.



8

К сожалению, это невозможно из коробки, поскольку оператор switch-case ожидает константное выражение. Чтобы преодолеть это, можно использовать значения перечислений с именами классов, например:

public enum MyEnum {
   A(A.class.getName()), 
   B(B.class.getName()),
   C(C.class.getName());

private String refClassname;
private static final Map<String, MyEnum> ENUM_MAP;

MyEnum (String refClassname) {
    this.refClassname = refClassname;
}

static {
    Map<String, MyEnum> map = new ConcurrentHashMap<String, MyEnum>();
    for (MyEnum instance : MyEnum.values()) {
        map.put(instance.refClassname, instance);
    }
    ENUM_MAP = Collections.unmodifiableMap(map);
}

public static MyEnum get(String name) {
    return ENUM_MAP.get(name);
 }
}

С этим можно использовать оператор switch следующим образом

MyEnum type = MyEnum.get(clazz.getName());
switch (type) {
case A:
    ... // it's A class
case B:
    ... // it's B class
case C:
    ... // it's C class
}

Я считаю, что до тех пор, пока проблема JEP 8213076 не будет полностью реализована, это самый чистый способ получить оператор switch на основе типа без изменения целевого класса.
Рик Шааф


5

Использование таких операторов переключения не является объектно-ориентированным способом. Вместо этого вы должны использовать силу полиморфизма . Просто пиши

this.do()

Предварительно настроив базовый класс:

abstract class Base {
   abstract void do();
   ...
}

который является базовым классом для A, Bи C:

class A extends Base {
    void do() { this.doA() }
}

class B extends Base {
    void do() { this.doB() }
}

class C extends Base {
    void do() { this.doC() }
}

@jmg suggeests ( stackoverflow.com/questions/5579309/switch-instanceof/… ) с использованием интерфейса вместо абстрактного базового класса. Это может быть лучше в некоторых обстоятельствах.
Raedwald

5

Это будет работать быстрее и иметь смысл в случае, если у
вас есть относительно много «дел»,
процесс выполняется в контексте, чувствительном к производительности

public <T> T process(Object model) {
    switch (model.getClass().getSimpleName()) {
        case "Trade":
            return processTrade();
        case "InsuranceTransaction":
            return processInsuranceTransaction();
        case "CashTransaction":
            return processCashTransaction();
        case "CardTransaction":
            return processCardTransaction();
        case "TransferTransaction":
            return processTransferTransaction();
        case "ClientAccount":
            return processAccount();
        ...
        default:
            throw new IllegalArgumentException(model.getClass().getSimpleName());
    }
}

1
Это не то же самое, что делать instanceof, поскольку это работает только в том случае, если для реализации используется класс реализации, но он не будет работать для интерфейсов / abstractclass /
superclasses

да, это не так
Майк

А для усилий, но, кроме комментария @ lifesondom, вы также упускаете типобезопасность, которая у вас обычно есть, потому что в этом ответе вместо строк классов используются жестко закодированные строки. Это довольно легко сделать опечатку, особенно если вам нужно расширить эту функциональность с полными каноническими именами, если здесь было какое-либо совпадение в именах классов с разными именами пакетов. Редактирование: исправлена ​​опечатка (что вроде подтверждает мою точку зрения)
Рик Шааф

4

Вы не можете переключаться, работает только с байтовым, коротким, типом char, int, String и перечисляемым типом (и объектными версиями примитивов, это также зависит от вашей версии Java, строки можно switchредактировать в Java 7)


Вы не можете включить Strings в Java 6. И вы не можете включить «объектные версии примитивов».
Лукас Эдер

@ Божо Я сказал, что это зависит от вашей версии Java, в Java 7 вы можете включить строки.
Tnem

@Lukas Eder проверить свои спецификации Java вы можете
Tnem

4

Мне лично нравится следующий код Java 1.8:

    mySwitch("YY")
            .myCase("AA", (o) -> {
                System.out.println(o+"aa");
            })
            .myCase("BB", (o) -> {
                System.out.println(o+"bb");
            })
            .myCase("YY", (o) -> {
                System.out.println(o+"yy");
            })
            .myCase("ZZ", (o) -> {
                System.out.println(o+"zz");
            });

Будет выводить:

YYyy

В примере кода используются строки, но вы можете использовать любой тип объекта, включая класс. например.myCase(this.getClass(), (o) -> ...

Требуется следующий фрагмент:

public Case mySwitch(Object reference) {
    return new Case(reference);
}

public class Case {

    private Object reference;

    public Case(Object reference) {
        this.reference = reference;
    }

    public Case myCase(Object b, OnMatchDo task) {
        if (reference.equals(b)) {
            task.task(reference);
        }
        return this;
    }
}

public interface OnMatchDo {

    public void task(Object o);
}

4

Java теперь позволяет переключаться в порядке OP. Они называют это Pattern Matching for switch. В настоящее время он находится на драфте, но, видя, сколько работы они вкладывают в коммутаторы в последнее время, я думаю, что он пройдет Пример, приведенный в JEP:

String formatted;
switch (obj) {
    case Integer i: formatted = String.format("int %d", i); break;
    case Byte b:    formatted = String.format("byte %d", b); break;
    case Long l:    formatted = String.format("long %d", l); break;
    case Double d:  formatted = String.format("double %f", d); break;
    case String s:  formatted = String.format("String %s", s); break
    default:        formatted = obj.toString();
}  

или используя их лямбда-синтаксис и возвращая значение

String formatted = 
    switch (obj) {
        case Integer i -> String.format("int %d", i)
        case Byte b    -> String.format("byte %d", b);
        case Long l    -> String.format("long %d", l); 
        case Double d  -> String.format("double %f", d); 
        case String s  -> String.format("String %s", s); 
        default        -> obj.toString();
    };

так или иначе, они делали классные вещи с выключателями.


3

Если вы можете манипулировать общим интерфейсом, вы можете добавить add в enum и заставить каждый класс возвращать уникальное значение. Вам не понадобится instanceof или шаблон посетителя.

Для меня логика должна быть записана в операторе switch, а не сам объект. Это было мое решение:

ClassA, ClassB, and ClassC implement CommonClass

Интерфейс:

public interface CommonClass {
   MyEnum getEnumType();
}

Enum:

public enum MyEnum {
  ClassA(0), ClassB(1), ClassC(2);

  private int value;

  private MyEnum(final int value) {
    this.value = value;
  }

  public int getValue() {
    return value;
  }

Impl:

...
  switch(obj.getEnumType())
  {
    case MyEnum.ClassA:
      ClassA classA = (ClassA) obj;
    break;

    case MyEnum.ClassB:
      ClassB classB = (ClassB) obj;
    break;

    case MyEnum.ClassC:
      ClassC classC = (ClassC) obj;
    break;
  }
...

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


valueПоле является излишним , если вы хотите , чтобы отличить константы перечислений - вы можете использовать константы непосредственно (как и вы).
user905686

2

Как насчет этого ?

switch (this.name) 
{
  case "A":
    doA();
    break;
  case "B":
    doB();
    break;
  case "C":
    doC();
    break;
  default:
    console.log('Undefined instance');
}

3
Следует отметить, что это работает только на Java 7. И что вам нужно позвонить this.getSimpleName()Not sure, если плакат перепутан с JS (да, он использует консоль, хе-хе).
Паблиско

5
Это проблема выпадения ссылочной прозрачности исходного кода. То есть ваша IDE не сможет поддерживать ссылочную целостность. Предположим, вы хотите переименовать ваше имя. Отражение это зло.
Val

1
Не отличная идея. Имена классов не являются уникальными, если у вас есть несколько загрузчиков классов.
Дорадус

Перерывы, когда дело доходит до сжатия кода (→ ProGuard)
Matthias Ronge

1

Я думаю, что есть причины использовать оператор switch. Если вы используете сгенерированный xText код, возможно. Или другой вид классов, генерируемых EMF.

instance.getClass().getName();

возвращает строку с именем реализации класса. то есть: org.eclipse.emf.ecore.util.EcoreUtil

instance.getClass().getSimpleName();

возвращает простое представление, то есть: EcoreUtil


Вы не можете использовать это switchкак caseусловие, потому что это не постоянное значение
B-GangsteR

1

Если вам нужно «переключиться» через тип класса «this», этот ответ является лучшим https://stackoverflow.com/a/5579385/2078368

Но если вам нужно применить «переключатель» к любой другой переменной. Я бы предложил другое решение. Определите следующий интерфейс:

public interface ClassTypeInterface {
    public String getType();
}

Реализуйте этот интерфейс в каждом классе, который вы хотите «переключить». Пример:

public class A extends Something implements ClassTypeInterface {

    public final static String TYPE = "A";

    @Override
    public String getType() {
        return TYPE;
    }
}

После этого вы можете использовать его следующим образом:

switch (var.getType()) {
    case A.TYPE: {
        break;
    }
    case B.TYPE: {
        break;
    }
    ...
}

Единственное, о чем вы должны заботиться - сохранять уникальность «типов» во всех классах, реализующих ClassTypeInterface. Это не большая проблема, потому что в случае любого пересечения вы получаете ошибку времени компиляции для оператора "switch-case".


Вместо использования String для TYPE вы можете использовать enum, и уникальность гарантируется (как сделано в этом ответе ). Однако при любом из подходов вам придется проводить рефакторинг в двух местах при переименовании.
user905686

@ user905686 переименовать что? В текущем примере тип «A» определен внутри класса Something, чтобы минимизировать количество кода. Но в реальной жизни вы, очевидно, должны определить его снаружи (в каком-то обычном месте), и нет никаких проблем с дальнейшим рефакторингом.
Сергей Кривенков

Я имею в виду переименование класса А. Автоматический рефакторинг может не включать переменную TYPE = "A"при переименовании. Особенно, если он находится за пределами соответствующего класса, можно также забыть об этом при выполнении этого вручную. На самом деле IntelliJ также находит вхождения строк в именах строк или комментариев, но это всего лишь текстовый поиск (вместо просмотра синтаксического дерева), и, таким образом, он содержит ложные срабатывания.
user905686

@ user905686 это просто пример, чтобы визуализировать идею. Не используйте String для определений типов в реальном проекте, объявите некоторый держатель класса MyTypes с целочисленными константами (или enum) и используйте их в классах, реализующих ClassTypeInterface.
Сергей Кривенков

1

Вот функциональный способ сделать это в Java 8, используя http://www.vavr.io/

import static io.vavr.API.*;
import static io.vavr.Predicates.instanceOf;
public Throwable liftRootCause(final Throwable throwable) {
        return Match(throwable).of(
                Case($(instanceOf(CompletionException.class)), Throwable::getCause),
                Case($(instanceOf(ExecutionException.class)), Throwable::getCause),
                Case($(), th -> th)
        );
    }

1

Несмотря на то, что невозможно написать оператор switch, возможно перейти к определенной обработке для каждого данного типа. Один из способов сделать это - использовать стандартный механизм двойной отправки. Примером, в котором мы хотим «переключаться» на основе типа, является сопоставитель исключений Jersey, где нам нужно сопоставить множество исключений с ответами об ошибках. В то время как для этого конкретного случая, вероятно, существует лучший способ (то есть использование полиморфного метода, который переводит каждое исключение в ответ об ошибке), использование механизма двойной отправки все еще полезно и практично.

interface Processable {
    <R> R process(final Processor<R> processor);
}

interface Processor<R> {
    R process(final A a);
    R process(final B b);
    R process(final C c);
    // for each type of Processable
    ...
}

class A implements Processable {
    // other class logic here

    <R> R process(final Processor<R> processor){
        return processor.process(this);
    }
}

class B implements Processable {
    // other class logic here

    <R> R process(final Processor<R> processor){
        return processor.process(this);
    }
}

class C implements Processable {
    // other class logic here

    <R> R process(final Processor<R> processor){
        return processor.process(this);
    }
}

Затем, когда бы ни понадобился «переключатель», вы можете сделать это следующим образом:

public class LogProcessor implements Processor<String> {
    private static final Logger log = Logger.for(LogProcessor.class);

    public void logIt(final Processable base) {
        log.info("Logging for type {}", process(base));
    }

    // Processor methods, these are basically the effective "case" statements
    String process(final A a) {
        return "Stringifying A";
    }

    String process(final B b) {
        return "Stringifying B";
    }

    String process(final C c) {
        return "Stringifying C";
    }
}

Это очень похоже на шаблон посетителя, который уже обсуждался в этом ответе: stackoverflow.com/a/5579385
typeracer

0

Создайте Enum с именами классов.

public enum ClassNameEnum {
    A, B, C
}

Найдите имя класса объекта. Напишите регистр переключателя над перечислением.

private void switchByClassType(Object obj) {

        ClassNameEnum className = ClassNameEnum.valueOf(obj.getClass().getSimpleName());

        switch (className) {
            case A:
                doA();
                break;
            case B:
                doB();
                break;
            case C:
                doC();
                break;
        }
    }
}

Надеюсь это поможет.


2
В отличие от этого подхода, где связь между константами перечисления и классами выполняется явно, вы делаете связь неявно по имени класса. Это нарушит ваш код, когда вы переименуете только одну из константы enum или класса, тогда как другой подход все равно будет работать.
user905686

0

Eclipse Modeling Framework имеет интересную идею, которая также учитывает наследование. Основная концепция определена в интерфейсе Switch : переключение выполняется с помощью метода doSwitch .

Что действительно интересно, так это реализация. Для каждого типа интереса,

public T caseXXXX(XXXX object);

метод должен быть реализован (с реализацией по умолчанию, возвращающей ноль). Реализация doSwitch попытается вызвать все методы caseXXX для объекта для всей его иерархии типов. Что-то в строках:

BaseType baseType = (BaseType)object;
T result = caseBaseType(eAttribute);
if (result == null) result = caseSuperType1(baseType);
if (result == null) result = caseSuperType2(baseType);
if (result == null) result = caseSuperType3(baseType);
if (result == null) result = caseSuperType4(baseType);
if (result == null) result = defaultCase(object);
return result;

Фактическая структура использует целочисленный идентификатор для каждого класса, таким образом, логика - фактически чистый переключатель:

public T doSwitch(Object object) {
    return doSwitch(object.class(), eObject);
}

protected T doSwitch(Class clazz, Object object) {
    return doSwitch(getClassifierID(clazz), object);
}

protected T doSwitch(int classifierID, Object theObject) {
    switch (classifierID) {
    case MyClasses.BASETYPE:
    {
      BaseType baseType = (BaseType)object;
      ...
      return result;
    }
    case MyClasses.TYPE1:
    {
      ...
    }
  ...

Вы можете посмотреть на полную реализацию ECoreSwitch, чтобы получить лучшую идею.


-1

Существует еще более простой способ эмуляции структуры переключателя, использующей instanceof, вы делаете это, создавая блок кода в своем методе и называя его меткой. Затем вы используете структуры if для эмуляции операторов case. Если дело истинно, то вы используете разрыв LABEL_NAME, чтобы выйти из структуры временного переключателя.

        DEFINE_TYPE:
        {
            if (a instanceof x){
                //do something
                break DEFINE_TYPE;
            }
            if (a instanceof y){
               //do something
                break DEFINE_TYPE;
            }
            if (a instanceof z){
                // do something
                break DEFINE_TYPE;
            }
        }

Чем это лучше, чем if... else ifкод, указанный ОП?
Typeracer

Просто чтобы уточнить мой предыдущий комментарий: то, что вы предлагаете, это по сути заменить if... else ifна операторы "goto", что является неправильным способом реализации потока управления в таких языках, как Java.
Typeracer
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.