¿Cómo hacer una clase Java que implemente una interfaz con dos tipos genéricos?

164

Tengo una interfaz genérica

public interface Consumer<E> {
    public void consume(E e);
}

Tengo una clase que consume dos tipos de objetos, por lo que me gustaría hacer algo como:

public class TwoTypesConsumer implements Consumer<Tomato>, Consumer<Apple>
{
   public void consume(Tomato t) {  .....  }
   public void consume(Apple a) { ...... }
}

Aparentemente no puedo hacer eso.

Por supuesto, puedo implementar el envío yo mismo, por ejemplo

public class TwoTypesConsumer implements Consumer<Object> {
   public void consume(Object o) {
      if (o instanceof Tomato) { ..... }
      else if (o instanceof Apple) { ..... }
      else { throw new IllegalArgumentException(...) }
   }
}

Pero estoy buscando la solución de verificación y envío de tipos en tiempo de compilación que ofrecen los genéricos.

La mejor solución que se me ocurre es definir interfaces separadas, p. Ej.

public interface AppleConsumer {
   public void consume(Apple a);
}

Funcionalmente, esta solución está bien, creo. Es simplemente detallado y feo.

¿Algunas ideas?

daphshez
fuente
¿Por qué necesita dos interfaces genéricas del mismo tipo de base?
akarnokd
66
Debido al tipo de borrado no puedes hacer eso. Mantenga dos clases diferentes que implementen el consumidor. Hace más clases pequeñas pero mantiene su código genérico (No use la respuesta aceptada, rompe todo el concepto ... no puede tratar a TwoTypesConsumer como un consumidor, lo cual es MALO).
Lewis Diamond
Compruebe esto para ver el estilo funcional impl - stackoverflow.com/a/60466413/4121845
mano_ksp

Respuestas:

78

Considere la encapsulación:

public class TwoTypesConsumer {
    private TomatoConsumer tomatoConsumer = new TomatoConsumer();
    private AppleConsumer appleConsumer = new AppleConsumer();

    public void consume(Tomato t) { 
        tomatoConsumer.consume(t);
    }

    public void consume(Apple a) { 
        appleConsumer.consume(a);
    }

    public static class TomatoConsumer implements Consumer<Tomato> {
        public void consume(Tomato t) {  .....  }
    }

    public static class AppleConsumer implements Consumer<Apple> {
        public void consume(Apple a) {  .....  }
    }
}

Si crear estas clases internas estáticas te molesta, puedes usar clases anónimas:

public class TwoTypesConsumer {
    private Consumer<Tomato> tomatoConsumer = new Consumer<Tomato>() {
        public void consume(Tomato t) {
        }
    };

    private Consumer<Apple> appleConsumer = new Consumer<Apple>() {
        public void consume(Apple a) {
        }
    };

    public void consume(Tomato t) {
        tomatoConsumer.consume(t);
    }

    public void consume(Apple a) {
        appleConsumer.consume(a);
    }
}
Steve McLeod
fuente
2
de alguna manera parece una duplicación de código ... Encontré el mismo problema y no encontré ninguna otra solución que se vea limpia.
bln-tom
109
Pero noTwoTypesConsumer cumple ningún contrato, ¿cuál es el punto? No se puede pasar a un método que quiera cualquier tipo de Consumer. La idea general de un consumidor de dos tipos sería que se lo puede dar a un método que quiere un consumidor de tomate, así como a un método que quiere un consumidor de manzana. Aquí no tenemos ninguno.
Jeff Axelrod
@JeffAxelrod Yo haría que las clases internas no sean estáticas para que tengan acceso a la TwoTypesConsumerinstancia de cierre si es necesario, y luego puede pasar twoTypesConsumer.getAppleConsumer()a un método que quiera un consumidor de manzanas. Otra opción sería agregar métodos similares addConsumer(Producer<Apple> producer)a TwoTypesConsumer.
herman
Esto no funciona si no tiene control sobre la interfaz (por ejemplo, cxf / rs ExceptionMapper) ...
vikingsteve
17
Lo diré: esto es una falla con Java. No hay absolutamente ninguna razón para que no se nos permita tener múltiples implementaciones de la misma interfaz, siempre que las implementaciones tomen diferentes argumentos.
gromit190
41

Debido a la eliminación de tipo, no puede implementar la misma interfaz dos veces (con diferentes parámetros de tipo).

Shimi Bandiel
fuente
66
Puedo ver cómo es un problema ... la pregunta es entonces cuál es la mejor manera (la más eficiente, segura y elegante) para evitar este problema.
daphshez
2
Sin entrar en la lógica de negocios, algo aquí 'huele' a patrón de visitante.
Shimi Bandiel
12

Aquí hay una posible solución basada en la de Steve McLeod :

public class TwoTypesConsumer {
    public void consumeTomato(Tomato t) {...}
    public void consumeApple(Apple a) {...}

