Evitando o instanceof em Java

Ter uma cadeia de operações “instanceof” é considerado um “cheiro de código”. A resposta padrão é “use polymorphism“. Como eu faria isso neste caso?

Existem várias subclasss de uma class base; nenhum deles está sob meu controle. Uma situação análoga seria com as classs Java Integer, Double, BigDecimal etc.

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

Eu tenho controle sobre NumberStuff e assim por diante.

Eu não quero usar muitas linhas de código onde algumas linhas fariam. (Às vezes eu faço um HashMap mapear Integer.class para uma instância de IntegerStuff, BigDecimal.class para uma instância de BigDecimalStuff etc. Mas hoje eu quero algo mais simples.)

Eu gostaria de algo tão simples como isto:

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

Mas o Java simplesmente não funciona dessa maneira.

Eu gostaria de usar methods estáticos ao formatar. As coisas que estou formatando são compostas, onde um Thing1 pode conter um array Thing2s e um Thing2 podem conter um array de Thing1s. Eu tive um problema quando implementei meus formatadores assim:

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

Sim, eu sei que o HashMap e um pouco mais de código podem consertar isso também. Mas o “exemplo de” parece tão legível e sustentável em comparação. Existe alguma coisa simples, mas não fedorenta?

Nota adicionada em 5/10/2010:

Acontece que novas subclasss provavelmente serão adicionadas no futuro, e meu código existente terá que lidar com elas graciosamente. O HashMap on Class não funcionará nesse caso porque a class não será encontrada. Uma cadeia de declarações if, começando com as mais específicas e terminando com as mais gerais, é provavelmente a melhor depois de tudo:

 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 } 

Você pode estar interessado nesta input do blog da Amazon de Steve Yegge: “when polymorphism fails” . Essencialmente, ele está lidando com casos como esse, quando o polymorphism causa mais problemas do que resolve.

A questão é que, para usar o polymorphism, é necessário fazer a lógica de “manipular” parte de cada class de “comutação” – isto é, Integer etc. neste caso. Claramente isso não é prático. Às vezes, nem é logicamente o lugar certo para colocar o código. Ele recomenda a abordagem do ‘exemplo de’ como sendo o menor de vários males.

Como em todos os casos em que você é forçado a escrever código fedorento, mantenha-o abotoado em um método (ou no máximo uma class) para que o cheiro não vaze.

Conforme destacado nos comentários, o padrão de visitantes seria uma boa escolha. Mas sem controle direto sobre o alvo / aceitador / visite você não pode implementar esse padrão. Aqui está uma maneira de o padrão de visitante poder ainda ser usado aqui mesmo que você não tenha controle direto sobre as subclasss usando wrappers (tomando Integer como exemplo):

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

É claro que envolver uma class final pode ser considerado um cheiro próprio, mas talvez seja um bom ajuste para suas subclasss. Pessoalmente, eu não acho que isso seja um cheiro ruim aqui, especialmente se estiver confinado a um método e eu ficaria feliz em usá-lo (provavelmente por causa da minha sugestão acima). Como você diz, é bastante legível, seguro e fácil de manter. Como sempre, mantenha as coisas simples.

Em vez de um enorme if , você pode colocar as instâncias manipuladas em um mapa (key: class, value: handler).

Se a consulta por chave retornar null , chame um método de manipulador especial que tente encontrar um manipulador correspondente (por exemplo, chamando isInstance() em cada chave no mapa).

Quando um manipulador é encontrado, registre-o sob a nova chave.

Isso torna o caso geral rápido e simples e permite que você manipule a inheritance.

Você pode usar a 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... } 

Você pode expandir a ideia para lidar genericamente com subclasss e classs que implementam determinadas interfaces.

Você pode considerar o padrão Cadeia de Responsabilidade . Para o seu primeiro exemplo, algo como:

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

e, em seguida, da mesma forma para seus outros manipuladores. Então é um caso de juntar os StuffHandlers em ordem (mais específico para menos específico, com um handler final de ‘fallback’), e seu código de despachante é apenas firstHandler.handle(o); .

(Uma alternativa é, em vez de usar uma cadeia, apenas ter uma List em sua class dispatcher e fazer com que ela passe pela lista até que handle() retorne true).

Eu acho que a melhor solução é HashMap com class como chave e manipulador como valor. Observe que a solução baseada em HashMap é executada em complexidade algorítmica constante θ (1), enquanto a cadeia olfativa if-instanceof-else é executada em complexidade algorítmica linear O (N), onde N é o número de links na cadeia if-instanceof-else (ou seja, o número de diferentes classs a serem tratadas). Portanto, o desempenho da solução baseada em HashMap é N mais assintoticamente maior que o desempenho da solução de cadeia if-instanceof-else. Considere que você precisa lidar com diferentes descendentes da class Message de maneira diferente: Message1, Message2, etc. Abaixo está o trecho de código para tratamento baseado em 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, Handler> messageHandling = new HashMap, 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); } } } 

Mais informações sobre o uso de variables ​​do tipo Classe em Java: http://docs.oracle.com/javase/tutorial/reflect/class/classNew.html

Apenas vá com o instanceof. Todas as soluções parecem mais complicadas. Aqui está uma postagem no blog que fala sobre isso: http://www.velocityreviews.com/forums/t302491-instanceof-not-always-bad-the-instanceof-myth.html

Eu resolvi esse problema usando a reflection (cerca de 15 anos atrás na era pré-genérica).

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

Eu defini uma class genérica (class base abstrata). Eu defini muitas implementações concretas da class base. Cada class concreta será carregada com className como parâmetro. Este nome de class é definido como parte da configuração.

A class base define o estado comum em todas as classs concretas e as classs concretas modificarão o estado sobrescrevendo regras abstratas definidas na class base.

Naquela época, não sei o nome desse mecanismo, conhecido como reflection .

Mais algumas alternativas estão listadas neste artigo : Map e enum apart from reflection.