在 Java 中避免 instanceof

拥有一个“ 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 无法工作,因为找不到 Class。一系列 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
}
81726 次浏览

You might be interested in this entry from Steve Yegge's Amazon blog: "when polymorphism fails". Essentially he's addressing cases like this, when polymorphism causes more trouble than it solves.

The issue is that to use polymorphism you have to make the logic of "handle" part of each 'switching' class - i.e. Integer etc. in this case. Clearly this is not practical. Sometimes it isn't even logically the right place to put the code. He recommends the 'instanceof' approach as being the lesser of several evils.

As with all cases where you are forced to write smelly code, keep it buttoned up in one method (or at most one class) so that the smell doesn't leak out.

As highlighted in the comments, the visitor pattern would be a good choice. But without direct control over the target/acceptor/visitee you can't implement that pattern. Here's one way the visitor pattern could possibly still be used here even though you have no direct control over the subclasses by using wrappers (taking Integer as an example):

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

Of course, wrapping a final class might be considered a smell of its own but maybe it's a good fit with your subclasses. Personally, I don't think instanceof is that bad a smell here, especially if it is confined to one method and I would happily use it (probably over my own suggestion above). As you say, its quite readable, typesafe and maintainable. As always, keep it simple.

Instead of a huge if, you can put the instances you handle in a map (key: class, value: handler).

If the lookup by key returns null, call a special handler method which tries to find a matching handler (for example by calling isInstance() on every key in the map).

When a handler is found, register it under the new key.

This makes the general case fast and simple and allows you to handle inheritance.

You could consider the Chain of Responsibility pattern. For your first example, something like:

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

and then similarly for your other handlers. Then it's a case of stringing together the StuffHandlers in order (most specific to least specific, with a final 'fallback' handler), and your despatcher code is just firstHandler.handle(o);.

(An alternative is to, rather than using a chain, just have a List<StuffHandler> in your dispatcher class, and have it loop through the list until handle() returns true).

You can use reflection:

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

You can expand on the idea to generically handle subclasses and classes that implement certain interfaces.

Just go with the instanceof. All the workarounds seem more complicated. Here is a blog post that talks about it: http://www.velocityreviews.com/forums/t302491-instanceof-not-always-bad-the-instanceof-myth.html

I think that the best solution is HashMap with Class as key and Handler as value. Note that HashMap based solution runs in constant algorithmic complexity θ(1), while the smelling chain of if-instanceof-else runs in linear algorithmic complexity O(N), where N is the number of links in the if-instanceof-else chain (i.e. the number of different classes to be handled). So the performance of HashMap based solution is asymptotically higher N times than the performance of if-instanceof-else chain solution. Consider that you need to handle different descendants of Message class differently: Message1, Message2, etc. . Below is the code snippet for HashMap based handling.

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

More info on usage of variables of type Class in Java: http://docs.oracle.com/javase/tutorial/reflect/class/classNew.html

I have solved this problem using reflection (around 15 years back in pre Generics era).

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

I have defined one Generic Class ( abstract Base class). I have defined many concrete implementations of base class. Each concrete class will be loaded with className as parameter. This class name is defined as part of configuration.

Base class defines common state across all concrete classes and concrete classes will modify the state by overriding abstract rules defined in base class.

At that time, I don't know the name of this mechanism, which has been known as reflection.

Few more alternatives are listed in this article : Map and enum apart from reflection.

Add a method in BaseClass which returns name of the class. And override the methods with the specific class name

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

Now use the switch case in following way-

switch(obj.classType()){
case SubClass1:
// do subclass1 task
break;
case SubClass2:
// do subclass2 task
break;
}

What I use for Java 8:

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