É possível usar o operador instanceof em uma instrução switch?


267

Eu tenho uma questão de usar switch case para instanceofobjeto:

Por exemplo: meu problema pode ser reproduzido em Java:

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

Como seria implementado usando switch...case?


6
Se você realmente acha que precisa de uma mudança, pode usar o nome da classe para um int e usá-lo, mas esteja atento a possíveis conflitos. Adicionando como comentário em vez de resposta, não gosto da ideia de que isso foi realmente usado. Talvez o que você realmente precise seja o padrão de visitantes.
vickirk

1
A partir do java 7, você pode até ativar o nome completo da classe para evitar conflitos de hash como o @vickirk apontou, mas ainda é feio.
Mitja

Respostas:


225

Esse é um cenário típico em que o polimorfismo de subtipo ajuda. Faça o seguinte

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() } ... }

Em seguida, você pode simplesmente chamar do()no this.

Se você não está livre para mudar A, Be C, você poderia aplicar o padrão do visitante para alcançar o mesmo.


33
O padrão Visitor significa que A, B e C precisam implementar uma interface com um método abstrato que leva um Visitor como parâmetro de entrada. E se você não puder alterar A, B, C e nenhum deles implementar essa interface?
thermz

21
O último comentário sobre o padrão de visitantes está errado. Você ainda precisará fazer A, B e C implementar uma interface.
Ben Thurley

10
Infelizmente, isso não funciona se o código do () - exigir o ambiente do host (ou seja, acesso a variáveis ​​não presentes no próprio do ()).
mafu 23/05

2
A pergunta do @mafu OP era sobre o envio baseado em tipo. Se o método do () precisar de mais informações para despachar, o IMHO está fora do escopo da pergunta discutida aqui.
JMG

3
esta resposta assume que você pode modificar as classes A, B, C, enquanto eu acho que o ponto é como fazer isso sem modificar A, B, C porque elas podem estar em uma biblioteca de terceira parte
cloudy_weather

96

se você absolutamente não puder codificar para uma interface, poderá usar uma enumeração como intermediário:

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;

}

thx, eu também tive que fazer algumas alterações: 1) inicializar cada ID de enumeração com a referência de classe; 2) afirme o nome simples da classe com o enum id .toString (); 3) encontre o enum através da referência de classe armazenada por id de enum. Eu acho que isso também é ofuscação segura então.
Aquarius Power

se this.getClass (). getSimpleName () não corresponde a um valor de CLAZZ, gera uma exceção ... é melhor cercar um bloco try catch e a exceção seria tratada como a opção "padrão" ou "else" de o switch
tetri

40

Basta criar um mapa em que a classe seja a chave e a funcionalidade, por exemplo, lambda ou similar, seja o valor.

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

// é claro, refatorar isso para inicializar apenas uma vez

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

Se você precisar verificar exceções, implemente um FunctionalInterface que lança a exceção e use-o em vez de Runnable.


3
A melhor solução é imho, principalmente porque permite refatoração fácil.
Feiteira 25/10

2
A única desvantagem dessa solução é que ela não pode usar variáveis ​​locais (método) nas lambdas (supondo que isso possa ser necessário, é claro).
precisa

1
@zapatero Você pode apenas mudar a função em vez de Runnable, passar a instância em que o parâmetro, então lançá-lo quando necessário
Novaterata

Voto a favor; esta é uma das poucas respostas que realmente ajuda o OP a fazer o que ele está solicitando (e sim, muitas vezes é possível refatorar o código para não ter que fazer instanceof, e não, meu cenário infelizmente não é um daqueles que se encaixam facilmente essa caixa ...)
Por Lundberg

@ SergioGutiérrez Obrigado. Bem, esse padrão só deve ser necessário com uma biblioteca externa. E mesmo assim, você pode simplesmente criar uma interface com uma implementação de adaptador, mas é útil onde você deseja que o DIFF comportamental, por assim dizer, seja mais óbvio. Semelhante ao roteamento da API fluente versus anotação, suponho.
Novaterata 11/11/19

36

Apenas no caso de alguém ler:

A melhor solução em java é:

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

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

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

    };

    abstract void doAction (...);
}

Os GRANDES benefícios desse padrão são:

  1. Você apenas faz isso (sem interruptores):

    void someFunction ( Action action ) {
        action.doAction(...);   
    }
  2. Caso você adicione uma nova ação chamada "d", DEVE aplicar o método doAction (...)

