Một ví dụ thực tế về generic <là gì? siêu T>?


76

Tôi hiểu rằng nó <? super T>đại diện cho bất kỳ siêu lớp nào của T(lớp cha Tcủa bất kỳ cấp nào). Nhưng tôi thực sự đấu tranh để tưởng tượng bất kỳ ví dụ thực tế nào cho ký tự đại diện liên kết chung này.

Tôi hiểu <? super T>nghĩa là gì và tôi đã thấy phương pháp này:

public class Collections {
  public static <T> void copy(List<? super T> dest, List<? extends T> src) {
      for (int i = 0; i < src.size(); i++)
        dest.set(i, src.get(i));
  }
}

Tôi đang tìm kiếm một ví dụ về trường hợp sử dụng thực tế trong đó cấu trúc này có thể được sử dụng chứ không phải để giải thích nó là gì.


18
này KHÔNG phải là một trùng lặp, một câu hỏi rất hợp lệ
Eugene

6
tôi cũng không nghĩ rằng một bản sao của nó, ông được hỏi cho các tình huống cụ thể, không phải là nguyên tắc đằng sau
ItFreak

2
Đây là phần mở đầu của câu trả lời mà tôi định viết khi câu trả lời này đã kết thúc: Tôi đồng ý ở một mức độ nào đó với những cử tri thân thiết: Câu trả lời có thể được rút ra, với một số sự cẩn trọng, từ stackoverflow.com/questions/2723397/… . Tuy nhiên, câu hỏi này (cũng như các câu trả lời ở đây) tập trung vào nguyên tắc kỹ thuật. Một ví dụ đơn giản, thực tế về nơi hợp lý để sử dụng ? super Tcó thể hữu ích.
Marco 13

4
Đừng nghĩ rằng điều này nên được đóng lại như một bản sao, vì tác giả đang yêu cầu các mô hình OOP trong thế giới thực, thay vì giải thích sâu về cách hoạt động của kế thừa trong Java.
John Stark

3
Ví dụ này ở đây không phải là một trường hợp sử dụng thực tế sao?
user253751

Câu trả lời:


49

Ví dụ dễ nhất mà tôi có thể nghĩ đến là:

public static <T extends Comparable<? super T>> void sort(List<T> list) {
    list.sort(null);
}

lấy từ cùng một Collections. Cách này a Dogcó thể thực hiện Comparable<Animal>và nếu Animalđã thực hiện Dogthì không phải làm gì cả.

CHỈNH SỬA cho một ví dụ thực tế:

Sau một số trận bóng bàn qua email, tôi được phép trình bày một ví dụ thực tế từ nơi làm việc của tôi (vâng!).

Chúng ta có một giao diện được gọi là Sink(nó không quan trọng nó làm gì), ý tưởng là tích lũy mọi thứ. Khai báo khá đơn giản (đơn giản hóa):

interface Sink<T> {
    void accumulate(T t);
}

Rõ ràng là có một phương thức trợ giúp lấy a Listvà rút các phần tử của nó thành a Sink(phức tạp hơn một chút, nhưng để làm cho nó đơn giản):

public static <T> void drainToSink(List<T> collection, Sink<T> sink) {
    collection.forEach(sink::accumulate);
}

Điều này thật đơn giản phải không? Tốt...

Tôi có thể có a List<String>, nhưng tôi muốn rút cạn nó thành a Sink<Object>- đây là một việc khá phổ biến đối với chúng ta; nhưng điều này sẽ thất bại:

Sink<Object> sink = null;
List<String> strings = List.of("abc");
drainToSink(strings, sink);

Để điều này hoạt động, chúng ta cần thay đổi khai báo thành:

public static <T> void drainToSink(List<T> collection, Sink<? super T> sink) {
    ....
}

1
Hướng dẫn sử dụng Kotlin Sourcelàm ví dụ ... về cơ bản là Sinkví dụ kép của bạn .
Bakuriu

2
Chắc chắn bạn có thể xác định nó theo cách ngược lại bằng cách lập danh sách? mở rộng T?
Weckar E.

@WeckarE. Không nếu phương thức nằm trong danh sách.
Phục hồi Monica

1
@WeckarE. Tôi có thể có, nhưng điều này sẽ thay đổi ngữ nghĩa một chút vì bây giờ tôi không thể thêm bất cứ điều gì vào đó list, nó chỉ trở thành một nhà sản xuất; như đã nói đây là một ví dụ đơn giản ...
Eugene

17

Giả sử bạn có hệ thống phân cấp lớp này: Cat kế thừa từ Mammal, đến lượt nó kế thừa từ Animal.

List<Animal> animals = new ArrayList<>();
List<Mammal> mammals = new ArrayList<>();
List<Cat> cats = ...

Các cuộc gọi này hợp lệ:

Collections.copy(animals, mammals); // all mammals are animals
Collections.copy(mammals, cats);    // all cats are mammals
Collections.copy(animals, cats);    // all cats are animals
Collections.copy(cats, cats);       // all cats are cats 

