Що є реальним прикладом загального <? супер T>?


76

Я розумію, що <? super T>представляє будь-який супер клас T(батьківський клас Tбудь-якого рівня). Але я справді намагаюся уявити будь-який приклад із реального життя для цього загального зв’язаного символу.

Я розумію, що <? super 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));
  }
}

Я шукаю приклад випадку реального використання, де можна використовувати цю конструкцію, а не пояснення того, що це таке.


18
це НЕ дублікат, дуже слушне запитання
Євген

6
я також не думаю, що це дублікат, він просить конкретних ситуацій, а не принципу
ItFreak

2
Це початок відповіді, яку я збирався написати, коли цю закрили: Я певною мірою погоджуюсь із близькими виборцями: Відповідь можна було б докласти, з певною ретельністю, зі stackoverflow.com/questions/2723397/… . Однак це питання (як і відповіді тут) зосереджується на технічному принципі. Простий, реалістичний приклад того, де має сенс використовувати, ? super Tможе бути корисним.
Marco13

4
Не думайте, що це слід було закрити як копію, оскільки автор вимагає реальних моделей ООП, а не глибоких пояснень того, як працює успадкування в Java.
Джон Старк

3
Чи не є цей приклад тут реальним випадком використання?
user253751

Відповіді:


49

Найпростіший приклад, який я можу придумати, це:

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

взято з того самого Collections. Таким чином a Dogможе реалізувати, Comparable<Animal>і якщо Animalвже реалізує це, Dogне потрібно нічого робити.

EDIT для реального прикладу:

Після кількох електронних пінг-понгів, мені дозволено подати реальний приклад зі свого робочого місця (так!).

У нас є інтерфейс, який називається Sink(неважливо, що він робить), ідея полягає в тому, що це накопичує речі. Заява досить тривіальна (спрощена):

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

Очевидно, що є допоміжний метод, який приймає a Listі зливає його елементи в a Sink(це дещо складніше, але для спрощення):

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

Це просто так? Ну...

Я можу взяти a List<String>, але я хочу злити його до a Sink<Object>- це досить звична справа для нас; але це не вдасться:

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

Щоб це працювало, нам потрібно змінити декларацію на:

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

1
Підручник з Kotlin використовує Sourceяк приклад ... що в основному є подвійним вашим Sinkприкладом.
Бакуріу

2
Звичайно, ви могли б визначити це навпаки, склавши список? продовжує T?
Веккар Е.

@WeckarE. Ні, якщо метод є у самому списку.
Поновити Моніку

1
@WeckarE. Я міг би так, але це трохи змінило б семантику, тепер я не можу до цього нічого додати list, він стає лише продюсером; як сказано, це спрощений приклад ...
Євген

17

Припустимо, у вас є така ієрархія класів: Кіт успадковує від Ссавців, а той, в свою чергу, від Animal.

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

Ці дзвінки є дійсними:

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 

Але ці дзвінки не є дійсними:

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

Отже, підпис методу просто гарантує, що ви копіюєте з більш конкретного (нижчого в ієрархії успадкування) класу в більш загальний клас (верхній в ієрархії успадкування), а не навпаки.


