Bagaimana cara membuat kelas Java yang mengimplementasikan satu antarmuka dengan dua tipe generik?

164

Saya memiliki antarmuka generik

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

Saya memiliki kelas yang mengkonsumsi dua jenis objek, jadi saya ingin melakukan sesuatu seperti:

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

Rupanya saya tidak bisa melakukan itu.

Tentu saja saya dapat mengimplementasikan pengiriman sendiri, misalnya

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

Tapi saya mencari solusi pengecekan dan pengiriman tipe kompilasi yang disediakan generik.

Solusi terbaik yang dapat saya pikirkan adalah mendefinisikan antarmuka yang terpisah, misalnya

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

Secara fungsional, solusi ini OK, saya pikir. Itu hanya verbose dan jelek.

Ada ide?

daphshez
sumber
Mengapa Anda memerlukan dua antarmuka umum dengan tipe dasar yang sama?
akarnokd
6
Karena untuk mengetik, Anda tidak dapat melakukannya. Simpan dua kelas berbeda yang mengimplementasikan konsumen. Buat lebih banyak kelas kecil tetapi pertahankan kode Anda generik (Jangan gunakan jawaban yang diterima, itu merusak seluruh konsep ... Anda tidak bisa memperlakukan TwoTypesConsumer sebagai konsumen, yang BAD).
Lewis Diamond
Lihat ini untuk impl gaya fungsional - stackoverflow.com/a/60466413/4121845
mano_ksp

Jawaban:

78

Pertimbangkan enkapsulasi:

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

Jika membuat kelas dalam statis ini mengganggu Anda, Anda bisa menggunakan kelas anonim:

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
sumber
2
entah bagaimana itu terlihat seperti duplikasi kode ... Saya mengalami masalah yang sama dan tidak menemukan solusi lain yang terlihat bersih.
bln-tom
109
Tetapi tidakTwoTypesConsumer memenuhi kontrak, jadi apa gunanya? Itu tidak dapat diteruskan ke metode yang menginginkan kedua jenis . Ide keseluruhan dari konsumen tipe dua adalah Anda dapat memberikannya pada metode yang menginginkan konsumen tomat dan juga metode yang menginginkan konsumen apel. Di sini kita tidak punya. Consumer
Jeff Axelrod
@ JeffAxelrod Saya akan membuat kelas-kelas dalam non-statis sehingga mereka memiliki akses ke TwoTypesConsumerinstance terlampir jika perlu, dan kemudian Anda dapat meneruskan twoTypesConsumer.getAppleConsumer()ke metode yang menginginkan konsumen apel. Pilihan lain adalah menambahkan metode yang mirip addConsumer(Producer<Apple> producer)dengan TwoTypesConsumer.
herman
Ini tidak berfungsi jika Anda tidak memiliki kendali atas antarmuka (mis. Cxf / rs ExceptionMapper) ...
vikingsteve
17
Saya akan mengatakannya: Ini adalah cacat dengan Java. Sama sekali tidak ada alasan mengapa kita tidak boleh memiliki beberapa implementasi dari antarmuka yang sama, asalkan implementasi mengambil argumen yang berbeda.
gromit190
41

Karena penghapusan tipe, Anda tidak dapat mengimplementasikan antarmuka yang sama dua kali (dengan parameter tipe yang berbeda).

Shimi Bandiel
sumber
6
Saya bisa melihat bagaimana itu menjadi masalah ... pertanyaannya adalah lalu apa cara terbaik (paling efisien, aman, anggun) untuk melewati masalah ini.
daphshez
2
Tanpa masuk ke logika bisnis, sesuatu di sini 'berbau' seperti pola Pengunjung.
Shimi Bandiel
12

Inilah solusi yang mungkin didasarkan pada solusi 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);
            }
        }
    }
}

Persyaratan implisit dari pertanyaan itu adalah Consumer<Tomato>dan Consumer<Apple>benda-benda yang berbagi keadaan. Kebutuhan akan Consumer<Tomato>, Consumer<Apple>objek berasal dari metode lain yang mengharapkan ini sebagai parameter. Saya membutuhkan satu kelas untuk mengimplementasikan keduanya agar dapat berbagi status.

Ide Steve adalah menggunakan dua kelas batin, masing-masing menerapkan tipe generik yang berbeda.

Versi ini menambahkan getter untuk objek yang mengimplementasikan antarmuka konsumen, yang kemudian dapat diteruskan ke metode lain yang mengharapkannya.

daphshez
sumber
2
Jika ada yang menggunakan ini: ada baiknya menyimpan Consumer<*>instance di bidang instance jika get*Consumersering dipanggil.
TWiStErRob
7

Setidaknya, Anda dapat melakukan sedikit peningkatan pada implementasi pengiriman Anda dengan melakukan sesuatu seperti berikut:

