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

challenge icon

Задание

Легко

Давайте создадим систему кормления в зоопарке, которая демонстрирует все три типа подстановочных знаков (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
    }
}
quiz iconПроверьте себя

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

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