Каков пример реальной жизни родовых <? super T> ?
Я понимаю, что <? super T>
<? super T>
представляет любой суперкласс T
(родительский класс T
любого уровня). Но я действительно изо всех сил пытаюсь представить себе пример реальной жизни для этого общего шаблона.
Я понимаю, что <? super 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));
}
}
Я ищу пример использования в реальной жизни, где эта конструкция может использоваться, а не для объяснения того, что это такое.
Ответы
Ответ 1
Самый простой пример, о котором я могу думать, это:
public static <T extends Comparable<? super T>> void sort(List<T> list) {
list.sort(null);
}
взятых из тех же Collections
. Таким образом, Dog
может реализовать Comparable<Animal>
и если Animal
уже реализует это, Dog
ничего не должен делать.
EDIT для реального примера:
После некоторых пинг-понг по электронной почте я могу представить настоящий пример из своего рабочего места (yay!).
У нас есть интерфейс под названием Sink
(неважно, что он делает), идея в том, что это накапливает вещи. Декларация довольно тривиальна (упрощена):
interface Sink<T> {
void accumulate(T t);
}
Очевидно, что есть вспомогательный метод, который принимает List
и истощает его элементы в Sink
(это немного сложнее, но сделать его простым):
public static <T> void drainToSink(List<T> collection, Sink<T> sink) {
collection.forEach(sink::accumulate);
}
Это просто так? Что ж...
Я могу иметь List<String>
, но я хочу слить его в 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) {
....
}
Ответ 2
Предположим, что у вас есть эта иерархия классов: Cat наследует от Млекопитающего, который, в свою очередь, наследует от 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
Таким образом, подпись метода просто гарантирует, что вы копируете более конкретный (ниже в иерархии наследования) класс в более общий класс (верхний в иерархии наследования), а не наоборот.
Ответ 3
Например, посмотрите в 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>
Collection<? super T>
.
Другой пример:
Predicate<T>
в Java, который использует <? super T>
<? super T>
в следующих методах:
default Predicate<T> and(Predicate<? super T> other);
default Predicate<T> or(Predicate<? super T> other);
<? super T>
<? 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
Ответ 4
Предположим, у вас есть метод:
passToConsumer(Consumer<? super SubType> consumer)
то вы вызываете этот метод с любым Consumer
который может потреблять SubType
:
passToConsumer(Consumer<SuperType> superTypeConsumer)
passToConsumer(Consumer<SubType> subTypeConsumer)
passToConsumer(Consumer<Object> rootConsumer)
Для exmaple:
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>
Ответ 5
Я написал 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>
. где результат был добавлен.
То, как общий диалог может обрабатывать конкретные реализации, а остальная часть кода может работать над суперклассом.
Надеюсь, что это станет более понятным.
Ответ 6
Рассмотрим этот простой пример:
List<Number> nums = Arrays.asList(3, 1.2, 4L);
Comparator<Object> numbersByDouble = Comparator.comparing(Object::toString);
nums.sort(numbersByDouble);
Надеюсь, это довольно убедительный случай: вы можете себе представить, что нужно сортировать числа для целей показа (для которых toString является разумным порядком), но Number
не является самим Сопоставимым.
Это компилируется, потому что integers::sort
принимает Comparator<? super E>
Comparator<? super E>
. Если для этого потребовался только Comparator<E>
(где E
в этом случае - Number
), код не будет скомпилирован, потому что Comparator<Object>
не является подтипом Comparator<Number>
(из-за причин, по которым ваш вопрос указывает на то, что вы уже понимайте, поэтому я не буду вдаваться).
Ответ 7
Хорошим примером здесь служат коллекции.
Как указано в 1, List<? super T>
List<? super T>
позволяет создавать List
котором будут храниться элементы типа, которые имеют меньше производных, чем T
, поэтому он может содержать элементы, которые наследуют от T
, которые являются типом T
и что наследует T
С другой стороны, List<? extends 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>
List<? super T>
уверяет нас, что все элементы из src
будут действительны в новой коллекции.
1: Разница между <? супер T> и <? расширяет T> в Java
Ответ 8
Скажем, у вас есть:
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
Ответ 9
Для этого приходят в голову несколько примеров реальной жизни. Первое, что мне нравится воспитывать, - это идея реального объекта, используемого для "импровизированной" функциональности. Представьте, что у вас есть торцевой ключ:
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)
таких методов, как applyTorque(100).withRotation("clockwise").withSocketSize(14)
если он используется как SocketWrench
а не только Wrench
, а не Hammer
.