public class TwoTypesConsumer implements Consumer<Fruit> {

Buah menjadi leluhur Tomat dan Apple.

Buhb
sumber
14
Terima kasih, tapi apa pun yang dikatakan pro, saya tidak menganggap Tomat sebagai buah. Sayangnya tidak ada kelas dasar umum selain Object.
daphshez
2
Anda selalu dapat membuat kelas dasar yang disebut: AppleOrTomato;)
Shimi Bandiel
1
Lebih baik, tambahkan Buah yang mendelegasikan ke Apple atau Tomat.
Tom Hawtin - tackline
@ Tom: Kecuali jika saya salah mengerti apa yang Anda katakan, saran Anda hanya mendorong masalah ke depan, karena, agar Fruit dapat mendelegasikan ke Apple atau Tomat, Fruit harus memiliki bidang superclass untuk Apple dan Tomat merujuk pada objek yang didelegasikan.
Buhb
1
Ini akan menyiratkan bahwa TwoTypesConsumer dapat mengkonsumsi segala jenis Buah, apa pun yang saat ini diterapkan dan siapa pun yang dapat menerapkan di masa depan.
Tom Gillen
3

hanya menemukan ini. Itu baru saja terjadi, bahwa saya memiliki Masalah yang sama, tetapi saya menyelesaikannya dengan cara yang berbeda: Saya baru saja membuat Antarmuka baru seperti ini

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

Sayangnya, ini dianggap Consumer<A>dan TIDAK Consumer<B>bertentangan dengan semua Logika. Jadi, Anda harus membuat Adaptor kecil untuk konsumen kedua seperti ini di dalam kelas Anda

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

jika Consumer<A>diperlukan, Anda dapat dengan mudah lulus this, dan jika Consumer<B>diperlukan hanya lewatconsumerAdapter

Rafael T
sumber
Jawaban Daphna sama, tetapi lebih bersih dan tidak berbelit-belit.
TWiStErRob
1

Anda tidak dapat melakukan ini secara langsung dalam satu kelas karena definisi kelas di bawah ini tidak dapat dikompilasi karena penghapusan tipe generik dan deklarasi antarmuka duplikat.

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

Solusi lain untuk mengemas operasi konsumsi yang sama dalam satu kelas mengharuskan untuk mendefinisikan kelas Anda sebagai:

class TwoTypesConsumer { ... }

yang tidak ada gunanya karena Anda perlu mengulang / menduplikasi definisi dari kedua operasi dan mereka tidak akan dirujuk dari antarmuka. IMHO melakukan ini adalah duplikasi kecil dan kode yang buruk yang saya coba hindari.

Ini mungkin menjadi indikator juga bahwa ada terlalu banyak tanggung jawab dalam satu kelas untuk mengkonsumsi 2 objek yang berbeda (jika tidak digabungkan).

Namun apa yang saya lakukan dan apa yang dapat Anda lakukan adalah menambahkan objek pabrik eksplisit untuk membuat konsumen yang terhubung dengan cara berikut:

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

Jika pada kenyataannya tipe-tipe tersebut benar-benar digabungkan (terkait) maka saya akan merekomendasikan untuk membuat implementasi dengan cara seperti ini:

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

Keuntungannya adalah kelas pabrik mengetahui kedua implementasi, ada status bersama (jika diperlukan) dan Anda dapat mengembalikan lebih banyak konsumen yang digabungkan jika diperlukan. Tidak ada pernyataan metode konsumsi berulang yang tidak berasal dari antarmuka.

Harap dicatat bahwa setiap konsumen mungkin kelas independen (masih pribadi) jika mereka tidak sepenuhnya terkait.

Kelemahan dari solusi itu adalah kompleksitas kelas yang lebih tinggi (bahkan jika ini bisa menjadi file satu java) dan untuk mengakses metode konsumsi Anda memerlukan satu panggilan lagi jadi alih-alih:

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

kamu punya:

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

Untuk meringkas, Anda dapat mendefinisikan 2 konsumen umum dalam satu kelas tingkat atas menggunakan 2 kelas batin tetapi dalam kasus panggilan Anda harus mendapatkan pertama referensi ke konsumen pelaksana yang tepat karena ini tidak bisa hanya satu objek konsumen.

kitarek
sumber
1

Dalam gaya Fungsional cukup mudah melakukan ini tanpa mengimplementasikan antarmuka dan juga melakukan pengecekan tipe waktu kompilasi.

Antarmuka fungsional kami untuk mengkonsumsi entitas

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

manajer kami untuk memproses dan mengkonsumsi entitas dengan tepat

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

Alternatif lain untuk menghindari penggunaan lebih banyak kelas. (contoh menggunakan 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);
}
sidik jari
sumber
0

Maaf untuk menjawab pertanyaan lama, tapi saya sangat menyukainya! Coba opsi ini:

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

Saya pikir itulah yang Anda cari.

Anda mendapatkan hasil ini:

Tomat dikonsumsi!

Aku makan sebuah apel

String yang dikonsumsi!

Awes0meM4n
sumber
Dalam pertanyaan: "Tapi saya mencari tipe-waktu memeriksa-memeriksa ..."
aeracode
@aeracode Tidak ada opsi untuk melakukan apa yang diinginkan OP. Penghapusan tipe membuat tidak mungkin untuk mengimplementasikan antarmuka yang sama dua kali dengan variabel tipe yang berbeda. Saya hanya mencoba memberi Anda cara lain. Tentu saja Anda dapat memeriksa jenis yang diterima sebelumnya untuk menggunakan onbject.
Awes0meM4n