是否可以在switch语句中使用instanceof运算符?


267

我对instanceof对象使用开关盒有一个问题:

例如:我的问题可以用Java复制:

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

如何使用实施switch...case


6
如果您确实感觉需要切换,则可以将类名哈希为int并使用它,但是请注意是否存在冲突。添加评论而不是答案,因为我不喜欢这样的想法。也许您真正需要的是访客模式。
vickirk 2011年

1
从Java 7开始,您甚至可以打开完全限定的类名,以避免@vickirk指出的此类哈希冲突,但这仍然很丑陋。
米塔(Mitja)2014年

Answers:


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

如果您不能随意更改ABC,则可以应用访问者模式来实现相同目的。


33
访客模式意味着A,B和C必须使用将访客作为输入参数的抽象方法来实现接口,如果您不能更改A,B,C并且它们都不实现该接口怎么办?
thermz 2014年

21
关于访客模式的最后评论是错误的。您仍然需要使A,B和C实现一个接口。
Ben Thurley,2015年

10
可悲的是,如果do()代码需要主机的环境(即,访问do()本身不存在的变量),这将无法正常工作。
mafu

2
@mafu OP的问题是关于基于类型的调度。如果您的do()方法需要更多的输入以便进行调度,而不是您的问题,恕我直言,不在本文讨论的问题范围之内。
jmg 2015年

3
这个答案假设您可以修改A,B,C类,而我认为重点是不修改A,B,C怎么办,因为它们可能位于第三方库中
–cloudyweather

96

如果您绝对不能编写接口代码,则可以使用枚举作为中介:

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)使用Class引用初始化每个枚举id;2)使用枚举ID .toString()声明类的简单名称;3)通过每个枚举id的存储的Class引用找到枚举。我认为这样做也是安全的。
Aquarius Power

如果this.getClass()。getSimpleName()与CLAZZ的值不匹配,则抛出Exception ...最好用try catch块将其包围,并且将该Exception视为“ default”或“ else”选项开关
tetri

40

只需创建一个Map,其中class是键,而功能即lambda或类似值就是值。

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

如果需要检查异常,则可以实现抛出该异常并使用它而不是Runnable的FunctionalInterface。


3
最好的解决方案恕我直言,特别是因为它允许轻松重构。
Feiteira

2
此解决方案的唯一缺点是,它无法利用lambda中的局部(方法)变量(当然当然需要使用此变量)。
zapatero

1
@zapatero您可以只更改为Function而不是Runnable,将实例作为参数传递,然后在需要时进行转换
Novaterata,

已投票;这是实际上可以帮助OP完成他所要求的少数几个答案之一(是的,通常可以将代码重构为不必要做的instanceof,而且不幸的是,我的情况不是很容易适应其中之一)那个盒子...)
Per Lundberg

@SergioGutiérrez谢谢。好吧,只有外部库才需要这种模式。即使这样,您也可以只使用适配器实现来创建接口,但是在您希望行为DIFF更明显的情况下很有用。我想类似于流利的vs注释API路由。
Novaterata,

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(...)方法

注意:此模式在Joshua's Bloch“有效Java(第二版)”中进行了描述


1
真好!是@Override需要的每个实现以上doAction()
mateuscb 2014年

9
这如何成为“最佳”解决方案?您将如何决定action使用哪个?通过外部级联调用someFunction()正确的action?这只是增加了另一个间接级别。
PureSpider

1
不,它将在运行时自动完成。如果调用someFunction(Action.a),则将调用a.doAction。
se.solovyev '16

11
我不明白 您怎么知道要使用哪个枚举?正如@PureSpider所说,这似乎只是要做的另一层工作。
James Manes

2
很遗憾您没有提供完整的示例,例如,如何将a,b或C的任何类实例映射到该枚举。我将尝试将实例强制转换为该枚举。
汤姆(Tom)

21

你不能 该switch语句只能包含case作为编译时间常数并且其结果为整数的语句(最多Java 6和Java 7中的字符串)。

您正在寻找的内容在功能编程中称为“模式匹配”。

另请参阅在Java中避免instanceof


1
不,在大多数功能语言中,您不能仅在构造函数上对类型进行模式匹配。至少在ML和Haskell中是这样。在Scala和OCaml中,可能但并非典型的模式匹配应用。
jmg 2011年

可以,但是检查构造函数将与上述方案“等效”。
Carlo V. Dango

1
在某些情况下,但不是一般情况。
jmg 2011年

开关也可以支持枚举。
Solomon Ucko '16

“您不能”看另一种语言很少有帮助。
L. Blanc

17

就像在最前面的答案中讨论的那样,传统的OOP方法是使用多态而不是切换。这个技巧甚至有一个记录良好的重构模式:用多态替换条件。每当我采用这种方法时,我都会喜欢实现一个Null对象以提供默认行为。

从Java 8开始,我们可以使用lambda和泛型为我们提供一些功能程序员非常熟悉的东西:模式匹配。它不是核心语言功能,但是Javaslang库提供了一种实现。来自javadoc的示例:

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

它不是Java世界中最自然的范例,因此请谨慎使用。尽管通用方法使您不必强制转换匹配的值,但是例如,缺少像Scala的case类那样分解匹配对象的标准方法。


