Evitar instanceof en Java


Tener una cadena de operaciones "instanceof" se considera un "olor a código". La respuesta estándar es "usar polimorfismo". ¿Cómo lo haría en este caso?

Hay un número de subclases de una clase base; ninguna de ellas está bajo mi control. Una situación análoga sería con las clases 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);}

Tengo control sobre NumberStuff y así sucesivamente.

No quiero usar muchas líneas de código donde bastarían unas pocas líneas. (A veces cree un entero de asignación de HashMap.clase a una instancia de IntegerStuff, BigDecimal.clase a una instancia de BigDecimalStuff etc. Pero hoy quiero algo más simple.)

Me gustaría algo tan simple como esto:

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

Pero Java simplemente no funciona de esa manera.

Me gustaría usar métodos estáticos al formatear. Las cosas que estoy formateando son compuestas, donde una Thing1 puede contener una matriz Thing2s y una Thing2 puede contener una matriz de Thing1s. Tuve un problema cuando implementé mi formateadores 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);
  }
}

Sí, sé que el HashMap y un poco más de código también pueden arreglar eso. Pero la "instanceof" parece tan legible y mantenible en comparación. ¿Hay algo simple pero no apestoso?

Nota añadida el 5/10/2010:

Resulta que probablemente se agregarán nuevas subclases en el futuro, y mi código existente tendrá que manejarlas con gracia. El HashMap en la Clase no funcionará en ese caso porque la Clase no será encontrada. Una cadena de sentencias if, comenzando con el más específico y terminando con el más general, es probablemente el mejor después de todo:

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
}
Author: Ravindra babu, 2010-05-07

8 answers

Te puede interesar esta entrada del blog de Steve Yegge en Amazon: "cuando el polimorfismo falla". Esencialmente está abordando casos como este, cuando el polimorfismo causa más problemas de los que resuelve.

El problema es que para usar polimorfismo tienes que hacer que la lógica de "handle" forme parte de cada clase 'switching' - es decir, Entero, etc. en este caso. Es evidente que esto no es práctico. A veces ni siquiera es lógicamente el lugar correcto para poner el código. Recomienda la "instanceof" enfoque como el menor de varios males.

Al igual que con todos los casos en los que se ve obligado a escribir código maloliente, manténgalo abotonado en un método (o a lo sumo en una clase) para que el olor no se escape.

 48
Author: DJClayworth,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2010-05-07 16:51:34

Como se destaca en los comentarios, el patrón de visitante sería una buena opción. Pero sin control directo sobre el destino/aceptador/visitante no se puede implementar ese patrón. Aquí hay una forma en que el patrón de visitante podría usarse aquí, aunque no tenga control directo sobre las subclases mediante el uso de envoltorios (tomando Entero como ejemplo):

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

Por supuesto, envolver una clase final podría considerarse un olor propio, pero tal vez sea un buen ajuste con sus subclases. Personalmente, no creo que instanceof sea un mal olor aquí, especialmente si se limita a un método y lo usaría felizmente (probablemente por encima de mi propia sugerencia anterior). Como usted dice, es bastante legible, typesafe y mantenible. Como siempre, hazlo simple.

 20
Author: Chris Knight,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2013-12-23 11:01:16

En lugar de un enorme if, puede poner las instancias que maneja en un mapa (key: class, value: handler).

Si la búsqueda por clave devuelve null, llame a un método de controlador especial que intente encontrar un controlador coincidente (por ejemplo, llamando a isInstance() en cada clave del mapa).

Cuando se encuentre un controlador, regístrelo bajo la nueva clave.

Esto hace que el caso general sea rápido y simple y le permite manejar la herencia.

 15
Author: Aaron Digulla,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2010-06-05 22:43:01

Puedes usar reflexión:

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

Puede ampliar la idea para manejar genéricamente subclases y clases que implementan ciertas interfaces.

 13
Author: Jordão,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2015-01-23 02:38:06

Podrías considerar el patrón de la Cadena de Responsabilidad. Para tu primer ejemplo, 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;
   }
}

Y luego de manera similar para sus otros controladores. Entonces es un caso de encadenar los StuffHandlers en orden (de lo más específico a lo menos específico, con un controlador de 'reserva' final), y su código de despatcher es simplemente firstHandler.handle(o);.

(Una alternativa es, en lugar de usar una cadena, simplemente tener un List<StuffHandler> en su clase dispatcher, y tenerlo en bucle a través de la lista hasta handle() devuelve true).

 9
Author: Cowan,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2010-06-06 04:21:51

Creo que la mejor solución es HashMap con Class como clave y Handler como valor. Tenga en cuenta que la solución basada en HashMap se ejecuta en complejidad algorítmica constante θ(1), mientras que la cadena olfativa de if-instanceof-else se ejecuta en complejidad algorítmica lineal O(N), donde N es el número de enlaces en la cadena if-instanceof-else (es decir, el número de diferentes clases a manejar). Por lo tanto, el rendimiento de la solución basada en HashMap es asintóticamente mayor N veces que el rendimiento de if-instanceof-else solución de cadena. Tenga en cuenta que necesita manejar diferentes descendientes de la clase Message de manera diferente: Message1, Message2, etc. . A continuación se muestra el fragmento de código para el manejo basado en 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);
        }
    }
}

Más información sobre el uso de variables de tipo Clase en Java: http://docs.oracle.com/javase/tutorial/reflect/class/classNew.html

 9
Author: Serge Rogatch,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2016-10-28 09:02:58

Simplemente vaya con la instanceof. Todas las soluciones parecen más complicadas. Aquí hay una entrada de blog que habla de ello: http://www.velocityreviews.com/forums/t302491-instanceof-not-always-bad-the-instanceof-myth.html

 5
Author: Jose Martinez,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2012-07-18 23:37:54

He resuelto este problema usando reflection (alrededor de 15 años atrás en la era pre Genéricos).

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

He definido una Clase Genérica (clase Base abstracta). He definido muchas implementaciones concretas de clase base. Cada clase concreta se cargará con className como parámetro. Este nombre de clase se define como parte de la configuración.

La clase base define el estado común en todas las clases concretas y las clases concretas modificarán el estado anulando las reglas abstractas definidas en clase base.

En ese momento, no conozco el nombre de este mecanismo, que se ha conocido como reflection.

Algunas alternativas más se enumeran en este artículo : Map y enum aparte de la reflexión.

 0
Author: Ravindra babu,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2016-01-14 18:28:28