Menu
Coddy logo textTech

Символы подстановки (Wildcards: ?, extends, super)

Часть раздела Объектно-ориентированное программирование путешествия по Java на Coddy. Урок 57 из 87.

Параметры типа bounded ограничивают обобщения определёнными типами, но что делать, если вы работаете с уже существующими обобщёнными типами и не знаете или не хотите знать точный параметр типа? Именно здесь появляются подстановочные знаки.

Неограниченный шаблон ? обозначает неизвестный тип. Он полезен, когда нужно принимать тип с любыми параметрами:

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

Полезное правило: используй extends, когда только читаешь (производитель), и используй super, когда только записываешь (потребитель). Это известно как принцип PECS: Producer Extends, Consumer Super.

challenge icon

Задание

Легко

Давайте создадим систему кормления в зоопарке, демонстрирующую все три типа подстановочных знаков! Вы создадите иерархию животных и еды, а затем напишете служебные методы, использующие неограниченные, ограниченные сверху и ограниченные снизу подстановочные знаки для обработки различных сценариев кормления.

Вы организуете код в четырёх файлах:

  • Food.java: Создайте простую иерархию классов для еды. Начните с базового класса Food, содержащего закрытое поле name (String), конструктор для его инициализации и метод getName(). Затем создайте в этом же файле два подкласса: Meat extends Food (конструктор вызывает super(name)) и Vegetable extends Food (по той же схеме).
  • Animal.java: Создайте базовый класс Animal с закрытым полем species (String), конструктором и методом getSpecies(). Создайте два подкласса: Carnivore и Herbivore, каждый из которых расширяет Animal и вызывает родительский конструктор.
  • ZooFeeder.java: Именно здесь подстановочные знаки раскрывают свои возможности! Создайте служебный класс с тремя статическими методами:

    printInventory(List<?> items) — использует неограниченный подстановочный знак для вывода любого списка. Пройдите по списку и выведите каждый item с помощью toString(), по одному в строке.

    calculateTotalFood(List<? extends Food> foods) — использует ограниченный сверху подстановочный знак для чтения из списка любого типа Food. Верните count элементов как 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 — при записи в коллекцию!

Попробуйте сами

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: Print "Food inventory:" then call printInventory
        
        // TODO: Print blank line, then "Total food items: [count]" using calculateTotalFood
        
        // TODO: Создайте новый ArrayList<Food> под названием meatStock
        
        // TODO: Print blank line, then "Adding to meat stock..."
        
        // TODO: Вызовите addMeatToStock с meatStock и "Beef"
        
        // TODO: Print "Meat stock after adding:" then call printInventory on meatStock
    }
}
quiz iconПроверьте себя

В этом уроке есть небольшой тест. Начните урок, чтобы ответить на вопросы и сохранить прогресс.

Все уроки раздела Объектно-ориентированное программирование

Потренируйтесь самостоятельно: Онлайн-компилятор Java