วิธีสร้างคลาส Java ที่ใช้หนึ่งอินเตอร์เฟสที่มีสองประเภททั่วไป?


164

ฉันมีอินเทอร์เฟซทั่วไป

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

ฉันมีคลาสที่ใช้วัตถุสองประเภทดังนั้นฉันต้องการทำสิ่งที่ชอบ:

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

เห็นได้ชัดว่าฉันไม่สามารถทำเช่นนั้นได้

แน่นอนฉันสามารถใช้การจัดส่งได้เองเช่น

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

แต่ฉันกำลังมองหาวิธีการตรวจสอบและรวบรวมข้อมูลเวลาคอมไพล์ที่ข้อมูลทั่วไปให้

ทางออกที่ดีที่สุดที่ฉันคิดได้คือการกำหนดส่วนต่อประสานแยกต่างหากเช่น

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

ฟังก์ชั่นการแก้ปัญหานี้ก็โอเคฉันคิดว่า เป็นเพียง verbose และน่าเกลียด

ความคิดใด ๆ


ทำไมคุณต้องการอินเทอร์เฟซทั่วไปสองรายการของ basetype เดียวกัน
akarnokd

6
เนื่องจากการลบประเภทคุณไม่สามารถทำได้ เก็บไว้สองคลาสที่แตกต่างกันที่ใช้ของผู้บริโภค ทำให้คลาสเล็กขึ้น แต่ทำให้โค้ดของคุณเป็นแบบทั่วไป (อย่าใช้คำตอบที่ยอมรับมันทำให้คอนเซปต์ทั้งหมด ... คุณไม่สามารถปฏิบัติกับ TwoTypesConsumer ในฐานะผู้บริโภคซึ่งก็คือ BAD)
Lewis Diamond

ตรวจสอบสิ่งนี้เพื่อ impl ลักษณะการทำงาน - stackoverflow.com/a/60466413/4121845
mano_ksp

คำตอบ:


78

พิจารณาการห่อหุ้ม:

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

หากการสร้างคลาสภายในแบบคงที่เหล่านี้รบกวนคุณคุณสามารถใช้คลาสแบบไม่ระบุชื่อได้:

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
อย่างใดที่ดูเหมือน tome เช่นการทำสำเนารหัส ... ฉันพบปัญหาเดียวกันและพบว่าไม่มีวิธีการอื่นที่ดูสะอาด
bln-tom

109
แต่TwoTypesConsumerบรรลุเป้าหมายการไม่มีสัญญาดังนั้นสิ่งที่เป็นจุด? Consumerมันไม่สามารถส่งผ่านไปยังวิธีการที่ต้องการประเภทของการอย่างใดอย่างหนึ่ง แนวคิดทั้งหมดของผู้บริโภคสองประเภทคือคุณสามารถให้วิธีการที่ต้องการผู้บริโภคมะเขือเทศรวมถึงวิธีที่ต้องการผู้บริโภคแอปเปิ้ล ที่นี่เราไม่มี
Jeff Axelrod

@JeffAxelrod ฉันจะทำให้ชั้นเรียนภายในไม่คงที่เพื่อให้พวกเขาสามารถเข้าถึงTwoTypesConsumerอินสแตนซ์ที่แนบมาหากจำเป็นและจากนั้นคุณสามารถส่งผ่านtwoTypesConsumer.getAppleConsumer()ไปยังวิธีการที่ต้องการผู้บริโภคแอปเปิ้ล ตัวเลือกอื่นจะเพิ่มวิธีการคล้ายaddConsumer(Producer<Apple> producer)กับ TwoTypesConsumer
เฮอร์แมน

วิธีนี้ใช้ไม่ได้หากคุณไม่สามารถควบคุมส่วนต่อประสาน (เช่น cxf / rs ExceptionMapper) ...
vikingsteve

17
ฉันจะบอกว่า: นี่คือข้อบกพร่องกับ Java ไม่มีเหตุผลที่เราไม่ควรได้รับอนุญาตให้มีการใช้งานหลายอย่างของอินเทอร์เฟซเดียวกันหากการใช้งานนั้นมีข้อโต้แย้งที่แตกต่างกัน
gromit190

41

เนื่องจากการลบประเภทคุณไม่สามารถใช้อินเทอร์เฟซเดียวกันสองครั้ง (ด้วยพารามิเตอร์ประเภทอื่น)


6
ฉันสามารถดูว่ามันเป็นปัญหา ... คำถามคือแล้วเป็นวิธีที่ดีที่สุด (มีประสิทธิภาพมากที่สุดปลอดภัยสง่างาม) เพื่อหลีกเลี่ยงปัญหานี้
daphshez

