Come creare una classe Java che implementa un'interfaccia con due tipi generici?


164

Ho un'interfaccia generica

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

Ho una classe che consuma due tipi di oggetti, quindi vorrei fare qualcosa del tipo:

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

Apparentemente non posso farlo.

Ovviamente posso implementare personalmente l'invio, ad es

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

Ma sto cercando la soluzione di verifica e dispacciamento del tipo in fase di compilazione fornita dai generici.

La migliore soluzione che mi viene in mente è quella di definire interfacce separate, ad es

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

Funzionalmente, questa soluzione è OK, penso. È solo verboso e brutto.

Qualche idea?


Perché hai bisogno di due interfacce generiche dello stesso tipo di base?
Akarnokd,

6
A causa della cancellazione del tipo non puoi farlo. Conservalo in due diverse classi che implementano il consumatore. Rende più piccole classi ma mantiene generico il tuo codice (non utilizzare la risposta accettata, interrompe l'intero concetto ... non puoi trattare TwoTypesConsumer come un consumatore, che è MALE).
Lewis Diamond,

Controllare questo per impl stile funzionale - stackoverflow.com/a/60466413/4121845
mano_ksp

Risposte:


78

Prendi in considerazione l'incapsulamento:

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 la creazione di queste classi interne statiche ti disturba, puoi utilizzare classi anonime:

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

2
in qualche modo sembra una duplicazione del codice ... Ho riscontrato lo stesso problema e non ho trovato altra soluzione che appaia pulita.
bln-tom,

109
Ma nonTwoTypesConsumer soddisfa alcun contratto, quindi qual è il punto? Non può essere passato a un metodo che desidera entrambi i tipi di Consumer. L'idea generale di un consumatore di due tipi sarebbe quella di poterlo dare a un metodo che desidera un consumatore di pomodori, nonché a un metodo che desidera un consumatore di mele. Qui non abbiamo nessuno dei due.
Jeff Axelrod, l'

@JeffAxelrod Vorrei rendere le classi interne non statiche in modo che possano accedere TwoTypesConsumerall'istanza che lo racchiude, se necessario, e quindi puoi passare twoTypesConsumer.getAppleConsumer()a un metodo che desidera un consumatore Apple. Un'altra opzione sarebbe quella di aggiungere metodi simili addConsumer(Producer<Apple> producer)a TwoTypesConsumer.
herman,

Questo non funziona se non hai il controllo dell'interfaccia (es. Cxf / rs ExceptionMapper) ...
vikingsteve,

17
Lo dirò: questo è un difetto con Java. Non c'è assolutamente alcun motivo per cui non dovremmo avere più implementazioni della stessa interfaccia, a condizione che le implementazioni prendano argomenti diversi.
gromit190,

41

A causa della cancellazione del tipo non è possibile implementare la stessa interfaccia due volte (con parametri di tipo diverso).


6
Vedo come è un problema ... la domanda è quindi qual è il modo migliore (più efficiente, sicuro, elegante) per aggirare questo problema.
Daphshez,

2
Senza entrare nella logica aziendale, qualcosa qui "profuma" come il modello di Visitatore.
Shimi Bandiel,

12

Ecco una possibile soluzione basata su quella di 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);
            }
        }
    }
}

Il requisito implicito della domanda era Consumer<Tomato>e gli Consumer<Apple>oggetti che condividono lo stato. La necessità di Consumer<Tomato>, Consumer<Apple>oggetti deriva da altri metodi che prevedono questi come parametri. Ho bisogno di una classe per implementarli entrambi per condividere lo stato.

L'idea di Steve era di usare due classi interne, ognuna implementando un diverso tipo generico.

Questa versione aggiunge getter per gli oggetti che implementano l'interfaccia Consumer, che possono quindi essere passati ad altri metodi che li prevedono.


2
Se qualcuno lo utilizza: vale la pena archiviare le Consumer<*>istanze nei campi di istanza se get*Consumerviene chiamato spesso.
TWiStErRob,

7

Almeno, puoi apportare un piccolo miglioramento alla tua implementazione della spedizione facendo qualcosa di simile al seguente:

public class TwoTypesConsumer implements Consumer<Fruit> {

La frutta è un antenato di pomodoro e mela.


14
Grazie, ma qualunque cosa dicano i professionisti, non considero Tomato come un frutto. Sfortunatamente non esiste una classe base comune diversa da Object.
daphshez,

2
Puoi sempre creare una classe base chiamata: AppleOrTomato;)
Shimi Bandiel,

