Menu
Coddy logo textTech

Символы подстановки (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 принимает указанный тип или любой из его суперклассов. Используйте её, когда вам нужно записать данные в обобщенную структуру:

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.

challenge icon

Задание

Легко

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

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

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

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

    calculateTotalFood(List<? extends Food> foods) — использует ограниченный сверху wildcard для чтения из списка любого типа Food. Верните количество элементов как int. Это демонстрирует чтение из производителя (producer).

    addMeatToStock(List<? super Meat> stock, String meatName) — использует ограниченный снизу wildcard для записи в список, который может содержать 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, чтобы он возвращал название еды, тогда вывод будет работать правильно. Обратите внимание, как каждый тип wildcard служит разным целям: ? для максимальной гибкости, когда вам нужно просто читать как 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: Выведите "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
    }
}
quiz iconПроверьте себя

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

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