2
โดยไม่ต้องไปสู่ตรรกะทางธุรกิจอะไรบางอย่างที่นี่ 'กลิ่น' เหมือนรูปแบบของผู้เข้าชม
Shimi Bandiel

12

นี่เป็นวิธีแก้ปัญหาที่เป็นไปได้โดยอ้างอิงจากSteve McLeod's :

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

ความต้องการโดยนัยของคำถามคือConsumer<Tomato>และConsumer<Apple>วัตถุที่ใช้ร่วมกันของรัฐ ความต้องการConsumer<Tomato>, Consumer<Apple>วัตถุมาจากวิธีการอื่นที่คาดว่าสิ่งเหล่านี้เป็นพารามิเตอร์ ฉันต้องการชั้นหนึ่งใช้พวกเขาทั้งสองเพื่อแบ่งปันรัฐ

แนวคิดของสตีฟคือการใช้คลาสภายในสองคลาสซึ่งแต่ละประเภทจะมีประเภททั่วไปที่แตกต่างกัน

รุ่นนี้เพิ่ม getters สำหรับวัตถุที่ใช้อินเทอร์เฟซสำหรับผู้บริโภคซึ่งจากนั้นสามารถส่งผ่านไปยังวิธีการอื่น ๆ ที่พวกเขาคาดหวัง


2
หากใครใช้สิ่งนี้มันคุ้มค่าที่จะเก็บConsumer<*>อินสแตนซ์ในฟิลด์อินสแตนซ์ถ้าget*Consumerถูกเรียกบ่อย
TWiStErRob

7

อย่างน้อยคุณสามารถปรับปรุงการดำเนินการจัดส่งได้เล็กน้อยโดยทำสิ่งต่อไปนี้:

public class TwoTypesConsumer implements Consumer<Fruit> {

ผลไม้เป็นบรรพบุรุษของมะเขือเทศและแอปเปิ้ล


14
ขอบคุณ แต่สิ่งที่ผู้เชี่ยวชาญบอกว่าฉันไม่คิดว่ามะเขือเทศเป็นผลไม้ น่าเสียดายที่ไม่มีคลาสพื้นฐานทั่วไปอื่นที่ไม่ใช่ Object
daphshez

2
คุณสามารถสร้างชั้นฐานที่เรียกว่า: AppleOrTomato;)
Shimi Bandiel

1
ดีกว่าเพิ่มผลไม้ที่มอบให้กับ Apple หรือ Tomato
Tom Hawtin - tackline

@ ทอม: ถ้าฉันไม่เข้าใจสิ่งที่คุณกำลังพูดอยู่ข้อเสนอแนะของคุณจะผลักดันปัญหาไปข้างหน้าเท่านั้นเนื่องจากเพื่อให้ Fruit สามารถมอบสิทธิ์ให้กับ Apple หรือ Tomato Fruit ต้องมีฟิลด์ superclass ทั้ง Apple และ Tomato หมายถึงวัตถุที่ได้รับมอบหมาย
Buhb

1
นี่อาจหมายความว่า TwoTypesConsumer สามารถบริโภคผลไม้ชนิดใดก็ได้ในปัจจุบันที่นำไปใช้และใครบางคนอาจนำไปใช้ในอนาคต
Tom Gillen

3

เพิ่งสะดุดเมื่อนี้ มันเพิ่งเกิดขึ้นว่าฉันมีปัญหาเดียวกัน แต่ฉันแก้ไขมันในวิธีที่ต่างกัน: ฉันเพิ่งสร้างอินเทอร์เฟซใหม่เช่นนี้

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

น่าเสียดายที่นี่ถือว่าเป็นConsumer<A>และไม่เป็นConsumer<B>กับลอจิกทั้งหมด ดังนั้นคุณต้องสร้างอะแดปเตอร์ขนาดเล็กสำหรับผู้บริโภครายที่สองในชั้นเรียนของคุณ

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

ถ้าConsumer<A>จำเป็นคุณสามารถผ่านthisได้และถ้าConsumer<B>จำเป็นก็แค่ผ่านconsumerAdapter


คำตอบของDaphnaนั้นเหมือนกัน แต่สะอาดและซับซ้อนน้อยกว่า
TWiStErRob

1

คุณไม่สามารถทำสิ่งนี้ได้โดยตรงในคลาสเดียวเนื่องจากคำจำกัดความของคลาสด้านล่างไม่สามารถรวบรวมได้เนื่องจากการลบประเภททั่วไปและการประกาศอินเตอร์เฟสที่ซ้ำกัน

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