1
Meglio, aggiungi un frutto che deleghi alla mela o al pomodoro.
Tom Hawtin - tackline il

@ Tom: A meno che io non fraintenda quello che stai dicendo, il tuo suggerimento non fa che portare avanti il ​​problema, poiché, per poter delegare Fruit o Apple o Tomato, Fruit deve avere un campo di una superclasse sia per Apple che per Tomato riferendosi all'oggetto a cui delega.
Buhb,

1
Ciò implicherebbe che TwoTypesConsumer può consumare qualsiasi tipo di frutta, qualsiasi attualmente implementato e chiunque possa implementare in futuro.
Tom Gillen,

3

Ci siamo imbattuti in questo. È appena successo che ho avuto lo stesso problema, ma l'ho risolto in un modo diverso: ho appena creato una nuova interfaccia come questa

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

sfortunatamente, questo è considerato come Consumer<A>e NON come Consumer<B>contro tutta la logica. Quindi devi creare un piccolo adattatore per il secondo consumatore come questo all'interno della tua 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>è necessario, puoi semplicemente passare thise, se Consumer<B>necessario, basta passareconsumerAdapter


La risposta di Daphna è la stessa, ma più pulita e meno contorta.
TWiStErRob

1

Non è possibile farlo direttamente in una classe poiché la definizione di classe riportata di seguito non può essere compilata a causa della cancellazione di tipi generici e della dichiarazione dell'interfaccia duplicata.

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

Qualsiasi altra soluzione per impacchettare le stesse operazioni di consumo in una classe richiede di definire la classe come:

class TwoTypesConsumer { ... }

che è inutile in quanto è necessario ripetere / duplicare la definizione di entrambe le operazioni e non saranno referenziate dall'interfaccia. Fare IMHO è una brutta duplicazione di codice e piccola che sto cercando di evitare.

Questo potrebbe essere un indicatore anche del fatto che in una classe c'è troppa responsabilità per consumare 2 oggetti diversi (se non sono accoppiati).

Tuttavia, ciò che sto facendo e ciò che puoi fare è aggiungere un oggetto factory esplicito per creare utenti connessi nel modo seguente:

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

Se in realtà quei tipi sono realmente accoppiati (correlati), consiglierei di creare un'implementazione in questo modo:

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

Il vantaggio è che la classe di fabbrica conosce entrambe le implementazioni, esiste uno stato condiviso (se necessario) e, se necessario, è possibile restituire più consumatori accoppiati. Non esiste alcuna dichiarazione del metodo di consumo ripetuta che non sia derivata dall'interfaccia.

Si prega di notare che ogni consumatore potrebbe essere una classe indipendente (ancora privata) se non completamente correlata.

Il rovescio della medaglia di quella soluzione è una complessità di classe superiore (anche se può trattarsi di un file java) e per accedere al metodo di consumo è necessaria un'altra chiamata, invece di:

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

hai:

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

Per riassumere è possibile definire 2 consumatori generici in una classe di livello superiore utilizzando 2 classi interne, ma in caso di chiamata è necessario innanzitutto ottenere un riferimento al consumatore di implementazione appropriato poiché questo non può essere semplicemente un oggetto consumatore.


1

In stile funzionale è abbastanza semplice farlo senza implementare l'interfaccia e fa anche il controllo del tipo di tempo di compilazione.

La nostra interfaccia funzionale per consumare entità

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

il nostro responsabile per elaborare e consumare l'entità in modo appropriato

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

0

Un'altra alternativa per evitare l'uso di più classi. (esempio 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);
}

0

Ci scusiamo per la risposta a vecchie domande, ma mi piace davvero tanto! Prova questa opzione:

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

Penso che sia quello che stai cercando.

Ottieni questo risultato:

Pomodoro consumato!

Mangio una mela

Stringa consumata!


In questione: "Ma sto cercando il controllo del tipo in fase di compilazione ..."
codice aereo

@aeracode Nessuna opzione per fare ciò che vuole OP. La cancellazione del tipo rende impossibile l'implementazione della stessa interfaccia due volte con variabili di tipo diverso. Cerco solo di darti un altro modo. Ovviamente puoi controllare i tipi accettati in precedenza per consumare un oggetto.
Awes0meM4n
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.