在Java中避免使用instanceof

Mar*_*ton 96 java reflection polymorphism instanceof chain-of-responsibility

具有一系列"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);}
Run Code Online (Sandbox Code Playgroud)

我确实可以控制NumberStuff等等.

我不想在几行代码中使用多行代码.(有时我将一个HashMap映射到一个IntegerStuff的实例,将BigDecimal.class映射到一个BigDecimalStuff的实例等等.但是今天我想要一些更简单的东西.)

我想要像这样简单的东西:

public static handle(Integer num) { ... }
public static handle(BigDecimal num) { ... }
Run Code Online (Sandbox Code Playgroud)

但是Java不会那样工作.

我想在格式化时使用静态方法.我正在格式化的东西是复合的,其中Thing1可以包含一个数组Thing2s和Thing2可以包含一个Thing1s数组.当我实现这样的格式化程序时,我遇到了问题:

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);
  }
}
Run Code Online (Sandbox Code Playgroud)

是的,我知道HashMap和更多代码也可以修复它.但相比之下,"instanceof"似乎更具可读性和可维护性.有什么简单但不臭吗?

注释已添加5/10/2010:

事实证明,将来可能会添加新的子类,而我现有的代码必须优雅地处理它们.在这种情况下,类上的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
}
Run Code Online (Sandbox Code Playgroud)

DJC*_*rth 52

您可能对Steve Yegge的亚马逊博客中的这篇文章感兴趣:"当多态失败时".基本上他正在解决这样的情况,当多态性导致比它解决的更多麻烦时.

问题是,要使用多态,你必须使"处理"逻辑成为每个"切换"类的一部分 - 即在这种情况下为整数等.显然这不切实际.有时甚至在逻辑上它都不是放置代码的正确位置.他建议将'instanceof'方法视为几种邪恶中较小的一种.

与您被迫编写臭代码的所有情况一样,请将其保持在一个方法(或最多一个类)中,以便气味不会泄漏.

  • 多态性不会失败.相反,Steve Yegge未能发明访问者模式,这是`instanceof`的完美替代品. (21认同)
  • 我不知道访客在这里有何帮助.关键是OpinionatedElf对NewMonster的响应不应该在NewMonster中编码,而是在OpinionatedElf中编码. (11认同)
  • 示例的要点是OpinionatedElf无法从可用数据中判断出它是喜欢还是不喜欢Monster.它必须知道怪物属于什么类.这需要一个instanceof,或者Monster必须以某种方式知道OpinionatedElf是否喜欢它.访客不能解决这个问题. (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); `,足以让每个怪物一劳永逸地检查. (2认同)
  • 有关某些情况下无法应用“访客”的原因,请参阅@Chris Knight的答案。 (2认同)

Chr*_*ght 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);
    }
}
Run Code Online (Sandbox Code Playgroud)

当然,包装最终类可能被认为是它自己的气味,但也许它很适合你的子类.就我个人而言,我不认为instanceof这里有一种不好的气味,特别是如果它仅限于一种方法,我很乐意使用它(可能超过我自己的建议).正如你所说,它具有可读性,类型安全性和可维护性.一如既往,保持简单.

  • 你如何确定你将使用哪个包装?通过if instanceof分支? (3认同)
  • 正如@fasttooth指出的那样,此解决方案只能解决问题。现在不必使用`instanceof`来调用正确的`handle()`方法,而必须使用它来调用正确的`XWrapper`构造函数... (2认同)

Aar*_*lla 15

if您可以将您处理的实例放在地图中(键:class,value:handler),而不是巨大的.

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

找到处理程序后,在新密钥下注册它.

这使得一般情况变得快速而简单,并允许您处理继承.


Jor*_*dão 13

你可以使用反射:

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...
}
Run Code Online (Sandbox Code Playgroud)

您可以扩展这个想法,一般地处理实现某些接口的子类和类.

  • 我认为这比实例运算符更有气味.应该工作. (31认同)
  • @TimBüthe:至少你不需要处理一个不断增长的`if then else`链来添加,删除或修改处理程序.代码不易变化.所以我会说因为这个原因它优于`instanceof`方法.无论如何,我只想提供一个有效的替代方案. (4认同)

Cow*_*wan 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;
   }
}
Run Code Online (Sandbox Code Playgroud)

然后类似地为您的其他处理程序.然后是按顺序将StuffHandler串起来的情况(最具体到最不具体,最后的'回退'处理程序),而你的调度程序代码就是firstHandler.handle(o);.

(另一种方法是,而不是使用链,只需List<StuffHandler>在您的调度程序类中有一个,并让它循环遍历列表,直到handle()返回true).


Ser*_*tch 9

我认为最好的解决方案是HashMap,其中Class为键,Handler为value.请注意,基于HashMap的解决方案以恒定的算法复杂度θ(1)运行,而if-instanceof-else的气味链以线性算法复杂度O(N)运行,其中N是if-instanceof-else链中的链接数(即要处理的不同类的数量).因此,基于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);
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

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

  • 对于少数情况(可能高于任何实际示例中这些类的数量),除了根本不使用堆内存之外,if-else 的性能会优于映射 (3认同)

Jos*_*nez 6

只需要使用instanceof.所有的解决方法似乎都更复杂.这是一篇关于它的博客文章:http: //www.velocityreviews.com/forums/t302491-instanceof-not-always-bad-the-instanceof-myth.html