NOTA: Esse padrão é descrito no livro de Joshua Bloch "Effective Java (2nd Edition)"


1
legais! O @Overridenecessário acima de cada implementação de doAction()?
mateuscb

9
Como é essa a solução "BEST"? Como você decide qual actionusar? Por uma instância externa de cascata que chama someFunction()com o correto action? Isso apenas adiciona outro nível de indireção.
PureSpider 14/01

1
Não, isso será feito automaticamente em tempo de execução. Se você chamar someFunction (Action.a), a.doAction será chamado.
se.solovyev

11
Eu não entendo isso. Como você saberia qual enum usar? Como o @PureSpider disse, isso parece apenas mais um nível de trabalho a ser feito.
James Manes

2
É muito triste que você não tenha oferecido um exemplo completo , por exemplo , como mapear qualquer Instância de Classe de a, b ou C para essa enumeração. Vou tentar converter a instância para esse Enum.
Tom

21

Você não pode. A switchinstrução pode conter apenas caseinstruções que são constantes de tempo de compilação e avaliadas como um número inteiro (até Java 6 e uma sequência em Java 7).

O que você está procurando é chamado de "correspondência de padrões" na programação funcional.

Consulte também Evitando instanceof em Java


1
Não, na maioria das linguagens funcionais não é possível combinar padrões nos tipos, apenas nos construtores. Isso é verdade pelo menos em ML e Haskell. No Scala e no OCaml, é possível, mas não a aplicação típica da correspondência de padrões.
jmg

Claro, mas a verificação contra construtores seria "equivalente" ao cenário descrito acima.
Carlo V. Dango

1
Em alguns casos, mas não em geral.
jmg

Os switches também podem suportar enumerações.
Solomon Ucko

"Você não pode" olhar para um idioma diferente raramente é uma resposta útil.
Blanc L.

17

Conforme discutido nas principais respostas, a abordagem tradicional de POO é usar polimorfismo em vez de alternar. Existe até um padrão de refatoração bem documentado para esse truque: Substituir Condicional pelo Polimorfismo . Sempre que busco essa abordagem, também gosto de implementar um objeto Nulo para fornecer o comportamento padrão.

A partir do Java 8, podemos usar lambdas e genéricos para fornecer algo que os programadores funcionais estão familiarizados com: correspondência de padrões. Não é um recurso de linguagem principal, mas a biblioteca Javaslang fornece uma implementação. Exemplo do javadoc :

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

Não é o paradigma mais natural do mundo Java, portanto, use-o com cautela. Embora os métodos genéricos evitem que você tenha que digitar o valor correspondente, estamos perdendo uma maneira padrão de decompor o objeto correspondente, como nas classes de caso do Scala, por exemplo.


9

Sei que é muito tarde, mas para futuros leitores ...

Cuidado com as abordagens acima, baseadas apenas no nome da classe de A , B , C ...:

A menos que você possa garantir que A , B , C ... (todas as subclasses ou implementadores de Base ) sejam finais , as subclasses de A , B , C ... não serão tratadas.

Embora a abordagem if, elseif, elseif .. seja mais lenta para um grande número de subclasses / implementadores, é mais precisa.


Na verdade, você nunca deve usar a polymorphimsm (aka OOP)
Val

8

Infelizmente, isso não é possível imediatamente, pois a instrução switch-case espera uma expressão constante. Para superar isso, uma maneira seria usar valores de enumeração com os nomes das classes, por exemplo

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);
 }
}

Com isso é possível usar a instrução switch como esta

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
}

Acredito que até a edição 8213076 do JEP ser totalmente implementada, essa é a maneira mais limpa de obter uma instrução switch com base no tipo, sem modificar a classe de destino.
Rik Schaaf


5

Usar instruções de opção como essa não é a maneira orientada a objetos. Você deve usar o poder do polimorfismo . Basta escrever

this.do()

Tendo configurado anteriormente uma classe base:

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

que é a classe de base para A, Be 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/… ) usando uma interface em vez de uma classe base abstrata. Isso pode ser superior em algumas circunstâncias.
Raedwald

5

