避免在Java中使用instanceof

113

一连串的“instanceof”操作被认为是一种“代码异味”。标准答案是“使用多态性”。在这种情况下,我该怎么做呢?

有许多基类的子类;它们中没有一个受到我的控制。类比的情况是Java类Integer、Double、BigDecimal等。

if (obj instanceof Integer) {NumberStuff.handle((Integer)obj);}
else if (obj instanceof BigDecimal) {BigDecimalStuff.handle((BigDecimal)obj);}
else if (obj instanceof Double) {DoubleStuff.handle((Double)obj);}

我对NumberStuff等具有控制权。

我不想使用很多行代码来完成只需要几行的工作。(有时我会创建一个HashMap,将Integer.class映射到一个IntegerStuff实例,BigDecimal.class映射到一个BigDecimalStuff实例等等。但是今天我想要更简单的方法。)

我想要像这样简单:

public static handle(Integer num) { ... }
public static handle(BigDecimal num) { ... }

但 Java 并不是这样工作的。

我想在格式化时使用静态方法。 我要格式化的对象是复合类型,其中 Thing1 可以包含一个 Thing2 数组,而 Thing2 可以包含一个 Thing1 数组。 当我像这样实现我的格式化程序时,出现了问题:

class Thing1Formatter {
  private static Thing2Formatter thing2Formatter = new Thing2Formatter();
  public format(Thing thing) {
      thing2Formatter.format(thing.innerThing2);
  }
}
class Thing2Formatter {
  private static Thing1Formatter thing1Formatter = new Thing1Formatter();
  public format(Thing2 thing) {
      thing1Formatter.format(thing.innerThing1);
  }
}

是的,我知道使用HashMap和更多代码也可以解决这个问题。但相比之下,“instanceof”似乎更易于阅读和维护。有没有什么简单的方法而不会让人感到困扰?

注:已于2010年5月10日添加说明:

事实证明,未来可能会添加新的子类,我的现有代码必须优雅地处理它们。在这种情况下,Class上的HashMap将无法正常工作,因为找不到该类。一系列的if语句,从最具体的开始,以最普遍的结束,可能是最好的选择:

if (obj instanceof SubClass1) {
    // Handle all the methods and properties of SubClass1
} else if (obj instanceof SubClass2) {
    // Handle all the methods and properties of SubClass2
} else if (obj instanceof Interface3) {
    // Unknown class but it implements Interface3
    // so handle those methods and properties
} else if (obj instanceof Interface4) {
    // likewise.  May want to also handle case of
    // object that implements both interfaces.
} else {
    // New (unknown) subclass; do what I can with the base class
}

4
我建议使用访问者模式(visitor pattern)。 - lexicore
31
访问者模式需要在目标类(例如Integer)中添加一个方法,这在JavaScript中很容易实现,但在Java中较为困难。当设计目标类时,此模式非常有效;但是,当尝试让旧类学习新技巧时,则不那么容易。 - Mark Lutton
5
评论中的Markdown功能受限。请使用“文字”格式在评论中发布链接。 - BalusC
2
但是Java并不是那样工作的。也许我误解了一些事情,但是Java很好地支持方法重载(甚至是静态方法)……只是你上面的方法缺少返回类型。 - Powerlord
4
@Powerlord 过载决策在编译时是静态的。 - Aleksandr Dubinsky
显示剩余4条评论
10个回答

60

您可能对 Steve Yegge 在亚马逊博客中的这篇文章感兴趣:“当多态性失败时”。他主要讨论了在某些情况下,多态性会带来比解决问题更多的麻烦。

问题在于,为了使用多态性,您必须将“处理”逻辑作为每个“切换”类的一部分 - 即在此案例中的Integer等类。显然,这是不现实的。有时甚至逻辑上不是放置代码的正确位置。他建议采用“instanceof”方法,这是几种恶劣方案中较小的一个。

与所有需要编写不良代码的情况一样,请将其封装在一个方法中(或最多一个类),以使其不产生影响。


