¿Es posible usar el operador instanceof en una instrucción switch?


267

Tengo una pregunta sobre el uso de mayúsculas y minúsculas para el instanceofobjeto:

Por ejemplo: mi problema se puede reproducir en Java:

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

¿Cómo se implementaría usando switch...case?


66
Si realmente siente que necesita un interruptor, puede cambiar el nombre de la clase a int y usarlo, tenga cuidado con los posibles conflictos. Agregar como comentario en lugar de una respuesta, ya que no me gusta la idea de que esto realmente se haya utilizado. Quizás lo que realmente necesita es el patrón de visitante.
vickirk

1
A partir de java 7, incluso podría activar el nombre de clase completamente calificado para evitar conflictos de hash como señaló @vickirk, pero aún es feo.
Mitja

Respuestas:


225

Este es un escenario típico donde ayuda el subtipo de polimorfismo. Haz lo siguiente

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

A continuación, puede llamar do()a this.

Si no está libre de cambiar A, By C, se podría aplicar el patrón de visitante para lograr el mismo.


33
El patrón de visitante significa que A, B y C tienen que implementar una interfaz con un método abstracto que toma un visitante como parámetro de entrada, ¿qué sucede si no puede cambiar A, B, C y ninguno de ellos implementa esa interfaz?
thermz

21
El último comentario sobre el patrón de visitante es incorrecto. Aún necesitaría hacer que A, B y C implementen una interfaz.
Ben Thurley

10
Lamentablemente, esto no funciona si el código do () requiere el entorno del host (es decir, acceso a variables que no están presentes en el propio do ()).
mafu

2
La pregunta de @mafu OP era sobre el envío basado en tipos. Si su método do () necesita más información para enviar que su problema, en mi humilde opinión está fuera del alcance de la pregunta discutida aquí.
jmg

3
esta respuesta asume que puede modificar las clases A, B, C, mientras que creo que el punto es cómo hacerlo sin modificar A, B, C porque podrían estar en una biblioteca de tercera parte
cloudy_weather

96

si absolutamente no puede codificar a una interfaz, entonces podría usar una enumeración como intermediario:

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;

}

gracias, también tuve que hacer algunos cambios: 1) inicializar cada id de enumeración con la referencia de Clase; 2) afirme el nombre simple de la clase con el ID de enumeración .toString (); 3) encuentre la enumeración a través de la referencia de clase almacenada por ID de enumeración. Creo que esto también es seguro para la ofuscación.
Acuario Power

si this.getClass (). getSimpleName () no coincide con un valor de CLAZZ, arroja una excepción ... es mejor rodear con un bloque try catch y la excepción se trataría como la opción "predeterminada" o "else" de el interruptor
tetri

40

Simplemente cree un mapa donde la clase sea la clave y la funcionalidad, es decir, lambda o similar, sea el 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());

// por supuesto, refactorice esto para inicializar solo una vez

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

Si necesita marcar Excepciones que implementar una interfaz funcional que arroje la excepción y la use en lugar de Runnable.


3
La mejor solución en mi humilde opinión, especialmente porque permite una fácil refactorización.
Feiteira

2
El único inconveniente de esta solución es que no puede hacer uso de variables locales (método) en las lambdas (suponiendo que podría ser necesario, por supuesto).
zapatero

1
@zapatero Puede simplemente cambiar a Función en lugar de Ejecutable, pasar la instancia como parámetro y luego lanzarla cuando sea necesario
Novaterata

Upvoted; esta es una de las pocas respuestas que realmente ayuda al OP a hacer lo que está pidiendo (y sí, a menudo es posible refactorizar el código para que no tenga que hacerlo instanceof, y no, desafortunadamente, mi escenario no es uno de esos que encaja fácilmente esa caja ...)
Por Lundberg

@ SergioGutiérrez Gracias. Bueno, este patrón solo debería ser necesario con una biblioteca externa. E incluso entonces, puede crear una interfaz con una implementación de adaptador, pero es útil donde desea que el DIFF de comportamiento, por así decirlo, sea más obvio. Supongo que es similar al enrutamiento de API fluido y de anotación.
Novaterata

36

Por si acaso alguien lo leerá:

La MEJOR solución en java es:

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

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

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

    };

    abstract void doAction (...);
}

