P.S. Эта статья написана в ответ на интересный вопрос, поднятый кем-то другим, и этот вопрос укрепил мое понимание обобщений и подстановочных знаков.
Предположим, у нас есть следующее:
A Демонстрационный класс, который может содержать список предметов.
Элементы могут быть любого типа, указанного клиентом/вызывающим абонентом.
В В классе Demo есть метод map , который принимает функцию и возвращает новый список элементов в исходном списке, но измененный этой функцией.
import java.util.List;
import java.util.ArrayList;
import java.util.function.Function;
class Demo {
List list;
Demo(List list) {
this.list = list;
}
Demo map(Function super T, ? extends U> f) {
List answer = new ArrayList();
for (T item : this.list) {
answer.add(f.apply(item));
}
return new Demo(answer);
}
}
дополнительная информация
, супер T, ? расширяет U> и т.д.
Генетика и Ограниченные подстановочные знаки
Функция <...>
Функциональные интерфейсы ( скоро напишу статью об этом ). Достаточно сказать, что это интерфейсы только для одного метода, которые предназначены для того, чтобы сделать функции первоклассными объектами в Java. Здесь пример функции, которую мы собираемся использовать ниже, можно интерпретировать следующим образом: Функция<Объект, целое число> -> x.Хэш-код();f – это функция, которая принимает входные данные типа Объект и выведите Целое число .
Итак, какие из Q1-10 способны компилироваться без ошибок?
// turns an input object into its hash value representation
Function
Ответы 1 ОК 6 ОК 2 ОК 7 ОШИБКА 3 ОК 8 ОШИБКА 4 ОШИБКА 9 ОШИБКА 5 ОШИБКА 10 ОК
Анализ
Давайте попробуем интерпретировать соответствующие параметры типа, и это должно прояснить ситуацию.
Когда мы создаем Демонстрационный объект с помощью его конструктора, нам нужно передать список, содержащий определенный тип элемента. Тип элементов становится типом T в Демонстрационном классе. Так,
// note that map() method is not applied here
// T is replaced by String
Demo listOfString = new Demo(Arrays.asList("a", "b"));
// T is replaced by Integer
List myList = new ArrayList();
myList.add(1);
Demo listOfInteger = new Demo(myList));
По сути, Демонстрационный класс выглядит следующим образом:
class Demo {
List list;
Demo(List list) {
this.list = list;
}
//...
}
class Demo {
List list;
Demo(List list) {
this.list = list;
}
//...
}
Помните, что из-за отношения инвариантности не допускается следующее:
// ERROR
Demo listOfNumber = new Demo(Arrays.asList(1, 1));
Теперь давайте рассмотрим вопрос 1-5
Вопросы, касающиеся назначения
Утверждения 1-5 пытаются сделать следующее:
Вызов конструктора Демо и передайте список строк
Вызов .карта(f) метод на вновь созданном экземпляре и возвращает новый Демонстрационный экземпляр
Назначьте Демонстрационный экземпляр в одну из объявленных переменных.
Конструктор
Демонстрационный список <Целое число> =новая демонстрационная версия <Строка>(строки).карта (f) ;
Здесь нет проблем, так как мы заявляем это T в Демо это Строка и мы передали список строк. Итак, что касается конструктора, мы передаем аргумент правильного типа, и он вернет нам экземпляр Demo<Строка> .
Вот тут-то и возникает путаница. Давайте перечислим важные связанные фрагменты кода:
Function f = x -> x.hashCode();
class Demo {
List list;
Demo(List list) {
this.list = list;
}
Demo map(Function super T, ? extends U> f) {
List answer = new ArrayList();
for (T item : this.list) {
answer.add(f.apply(item));
}
return new Demo(answer);
}
}
Мы знаем, что экземпляр, из которого мы вызываем метод map , имеет Демо<Строка> , из предыдущего обсуждения конструктора.
Поэтому, когда карта вызывается, T в Демо классе все еще Строка . Поэтому мы можем просмотреть наш Демонстрационный класс с T замененным Строкой :
class Demo {
List list;
Demo(List list) {
this.list = list;
}
Demo map(Function super String, ? extends U> f) {
List answer = new ArrayList();
for (String item : this.list) {
answer.add(f.apply(item));
}
return new Demo(answer);
}
}
Теперь у метода map все еще есть два “неизвестных”:
?
U
Когда мы вызываем метод map , мы передаем функцию f который “предоставит” тип методу map , и они должны согласиться, чтобы метод принял f в качестве аргумента.
Давайте сравним их очень внимательно…
Function f = x -> x.hashCode();
Demo map(Function super String, ? extends U> f)
// for better visual, I will add spaces to align them
Function< Object, Integer>
Demo map(Function super String, ? extends U>)
Ввод
Объект из функции f соответствует ? супер строка в карта первый параметр. Угадайте, что ? будет…
? становится Объектом .
Следовательно Супер Строка объекта
Что само по себе является допустимым утверждением, поскольку Объект действительно является родителем Строка
Demo map(Function)
Выход
Целое число из функции f соответствует ? расширяет U в карту второй параметр. Угадайте, что ? будет…
? становится Целым числом , что неудивительно
Это нормально, потому что ? является подстановочным знаком и может быть любого типа
Demo map(Function)
Угадай, что U будет…
Так как нигде нет U прямо указано, U будет выведен компилятором. Как он делает вывод?
Если возвращаемый объект назначен объявленной переменной, то U становится типом, указанным назначенной переменной. операционная
Если возвращаемый объект ничему не назначен, компилятору больше нечего выводить, кроме аргумента метода, который является ?
В нашем контексте
U становится типом, указанным в левой части (LHS) инструкции присваивания. операционная
U становится типом, который находится в ? .
Задание
Наконец, давайте рассмотрим отдельные вопросы
// Q1 - 5
Demo list = new Demo(strings).map(f);
Demo list = new Demo(strings).map(f);
Demo list = new Demo(strings).map(f);
Demo list = new Demo(strings).map(f);
Demo<> list = new Demo(strings).map(f);
Для Q1-4, по логике вывода типа, U станет Целым числом , Номер , Объект , Строка соответственно.
Просто для иллюстрации использования Q2
Demo map(Function f)
Обратите внимание, что Целое число расширяет число само по себе является допустимым утверждением, потому что Число является родителем Целого числа .
Итак, сосредоточив внимание на выводе, мы могли бы сказать, что вызов метода в Q2 вернет Демо<Номер> , который будет назначен Демо<Номер> список , отлично!
То же самое касается Q1 и Q3.
Почему Q3 неправильный?
Demo map(Function f)
Замена U с Строка , мы получаем это противоречие:
Целое число расширяет строку
Недействителен, потому что Строка не является родительской для Целого числа .
Ошибка, предоставленная компилятором, совершенно ясна ( обратите внимание на следующую ошибку, потому что ошибка, которую мы увидим в Q6-10, лишь немного отличается ):
incompatible types: inference variable U has incompatible bounds,
equality constraints: java.lang.String
lower bounds: java.lang.Integer
Q5 также является плохим утверждением. Это связано с тем, что мы должны указывать аргумент типа при объявлении объекта универсального типа.
Вопросы, касающиеся типизации
// Q6 - 10
(Demo) new Demo(strings).map(f);
(Demo) new Demo(strings).map(f);
(Demo) new Demo(strings).map(f);
(Demo) new Demo(strings).map(f);
(Demo) new Demo(strings).map(f);
Основная логика была объяснена в предыдущем разделе. Единственное отличие здесь заключается в том, что вместо присвоения возвращаемого объекта переменной мы выполняем только типизацию. Или мы типизируем возвращаемый объект перед он присваивается любой переменной.
Подобно Q1-5, нам все еще нужно сделать вывод, что U – это до того, как мы продолжим что-то возвращать. В отличие от операторов присваивания, компилятор не имеет переменных для вывода. Следовательно, он будет задействовать стратегию номер два, которая заключается в выводе из аргумента метода.
Помните, что у нас есть карта(<Супер строка объекта, ? расширяет U>) Итак, единственное, на что можно ссылаться, это ? . ? было установлено выше, чтобы быть целым числом , и следовательно, U должно быть целым числом Сейчас у нас есть
Demo map(Function f)
Поскольку U является Целым числом , возвращаемый тип метода map становится Demo<Целое число> .
Зная, что обобщения инвариантны, мы будем знать, что, за исключением Q6 (и Q10), остальные утверждения проблематичны.
Иллюстрация с использованием Q7
// ERROR
(Demo) Demo ...
// Or view it as
List listOfIntegers = new ArrayList();
listOfIntegers.add(1);
listOfIntegers.add(2);
Demo list = new Demo(listOfIntegers);
Ошибка компилятора теперь:
incompatible types: Demo
cannot be converted to Demo
Для Q10, даже если он компилируется, это не очень хорошая практика, потому что мы присваиваем возвращаемый объект его необработанному типу.
List listOfIntegers = new ArrayList();
listOfIntegers.add(1);
listOfIntegers.add(2);
// View it as
Demo list = new Demo(listOfIntegers);
Теперь список имеет тип Демонстрация .
Тип цели и Свидетель типа
В моих объяснениях выше не использовались технические термины, введенные в Учебник Oracle по java . Вот как я бы объяснил это, используя правильную терминологию.
Потому что метод map(f) возвращает значение типа Demo , компилятор делает вывод, что аргумент типа U должно быть значение Номер
В качестве альтернативы мы также можем быть явными и указать свидетель типа прямо перед именем метода Demo<Число> Demo<Строка>(строки).<Номер> карта (f);