Làm thế nào để tạo một lớp Java thực hiện một giao diện với hai loại chung?


164

Tôi có một giao diện chung

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

Tôi có một lớp tiêu thụ hai loại đối tượng, vì vậy tôi muốn làm một cái gì đó như:

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

Rõ ràng tôi không thể làm điều đó.

Tất nhiên tôi có thể tự thực hiện công văn, vd

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

Nhưng tôi đang tìm kiếm giải pháp kiểm tra và điều phối kiểu thời gian biên dịch mà thuốc generic cung cấp.

Giải pháp tốt nhất tôi có thể nghĩ đến là xác định các giao diện riêng biệt, vd

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

Về mặt chức năng, giải pháp này là OK, tôi nghĩ vậy. Nó chỉ dài dòng và xấu xí.

Có ý kiến ​​gì không?


Tại sao bạn cần hai giao diện chung của cùng một kiểu cơ sở?
akarnokd

6
Do loại tẩy bạn không thể làm điều đó. Giữ cho nó hai lớp khác nhau thực hiện người tiêu dùng. Tạo nhiều lớp nhỏ hơn nhưng giữ mã chung của bạn (Không sử dụng câu trả lời được chấp nhận, nó phá vỡ toàn bộ khái niệm ... bạn không thể coi TwoTypesConsumer là người tiêu dùng, đó là BAD).
Lewis Diamond

Kiểm tra cái này để biết phong cách chức năng - stackoverflow.com/a/60466413/4121845
mano_ksp

Câu trả lời:


78

Xem xét đóng gói:

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

Nếu tạo các lớp bên trong tĩnh này làm phiền bạn, bạn có thể sử dụng các lớp ẩn danh:

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
bằng cách nào đó trông có vẻ giống như sao chép mã ... Tôi gặp phải vấn đề tương tự và không tìm thấy giải pháp nào khác có vẻ sạch sẽ.
bln-tom

109
Nhưng TwoTypesConsumerhoàn thành không có hợp đồng, vậy vấn đề là gì? Nó không thể được truyền cho một phương thức muốn một trong hai loại Consumer. Toàn bộ ý tưởng của một người tiêu dùng hai loại sẽ là bạn có thể đưa nó cho một phương pháp muốn một người tiêu dùng cà chua cũng như một phương pháp muốn một người tiêu dùng táo. Ở đây chúng tôi không có.
Jeff Axelrod

@JeffAxelrod Tôi sẽ làm cho các lớp bên trong không tĩnh để chúng có quyền truy cập vào thể hiện kèm theo TwoTypesConsumernếu cần, và sau đó bạn có thể chuyển twoTypesConsumer.getAppleConsumer()sang một phương thức muốn có một người tiêu dùng táo. Một tùy chọn khác là thêm các phương thức tương tự như addConsumer(Producer<Apple> producer)TwoTypesConsumer.
herman

Điều này không hoạt động nếu bạn không có quyền kiểm soát giao diện (ví dụ: cxf / rs ExceptionMapper) ...
vikingsteve

17
Tôi sẽ nói rằng: Đây là một lỗ hổng với Java. Hoàn toàn không có lý do gì chúng ta không nên được phép có nhiều triển khai của cùng một giao diện, miễn là các triển khai đó có các đối số khác nhau.
gromit190

41

Do xóa kiểu, bạn không thể thực hiện cùng một giao diện hai lần (với các tham số loại khác nhau).


6
Tôi có thể thấy nó là một vấn đề như thế nào ... câu hỏi đặt ra là cách tốt nhất (hiệu quả nhất, an toàn, thanh lịch) là gì để vượt qua vấn đề này.
daphshez

2
Không đi sâu vào logic kinh doanh, một cái gì đó ở đây 'có mùi' giống như mẫu Khách truy cập.
Shimi Bandiel

12

Đây là một giải pháp khả thi dựa trên cơ sở của 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);
            }
        }
    }
}

Yêu cầu ngầm của câu hỏi là Consumer<Tomato>Consumer<Apple>các đối tượng chia sẻ trạng thái. Sự cần thiết cho Consumer<Tomato>, Consumer<Apple>các đối tượng đến từ các phương thức khác mong đợi chúng là các tham số. Tôi cần một lớp thực hiện cả hai để chia sẻ trạng thái.

Ý tưởng của Steve là sử dụng hai lớp bên trong, mỗi lớp thực hiện một kiểu chung khác nhau.

Phiên bản này thêm getters cho các đối tượng thực hiện giao diện Người tiêu dùng, sau đó có thể được chuyển sang các phương thức khác mong đợi chúng.


2
Nếu bất cứ ai sử dụng điều này: đáng để lưu trữ các Consumer<*>thể hiện trong các trường ví dụ nếu get*Consumerđược gọi thường xuyên.
TWiStErRob

7

Ít nhất, bạn có thể thực hiện một cải tiến nhỏ cho việc triển khai công văn bằng cách thực hiện một số việc như sau:

public class TwoTypesConsumer implements Consumer<Fruit> {

Trái cây là tổ tiên của cà chua và táo.


14
Cảm ơn, nhưng bất cứ điều gì các chuyên gia nói, tôi không coi Tomato là trái cây. Thật không may, không có lớp cơ sở chung nào ngoài Object.
daphshez

2
Bạn luôn có thể tạo một lớp cơ sở có tên: AppleOrTomato;)
Shimi Bandiel