23
多态并不会失败。相反,是 Steve Yegge 没有发明访问者模式,而它是 instanceof 的完美替代品。 - Rotsor
13
我不明白访客在这里有什么帮助。关键是OpinionatedElf对NewMonster的回应不应该编码在NewMonster中,而应该编码在OpinionatedElf中。 - DJClayworth
2
示例的重点是OpinionatedElf无法从可用数据中判断它是否喜欢或不喜欢Monster。它必须知道Monster属于哪个类。这要求使用instanceof,或者Monster以某种方式知道OpinionatedElf是否喜欢它。访问者模式无法解决这个问题。 - DJClayworth
2
@DJClayworth 访问者模式通过向 Monster 类添加一个方法来解决这个问题,其职责基本上是介绍对象,例如“你好,我是兽人。你对我有什么看法?”。有主见的精灵可以根据这些“问候”来评判怪物,使用类似于 bool visitOrc(Orc orc) { return orc.stench()<threshold; } bool visitFlower(Flower flower) { return flower.colour==magenta; } 的代码。然后,唯一与怪物相关的代码将是 class Orc { <T> T accept(MonsterVisitor<T> v) { v.visitOrc(this); } },足以进行所有怪物检查。 - Rotsor
2
请参考@Chris Knight的答案,了解为什么访问者模式在某些情况下无法应用。 - James P.
显示剩余7条评论

20

正如评论中所强调的那样,访问者模式是一个不错的选择。但是如果没有对目标/接收器/被访问对象的直接控制,则无法实现该模式。这里有一种方法可以可能地在没有直接控制子类的情况下仍然使用访问者模式,即使用包装器(以Integer为例):

public class IntegerWrapper {
    private Integer integer;
    public IntegerWrapper(Integer anInteger){
        integer = anInteger;
    }
    //Access the integer directly such as
    public Integer getInteger() { return integer; }
    //or method passthrough...
    public int intValue() { return integer.intValue(); }
    //then implement your visitor:
    public void accept(NumericVisitor visitor) {
        visitor.visit(this);
    }
}
当然,将一个final类包装起来可能被认为是一种代码异味,但也许它与你的子类很搭配。就个人而言,在这里我不认为instanceof是一种多么糟糕的代码异味,特别是如果它被限制在一个方法中,我会很高兴地使用它(可能比上述建议更好)。正如你所说,它非常易读、类型安全和可维护。像往常一样,保持简单。

是的,“Formatter”、“Composite”、“Different types”似乎都指向了访问者模式的方向。 - Thomas Ahle
4
你如何确定要使用哪个封装器?通过 if instanceof 分支吗? - fast tooth
3
如@fasttooth所指出,这种解决方案只是转移了问题。现在你需要使用instanceof来调用正确的XWrapper构造函数,而不是使用它来调用正确的handle()方法... - Matthias

18

不要使用一个庞大的if语句,你可以将处理实例放入一个映射中(键:类,值:处理器)。

如果按键查找返回null,则调用一个特殊的处理程序方法来尝试查找匹配处理程序(例如通过在映射中的每个键上调用isInstance()来查找)。

当找到处理程序时,将其注册为新键下的处理程序。

这使得一般情况变得快速简便,并且允许你处理继承。


+1 当处理从XML模式或消息系统生成的代码时,我经常使用这种方法。在这些情况下,我的代码以一种基本上非类型安全的方式接收了数十种对象类型。 - DNA

16

您可以使用反射:

public final class Handler {
  public static void handle(Object o) {
    try {
      Method handler = Handler.class.getMethod("handle", o.getClass());
      handler.invoke(null, o);
    } catch (Exception e) {
      throw new RuntimeException(e);
    }
  }
  public static void handle(Integer num) { /* ... */ }
  public static void handle(BigDecimal num) { /* ... */ }
  // to handle new types, just add more handle methods...
}

你可以扩展这个想法,以通用的方式处理子类和实现特定接口的类。


42
我认为这比 instanceof 运算符更臭。但它应该能够工作。 - Tim Büthe
6
至少你不必处理日益增长的“if then else”链来添加、删除或修改处理程序。这样代码在更改时就不那么脆弱。因此,我认为它比“instanceof”方法优越。无论如何,我只是想提供一个有效的替代方案。 - Jordão
1
这基本上是动态语言如何通过鸭子类型处理情况的方式。 - DNA
1
为什么要遍历所有方法而不是使用 getMethod(String name, Class<?>... parameterTypes)?否则,我会将参数类型检查中的 == 替换为 isAssignableFrom - Aleksandr Dubinsky
1
@DipeshGupta:你说得完全正确,我想那应该是不言而喻的。 - Jordão
显示剩余4条评论

