Evitando instanceof in Java


102

Avere una catena di operazioni "instanceof" è considerato un "odore di codice". La risposta standard è "usa il polimorfismo". Come lo farei in questo caso?

Esistono numerose sottoclassi di una classe base; nessuno di loro è sotto il mio controllo. Una situazione analoga sarebbe con le classi Java Integer, Double, BigDecimal ecc.

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

Ho il controllo su NumberStuff e così via.

Non voglio usare molte righe di codice dove poche righe andrebbero bene. (A volte creo un HashMap mappando Integer.class su un'istanza di IntegerStuff, BigDecimal.class su un'istanza di BigDecimalStuff ecc. Ma oggi voglio qualcosa di più semplice.)

Vorrei qualcosa di semplice come questo:

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

Ma Java semplicemente non funziona in questo modo.

Vorrei utilizzare metodi statici durante la formattazione. Le cose che sto formattando sono composte, dove una Thing1 può contenere un array Thing2s e una Thing2 può contenere un array di Thing1s. Ho avuto un problema quando ho implementato i miei formattatori in questo modo:

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

Sì, conosco HashMap e un po 'più di codice può risolvere anche questo. Ma l '"istanza di" sembra così leggibile e gestibile al confronto. C'è qualcosa di semplice ma non puzzolente?

Nota aggiunta il 5/10/2010:

Si scopre che probabilmente in futuro verranno aggiunte nuove sottoclassi e il mio codice esistente dovrà gestirle con garbo. La HashMap sulla classe non funzionerà in questo caso perché la classe non verrà trovata. Una catena di istruzioni if, che inizia con la più specifica e termina con la più generale, è probabilmente la migliore dopo tutto:

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
Suggerirei un [modello visitatore] [1]. [1]: en.wikipedia.org/wiki/Visitor_pattern
lexicore

25
Il pattern Visitor richiede l'aggiunta di un metodo alla classe di destinazione (Integer per esempio): facile in JavaScript, difficile in Java. Modello eccellente durante la progettazione delle classi target; non è così facile quando si cerca di insegnare nuovi trucchi a una vecchia classe.
Mark Lutton

4
@lexicore: il markdown nei commenti è limitato. Utilizzare [text](link)per inserire collegamenti nei commenti.
BalusC

2
"Ma Java non funziona in questo modo." Forse sto fraintendendo le cose, ma Java supporta il sovraccarico del metodo (anche su metodi statici) bene ... è solo che ai tuoi metodi sopra manca il tipo di ritorno.
Powerlord

4
La risoluzione del sovraccarico di @Powerlord è statica in fase di compilazione .
Aleksandr Dubinsky

Risposte:


55

Potrebbe interessarti questa voce dal blog Amazon di Steve Yegge: "quando il polimorfismo fallisce" . Essenzialmente sta affrontando casi come questo, quando il polimorfismo causa più problemi di quanti ne risolva.

Il problema è che per usare il polimorfismo devi rendere la logica di "handle" parte di ogni classe di 'switching' - cioè Integer ecc. In questo caso. Chiaramente questo non è pratico. A volte non è nemmeno logicamente il posto giusto per mettere il codice. Raccomanda l'approccio "istanza di" come il minore di molti mali.

Come in tutti i casi in cui sei costretto a scrivere codice puzzolente, tienilo abbottonato in un metodo (o al massimo in una classe) in modo che l'odore non fuoriesca.


22
Il polimorfismo non fallisce. Piuttosto, Steve Yegge non riesce a inventare il modello Visitor, che è il sostituto perfetto per instanceof.
Rotsor

12
Non vedo come il visitatore aiuta qui. Il punto è che la risposta di OpinionatedElf a NewMonster non dovrebbe essere codificata in NewMonster, ma in OpinionatedElf.
DJClayworth

2
Il punto dell'esempio è che OpinionatedElf non può dire dai dati disponibili se gli piace o non gli piace il Mostro. Deve sapere a quale classe appartiene il mostro. Ciò richiede o un'istanza di, oppure il Mostro deve sapere in qualche modo se piace all'Elfo Opinione. Il visitatore non lo aggira.
DJClayworth,

2
@DJClayworth Il pattern Visitor lo aggira aggiungendo un metodo alla Monsterclasse, la cui responsabilità è fondamentalmente quella di introdurre l'oggetto, come "Ciao, sono un Orco. Cosa ne pensi di me?". L'elfo opinionista può quindi giudicare i mostri in base a questi "saluti", con un codice simile a bool visitOrc(Orc orc) { return orc.stench()<threshold; } bool visitFlower(Flower flower) { return flower.colour==magenta; }. L'unico codice specifico del mostro sarà quindi class Orc { <T> T accept(MonsterVisitor<T> v) { v.visitOrc(this); } }sufficiente per ogni ispezione del mostro una volta per tutte.
Rotsor

2
Vedi la risposta di @Chris Knight per il motivo per cui Visitor non può essere applicato in alcuni casi.
James P.

20

Come evidenziato nei commenti, il pattern del visitatore sarebbe una buona scelta. Ma senza il controllo diretto sulla destinazione / accettatore / visitatore non è possibile implementare quel modello. Ecco un modo in cui il pattern del visitatore potrebbe essere ancora utilizzato qui anche se non hai il controllo diretto sulle sottoclassi utilizzando wrapper (prendendo Integer come esempio):

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