1
Tốt hơn, thêm một Trái cây ủy nhiệm cho Apple hoặc Tomato.
Tom Hawtin - tackline

@Tom: Trừ khi tôi hiểu sai những gì bạn đang nói, đề xuất của bạn chỉ đẩy vấn đề về phía trước, vì, để Fruit có thể ủy thác cho Apple hoặc Tomato, Fruit phải có một lĩnh vực siêu lớp cho cả Apple và Tomato đề cập đến đối tượng mà nó ủy thác.
Buhb

1
Điều này có nghĩa là TwoTypesConsumer có thể tiêu thụ bất kỳ loại Trái cây nào, bất kỳ loại nào hiện đang được triển khai và bất kỳ ai đó có thể thực hiện trong tương lai.
Tom Gillen

3

chỉ vấp phải điều này Nó đã xảy ra, rằng tôi có cùng một vấn đề, nhưng tôi đã giải quyết nó theo một cách khác: tôi vừa tạo một Giao diện mới như thế này

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

thật không may, điều này được coi là Consumer<A>và KHÔNG Consumer<B>chống lại tất cả Logic. Vì vậy, bạn phải tạo một Bộ chuyển đổi nhỏ cho người tiêu dùng thứ hai như thế này trong lớp của bạn

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

nếu Consumer<A>cần, bạn có thể vượt qua thisvà nếu Consumer<B>cần chỉ cần vượt quaconsumerAdapter


Câu trả lời của Daphna là như vậy, nhưng sạch sẽ hơn và ít bị gây rối hơn.
TWiStErRob

1

Bạn không thể thực hiện điều này trực tiếp trong một lớp vì định nghĩa lớp bên dưới không thể được biên dịch do xóa các kiểu chung và khai báo giao diện trùng lặp.

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

Bất kỳ giải pháp nào khác để đóng gói các hoạt động tiêu thụ giống nhau trong một lớp đều yêu cầu xác định lớp của bạn là:

class TwoTypesConsumer { ... }

Điều này là vô nghĩa khi bạn cần lặp lại / sao chép định nghĩa của cả hai thao tác và chúng sẽ không được tham chiếu từ giao diện. IMHO làm điều này là một sự trùng lặp mã nhỏ và xấu mà tôi đang cố gắng tránh.

Đây cũng có thể là một chỉ báo cho thấy có quá nhiều trách nhiệm trong một lớp để tiêu thụ 2 đối tượng khác nhau (nếu chúng không được ghép nối).

Tuy nhiên, những gì tôi đang làm và những gì bạn có thể làm là thêm đối tượng nhà máy rõ ràng để tạo người tiêu dùng được kết nối theo cách sau:

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

Nếu trong thực tế các loại đó thực sự được ghép nối (liên quan) thì tôi khuyên bạn nên tạo một triển khai theo cách như sau:

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

Ưu điểm là lớp nhà máy biết cả hai triển khai, có trạng thái chia sẻ (nếu cần) và bạn có thể trả lại nhiều người tiêu dùng được ghép nối hơn nếu cần. Không có khai báo phương thức tiêu thụ lặp lại mà không xuất phát từ giao diện.

Xin lưu ý rằng mỗi người tiêu dùng có thể là lớp độc lập (vẫn riêng tư) nếu họ không hoàn toàn liên quan.

Nhược điểm của giải pháp đó là độ phức tạp của lớp cao hơn (ngay cả khi đây có thể là một tệp java) và để truy cập phương thức tiêu thụ, bạn cần thêm một cuộc gọi thay vì:

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

bạn có:

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

Để tóm tắt, bạn có thể định nghĩa 2 người tiêu dùng chung trong một lớp cấp cao nhất bằng cách sử dụng 2 lớp bên trong nhưng trong trường hợp gọi bạn cần có một tham chiếu trước cho người tiêu dùng thực hiện phù hợp vì đây không thể chỉ là một đối tượng người tiêu dùng.


1

Trong kiểu Chức năng, việc thực hiện giao diện này khá dễ dàng mà không cần thực hiện kiểm tra kiểu thời gian biên dịch.

Giao diện chức năng của chúng tôi để tiêu thụ thực thể

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

quản lý của chúng tôi để xử lý và tiêu thụ thực thể một cách thích hợp

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

Một cách khác để tránh việc sử dụng nhiều lớp hơn. (ví dụ sử dụng 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

Xin lỗi vì đã trả lời những câu hỏi cũ, nhưng tôi thực sự thích nó! Hãy thử tùy chọn này:

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

Tôi nghĩ rằng đó là những gì bạn đang tìm kiếm.

Bạn nhận được đầu ra này:

Cà chua tiêu thụ!

Tôi ăn một trái táo

Chuỗi tiêu thụ!


Trong câu hỏi: "Nhưng tôi đang tìm kiếm kiểu kiểm tra thời gian biên dịch ..."
aeracode

@aeracode Không có tùy chọn để làm những gì OP muốn. Loại xóa làm cho có thể áp dụng để thực hiện cùng một giao diện hai lần với các biến loại khác nhau. Tôi chỉ cố gắng để cho bạn một cách khác. Tất nhiên bạn có thể kiểm tra các loại được chấp nhận trước đây để sử dụng một onbject.
Awes0meM4n
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.