Evitando instanceof em Java


102

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

Existem várias subclasses de uma classe base; nenhum deles está sob meu controle. Uma situação análoga seria com as classes 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.

Não quero usar muitas linhas de código onde algumas linhas serviriam. (Às vezes eu faço um HashMap mapeando 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 quanto isto:

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

Mas Java simplesmente não funciona dessa maneira.

Eu gostaria de usar métodos estáticos durante a formatação. As coisas que estou formatando são compostas, em que Thing1 pode conter um array Thing2s e um Thing2 pode conter um array de Thing1s. Tive um problema quando implementei meus formatadores como este:

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 conheço o HashMap e um pouco mais de código pode consertar isso também. Mas o "instanceof" parece tão legível e sustentável em comparação. Existe algo simples, mas não fedorento?

Nota adicionada em 10/05/2010:

Acontece que novas subclasses provavelmente serão adicionadas no futuro e meu código existente terá que lidar com elas normalmente. O HashMap na classe não funcionará nesse caso porque a classe não será encontrada. Uma cadeia de instruções if, começando com a mais específica e terminando com a mais geral, é provavelmente a melhor, afinal:

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
Eu sugeriria um [padrão de visitante] [1]. [1]: en.wikipedia.org/wiki/Visitor_pattern
lexicore

25
O padrão Visitor requer a adição de um método à classe de destino (Integer, por exemplo) - fácil em JavaScript, difícil em Java. Excelente padrão ao projetar as classes de destino; não é tão fácil quando se tenta ensinar novos truques a uma velha classe.
Mark Lutton

4
@lexicore: a redução nos comentários é limitada. Use [text](link)para postar links em comentários.
BalusC

2
"Mas Java simplesmente não funciona assim." Talvez eu esteja entendendo mal as coisas, mas Java suporta sobrecarga de método (mesmo em métodos estáticos) muito bem ... é que seus métodos acima não têm o tipo de retorno.
Powerlord

4
@Powerlord A resolução de sobrecarga é estática em tempo de compilação .
Aleksandr Dubinsky

Respostas:


55

Você pode estar interessado nesta entrada do blog de Steve Yegge na Amazon: "quando o polimorfismo falha" . Essencialmente, ele está tratando de casos como este, quando o polimorfismo causa mais problemas do que resolve.

O problema é que para usar o polimorfismo você tem que fazer a lógica de "manipular" parte de cada classe de 'comutação' - ou seja, inteiro etc. neste caso. Obviamente, isso não é prático. Às vezes, nem é logicamente o lugar certo para colocar o código. Ele recomenda a abordagem 'instância' como sendo o menor de vários males.

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


22
O polimorfismo não falha. Em vez disso, Steve Yegge falha em inventar o padrão Visitor, que é o substituto perfeito para instanceof.
Rotsor

12
Não vejo como o visitante ajuda aqui. A questão é que a resposta de OpinionatedElf para NewMonster não deve ser codificada em NewMonster, mas em OpinionatedElf.
DJClayworth

2
O ponto do exemplo é que OpinionatedElf não pode dizer, a partir dos dados disponíveis, se ele gosta ou não do Monstro. Ele tem que saber a que classe o monstro pertence. Isso requer uma instância de, ou o Monstro precisa saber de alguma forma se o OpinionatedElf gosta. O visitante não contorna isso.
DJClayworth

2
Padrão @DJClayworth Visitor se contornar isso adicionando um método para Monsterclasse, responsabilidade que é basicamente para introduzir o objeto, como "Olá, eu sou um Orc. O que você acha sobre mim?". O elfo opinativo pode então julgar os monstros com base nessas "saudações", com um código semelhante a bool visitOrc(Orc orc) { return orc.stench()<threshold; } bool visitFlower(Flower flower) { return flower.colour==magenta; }. O único código específico do monstro será o class Orc { <T> T accept(MonsterVisitor<T> v) { v.visitOrc(this); } }suficiente para cada inspeção de monstro de uma vez por todas.
Rotsor

2
Veja a resposta de @Chris Knight para a razão pela qual o Visitante não pode ser aplicado em alguns casos.
James P.

20

Conforme destacado nos comentários, o padrão do visitante seria uma boa escolha. Mas sem controle direto sobre o alvo / aceitador / visitee você não pode implementar esse padrão. Esta é uma maneira pela qual o padrão de visitante ainda pode ser usado aqui, mesmo que você não tenha controle direto sobre as subclasses 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, encerrar uma aula final pode ser considerado um cheiro próprio, mas talvez seja um bom ajuste para suas subclasses. Pessoalmente, não acho instanceofque seja um cheiro tão ruim aqui, especialmente se for confinado a um método e eu ficaria feliz em usá-lo (provavelmente sobre minha própria sugestão acima). Como você disse, é bastante legível, seguro para digitação e manutenção. Como sempre, mantenha as coisas simples.


Sim, "Formatador", "Composto", "Tipos diferentes" todas as costuras apontam na direção do visitante.
Thomas Ahle

3
como você determina qual wrapper usará? por meio de uma instância if de ramificação?
dente rápido

2
Como @fasttooth aponta, essa solução apenas muda o problema. Em vez de usar instanceofpara chamar o handle()método certo , você agora terá que usá-lo para chamar o XWrapperconstrutor certo ...
Matthias

15

Em vez de um enorme if, você pode colocar as instâncias que manipula em um mapa (chave: classe, valor: manipulador).

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

Quando um manipulador for encontrado, registre-o na nova chave.

Isso torna o caso geral rápido e simples e permite que você controle a herança.


+1 Eu usei essa abordagem ao lidar com o código gerado a partir de esquemas XML, ou sistema de mensagens, onde existem dezenas de tipos de objetos, entregues ao meu código de uma forma essencialmente não segura de tipos.
DNA de

13

Você pode usar reflexão:

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 de lidar genericamente com subclasses e classes que implementam certas interfaces.


34
Eu diria que isso cheira ainda mais do que o operador instanceof. Deve funcionar embora.
Tim Büthe de

5
@Tim Büthe: Pelo menos você não precisa lidar com uma if then elsecadeia crescente para adicionar, remover ou modificar manipuladores. O código é menos frágil a mudanças. Então eu diria que por esse motivo é superior à instanceofabordagem. De qualquer forma, só queria dar uma alternativa válida.
Jordão

1
É essencialmente assim que uma linguagem dinâmica lidaria com a situação, por meio de digitação de pato
DNA de

@DNA: isso não seria multimétodos ?
Jordão

1
Por que você itera em todos os métodos em vez de usar getMethod(String name, Class<?>... parameterTypes)? Ou então eu substituiria ==por isAssignableFrompara a verificação de tipo do parâmetro.
Aleksandr Dubinsky

9

Você pode considerar o padrão da 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 da mesma forma para seus outros manipuladores. Então, é o caso de amarrar os StuffHandlers em ordem (do mais específico para o menos específico, com um manipulador de 'fallback' final) e seu código de despachante é justo firstHandler.handle(o);.

(Uma alternativa é, em vez de usar uma cadeia, apenas ter um List<StuffHandler>em sua classe de despachante e fazê-lo percorrer a lista até handle()retornar verdadeiro).


9

Acho que a melhor solução é HashMap com Class como chave e Handler como valor. Observe que a solução baseada em HashMap é executada em complexidade algorítmica constante θ (1), enquanto a cadeia de cheiro de 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 classes diferentes a serem tratadas). Portanto, o desempenho da solução baseada em HashMap é assintoticamente maior N vezes do que o desempenho da solução de cadeia if-instanceof-else. Considere que você precisa lidar com diferentes descendentes da classe Message de forma diferente: Message1, Message2, etc. Abaixo está o snippet de código para manipulação baseada 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<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);
        }
    }
}

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


para um pequeno número de casos (provavelmente maior do que o número dessas classes para qualquer exemplo real) o if-else teria um desempenho melhor do que o mapa, além de não usar a memória heap
idelvall


0

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

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

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

A classe base define o estado comum em todas as classes concretas e as classes concretas modificarão o estado substituindo as regras abstratas definidas na classe base.

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

Poucas alternativas são listadas neste artigo : Mape enumalém da reflexão.


Só por curiosidade, por que você não fez GenericClassum interface?
Ztyx

Eu tinha estado comum e comportamento padrão, que deve ser compartilhado por muitos objetos relacionados
Ravindra babu
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.