9

我知道这已经很晚了,但对于将来的读者来说...

上面的方法当心仅基于该名称类的Ç ...:

除非您可以保证ABC ...(Base的所有子类或实现者)是最终,否则将不处理ABC ...的子类。

即使if,elseif,elseif .. 方法对于大量的子类/实现者来说较慢,它也更准确。


确实,您永远不应该使用polymorphimsm(aka OOP)
2014年

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语句的最干净的方法。
Rik Schaaf


5

像这样使用switch语句不是面向对象的方法。您应该改用多态的力量。只需写

this.do()

先前已建立基类:

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

这是基类ABC

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建议使用接口(而不是抽象基类)(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并不相同,因为这仅在使用实现类进行切换时才有效,但不适用于接口/抽象类/超类
lifesoordinary

是的,这不是
Mike

A是一种努力,但是除了@lifesoordinary的注释之外,您还会错过通常具有的类型安全性,因为此答案使用了硬编码的字符串,而不是类引用。这是很容易使一个错字,特别是如果你需要具有完全规范名称,如果这里是在不同的包names.Edit类名称的任何重叠扩展此功能:固定错字(这有点儿证明我的观点)
里克·沙夫

4

您不能只使用byte,short,char,int,String和枚举类型(以及原始对象的对象版本,这也取决于您的Java版本,String可以switch在Java 7中使用)的开关。


您不能在Java 6中打开字符串。也不能打开“基元的对象版本”。
卢卡斯·埃德

@Bozho我确实说过,这取决于您的Java版本,在Java 7中可以打开Strings。
特内姆2011年

@Lukas Eder可以检查您的Java规范
Tnem'4

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

示例代码使用字符串,但是您可以使用任何对象类型,包括Class。例如.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的方式进行切换。他们称其为模式匹配开关。它目前处于草案阶段,但是我看到他们最近在交换机中投入了多少工作,我认为它将完成。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();
}  

或使用其lambda语法并返回一个值

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

如果可以操纵公共接口,则可以添加一个枚举,并让每个类返回唯一值。您将不需要instanceof或访客模式。

对我来说,逻辑需要写在switch语句中,而不是对象本身。这是我的解决方案:

ClassA, ClassB, and ClassC implement CommonClass

接口:

public interface CommonClass {
   MyEnum getEnumType();
}

枚举:

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,则可以为枚举放入字符串值,并且switch case块仍将起作用。


value如果您只想区分枚举常量,则该字段是多余的-您可以直接使用常量(就像您一样)。
user905686 '18年

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()不确定发帖人是否与JS混淆(是的,他正在使用控制台,呵呵)。
pablisco 2014年

5
这有一个问题,就是没有达到源代码引用透明性。也就是说,您的IDE将无法保持参考完整性。假设您想重命名。反映是邪恶的。
2014年

1
这不是一个好主意。如果您有多个类加载器,则类名称不是唯一的。
Doradus

中断代码压缩(→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

如果您需要通过“此”对象的类类型“切换”,则此答案是最好的https://stackoverflow.com/a/5579385/2078368

但是,如果您需要将“ switch”应用于任何其他变量。我建议另一种解决方案。定义以下接口:

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,还可以使用枚举,并且可以保证唯一性(如本答案所示)。但是,使用任何一种方法都必须在重命名时在两个地方进行重构。
user905686 '18年

@ user905686更名是什么?在当前示例中,在Something类中定义了类型“ A”,以最大程度地减少代码量。但是,在现实生活中,您显然应该在外部(在某个公共位置)定义它,并且进一步重构没有任何问题。
谢尔盖·克里文科夫

我的意思是重命名类A。重命名时,自动重构可能不包含变量TYPE = "A"。特别是如果它不在相应的类中,则手动进行操作时可能也会忘记它。IntelliJ实际上还可以在字符串或注释中找到类名称的出现,但这只是文本搜索(而不是查看语法树),因此包含误报。
user905686

@ user905686只是一个例子,可以使这个想法形象化。不要在实际项目中将String用于类型定义,而是使用整数常量(或枚举)声明一些MyTypes类持有人,并在实现ClassTypeInterface的类中使用它们。
谢尔盖·克里文科夫

1

这是使用http://www.vavr.io/在Java 8中完成此功能的功能方法

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

用类名创建一个枚举

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
与显式完成枚举常量和类之间的耦合的方法相反,您可以通过类名隐式地进行耦合。当您仅重命名枚举常量或类中的一个而另一种方法仍然有效时,这将破坏您的代码。
user905686 '18年

0

Eclipse Modeling Framework有一个有趣的想法,也考虑了继承。基本概念在Switch 接口中定义:切换是通过调用doSwitch方法来完成的。

真正有趣的是实现。对于每种兴趣类型,

public T caseXXXX(XXXX object);

方法必须实现(默认实现返回null)。该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;

实际的框架为每个类使用一个整数id,因此逻辑实际上是一个纯开关:

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

这比OP给定的if... else if代码好吗?
typeracer

只是为了详细说明我先前的评论:您的建议实质上是用“ goto”语句替换if... else if,这是在Java之类的语言中实现控制流的错误方法。
typeracer
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.