โซลูชันอื่นใดสำหรับการบรรจุการดำเนินการสิ้นเปลืองเดียวกันในคลาสหนึ่งต้องกำหนดคลาสของคุณเป็น:

class TwoTypesConsumer { ... }

ซึ่งไม่มีประโยชน์เพราะคุณต้องทำซ้ำ / ซ้ำคำจำกัดความของการดำเนินการทั้งสองและพวกเขาจะไม่ถูกอ้างอิงจากอินเทอร์เฟซ IMHO การทำเช่นนี้เป็นการทำซ้ำขนาดเล็กและการทำสำเนารหัสที่ฉันพยายามหลีกเลี่ยง

นี่อาจเป็นตัวบ่งชี้ว่ามีความรับผิดชอบมากเกินไปในคลาสเดียวที่จะใช้วัตถุที่แตกต่างกัน 2 รายการ (ถ้าพวกเขาไม่ได้เชื่อมต่อกัน)

อย่างไรก็ตามสิ่งที่ฉันทำและสิ่งที่คุณสามารถทำได้คือการเพิ่มวัตถุโรงงานชัดเจนเพื่อสร้างผู้บริโภคที่เชื่อมต่อด้วยวิธีต่อไปนี้:

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

หากในความเป็นจริงประเภทเหล่านั้นเชื่อมโยงกัน (เกี่ยวข้อง) จริง ๆ ฉันขอแนะนำให้สร้างการนำไปใช้ในลักษณะดังกล่าว:

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

ข้อได้เปรียบคือคลาสโรงงานรู้ถึงการใช้งานทั้งสองแบบมีสถานะที่ใช้ร่วมกัน (หากจำเป็น) และคุณสามารถส่งคืนผู้บริโภคเพิ่มขึ้นหากต้องการ ไม่มีการประกาศวิธีการใช้ซ้ำซึ่งไม่ได้มาจากอินเทอร์เฟซ

โปรดทราบว่าผู้บริโภคแต่ละคนอาจมีชั้นเรียนที่เป็นอิสระ

ข้อเสียของการแก้ปัญหานั้นคือความซับซ้อนในระดับที่สูงขึ้น (แม้ว่าจะเป็นไฟล์จาวาหนึ่งไฟล์) และในการเข้าถึงวิธีการใช้งานคุณต้องใช้การเรียกอีกหนึ่งครั้งแทน:

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

คุณมี:

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

เพื่อสรุปคุณสามารถกำหนดผู้ใช้ทั่วไป 2 รายในหนึ่งระดับบนสุดโดยใช้คลาสภายใน 2 ชั้น แต่ในกรณีของการโทรคุณต้องได้รับการอ้างอิงถึงผู้บริโภคที่ใช้งานเป็นครั้งแรกเนื่องจากนี่ไม่ใช่วัตถุสำหรับผู้บริโภคเพียงชิ้นเดียว


1

ในรูปแบบการทำงานมันค่อนข้างง่ายโดยไม่ต้องใช้อินเทอร์เฟซและทำการตรวจสอบเวลาแบบคอมไพล์

ส่วนต่อประสานการทำงานของเราใช้งานเอนทิตี

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

ผู้จัดการของเราดำเนินการและใช้งานเอนทิตีอย่างเหมาะสม

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

ทางเลือกอื่นเพื่อหลีกเลี่ยงการใช้คลาสเพิ่มเติม (ตัวอย่างโดยใช้ 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

ขอโทษที่ตอบคำถามเก่า ๆ แต่ฉันรักมันจริงๆ! ลองตัวเลือกนี้:

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

ฉันคิดว่านั่นคือสิ่งที่คุณกำลังมองหา

คุณได้รับผลลัพธ์นี้:

มะเขือเทศกิน!

ฉันกินแอปเปิ้ล

การบริโภคสตริง!


ในคำถาม: "แต่ฉันกำลังมองหาการตรวจสอบเวลารวบรวม ... "
aeracode

@ aeracode ไม่มีตัวเลือกให้ทำตามที่ OP ต้องการ การลบประเภททำให้ไม่สามารถใช้อินเทอร์เฟซเดิมซ้ำสองครั้งพร้อมกับตัวแปรประเภทต่างๆ ฉันแค่พยายามให้วิธีอื่นแก่คุณ แน่นอนคุณสามารถตรวจสอบประเภทที่ได้รับการยอมรับก่อนหน้านี้เพื่อบริโภค onbject
Awes0meM4n
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.