Los GRANDES beneficios de dicho patrón son:

  1. Simplemente hazlo como (NO hay interruptores):

    void someFunction ( Action action ) {
        action.doAction(...);   
    }
  2. En caso de que si agrega una nueva Acción llamada "d", DEBE implementar el método doAction (...)

NOTA: Este patrón se describe en el Bloch de Joshua "Java efectivo (2ª edición)"


1
¡bonito! ¿Se @Overriderequiere lo anterior sobre cada implementación de doAction()?
mateuscb

9
¿Cómo es esta la "MEJOR" solución? ¿Cómo decidirías cuál actionusar? ¿Por una instancia externa de cascada que llama someFunction()con el correcto action? Esto solo agrega otro nivel de indirección.
PureSpider

1
No, se realizará automáticamente en tiempo de ejecución. Si llama a someFunction (Action.a), se llamará a.doAction.
se.solovyev

11
No entiendo esto ¿Cómo sabrías qué enumeración usar? Como dijo @PureSpider, esto parece ser solo otro nivel de trabajo por hacer.
James Manes

2
Es muy triste que no haya ofrecido un ejemplo completo , por ejemplo , cómo asignar cualquier instancia de clase de a, bo C a esta enumeración. Intentaré lanzar la Instancia a esta Enum.
Tom

21

No puedes La switchdeclaración solo puede contener casedeclaraciones que son constantes de tiempo de compilación y que evalúan a un entero (hasta Java 6 y una cadena en Java 7).

Lo que está buscando se llama "coincidencia de patrones" en la programación funcional.

Consulte también Evitar instanceof en Java


1
No, en la mayoría de los lenguajes funcionales no se pueden combinar patrones en tipos, solo en constructores. Eso es al menos cierto en ML y Haskell. En Scala y OCaml es posible pero no la aplicación típica de coincidencia de patrones.
jmg

Claro, pero verificar contra constructores sería "equivalente" al escenario descrito anteriormente.
Carlo V. Dango

1
En algunos casos, pero no en general.
jmg

Los conmutadores también pueden admitir enumeraciones.
Solomon Ucko

"No puedes" mirar un idioma diferente rara vez es una respuesta útil.
L. Blanc

17

Como se discutió en las respuestas principales, el enfoque OOP tradicional es usar polimorfismo en lugar de cambiar. Incluso hay un patrón de refactorización bien documentado para este truco: Reemplazar condicional por polimorfismo . Cada vez que busco este enfoque, me gusta implementar también un objeto nulo para proporcionar el comportamiento predeterminado.

Comenzando con Java 8, podemos usar lambdas y genéricos para darnos algo con lo que los programadores funcionales están muy familiarizados: coincidencia de patrones. No es una característica central del lenguaje, pero la biblioteca Javaslang proporciona una implementación. Ejemplo del javadoc :

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

No es el paradigma más natural en el mundo de Java, así que úselo con precaución. Si bien los métodos genéricos le evitarán tener que escribir el valor coincidente, nos falta una forma estándar de descomponer el objeto coincidente como, por ejemplo, con las clases de caso de Scala .


9

Sé que es muy tarde, pero para futuros lectores ...

Tenga cuidado con los enfoques anteriores que se basan solo en el nombre de la clase de A , B , C ...:

A menos que pueda garantizar que A , B , C ... (todas las subclases o implementadores de Base ) sean finales , las subclases de A , B , C ... no se tratarán.

Aunque el enfoque if, elseif, elseif .. es más lento para un gran número de subclases / implementadores, es más preciso.


De hecho, nunca debes usar el polimorfismo (también conocido como OOP)
Val

8

Desafortunadamente, no es posible fuera de la caja ya que la declaración de cambio de caso espera una expresión constante. Para superar esto, una forma sería usar valores de enumeración con los nombres de clase, por ejemplo

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

Con eso es posible usar la declaración de cambio 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
}

Creo que hasta que el problema 8213076 de JEP esté completamente implementado, esta es la forma más limpia de obtener una declaración de cambio basada en el tipo, sin modificar la clase de destino.
Rik Schaaf


5

El uso de declaraciones de cambio como esta no es la forma orientada a objetos. En su lugar, debes usar el poder del polimorfismo . Simplemente escribe

this.do()

Habiendo configurado previamente una clase base:

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

que es la clase base para A, By 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 una interfaz en lugar de una clase base abstracta. Eso puede ser superior en algunas circusmtances.
Raedwald

5