    public Consumer<Tomato> getTomatoConsumer() {
        return new Consumer<Tomato>() {
            public void consume(Tomato t) {
                consumeTomato(t);
            }
        }
    }

    public Consumer<Apple> getAppleConsumer() {
        return new Consumer<Apple>() {
            public void consume(Apple a) {
                consumeApple(t);
            }
        }
    }
}

El requisito implícito de la pregunta era Consumer<Tomato>y los Consumer<Apple>objetos que comparten el estado. La necesidad de Consumer<Tomato>, Consumer<Apple>objetos proviene de otros métodos que esperan estos como parámetros. Necesito una clase para implementar ambos para compartir el estado.

La idea de Steve era usar dos clases internas, cada una implementando un tipo genérico diferente.

Esta versión agrega captadores para los objetos que implementan la interfaz del consumidor, que luego se pueden pasar a otros métodos que los esperan.

daphshez
fuente
2
Si alguien usa esto: vale la pena almacenar las Consumer<*>instancias en los campos de instancia si get*Consumerse llama con frecuencia.
TWiStErRob
7

Al menos, puede hacer una pequeña mejora en su implementación del despacho haciendo algo como lo siguiente:

public class TwoTypesConsumer implements Consumer<Fruit> {

La fruta es un antepasado del tomate y la manzana.

Buhb
fuente
14
Gracias, pero digan lo que digan los profesionales, no considero al tomate como una fruta. Desafortunadamente, no hay una clase base común que no sea Object.
daphshez
2
Siempre puede crear una clase base llamada: AppleOrTomato;)
Shimi Bandiel
1
Mejor, agregue una fruta que delegue a Apple o Tomato.
Tom Hawtin - tackline
@Tom: A menos que esté malinterpretando lo que está diciendo, su sugerencia solo empuja el problema hacia adelante, ya que, para que Fruit pueda delegar a Apple o Tomato, Fruit debe tener un campo de superclase tanto para Apple como para Tomato refiriéndose al objeto al que delega.
Buhb
1
Esto implicaría que TwoTypesConsumer puede consumir cualquier tipo de Fruit, cualquiera implementado actualmente y cualquiera que pueda implementar en el futuro.
Tom Gillen
3

me topé con esto. Simplemente sucedió que tenía el mismo problema, pero lo resolví de una manera diferente: acabo de crear una nueva interfaz como esta

public interface TwoTypesConsumer<A,B> extends Consumer<A>{
    public void consume(B b);
}

desafortunadamente, esto se considera como Consumer<A>y NO como Consumer<B>contra toda lógica. Por lo tanto, debe crear un pequeño adaptador para el segundo consumidor como este dentro de su clase

public class ConsumeHandler implements TwoTypeConsumer<A,B>{

    private final Consumer<B> consumerAdapter = new Consumer<B>(){
        public void consume(B b){
            ConsumeHandler.this.consume(B b);
        }
    };

    public void consume(A a){ //...
    }
    public void conusme(B b){ //...
    }
}

si Consumer<A>es necesario, simplemente puede pasar this, y si Consumer<B>es necesario, simplemente paseconsumerAdapter

Rafael T
fuente
La respuesta de Daphna es la misma, pero más limpia y menos complicada.
TWiStErRob
1

No puede hacer esto directamente en una clase ya que la definición de clase a continuación no se puede compilar debido a la eliminación de tipos genéricos y la declaración de interfaz duplicada.

class TwoTypesConsumer implements Consumer<Apple>, Consumer<Tomato> { 
 // cannot compile
 ...
}

Cualquier otra solución para empacar las mismas operaciones de consumo en una clase requiere definir su clase como:

class TwoTypesConsumer { ... }

lo cual no tiene sentido ya que necesita repetir / duplicar la definición de ambas operaciones y no se hará referencia a ellas desde la interfaz. En mi humilde opinión, hacer esto es una pequeña duplicación de código y mal que estoy tratando de evitar.

Esto podría ser un indicador también de que hay demasiada responsabilidad en una clase para consumir 2 objetos diferentes (si no están acoplados).

Sin embargo, lo que estoy haciendo y lo que puede hacer es agregar un objeto de fábrica explícito para crear consumidores conectados de la siguiente manera:

interface ConsumerFactory {
     Consumer<Apple> createAppleConsumer();
     Consumer<Tomato> createTomatoConsumer();
}

Si en realidad esos tipos están realmente acoplados (relacionados), recomendaría crear una implementación de esta manera:

class TwoTypesConsumerFactory {

    // shared objects goes here

    private class TomatoConsumer implements Consumer<Tomato> {
        public void consume(Tomato tomato) {
            // you can access shared objects here
        }
    }

    private class AppleConsumer implements Consumer<Apple> {
        public void consume(Apple apple) {
            // you can access shared objects here
        }
    }


