Символы подстановки (Wildcards: ?, extends, super)
Часть раздела Объектно-ориентированное программирование путешествия по Java на Coddy — урок 57 из 87.
Ограниченные параметры типов ограничивают обобщения конкретными типами, но как быть, когда вы работаете с существующими обобщенными типами и не знаете — или вам не важен — точный параметр типа? Именно здесь на помощь приходят wildcards (символы подстановки).
Неограниченный подстановочный символ (unbounded wildcard) ? представляет собой неизвестный тип. Он полезен, когда вы хотите принимать любой параметризованный тип:
public static void printList(List<?> list) {
for (Object item : list) {
System.out.println(item);
}
}
printList(new ArrayList<String>()); // Работает
printList(new ArrayList<Integer>()); // РаботаетСимвол подстановки с верхней границей (upper bounded wildcard) ? extends Type принимает указанный тип или любой из его подклассов. Используйте его, когда вам нужно читать из универсальной структуры:
public static double sumNumbers(List<? extends Number> numbers) {
double sum = 0;
for (Number n : numbers) {
sum += n.doubleValue();
}
return sum;
}
sumNumbers(Arrays.asList(1, 2, 3)); // Список Integer
sumNumbers(Arrays.asList(1.5, 2.5)); // Список DoubleНижняя граница подстановочного типа (lower bounded wildcard) ? super Type принимает указанный тип или любой из его суперклассов. Используйте его, когда вам нужно произвести запись в универсальную (generic) структуру:
public static void addIntegers(List<? super Integer> list) {
list.add(1);
list.add(2);
}
List<Number> numbers = new ArrayList<>();
addIntegers(numbers); // Работает - Number является супертипом для IntegerПолезное правило: используйте extends, когда вы только читаете (producer), используйте super, когда вы только записываете (consumer). Это известно как принцип PECS — Producer Extends, Consumer Super.
Задание
ЛегкоДавайте создадим систему кормления в зоопарке, которая демонстрирует все три типа подстановочных знаков (wildcards)! Вы создадите иерархию животных и еды, а затем напишете вспомогательные методы, использующие неограниченные, ограниченные сверху и ограниченные снизу подстановочные знаки для обработки различных сценариев кормления.
Вы распределите свой код по четырем файлам:
Food.java: Создайте простую иерархию классов для еды. Начните с базового классаFood, который имеет приватное полеname(String), конструктор для его инициализации и методgetName(). Затем создайте два подкласса в том же файле:Meatрасширяет Food (конструктор вызываетsuper(name)) иVegetableрасширяет Food (по той же схеме).Animal.java: Создайте базовый классAnimalс приватным полемspecies(String), конструктором и методомgetSpecies(). Создайте два подкласса:CarnivoreиHerbivore, каждый из которых расширяет Animal и вызывает конструктор родителя.ZooFeeder.java: Здесь подстановочные знаки проявляют себя во всей красе! Создайте вспомогательный класс с тремя статическими методами:printInventory(List<?> items)— использует неограниченный подстановочный знак для вывода любого списка. Переберите элементы в цикле и выведите каждый элемент, используяtoString(), по одному в строке.calculateTotalFood(List<? extends Food> foods)— использует ограниченный сверху подстановочный знак для чтения из списка любого типа Food. Возвращает количество элементов в видеint. Это демонстрирует чтение из производителя (producer).addMeatToStock(List<? super Meat> stock, String meatName)— использует ограниченный снизу подстановочный знак для записи в список, который может содержать Meat. Создайте новый объектMeatс заданным именем и добавьте его в stock. Это демонстрирует запись в потребителя (consumer).Main.java: Соберите вашу систему кормления зоопарка воедино! Вы получите два входных значения: название мяса (String) и название овоща (String).Сначала создайте
ArrayList<Food>и добавьте объектMeatс названием мяса и объектVegetableс названием овоща. ВыведитеFood inventory:, затем вызовитеprintInventoryс этим списком.Затем выведите пустую строку, а после —
Total food items: [count], используяcalculateTotalFoodдля вашего списка еды.Наконец, создайте новый
ArrayList<Food>с именемmeatStock. Выведите пустую строку, затемAdding to meat stock.... ВызовитеaddMeatToStockс этим списком и строкой"Beef". Затем выведитеMeat stock after adding:и вызовитеprintInventoryдля meat stock.
Вы получите два входных значения по порядку: название мяса и название овоща.
Переопределите toString() в вашем классе Food, чтобы он возвращал название еды, тогда вывод будет работать правильно. Обратите внимание, как каждый тип подстановочного знака служит своей цели: ? для максимальной гибкости, когда вам нужно просто читать объекты как Object, ? extends при чтении определенных типов и ? super при записи в коллекцию!
Шпаргалка
Java предоставляет три типа подстановочных знаков (wildcards) для работы с универсальными типами (generics), когда точный параметр типа неизвестен или не имеет значения.
Неограниченный подстановочный знак (?)
Представляет неизвестный тип и принимает любой параметризованный тип:
public static void printList(List<?> list) {
for (Object item : list) {
System.out.println(item);
}
}
printList(new ArrayList<String>()); // Работает
printList(new ArrayList<Integer>()); // РаботаетПодстановочный знак с верхней границей (? extends Type)
Принимает указанный тип или любой из его подклассов. Используйте, когда вам нужно читать из универсальной структуры:
public static double sumNumbers(List<? extends Number> numbers) {
double sum = 0;
for (Number n : numbers) {
sum += n.doubleValue();
}
return sum;
}
sumNumbers(Arrays.asList(1, 2, 3)); // Список Integer
sumNumbers(Arrays.asList(1.5, 2.5)); // Список DoubleПодстановочный знак с нижней границей (? super Type)
Принимает указанный тип или любой из его суперклассов. Используйте, когда вам нужно записывать в универсальную структуру:
public static void addIntegers(List<? super Integer> list) {
list.add(1);
list.add(2);
}
List<Number> numbers = new ArrayList<>();
addIntegers(numbers); // Работает - Number является супертипом для IntegerПринцип PECS
Producer Extends, Consumer Super: Используйте extends, когда вы только читаете (producer), и super, когда вы только записываете (consumer).
Попробуйте сами
import java.util.Scanner;
import java.util.ArrayList;
import java.util.List;
class Main {
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
String meatName = scanner.nextLine();
String vegetableName = scanner.nextLine();
// TODO: Создайте ArrayList<Food> и добавьте объекты Meat и Vegetable
// TODO: Выведите "Food inventory:", затем вызовите printInventory
// TODO: Выведите пустую строку, затем "Total food items: [count]", используя calculateTotalFood
// TODO: Создайте новый ArrayList<Food> с именем meatStock
// TODO: Выведите пустую строку, затем "Adding to meat stock..."
// TODO: Вызовите addMeatToStock с meatStock и "Beef"
// TODO: Выведите "Meat stock after adding:", затем вызовите printInventory для meatStock
}
}
В этом уроке есть небольшой тест. Начните урок, чтобы ответить на вопросы и сохранить прогресс.
Все уроки раздела Объектно-ориентированное программирование
1Основы ООП
Внешние файлыВведение в ООПКлассы и объектыКлючевое слово thisМетодыПоля (атрибуты)Метод-конструкторПерегрузка конструкторовИтоги — Простой калькулятор4Наследование
Основы наследования (extends)Ключевое слово superПереопределение методов (@Override)Цепочка конструкторовКласс ObjectОдиночное и многоуровневое наследованиеПочему нет множественного наследования классовИтоги — Иерархия сотрудников7Специальные методы и класс Object
Метод toString()equals() и hashCode()Метод clone()compareTo() и ComparableИнтерфейс ComparatorПовторение — Кастомная сортировка2Модификаторы доступа и инкапсуляция
Обзор уровней доступаМетоды геттеры и сеттерыСокрытие данныхКлючевое слово finalИтоги — Менеджер банковского счета5Полиморфизм
Основы перегрузки методовПереопределение методов (Run-Time)Upcasting и DowncastingОператор instanceofАбстрактные классы и методыИтоги: Калькулятор фигур8Продвинутые концепции ООП
Композиция против наследованияАгрегация против композицииВнутренние, вложенные и анонимные классыEnums и методы перечисленийRecords (Java 16+)Sealed-классы (Java 17+)11Паттерны проектирования. Часть 1
Введение в паттерны проектированияПаттерн SingletonПаттерн FactoryПаттерн BuilderПаттерн ObserverПаттерн Strategy3Свойства класса и статические члены
Переменные экземпляра и статические переменныеСтатические методыСтатические блокиКонстанты (static final)Итоги: Счетчик и утилиты6Интерфейсы и абстрактные классы
Введение в интерфейсыРеализация интерфейсовРеализация нескольких интерфейсовDefault и Static методы в интерфейсахАбстрактные классы vs ИнтерфейсыФункциональные интерфейсыИтоги — Платежная система9Обобщения
Введение в обобщенияОбобщенные классыОбобщенные методыОграниченные параметры типаСимволы подстановки (Wildcards: ?, extends, super)Итоги — Обобщенный контейнер12Паттерны проектирования. Часть 2
Паттерн «Команда»Паттерн «Адаптер»Паттерн «Декоратор»Паттерн «Шаблонный метод»Паттерн «Состояние»Паттерн «Компоновщик»Паттерн «Итератор»