Esto funcionará más rápido y tendrá sentido en caso de
que tenga muchos casos
, el proceso se ejecuta en un contexto sensible al rendimiento

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
Esto no es lo mismo que hacer una instancia de, ya que esto solo funciona si la clase de implementación se usa para cambiar, pero no funcionará para interfaces / abstractclass / superclasses
lifesoordinary

sí, esto no es
Mike

A por esfuerzo, pero aparte del comentario de @ lifesoordinary, también extrañas la seguridad de tipografía que normalmente tienes, porque esta respuesta usa cadenas codificadas, en lugar de referencias de clase. Es muy fácil hacer un error tipográfico, sobre todo si lo que se necesita para extender esta funcionalidad con nombres canónicos completos si aquí había alguna coincidencia en los nombres de clases con diferentes names.Edit paquete: error tipográfico fijo (lo que demuestra un poco mi punto)
Rik Schaaf

4

No puede un switch solo funciona con los tipos byte, short, char, int, String y enumerados (y las versiones de objeto de las primitivas, también depende de su versión de Java, las cadenas se pueden switcheditar en Java 7)


No puede activar cadenas en Java 6. Y no puede activar "versiones de objetos de las primitivas".
Lukas Eder

@Bozho Dije que depende de tu versión de Java, en Java 7 puedes activar Strings.
Tnem

@Lukas Eder comprobar su especificación Java que pueda
Tnem

4

Personalmente me gusta el siguiente 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");
            });

Saldrá:

YYyy