Nhưng những cuộc gọi này không hợp lệ:

Collections.copy(mammals, animals); // not all animals are mammals
Collections.copy(cats, mammals);    // not all mammals are cats
Collections.copy(cats, animals);    // mot all animals are cats

Vì vậy, chữ ký phương thức chỉ đơn giản là đảm bảo rằng bạn sao chép từ một lớp cụ thể hơn (thấp hơn trong hệ thống phân cấp kế thừa) sang một lớp chung chung hơn (cao hơn trong hệ thống phân cấp kế thừa), chứ không phải ngược lại.


2
Tuy nhiên, supertừ khóa không cần thiết để điều này hoạt động. Chữ ký này cũng sẽ tiếp xúc với hành vi giống hệt nhau: public static <T> void copy(List<T> dest, List<? extends T> src) {
Nick

1
@Nick Bắt tốt. Tại sao lại có chữ ký này? Câu hỏi này nên được đặt ra cho các nhà thiết kế ngôn ngữ Java. Cho đến nay lý do duy nhất tôi thấy là bạn có thể viết mã như: Collections.<Mammal>copy(animals, cats);- nhưng tôi không biết tại sao ai đó sẽ / nên viết mã như thế này ...
Benoit

6

Ví dụ, hãy xem xét Collections.addAllphương thức nhúng:

public static <T> boolean addAll(Collection<? super T> c, T... elements) {
    boolean result = false;
    for (T element : elements)
        result |= c.add(element);
    return result;
}

Ở đây, các phần tử có thể được chèn vào bất kỳ tập hợp nào có kiểu phần tử là siêu kiểu Tcủa kiểu phần tử.

Không có ký tự đại diện giới hạn dưới:

public static <T> boolean addAll(Collection<T> c, T... elements) { ... }

những điều sau sẽ không hợp lệ:

List<Number> nums = new ArrayList<>();
Collections.<Integer>addAll(nums , 1, 2, 3);

bởi vì thuật ngữ hạn Collection<T>chế hơn Collection<? super T>.


Một vi dụ khac:

Predicate<T>giao diện trong Java, sử dụng <? super T>ký tự đại diện trong các phương pháp sau:

default Predicate<T> and(Predicate<? super T> other);

default Predicate<T>  or(Predicate<? super T> other);

<? super T> cho phép xâu chuỗi một loạt các vị từ khác nhau, ví dụ:

Predicate<String> p1 = s -> s.equals("P");
Predicate<Object> p2 = o -> o.equals("P");

p1.and(p2).test("P"); // which wouldn't be possible with a Predicate<T> as a parameter

Tôi không nghĩ ví dụ này đặc biệt hấp dẫn. Nếu bạn bỏ qua phần khởi tạo rõ ràng của biến kiểu, bạn có thể viết Collections.addAll(nums, 1, 2, 3)bằng một trong hai chữ ký phương thức.
Nick

2

Giả sử bạn có một phương pháp:

passToConsumer(Consumer<? super SubType> consumer)

thì bạn gọi phương thức này với bất kỳ phương thức nào Consumercó thể sử dụng SubType:

passToConsumer(Consumer<SuperType> superTypeConsumer)
passToConsumer(Consumer<SubType> subTypeConsumer)
passToConsumer(Consumer<Object> rootConsumer)

Đối với exmaple:

class Animal{}

class Dog extends Animal{

    void putInto(List<? super Dog> list) {
        list.add(this);
    }
}

Vì vậy, tôi có thể đưa Dogvào List<Animal>hoặc List<Dog>:

List<Animal> animals = new ArrayList<>();
List<Dog> dogs = new ArrayList<>();

Dog dog = new Dog();
dog.putInto(dogs);  // OK
dog.putInto(animals);   // OK

Nếu bạn thay đổi putInto(List<? super Dog> list)phương thức thành putInto(List<Animal> list):

Dog dog = new Dog();

List<Dog> dogs = new ArrayList<>();
dog.putInto(dogs);  // compile error, List<Dog> is not sub type of List<Animal>

hoặc putInto(List<Dog> list):

Dog dog = new Dog();

List<Animal> animals = new ArrayList<>();
dog.putInto(animals); // compile error, List<Animal> is not sub type of List<Dog>

1
bất cứ khi nào bạn nói Consumerhay consumenày sẽ tự động rơi vào PECSthể loại, không nói rằng đây là mặc dù xấu, ví dụ tốt
Eugene

1

Tôi đã viết một webradio, vì vậy tôi có lớp MetaInformationObject, là lớp cha cho danh sách phát PLS và M3U. Tôi đã có một cuộc đối thoại lựa chọn, vì vậy tôi đã:

public class SelectMultipleStreamDialog <T extends MetaInformationObject>
public class M3UInfo extends MetaInformationObject
public class PLSInfo extends MetaInformationObject

Lớp này có một phương thức public T getSelectedStream().
Vì vậy, người gọi nhận được T mà là của các loại bê tông (PLS hoặc M3U), nhưng cần thiết để làm việc trên các lớp cha, vì vậy đã có một danh sách: List<T super MetaInformationObject>. nơi kết quả đã được thêm vào.
Đó là cách một cuộc đối thoại chung có thể xử lý các triển khai cụ thể và phần còn lại của mã có thể hoạt động trên lớp cha.
Hy vọng điều đó làm cho nó rõ ràng hơn một chút.


1

Hãy xem xét ví dụ đơn giản này:

List<Number> nums = Arrays.asList(3, 1.2, 4L);
Comparator<Object> numbersByDouble = Comparator.comparing(Object::toString);
nums.sort(numbersByDouble);

Hy vọng rằng đây là một trường hợp hấp dẫn: Bạn có thể tưởng tượng muốn sắp xếp các số cho mục đích hiển thị (mà chuỗi toString là một thứ tự hợp lý), nhưng Numberbản thân nó không phải là So sánh được.

Điều này biên dịch vì integers::sortmất a Comparator<? super E>. Nếu nó chỉ mất một Comparator<E>( Etrong trường hợp này là Number), thì mã sẽ không thể biên dịch vì Comparator<Object>không phải là một kiểu con của Comparator<Number>(do những lý do mà câu hỏi của bạn cho biết bạn đã hiểu, vì vậy tôi sẽ không đi sâu vào).


1

Bộ sưu tập là một ví dụ điển hình ở đây.

Như đã nêu trong 1 , List<? super T>cho phép bạn tạo Listsẽ chứa các phần tử của kiểu, ít được dẫn xuất hơn T, vì vậy nó có thể giữ các phần tử kế thừa từ T, kiểu Tđó và Tkế thừa từ.

Mặt khác, List<? extends T>cho phép bạn xác định một Listchỉ có thể chứa các phần tử kế thừa từ đó T(trong một số trường hợp thậm chí không thuộc loại T).

Đây là một ví dụ tốt:

public class Collections {
  public static <T> void copy(List<? super T> dest, List<? extends T> src) {
      for (int i = 0; i < src.size(); i++)
        dest.set(i, src.get(i));
  }
}

Ở đây bạn muốn chiếu Listtừ loại dẫn xuất ít hơn sang Listloại dẫn xuất ít hơn. Ở đây List<? super T>đảm bảo với chúng tôi rằng tất cả các phần tử từ srcsẽ hợp lệ trong bộ sưu tập mới.

1 : Sự khác biệt giữa <? super T> và <? mở rộng T> trong Java


0

Giả sử bạn có:

class T {}
class Decoder<T>
class Encoder<T>

byte[] encode(T object, Encoder<? super T> encoder);    // encode objects of type T
T decode(byte[] stream, Decoder<? extends T> decoder);  // decode a byte stream into a type T

Và sau đó:

class U extends T {}
Decoder<U> decoderOfU;
decode(stream, decoderOfU);     // you need something that can decode into T, I give you a decoder of U, you'll get U instances back

Encoder<Object> encoderOfObject;
encode(stream, encoderOfObject);// you need something that can encode T, I give you something that can encode all the way to java.lang.Object

0

Một vài ví dụ thực tế được ghi nhớ cho điều này. Ý tưởng đầu tiên tôi muốn đưa ra là ý tưởng về một vật thể trong thế giới thực được sử dụng cho chức năng 'ngẫu hứng'. Hãy tưởng tượng rằng bạn có một cờ lê ổ cắm:

public class SocketWrench <T extends Wrench>

Mục đích rõ ràng của cờ lê ổ cắm nên nó được sử dụng như một Wrench. Tuy nhiên, nếu bạn cho rằng một chiếc cờ lê có thể được sử dụng trong một cái chốt để đập vào một chiếc đinh, bạn có thể có một hệ thống phân cấp thừa kế giống như sau:

public class SocketWrench <T extends Wrench>
public class Wrench extends Hammer

Trong trường hợp này, bạn có thể gọi socketWrench.pound(Nail nail = new FinishingNail()), mặc dù đó sẽ được coi là một cách sử dụng không điển hình cho a SocketWrench.

Trong khi tất cả cùng, SocketWrenchsẽ có quyền truy cập để có thể gọi các phương thức như applyTorque(100).withRotation("clockwise").withSocketSize(14)thể nó được sử dụng như một SocketWrenchthay vì chỉ a Wrench, thay vì a Hammer.


Tôi không thấy điều này hấp dẫn lắm: tại sao SocketWrench lại là một loại chung?
Tối đa

Bạn có thể có tất cả các loại cờ lê ổ cắm: ổ 1/4 ”, ổ 1/2”, cờ lê ổ cắm góc tay cầm có thể điều chỉnh, cờ-lê mô-men xoắn, số lượng răng bánh cóc khác nhau, v.v. Nhưng nếu bạn chỉ cần một cờ lê có bánh cóc, bạn có thể sử dụng một cờ lê Ổ cắm chung thay vì cờ lê ổ cắm 32 răng góc có ổ 3/4 ”cụ thể.
John Stark
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.