Isso funcionará mais rápido e fará sentido caso
- você tenha relativamente "casos"
- o processo seja executado em um contexto sensível ao desempenho

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
Este não é o mesmo que fazer um instanceof, pois isso só funciona se a classe de implementação é utilizado para switch, mas não vai funcionar para interfaces / abstractclass / superclasses
lifesoordinary

Sim, isso não é
Mike

Um esforço, mas além do comentário do @ lifesoordinary, você também perde a segurança de tipo que normalmente possui, porque esta resposta usa seqüências codificadas, em vez de referências de classe. É muito fácil fazer um erro de digitação, especialmente se você precisa estender essa funcionalidade com nomes canônicos completos se aqui foi qualquer sobreposição de nomes de classe com diferentes names.Edit pacote: erro de digitação fixo (o que prova um bocado o meu ponto)
Rik Schaaf

4

Você não pode alternar apenas com os tipos byte, short, char, int, String e enumerados (e as versões de objeto das primitivas, isso também depende da sua versão java. As strings podem ser switcheditadas no java 7)


Você não pode ativar as Strings no Java 6. E não pode ativar "versões de objetos das primitivas".
Lukas Eder

@ Ozoz eu disse que depende da sua versão java, no Java 7 você pode ativar o Strings.
Tnem

@Lukas Eder verificar o seu java especificação que puder
Tnem

4

Eu pessoalmente gosto do seguinte código 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");
            });

Saída:

YYyy

O código de exemplo usa Strings, mas você pode usar qualquer tipo de objeto, incluindo Classe. por exemplo.myCase(this.getClass(), (o) -> ...

Precisa do seguinte snippet:

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

O Java agora permite que você alterne da maneira do OP. Eles chamam de Correspondência de Padrões para switch. Atualmente, ele está em rascunho, mas vendo quanto trabalho eles colocaram em switches recentemente, acho que ele será executado. O exemplo dado no 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();
}  

ou usando sua sintaxe lambda e retornando um valor

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();
    };

de qualquer maneira, eles estão fazendo coisas legais com os interruptores.


3

Se você pode manipular a interface comum, pode adicionar uma enumeração e fazer com que cada classe retorne um valor exclusivo. Você não precisará de instanceof ou um padrão de visitante.

Para mim, a lógica precisava estar escrita na instrução switch, não no objeto em si. Esta foi a minha solução:

ClassA, ClassB, and ClassC implement CommonClass

Interface:

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;
  }
...

Se você estiver no java 7, poderá colocar valores de sequência para a enumeração e o bloco de caixa do comutador ainda funcionará.


O valuecampo é redundante se você deseja distinguir apenas as constantes enum - você pode usá-las diretamente (como você faz).
User905686

2

Que tal agora ?

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

3
Deve-se salientar que isso funciona apenas no Java 7. E que você deve chamar this.getSimpleName()Não tenho certeza se o pôster está confuso com JS (sim, ele está usando console, hehe).
pablisco

5
Isso tem um problema de ficar sem transparência referencial do código fonte. Ou seja, seu IDE não poderá manter a integridade de referência. Suponha que você queira renomear seu nome. O reflexo é mau.
Val

1
Não é uma ótima ideia. Os nomes de classe não são exclusivos se você tiver vários carregadores de classe.
Doradus

Pausas quando se trata de compressão de código (→ ProGuard)
Matthias Ronge

1

Eu acho que existem razões para usar uma instrução switch. Se você estiver usando o código gerado pelo xText, talvez. Ou outro tipo de classes geradas pela EMF.

instance.getClass().getName();

retorna uma String do nome de implementação da classe. ou seja: org.eclipse.emf.ecore.util.EcoreUtil

instance.getClass().getSimpleName();

retorna a representação simples, ou seja: EcoreUtil


Você não pode usá-lo em switchcomo casecondição porque não é valor constante
B-gangster

1

Se você precisar "alternar" pelo tipo de classe do objeto "this", esta resposta é a melhor https://stackoverflow.com/a/5579385/2078368

Mas se você precisar aplicar "switch" a qualquer outra variável. Eu sugeriria outra solução. Defina a seguinte interface:

public interface ClassTypeInterface {
    public String getType();
}

Implemente essa interface em todas as classes que você deseja "alternar". Exemplo:

public class A extends Something implements ClassTypeInterface {