Naturalmente, concludere una lezione finale potrebbe essere considerato un odore a sé stante, ma forse si adatta bene alle tue sottoclassi. Personalmente, non penso instanceofche qui sia un cattivo odore, specialmente se è limitato a un metodo e lo userei volentieri (probabilmente sopra il mio suggerimento sopra). Come dici tu, è abbastanza leggibile, sicuro per i caratteri e manutenibile. Come sempre, mantienilo semplice.


Sì, "Formatter", "Composite", "Diversi tipi" puntano tutti nella direzione del visitatore.
Thomas Ahle

3
come si determina quale wrapper si intende utilizzare? attraverso un'istanza di ramificazione if?
dente veloce

2
Come sottolinea @fasttooth, questa soluzione sposta solo il problema. Invece di usare instanceofper chiamare il handle()metodo giusto , ora dovrai usarlo per chiamare il XWrappercostruttore giusto ...
Matthias

15

Invece di un enorme if, puoi mettere le istanze che gestisci in una mappa (chiave: classe, valore: gestore).

Se la ricerca per chiave ritorna null, chiama un metodo gestore speciale che cerca di trovare un gestore corrispondente (ad esempio chiamando isInstance()su ogni chiave nella mappa).

Quando viene trovato un gestore, registralo con la nuova chiave.

Ciò rende il caso generale veloce e semplice e consente di gestire l'ereditarietà.


+1 Ho utilizzato questo approccio durante la gestione del codice generato da schemi XML, o sistema di messaggistica, dove ci sono dozzine di tipi di oggetti, passati al mio codice in un modo essenzialmente non tipografico.
DNA

13

Puoi usare la riflessione:

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

È possibile espandere l'idea di gestire genericamente sottoclassi e classi che implementano determinate interfacce.


34
Direi che questo odora ancora di più dell'operatore instanceof. Dovrebbe funzionare comunque.
Tim Büthe

5
@Tim Büthe: Almeno non devi affrontare una if then elsecatena in crescita per aggiungere, rimuovere o modificare i gestori. Il codice è meno fragile ai cambiamenti. Quindi direi che per questo motivo è superiore instanceofall'approccio. Comunque volevo solo dare una valida alternativa.
Jordão

1
Questo è essenzialmente il modo in cui un linguaggio dinamico gestirà la situazione, tramite la digitazione a papera
DNA

@ DNA: non sarebbero multimetodi ?
Jordão

1
Perché iterate su tutti i metodi invece di usarli getMethod(String name, Class<?>... parameterTypes)? Oppure lo sostituirei ==con isAssignableFromper il controllo del tipo del parametro.
Aleksandr Dubinsky

9

Potresti considerare il modello della catena di responsabilità . Per il tuo primo esempio, qualcosa come:

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 poi allo stesso modo per gli altri tuoi gestori. Quindi si tratta di mettere insieme gli StuffHandlers in ordine (dal più specifico al meno specifico, con un gestore finale di "fallback") e il codice del mittente è giusto firstHandler.handle(o);.

(Un'alternativa è, piuttosto che usare una catena, avere semplicemente un List<StuffHandler>nella tua classe dispatcher e farlo scorrere nell'elenco finché non handle()restituisce true).


9

Penso che la soluzione migliore sia HashMap con Class come chiave e Handler come valore. Si noti che la soluzione basata su HashMap viene eseguita in una complessità algoritmica costante θ (1), mentre la catena dell'odore di if-instanceof-else funziona nella complessità algoritmica lineare O (N), dove N è il numero di collegamenti nella catena if-instanceof-else (cioè il numero di classi differenti da trattare). Quindi le prestazioni della soluzione basata su HashMap sono N volte asintoticamente superiori rispetto alle prestazioni della soluzione a catena if-instanceof-else. Considera che devi gestire diversi discendenti della classe Message in modo diverso: Message1, Message2, ecc. Di seguito è riportato lo snippet di codice per la gestione basata su 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);
        }
    }
}

Maggiori informazioni sull'utilizzo delle variabili di tipo Class in Java: http://docs.oracle.com/javase/tutorial/reflect/class/classNew.html


per un piccolo numero di casi (probabilmente superiore al numero di quelle classi per qualsiasi esempio reale) il if-else sovraperformerebbe la mappa, oltre a non usare affatto la memoria heap
idelvall


0

Ho risolto questo problema usando reflection(circa 15 anni fa nell'era pre Generics).

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

Ho definito una classe generica (classe base astratta). Ho definito molte implementazioni concrete della classe base. Ogni classe concreta verrà caricata con className come parametro. Questo nome di classe è definito come parte della configurazione.

La classe base definisce lo stato comune tra tutte le classi concrete e le classi concrete modificheranno lo stato sovrascrivendo le regole astratte definite nella classe base.

A quel tempo, non conosco il nome di questo meccanismo, che è stato conosciuto come reflection.

Poche altre alternative sono elencate in questo articolo : Mapea enumparte la riflessione.


Solo curioso, perché non ne hai fatto GenericClassun interface?
Ztyx

Avevo uno stato comune e un comportamento predefinito, che deve essere condiviso tra molti oggetti correlati
Ravindra babu
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.