10
我认为最好的解决方案是使用Class作为键,Handler作为值的HashMap。请注意,基于HashMap的解决方案在常数算法复杂度θ(1)下运行,而if-instanceof-else的代码块则在线性算法复杂度O(N)下运行,其中N是要处理的不同类别的数量(即要处理的不同类别的数量)。因此,HashMap的基于解决方案的性能渐近高于if-instanceof-else链解决方案的性能N倍。 考虑到您需要以不同的方式处理Message类的不同子类:Message1、Message2等。下面是基于HashMap的处理代码片段。
public class YourClass {
    private class Handler {
        public void go(Message message) {
            // the default implementation just notifies that it doesn't handle the message
            System.out.println(
                "Possibly due to a typo, empty handler is set to handle message of type %s : %s",
                message.getClass().toString(), message.toString());
        }
    }
    private Map<Class<? extends Message>, Handler> messageHandling = 
        new HashMap<Class<? extends Message>, Handler>();

    // Constructor of your class is a place to initialize the message handling mechanism    
    public YourClass() {
        messageHandling.put(Message1.class, new Handler() { public void go(Message message) {
            //TODO: IMPLEMENT HERE SOMETHING APPROPRIATE FOR Message1
        } });
        messageHandling.put(Message2.class, new Handler() { public void go(Message message) {
            //TODO: IMPLEMENT HERE SOMETHING APPROPRIATE FOR Message2
        } });
        // etc. for Message3, etc.
    }

    // The method in which you receive a variable of base class Message, but you need to
    //   handle it in accordance to of what derived type that instance is
    public handleMessage(Message message) {
        Handler handler = messageHandling.get(message.getClass());
        if (handler == null) {
            System.out.println(
                "Don't know how to handle message of type %s : %s",
                message.getClass().toString(), message.toString());
        } else {
            handler.go(message);
        }
    }
}

关于Java中类型为Class的变量的使用更多信息: http://docs.oracle.com/javase/tutorial/reflect/class/classNew.html


3
对于少数情况(可能比任何真实示例中的类别数量都要高),if-else 语句会表现更好,此外它完全不使用堆内存。 - idelvall

9
你可以考虑使用责任链模式。对于你的第一个例子,可以这样实现:
public abstract class StuffHandler {
   private StuffHandler next;

   public final boolean handle(Object o) {
      boolean handled = doHandle(o);
      if (handled) { return true; }
      else if (next == null) { return false; }
      else { return next.handle(o); }
   }

   public void setNext(StuffHandler next) { this.next = next; }

   protected abstract boolean doHandle(Object o);
}

public class IntegerHandler extends StuffHandler {
   @Override
   protected boolean doHandle(Object o) {
      if (!o instanceof Integer) {
         return false;
      }
      NumberHandler.handle((Integer) o);
      return true;
   }
}

然后按照类似的方式处理其他处理程序。接下来需要按顺序(从最具体到最不具体,最后是“回退”处理程序)将StuffHandlers串联起来,您的分派器代码只需使用firstHandler.handle(o);即可。
(另一种选择是,在调度程序类中,而不是使用链,只需拥有一个List<StuffHandler>,并循环遍历该列表,直到handle()返回true)。

7

0

我曾经在泛型时代之前(大约15年前)使用反射解决了这个问题。

GenericClass object = (GenericClass) Class.forName(specificClassName).newInstance();

我定义了一个泛型类(抽象基类)。我定义了许多基类的具体实现。每个具体类将加载className作为参数。该类名是作为配置的一部分定义的。

基类定义了所有具体类中通用的状态,而具体类将通过覆盖在基类中定义的抽象规则来修改状态。

当时,我不知道这个机制的名称,它后来被称为反射

除了反射之外,在此文章中还列出了几种选择: Mapenum


只是好奇,为什么你没有将GenericClass设计成一个接口? - Ztyx
我有一些常见的状态和默认行为,需要在许多相关对象之间共享。 - Ravindra babu

0
在BaseClass中添加一个返回类名的方法。并使用特定的类名覆盖该方法。
在BaseClass中添加一个返回类名的方法。并使用特定的类名覆盖该方法。
public class BaseClass{
  // properties and methods
  public String classType(){
      return BaseClass.class.getSimpleName();
  }
}

public class SubClass1 extends BaseClass{
 // properties and methods
  @Override
  public String classType(){
      return SubClass1.class.getSimpleName();
  }
}

public class SubClass2 extends BaseClass{
 // properties and methods
  @Override
  public String classType(){
      return SubClass1.class.getSimpleName();
  }
}

现在以以下方式使用switch case-
switch(obj.classType()){
    case SubClass1:
        // do subclass1 task
        break;
    case SubClass2:
        // do subclass2 task
        break;
}

-1

我在Java 8中使用的工具:

void checkClass(Object object) {
    if (object.getClass().toString().equals("class MyClass")) {
    //your logic
    }
}

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