    public final static String TYPE = "A";

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

Depois disso, você pode usá-lo da seguinte maneira:

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

A única coisa com a qual você deve se preocupar - mantenha os "tipos" exclusivos em todas as classes que implementam o ClassTypeInterface. Não é um grande problema, porque, no caso de qualquer interseção, você recebe um erro em tempo de compilação para a instrução "switch-case".


Em vez de usar String para the TYPE, você pode usar uma enumeração e a exclusividade é garantida (como feito nesta resposta ). No entanto, com qualquer uma das abordagens, você precisará refatorar em dois lugares ao renomear.
User905686

@ user905686 renomear o quê? No exemplo atual, o tipo "A" é definido dentro da classe Something para minimizar a quantidade de código. Mas na vida real você obviamente deve defini-lo fora (em algum lugar comum) e não há nenhum problema com refatoração adicional.
Sergey Krivenkov

Quero dizer, renomear a classe A. A refatoração automática pode não incluir a variável TYPE = "A"ao renomear. Especialmente se estiver fora da classe correspondente, também é possível esquecê-lo ao fazê-lo manualmente. Na verdade, o IntelliJ também encontra as ocorrências do nome da classe em seqüências de caracteres ou comentários, mas isso é apenas uma pesquisa de texto (em vez de olhar para a árvore de sintaxe) e, portanto, inclui falsos positivos.
User905686

@ user905686 é apenas um exemplo, para visualizar a ideia. Não use String para definições de tipo no projeto real, declare algum detentor da classe MyTypes com constantes inteiras (ou enum) e use-as nas classes que implementam ClassTypeInterface.
Sergey Krivenkov

1

Aqui está uma maneira funcional de realizá-lo no Java 8 usando 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

Embora não seja possível escrever uma instrução switch, é possível ramificar para um processamento específico para cada tipo especificado. Uma maneira de fazer isso é usar o mecanismo padrão de despacho duplo. Um exemplo em que queremos "alternar" com base no tipo é o mapeador de exceções de Jersey, onde precisamos mapear várias exceções para respostas de erro. Embora neste caso específico exista provavelmente uma maneira melhor (por exemplo, usar um método polimórfico que traduza cada exceção em uma resposta de erro), o uso do mecanismo de despacho duplo ainda é útil e prático.

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);
    }
}

Então, sempre que o "switch" for necessário, você poderá fazer o seguinte:

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";
    }
}

Isso se parece muito com o padrão Visitor, que já foi discutido nesta resposta: stackoverflow.com/a/5579385
TypeRacer

0

Crie um Enum com nomes de classe.

public enum ClassNameEnum {
    A, B, C
}

Encontre o nome da classe do objeto. Escrever um interruptor de caso sobre o enum.

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;
        }
    }
}

Espero que isto ajude.


2
Em contraste com essa abordagem em que o acoplamento entre constantes enum e classes é feito explicitamente, você faz o acoplamento implicitamente pelo nome da classe. Isso interromperá seu código quando você renomeia apenas uma constante de enum ou a classe, enquanto a outra abordagem ainda funcionaria.
User905686

0

O Eclipse Modeling Framework tem uma ideia interessante que também considera herança. O conceito básico é definido na interface Switch : a troca é feita invocando o método doSwitch .

O que é realmente interessante é a implementação. Para cada tipo de interesse, um

public T caseXXXX(XXXX object);

O método deve ser implementado (com uma implementação padrão retornando nulo). A implementação doSwitch tentará chamar todos os métodos caseXXX no objeto para toda a sua hierarquia de tipos. Algo nas linhas de:

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;

A estrutura atual usa um ID inteiro para cada classe, portanto, a lógica é realmente uma opção pura:

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:
    {
      ...
    }
  ...

Você pode ver uma implementação completa do ECoreSwitch para ter uma idéia melhor.


-1

existe uma maneira ainda mais simples de emular uma estrutura de switch que usa instanceof, você faz isso criando um bloco de código no seu método e nomeando-o com um rótulo. Então você usa estruturas if para emular as instruções de caso. Se um caso for verdadeiro, use a interrupção LABEL_NAME para sair da sua estrutura de comutador improvisado.

        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;
            }
        }

Como isso é melhor do que o if... else ifcódigo fornecido pelo OP?
TypeRacer

Apenas para elaborar meu comentário anterior: o que você está propondo é essencialmente substituir if... else ifpor instruções "goto", que é a maneira errada de implementar o fluxo de controle em linguagens como Java.
TypeRacer
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.