El código de muestra usa cadenas, pero puede usar cualquier tipo de objeto, incluida la clase. p.ej.myCase(this.getClass(), (o) -> ...

Necesita el siguiente fragmento:

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 ahora le permite cambiar a la manera del OP. Lo llaman Pattern Matching para cambiar. Actualmente está en borrador, pero al ver cuánto trabajo han estado poniendo en los interruptores recientemente, creo que pasará. El ejemplo dado en el JEP es

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

o usando su sintaxis lambda y devolviendo un 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 cualquier manera, han estado haciendo cosas geniales con los interruptores.


3

Si puede manipular la interfaz común, puede agregar una enumeración y hacer que cada clase devuelva un valor único. No necesitará instanceof o un patrón de visitante.

Para mí, la lógica tenía que estar escrita en la declaración de cambio, no el objeto en sí. Esta fue mi solución:

ClassA, ClassB, and ClassC implement CommonClass

Interfaz:

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

Si está en Java 7, puede poner valores de cadena para la enumeración y el bloque de mayúsculas y minúsculas seguirá funcionando.


El valuecampo es redundante si solo desea distinguir las constantes de enumeración; puede usar las constantes directamente (como lo hace).
user905686

2

Qué tal esto ?

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

3
Debería señalar que esto solo funciona en Java 7. Y que debe llamar this.getSimpleName()No estoy seguro de si el póster está confundido con JS (sí, está usando la consola, jeje).
pablisco

55
Esto tiene el problema de caerse del código fuente de transparencia referencial. Es decir, su IDE no podrá mantener la integridad de referencia. Supongamos que desea cambiar el nombre de su nombre. El reflejo es malvado.
Val

1
No es una gran idea Los nombres de clase no son únicos si tiene múltiples cargadores de clases.
Doradus el

Se rompe cuando se trata de compresión de código (→ ProGuard)
Matthias Ronge

1

Creo que hay razones para usar una declaración de cambio. Si está utilizando el código generado por xText, tal vez. O otro tipo de clases generadas por EMF.

instance.getClass().getName();

devuelve una cadena del nombre de implementación de clase. es decir: org.eclipse.emf.ecore.util.EcoreUtil

instance.getClass().getSimpleName();

devuelve la representación simple, es decir: EcoreUtil


No puede usarlo switchcomo casecondición porque no es un valor constante
B-GangsteR

1

Si necesita "cambiar" a través del tipo de clase de "este" objeto, esta respuesta es la mejor https://stackoverflow.com/a/5579385/2078368

Pero si necesita aplicar "cambiar" a cualquier otra variable. Sugeriría otra solución. Defina la siguiente interfaz:

public interface ClassTypeInterface {
    public String getType();
}

Implemente esta interfaz en cada clase que desee "cambiar". Ejemplo:

public class A extends Something implements ClassTypeInterface {

    public final static String TYPE = "A";

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

Después de eso, puede usarlo de la siguiente manera:

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

Lo único que debería importarle es mantener los "tipos" únicos en todas las clases que implementan ClassTypeInterface. No es un gran problema, porque en caso de cualquier intersección, recibirá un error en tiempo de compilación para la instrucción "switch-case".


En lugar de usar String para el TYPE, puede usar una enumeración y se garantiza la unicidad (como se hace en esta respuesta ). Sin embargo, con cualquiera de los enfoques, tendrá que refactorizar en dos lugares cuando cambie el nombre.
user905686

@ user905686 cambiar el nombre de qué? En el ejemplo actual, el tipo "A" se define dentro de la clase Something para minimizar la cantidad de código. Pero en la vida real, obviamente, debe definirlo afuera (en algún lugar común) y no hay ningún problema con una mayor refactorización.
Sergey Krivenkov

Me refiero a cambiar el nombre de la clase A. La refactorización automática podría no incluir la variable TYPE = "A"al cambiar el nombre. Especialmente si está fuera de la clase correspondiente, uno también podría olvidarlo al hacerlo manualmente. IntelliJ también encuentra las ocurrencias del nombre de la clase en cadenas o comentarios, pero eso es solo una búsqueda de texto (en lugar de mirar el árbol de sintaxis) y, por lo tanto, incluye falsos positivos.
user905686

@ user905686 es solo un ejemplo, para visualizar la idea. No use String para definiciones de tipo en un proyecto real, declare algún titular de clase MyTypes con constantes enteras (o enumeración) y utilícelas en las clases que implementan ClassTypeInterface.
Sergey Krivenkov

1

Aquí hay una forma funcional de lograrlo en 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

Si bien no es posible escribir una declaración de cambio, es posible ramificarse a un procesamiento específico para cada tipo dado. Una forma de hacerlo es utilizar un mecanismo estándar de despacho doble. Un ejemplo en el que queremos "cambiar" según el tipo es el mapeador de excepciones de Jersey, donde necesitamos mapear multitud de excepciones a las respuestas de error. Si bien para este caso específico probablemente haya una mejor manera (es decir, usar un método polimórfico que traduzca cada excepción a una respuesta de error), el uso del mecanismo de doble despacho sigue siendo útil y práctico.

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

Entonces, siempre que se necesite el "interruptor", puede hacerlo de la siguiente manera:

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

Esto se parece mucho al patrón Visitor, que ya se discutió en esta respuesta: stackoverflow.com/a/5579385
typeracer

0

Crear una enumeración con nombres de clase.

public enum ClassNameEnum {
    A, B, C
}

Encuentra el nombre de la clase del objeto. Escriba un caso de cambio sobre la enumeración.

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 esto ayude.


2
En contraste con este enfoque donde el acoplamiento entre constantes enum y clases se realiza explícitamente, el acoplamiento se realiza implícitamente por nombre de clase. Esto romperá su código cuando cambie el nombre de solo una de la constante enum o la clase, mientras que el otro enfoque aún funcionaría.
user905686

0

El Marco de modelado de Eclipse tiene una idea interesante que también considera la herencia. El concepto básico se define en la interfaz Switch : el cambio se realiza invocando el método doSwitch .

Lo que es realmente interesante es la implementación. Para cada tipo de interés, un

public T caseXXXX(XXXX object);

El método debe implementarse (con una implementación predeterminada que devuelve nulo). La implementación de doSwitch intentará llamar a todos los métodos caseXXX en el objeto para toda su jerarquía de tipos. Algo en las líneas 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;

El marco real utiliza una identificación entera para cada clase, por lo que la lógica es en realidad un interruptor puro:

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

Puede ver una implementación completa de ECoreSwitch para tener una mejor idea.


-1

hay una forma aún más simple de emular una estructura de conmutador que usa instanceof, lo haces creando un bloque de código en tu método y nombrándolo con una etiqueta. Luego usas estructuras if para emular las declaraciones de casos. Si un caso es verdadero, entonces usa la ruptura LABEL_NAME para salir de su estructura de interruptor improvisada.

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

¿Cómo es esto mejor que el if... else ifcódigo proporcionado por el OP?
máquina de escribir

Solo para elaborar mi comentario anterior: lo que está proponiendo es esencialmente reemplazar if... else ifcon declaraciones "goto", que es la forma incorrecta de implementar el flujo de control en lenguajes como Java.
typeracer
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.