Como criar uma classe Java que implemente uma interface com dois tipos genéricos?

164

Eu tenho uma interface genérica

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

Eu tenho uma classe que consome dois tipos de objetos, então eu gostaria de fazer algo como:

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

Aparentemente, não posso fazer isso.

Obviamente, eu mesmo posso implementar o envio, por exemplo

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

Mas estou procurando a solução de verificação e despacho de tipos em tempo de compilação que os genéricos fornecem.

A melhor solução que posso pensar é definir interfaces separadas, por exemplo

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

Funcionalmente, esta solução está bem, eu acho. É apenas verboso e feio.

Alguma ideia?

daphshez
fonte
Por que você precisa de duas interfaces genéricas do mesmo tipo de base?
akarnokd
6
Devido ao apagamento do tipo, você não pode fazer isso. Mantenha duas classes diferentes que implementam o consumidor. Cria mais classes pequenas, mas mantém seu código genérico (não use a resposta aceita, isso quebra todo o conceito ... você não pode tratar o TwoTypesConsumer como um consumidor, o que é MAU).
Lewis Diamond
Verifique isso no estilo funcional impl - stackoverflow.com/a/60466413/4121845
mano_ksp

Respostas:

78

Considere o encapsulamento:

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

Se a criação dessas classes internas estáticas o incomoda, você pode usar classes 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
fonte
2
de alguma forma isso parece com duplicação de código ... Encontrei o mesmo problema e não encontrei outra solução que pareça limpa.
bi-tom
109
Mas nãoTwoTypesConsumer cumpre contratos, então qual é o objetivo? Não pode ser passado para um método que deseja um ou outro tipo de . A idéia inteira de um consumidor de dois tipos seria que você pode aplicá-lo a um método que deseja um consumidor de tomate e a um método que deseja um consumidor de maçã. Aqui não temos nenhum. Consumer
precisa saber é o seguinte
@JeffAxelrod Gostaria de tornar as classes internas não estáticas para que elas tenham acesso à TwoTypesConsumerinstância envolvente, se necessário, e então você pode passar twoTypesConsumer.getAppleConsumer()para um método que deseja um consumidor da Apple. Outra opção seria adicionar métodos semelhantes addConsumer(Producer<Apple> producer)ao TwoTypesConsumer.
herman
Isso não funciona se você não tem controle sobre a interface (por exemplo, CXF / rs ExceptionMapper) ...
vikingsteve
17
Vou dizer: esta é uma falha no Java. Não há absolutamente nenhuma razão para não podermos ter várias implementações da mesma interface, desde que as implementações tenham argumentos diferentes.
Gromit190
41

Por causa do apagamento do tipo, você não pode implementar a mesma interface duas vezes (com parâmetros de tipo diferentes).

Shimi Bandiel
fonte
6
Percebo como é um problema ... a questão é qual é a melhor (mais eficiente, segura e elegante) maneira de contornar esse problema.
daphshez
2
Sem entrar na lógica de negócios, algo aqui 'cheira' como o padrão Visitor.
Shimi Bandiel
12

Aqui está uma possível solução baseada na 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);
            }
        }
    }
}

O requisito implícito da pergunta era Consumer<Tomato>e Consumer<Apple>objetos que compartilham estado. A necessidade de Consumer<Tomato>, Consumer<Apple>objetos vem de outros métodos que os esperam como parâmetros. Eu preciso de uma classe para implementar os dois para compartilhar o estado.

A ideia de Steve era usar duas classes internas, cada uma implementando um tipo genérico diferente.

Esta versão adiciona getters para os objetos que implementam a interface Consumer, que podem ser passados ​​para outros métodos que os esperam.

daphshez
fonte
2
Se alguém usa isso: vale a pena armazenar as Consumer<*>instâncias nos campos de instância, se get*Consumerfor chamado com frequência.
TWIStErRob
7

Pelo menos, você pode fazer uma pequena melhoria na implementação do despacho fazendo algo como o seguinte:

