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

358

我有一个关于使用switch case处理 instanceof 对象的问题:

例如:我的问题可以在Java中重现:

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

如何使用 switch...case 实现它?


7
如果你真的觉得需要一个开关,你可以将类名哈希为整数并使用它,但要注意可能会发生冲突。我将这句话作为注释添加,而不是作为答案,因为我不喜欢这个想法被实际使用。也许你真正需要的是访问者模式。 - vickirk
1
从Java 7开始,您甚至可以切换到完全限定的类名以避免像@vickirk指出的哈希冲突,但这仍然很丑陋。 - Mitja
可以使用将类名作为枚举值来实现。 - Murat Karagöz
29个回答

270

这是一个典型的情况,其中子类型多态性有所帮助。请按照以下步骤进行操作

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

那么你可以在this上简单调用do()

如果你无法自由更改ABC,你可以应用访问者模式来实现相同的功能。


46
访问者模式意味着A、B和C必须实现一个接口,该接口有一个抽象方法,它以访问者作为输入参数。如果您无法更改A、B、C,并且它们都没有实现该接口,该怎么办? - thermz
27
关于访问者模式的最后一条评论是错误的。您仍需要使A、B和C实现一个接口。 - Ben Thurley
12
遗憾的是,如果do()代码需要主机环境(即访问do()本身不存在的变量),则此方法不起作用。 - mafu
3
@mafu的问题是关于基于类型分派的。如果你的do()方法需要更多输入来进行分派,那么我认为你的问题超出了这里讨论的范围。 - jmg
9
本回答假设您可以修改类A、B、C,但实际上问题在于如何在不修改A、B、C的情况下进行操作,因为它们可能属于第三方库。 - cloudy_weather
显示剩余7条评论

123

如果你绝对无法编写接口代码,那么你可以使用枚举作为中介:

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;

}

2
谢谢,我还需要做一些更改:1)使用类引用初始化每个枚举ID;2)通过枚举ID的.toString()方法来断言类的简单名称;3)通过存储的类引用查找枚举。我认为这也是混淆安全的。 - Aquarius Power
1
如果this.getClass().getSimpleName()的值不匹配CLAZZ的任何一个值,它会抛出异常...最好用try catch块包围起来,这样异常就会被视为switch语句的“默认”或“else”选项。 - tetri
通常人们会考虑使用switch语句来提高性能,而不应该在这里提到Enum.instanceOf。 - Mike
1
当使用最常用的IDE进行重构时,这会破坏case Acase Bcase C中类名的自动重命名。 - logi-kal

77
创建一个 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());

// of course, refactor this to only initialize once

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

如果需要使用已检查异常,则实现一个抛出该异常的FunctionalInterface,并将其用于代替Runnable


下面是一个真实的示例,展示了这种方法如何简化代码。

重构为 map 之前的代码:

private Object unmarshall(
  final Property<?> property, final Object configValue ) {
  final Object result;
  final String value = configValue.toString();

  if( property instanceof SimpleDoubleProperty ) {
    result = Double.parseDouble( value );
  }
  else if( property instanceof SimpleFloatProperty ) {
    result = Float.parseFloat( value );
  }
  else if( property instanceof SimpleBooleanProperty ) {
    result = Boolean.parseBoolean( value );
  }
  else if( property instanceof SimpleFileProperty ) {
    result = new File( value );
  }
  else {
    result = value;
  }

  return result;
}

代码重构为映射后的结果:
private final Map<Class<?>, Function<String, Object>> UNMARSHALL = 
Map.of(
  SimpleBooleanProperty.class, Boolean::parseBoolean,
  SimpleDoubleProperty.class, Double::parseDouble,
  SimpleFloatProperty.class, Float::parseFloat,
  SimpleFileProperty.class, File::new
);

private Object unmarshall(
  final Property<?> property, final Object configValue ) {
  return UNMARSHALL
    .getOrDefault( property.getClass(), ( v ) -> v )
    .apply( configValue.toString() );
}

这样可以避免重复,消除几乎所有的分支语句,并简化维护。


5
点赞了!这是为数不多的能够帮助提问者完成他要求的回答之一(是的,通常可以重构代码而不必使用instanceof,但不幸的是,我的情况不是那种容易适应这种方法的情况...)。 - Per Lundberg
@SergioGutiérrez 谢谢。嗯,这种模式只需要一个外部库就可以了。即使如此,您也可以创建一个带有适配器实现的接口,但在您想要行为差异更加明显的情况下,它非常有用。类似于流畅与注释API路由吧。 - Novaterata
这仅适用于精确匹配。Class<ArrayList>将无法匹配Class<List> - Gili
这是对问题的一个不错的回答,但我不喜欢这段代码。它很难理解,相比之下,我更喜欢最初的代码,尽管它可以放在一个单独的服务中,你很少需要查看/接触它。 - html_programmer
@html_programmer 说得好,但在函数式编程方面,“晦涩难懂”和“无知”之间有一条很细微的界限。 - Novaterata
@Novaterata 我会称呼初始代码为第二个代码的可读性重构。但每个人都有自己的看法。 - html_programmer

76

现在Java允许您以OP的方式进行切换。他们称之为Pattern Matching for switch。它是作为Java 17的预览功能发布的。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();
                   };

无论哪种方式,他们一直在使用开关做很酷的事情。