2
Однак superдля роботи цього ключового слова не потрібно. Цей підпис також виявив би ідентичну поведінку: public static <T> void copy(List<T> dest, List<? extends T> src) {
Нік

1
@ Нік Хороший улов. Навіщо тоді цей підпис? Це питання слід порушити дизайнерам мови Java. Поки що єдиною причиною, яку я виявив, є те, що ви можете писати код на зразок: Collections.<Mammal>copy(animals, cats);- але я не знаю, чому хтось повинен / повинен писати такий код ...
Бенуа

6

Наприклад, розглянемо Collections.addAllметод implmenetation:

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

Тут елементи можна вставити в будь-яку колекцію, тип елемента якої є надтипом типу Tелемента.

Без нижнього обмеженого символу:

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

наступне було б недійсним:

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

оскільки термін Collection<T>є більш обмежувальним, ніж Collection<? super T>.


Інший приклад:

Predicate<T>інтерфейс в Java, який використовує <? super T>підстановочний знак у таких методах:

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

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

<? super T> дозволяє зв’язати ширший спектр різних предикатів, наприклад:

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

Я не думаю, що цей приклад є особливо переконливим. Якщо залишити явний екземпляр змінної типу, ви настільки ж зможете писати Collections.addAll(nums, 1, 2, 3)підписом будь-якого методу.
Нік

2

Припустимо, у вас є метод:

passToConsumer(Consumer<? super SubType> consumer)

тоді ви викликаєте цей метод із будь- Consumerяким, який може споживати SubType:

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

Для прикладу:

class Animal{}

class Dog extends Animal{

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

Так що я можу поставити Dogв List<Animal>або List<Dog>:

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

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

Якщо ви зміните putInto(List<? super Dog> list)метод на 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>

або 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
щоразу, коли ви говорите, Consumerабо consumeце автоматично потрапляє в PECSкатегорію, але не кажучи, що це погано, хороші приклади
Євген

1

Я написав webradio, тому мав клас MetaInformationObject, який був суперкласом для списків відтворення PLS та M3U. У мене був діалог з відбору, тому я мав:

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

Цей клас мав метод public T getSelectedStream().
Таким чином, абонент отримує T , який був типу бетону (PLS або M3U), але необхідне для роботи на суперкласу, так що був список: List<T super MetaInformationObject>. де був доданий результат.
Ось як загальний діалог може обробляти конкретні реалізації, а решта коду може працювати над суперкласом.
Сподіваюся, це робить це трохи зрозумілішим.


1

Розглянемо цей простий приклад:

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

Сподіваємось, це дещо вагомий випадок: ви могли б уявити, що хочете відсортувати номери для цілей відображення (для яких toString є розумним замовленням), але Numberсам по собі не порівнянний.

Це компілюється, оскільки integers::sortбере a Comparator<? super E>. Якби це взяло лише a Comparator<E>(де Eв даному випадку є Number), тоді код не вдалося б скомпілювати, оскільки Comparator<Object>він не є підтипом Comparator<Number>(через причини, за якими ваше питання вказує, що ви вже зрозуміли, тому я не буду вдаватися до них).


1

Хорошим прикладом тут служать колекції.

Як зазначено в 1 , List<? super T>дозволяє створювати Listелементи, що містять елементи типу, які є менш похідними, ніж T, таким чином, він може містити елементи, які успадковують від T, є типом Tі що Tуспадковує від.

З іншого боку, List<? extends T>дозволяє визначити файл, Listякий може містити лише елементи, які успадковуються від 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));
  }
}

Тут ви хочете проектувати Listменш похідний тип на Listменш похідний тип. Тут List<? super T>ми запевняємо, що всі елементи від srcбудуть дійсними в новій колекції.

1 : Різниця між <? супер Т> і <? розширює T> на Java


0

Скажімо, у вас є:

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

І потім:

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

Для цього мені спадає на думку кілька прикладів із реального життя. Перше, що я люблю згадувати, - це ідея реального об’єкта, який використовується для «імпровізованої» функціональності. Уявіть, що у вас є торцевий ключ:

public class SocketWrench <T extends Wrench>

Очевидне призначення торцевого ключа, щоб він використовувався як Wrench. Однак, якщо ви вважаєте, що гайковим ключем можна затиснути цвях, можна отримати ієрархію успадкування, яка виглядає так:

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

У цьому випадку ви зможете зателефонувати socketWrench.pound(Nail nail = new FinishingNail()), хоча це буде вважатися нетиповим використанням дляSocketWrench .

У той час як весь час, у них SocketWrenchбуде доступ, щоб мати можливість викликати методи, наприклад, applyTorque(100).withRotation("clockwise").withSocketSize(14)якщо він використовується як SocketWrenchзамість просто Wrench, а не як Hammer.


Я не вважаю це дуже переконливим: чому SocketWrench був би загальним?
Макс.

Ви можете мати будь-які типи торцевих ключів: привід 1/4 ”, привід 1/2”, гайкові ключі з регульованим кутом нахилу ручки, динамометричні ключі, різну кількість зубців з храповим механізмом і т. Д. Але якщо вам просто потрібен гайковий ключ, що має храповик, ви можете використовувати універсальний торцевий ключ замість конкретного 3/4-дюймового 32-зубчатого кутового гайкового ключа.
Джон Старк
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.