public class TwoTypesConsumer implements Consumer<Fruit> {

Frutas sendo um ancestral do tomate e da maçã.

Buhb
fonte
14
Obrigado, mas o que dizem os profissionais, não considero tomate como fruta. Infelizmente, não há classe base comum além de Object.
daphshez
2
Você sempre pode criar uma classe base chamada: AppleOrTomato;)
Shimi Bandiel
1
Melhor, adicione uma fruta que delegue à maçã ou ao tomate.
Tom Hawtin # tackline 19/08/09
@ Tom: A menos que eu esteja entendendo errado o que você está dizendo, sua sugestão apenas leva o problema adiante, pois, para que o Fruit possa delegar à Apple ou ao Tomate, a Fruit deve ter um campo de superclasse para a Apple e o Tomato referindo-se ao objeto ao qual ele delega.
Buhb
1
Isso implicaria que o TwoTypesConsumer possa consumir qualquer tipo de fruta, qualquer atualmente implementado e qualquer pessoa que possa implementar no futuro.
precisa
3

apenas tropeçou nisso. Aconteceu que eu tinha o mesmo problema, mas resolvi de uma maneira diferente: acabei de criar uma nova interface como esta

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

infelizmente, isso é considerado como Consumer<A>e NÃO Consumer<B>contra toda a lógica. Então você precisa criar um pequeno adaptador para o segundo consumidor como esse dentro da sua classe

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){ //...
    }
}

se Consumer<A>for necessário, você pode simplesmente passar thise, se Consumer<B>necessário, basta passarconsumerAdapter

Rafael T
fonte
A resposta de Daphna é a mesma, mas mais limpa e menos complicada.
TWIStErRob
1

Você não pode fazer isso diretamente em uma classe, pois a definição de classe abaixo não pode ser compilada devido ao apagamento de tipos genéricos e à declaração de interface duplicada.

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

Qualquer outra solução para empacotar as mesmas operações de consumo em uma classe requer definir sua classe como:

class TwoTypesConsumer { ... }

o que é inútil, pois você precisa repetir / duplicar a definição de ambas as operações e elas não serão referenciadas na interface. IMHO fazer isso é uma duplicação ruim de código e pequeno que estou tentando evitar.

Isso também pode ser um indicador de que existe muita responsabilidade em uma classe para consumir 2 objetos diferentes (se não estiverem acoplados).

No entanto, o que estou fazendo e o que você pode fazer é adicionar um objeto de fábrica explícito para criar consumidores conectados da seguinte maneira:

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

Se, na realidade, esses tipos são realmente acoplados (relacionados), eu recomendaria criar uma implementação da seguinte maneira:

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

A vantagem é que a classe factory conhece as duas implementações, existe um estado compartilhado (se necessário) e você pode retornar mais consumidores acoplados, se necessário. Não há nenhuma declaração repetida do método de consumo que não seja derivada da interface.

Observe que cada consumidor pode ser de classe independente (ainda privada) se não estiver completamente relacionado.

A desvantagem dessa solução é uma complexidade de classe mais alta (mesmo que possa ser um arquivo java) e para acessar o método de consumo, você precisa de mais uma chamada, em vez de:

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

Você tem:

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

Para resumir, você pode definir 2 consumidores genéricos em uma classe de nível superior usando 2 classes internas, mas, no caso de chamar, é necessário primeiro obter uma referência ao consumidor de implementação apropriado, pois esse não pode ser simplesmente um objeto de consumidor.

kitarek
fonte
1

No estilo Funcional, é bastante fácil fazer isso sem implementar a interface e também faz a verificação do tipo de tempo de compilação.

Nossa interface funcional para consumir entidade

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

nosso gerente para processar e consumir a entidade adequadamente

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
fonte
0

Outra alternativa para evitar o uso de mais classes. (exemplo 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);
}
impressões digitais
fonte
0

Desculpe por responder perguntas antigas, mas eu realmente adoro! Tente esta opção:

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

Eu acho que é isso que você está procurando.

Você obtém esta saída:

Tomate consumido!

Eu como uma maçã

String consumida!

Awes0meM4n
fonte
Em questão: "Mas estou procurando a verificação de tipo em tempo de compilação ..."
aeracode 08/07/19
@aeracode Não há opções para fazer o que o OP deseja. O apagamento de tipo torna impossível implementar a mesma interface duas vezes com variáveis ​​de tipo diferentes. Eu só tento lhe dar outra maneira. Obviamente, você pode verificar os tipos aceitos anteriormente para consumir um onbject.
Awes0meM4n