2
@olidev 考虑将此作为(新的)接受答案。 - Chris
12
这只是Java 17的预览模式。 - cquezel
4
这是一个预览功能,其设计、规范和实现已经完成,但不是永久性的,也就是说这个功能在未来的Java SE版本中可能以不同形式存在,甚至可能被取消。参考文献:https://docs.oracle.com/en/java/javase/17/language/pattern-matching-switch-expressions-and-statements.html - cquezel
3
这是Java 17中的预览功能,其语法可能会更改,并且必须通过VM参数打开。 - Michael

41

以防万一有人会读到:

Java中最好的解决方案是:

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

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

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

    };

    abstract void doAction (...);
}

这种模式的优点如下:

  1. 你只需要像这样实现(完全没有switch语句):

    void someFunction ( Action action ) {
        action.doAction(...);   
    }
    
  2. 如果你添加了名为"d"的新操作,你必须实现doAction(...)方法。

注意:这个模式在Joshua Bloch的《Effective Java (第二版)》中有描述。


1
很好!在每个doAction()的实现上方是否需要使用@Override - mateuscb
17
这为什么是“最佳”解决方案?你会如何决定使用哪个“操作”?通过一个外部的instanceof级联调用带有正确“操作”的someFunction()吗?这只是增加了另一层间接性。 - PureSpider
1
不会,它将在运行时自动完成。如果您调用someFunction(Action.a),那么a.doAction将被调用。 - se.solovyev
15
我不理解这个。你怎么知道要使用哪个枚举?正如@PureSpider所说,这似乎只是另一层需要做的工作。 - James Manes
3
很遗憾你没有提供一个完整的例子,比如如何将a、b或C类的任何实例映射到这个枚举类型。我会尝试将实例强制转换为这个枚举类型。 - Tom
显示剩余3条评论

30

你无法这样做。 switch 语句只能包含编译时常量且计算结果为整数的 case 语句(Java6及以下版本)或字符串(Java7)。

你需要的是函数式编程中所谓的 "模式匹配"。

另请参见避免在Java中使用instanceof关键字


1
不,大多数函数式语言中你不能基于类型进行模式匹配,只能基于构造函数。至少在ML和Haskell中是这样的。在Scala和OCaml中虽然也可以,但这并不是模式匹配的典型应用。 - jmg
当然可以,但是针对构造函数的检查将是“等效于”上述描述的情况。 - Carlo V. Dango
1
在某些情况下,但并不是普遍情况。 - jmg
开关也可以支持枚举。 - Solomon Ucko

20

如前面的答案所讨论的那样,传统的OOP方法是使用多态而不是switch语句。甚至有一个经过充分文档化的重构模式来实现这个技巧:用多态替换条件表达式。每当我使用这种方法时,我还喜欢实现Null对象来提供默认行为。

从Java 8开始,我们可以使用Lambda和泛型,类似于函数式编程语言中非常熟悉的模式匹配。虽然它不是一种核心语言特性,但VAVR库(以前是Javaslang库)提供了一种实现。以下是文档中的示例:

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

在Java世界中,这不是最自然的范例,因此请谨慎使用。虽然通用方法可以避免您必须对匹配的值进行强制类型转换,但我们缺少一种标准方式来分解匹配对象,例如Scala的case classes


14

很遗憾,由于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
}

1
我相信在 JEP issue 8213076 完全实现之前,这是最干净的方法来获取基于类型的 switch 语句,而不需要修改目标类。 - Rik Schaaf
由于查找Map是只读的,并且仅由一个线程初始化,因此从我的观点来看,应该优先选择简单的HashMap - Glains

10

java 7+

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

你可以更快地通过在getSimpleName中省略字符串操作来引入常量并使用完整的类名:
public static final TRADE = Trade.class.getName();
...
switch (model.getClass().getName()) {
case TRADE:

4
这与使用 instanceof 不同,因为这只适用于使用实现类进行切换,而对于接口/抽象类/超类则不起作用。 - lifesoordinary
1
虽然你的努力值得肯定,但除了@lifesoordinary的评论之外,你还错过了通常具有的类型安全性,因为这个答案使用硬编码字符串而不是类引用。如果需要在类名存在重叠的情况下扩展此功能并使用完整的规范名称,很容易出现拼写错误。编辑:修正了一个拼写错误(这也证明了我的观点)。 - Rik Schaaf
@Mike同意在equals()方法的实现中保持对称性。此外,在许多情况下,您可以重写代码以使用多态性,而不是instanceof。然而仍然存在一些有效的用例。 - Rik Schaaf
@RikSchaaf,你的错别字将会被测试揭示出来,你写单元测试了吗?如果你能用多态重写你的案例,那么它就是“多态案例”,与所问问题无关。打开java.awt.Component#process...方法,告诉我在哪里可以使用多态。我个人在类似情况下使用哈希映射,但问题是关于switch语句的,我相信这可能有性能原因,因为即使哈希映射很快,switch更快。 - Mike
从性能角度来看,Enum.valueOf似乎要差得多,并且不应该与switchHashMap内联提及。 - Mike
显示剩余3条评论

10

我知道这已经很晚了,但对于未来的读者……

要注意上述方法仅基于ABC……类的名称:

除非您可以保证ABC……(所有Base的子类或实现者)都是final,否则将无法处理ABC……的子类。

即使if、elseif、elseif......的方法在大量子类/实现者时速度较慢,但更准确。


确实,你不应该使用多态(又称面向对象编程)。 - Val

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接