Java Generics (ограниченные подстановочные знаки)
Согласно книге "Эффективная Java" Джошуа Блоха существует правило о том, как / когда использовать ограниченные символы подстановки в дженериках. Это правило PECS (Производитель-Расширяет, Comsumer-Super). Когда я изучаю следующий пример:
Stack<Number> numberStack = new Stack<Number>();
Iterable<Integer> integers = ... ;
numberStack.pushAll(integers);
Я понимаю, что это правило идеально подходит в этом примере. Я должен объявить метод pushAll
как следующий образец:
// Wildcard type for parameter that serves as an E producer
public void pushAll(Iterable<? extends E> src) {
for (E e : src)
{
push(e);
}
}
Но что произойдет, если у меня есть следующий пример?
Stack<Integer> integerStack = new Stack<Integer>();
Iterable<Number> numbers = ... ;
integerStack.pushAll(numbers);
Я должен объявить pushAll
как следует:
public void pushAll(Iterable<? super E> src) {
for (E e : src)
{
push(e);
}
}
Согласно правилу PECS вышеуказанная декларация неверна. Но я хочу иметь Stack
из Integer
и перейти к этому Stack
Number
, Почему бы не сделать это?
Почему я всегда должен использовать extends
ключевое слово? Зачем использовать super
неправильно?
Конечно, то же самое относится и к точке зрения потребителя. Почему потребитель всегда должен быть super
?
PS: Чтобы быть более конкретным, вы можете найти этот пример выше в разделе "Пункт 28" упомянутой книги.
5 ответов
Когда вы объявляете Stack<Foo>
Вы имеете в виду стек Фу или подклассы Фу. Как пример, вы ожидаете, что сможете поставить String
в Stack<Object>
, Другой способ неверен, вы не сможете вставить другой объект в Stack<String>
,
В вашем примере вы объявляете Stack<Integer>
, Вы должны иметь возможность помещать целые числа в этот стек, но не другие числа (например, двойные), как если бы вы объявили параметр <? super E>
, Вот почему метод put должен иметь параметр типа <? extends E>
,
Попытка сохранить произвольные числа в стеке может не сработать, поскольку число может быть чем-то другим, чем целым числом. Так что твой пример не имеет особого смысла.
Вы бы использовали super, когда объект указывается как потребитель, т.е. когда экземпляры универсального типа объекта передаются в качестве аргументов методам объекта. Например:
Collections.sort(List<T>, Comparator<? super T>)
В этом примере метод sort берет T экземпляров из коллекции и передает их в качестве аргумента compare(T o1, T o2)
компаратора.
Сравните это с вашим первым примером, где Iterable src
является производителем. pushAll()
метод вызывает метод Iterable, который обрабатывает (то есть возвращает) экземпляры T. В этом случае iterable является производителем, поэтому используется ? extends T
Первое, на что следует обратить внимание, это то, что Integer расширяет Number, поэтому не следует помещать объекты Number в стек целых чисел. Однако первый пример будет работать с целыми числами, числами с плавающей точкой, BigDecimal и всеми другими подклассами Number.
Ваш пример не имеет особого смысла. Конструкция как <? extends Number>
означает, что Number и каждый тип разрешены, который наследуется от Number. Таким образом, вы определяете верхнюю и нижнюю границу от типа Number до наиболее конкретной. Наоборот, <? super Number>
означает, что номер и любой из его суперпользователей разрешены. Так как Number расширяет Object и реализует Serializable, допускаются следующие три типа:
- java.lang.Number
- java.lang.Object
- java.io.Serializable
В вашем примере вы объявляете универсальный тип Stack<Integer>
, Давайте рассмотрим следующее.
- Ваш стек никогда не сможет содержать предметы любого супер тип Integer
- Ваш стек никогда не сможет содержать элементы любого подтипа Integer, поскольку класс Integer является окончательным и, следовательно, он не может быть разделен на подклассы.
Итак, если вы хотите объявить универсальный тип Stack<Integer>
Ваш повторяемый тип Iterable<Integer>
и, таким образом, ваш стек может содержать только предметы типа Integer. Вы абсолютно правы с мнемоническим PECS, но это работает, только если вы выбрали конкретный тип, который имеет по крайней мере один супертип и хотя бы один подтип.
В pushAll
метод, вы не передаете тип E
, но любой тип, который расширяется E
, Таким образом, вместо прохождения Iterable
из Number
с, вы можете передать любой Iterable
типа, который расширяется Number
,
Исходный пример использует Number
типа, потому что вы можете передать любой тип, который является подклассом Number
, лайк Integer
, BigDecimal
и так далее.
В вашем примере вы делаете это наоборот. Ты используешь Integer
объявить свой Stack
, Следовательно, pushAll
сможет принимать только те классы, которые расширены Integer
, Вы не сможете использовать Numbers
(или любой другой класс, потому что Integer
это последний класс).