    // It is really important to return generic Consumer<Apple> here
    // instead of AppleConsumer. The classes should be rather private.
    public Consumer<Apple> createAppleConsumer() {
        return new AppleConsumer();
    }

    // ...and the same here
    public Consumer<Tomato> createTomatoConsumer() {
        return new TomatoConsumer();
    }
}

La ventaja es que la clase de fábrica conoce ambas implementaciones, hay un estado compartido (si es necesario) y puede devolver más consumidores acoplados si es necesario. No hay declaración de método de consumo repetitivo que no se derive de la interfaz.

Tenga en cuenta que cada consumidor puede ser una clase independiente (aún privada) si no están completamente relacionados.

La desventaja de esa solución es una mayor complejidad de clase (incluso si este puede ser un archivo java) y para acceder al método de consumo necesita una llamada más, en lugar de:

twoTypesConsumer.consume(apple)
twoTypesConsumer.consume(tomato)

tienes:

twoTypesConsumerFactory.createAppleConsumer().consume(apple);
twoTypesConsumerFactory.createTomatoConsumer().consume(tomato);

Para resumir, puede definir 2 consumidores genéricos en una clase de nivel superior utilizando 2 clases internas, pero en caso de llamar, primero debe obtener una referencia al consumidor de implementación apropiado , ya que este no puede ser simplemente un objeto de consumidor.

kitarek
fuente
1

En el estilo funcional, es bastante fácil hacerlo sin implementar la interfaz y también realiza la comprobación del tipo de tiempo de compilación.

Nuestra interfaz funcional para consumir entidad

@FunctionalInterface
public interface Consumer<E> { 
     void consume(E e); 
}

nuestro gerente para procesar y consumir la entidad adecuadamente

public class Manager {
    public <E> void process(Consumer<E> consumer, E entity) {
        consumer.consume(entity);
    }

    public void consume(Tomato t) {
        // Consume Tomato
    }

    public void consume(Apple a) {
        // Consume Apple
    }

    public void test() {
        process(this::consume, new Tomato());
        process(this::consume, new Apple());
    }
}
mano_ksp
fuente
0

Otra alternativa para evitar el uso de más clases. (ejemplo usando java8 +)

// Mappable.java
public interface Mappable<M> {
    M mapTo(M mappableEntity);
}

// TwoMappables.java
public interface TwoMappables {
    default Mappable<A> mapableA() {
         return new MappableA();
    }

    default Mappable<B> mapableB() {
         return new MappableB();
    }

    class MappableA implements Mappable<A> {}
    class MappableB implements Mappable<B> {}
}

// Something.java
public class Something implements TwoMappables {
    // ... business logic ...
    mapableA().mapTo(A);
    mapableB().mapTo(B);
}
huellas dactilares
fuente
0

Perdón por responder viejas preguntas, ¡pero realmente me encanta! Prueba esta opción:

public class MegaConsumer implements Consumer<Object> {

  Map<Class, Consumer> consumersMap = new HashMap<>();
  Consumer<Object> baseConsumer = getConsumerFor(Object.class);

  public static void main(String[] args) {
    MegaConsumer megaConsumer = new MegaConsumer();
    
    //You can load your customed consumers
    megaConsumer.loadConsumerInMapFor(Tomato.class);
    megaConsumer.consumersMap.put(Apple.class, new Consumer<Apple>() {
        @Override
        public void consume(Apple e) {
            System.out.println("I eat an " + e.getClass().getSimpleName());
        }
    });
    
    //You can consume whatever
    megaConsumer.consume(new Tomato());
    megaConsumer.consume(new Apple());
    megaConsumer.consume("Other class");
  }

  @Override
  public void consume(Object e) {
    Consumer consumer = consumersMap.get(e.getClass());
    if(consumer == null) // No custom consumer found
      consumer = baseConsumer;// Consuming with the default Consumer<Object>
    consumer.consume(e);
  }

  private static <T> Consumer<T> getConsumerFor(Class<T> someClass){
    return t -> System.out.println(t.getClass().getSimpleName() + " consumed!");
  }

  private <T> Consumer<T> loadConsumerInMapFor(Class<T> someClass){
    return consumersMap.put(someClass, getConsumerFor(someClass));
  }
}

Creo que eso es lo que estás buscando.

Obtienes esta salida:

Tomate consumido!

Yo como una manzana

Cadena consumida!

Awes0meM4n
fuente
En cuestión: "Pero estoy buscando la verificación de tipos en tiempo de compilación ..."
aeracode
@aeracode No hay opciones para hacer lo que OP quiere. El borrado de tipo hace imposible implementar la misma interfaz dos veces con diferentes variables de tipo. Solo trato de darte otra forma. Por supuesto, puede verificar los tipos aceptados previamente para consumir un objeto.
Awes0meM4n