Menu
Coddy logo textTech

Symbole wieloznaczne (?, extends, super)

Część sekcji Programowanie obiektowe ścieżki Java w Coddy. Lekcja 57 z 87.

Parametry typów z ograniczeniem zawężają typy generyczne do określonych typów, ale co w sytuacji, gdy pracujesz z istniejącymi typami generycznymi i nie znasz dokładnego parametru typu lub nie ma to dla ciebie znaczenia? Właśnie wtedy przydają się typy wieloznaczne.

Nieograniczony symbol wieloznaczny ? reprezentuje nieznany typ. Jest przydatny, gdy chcesz zaakceptować dowolny typ sparametryzowany:

public static void printList(List<?> list) {
    for (Object item : list) {
        System.out.println(item);
    }
}

printList(new ArrayList<String>());   // Działa
printList(new ArrayList<Integer>());  // Działa

Typ wieloznaczny z górnym ograniczeniem ? extends Type akceptuje określony typ lub dowolny z jego podtypów. Używaj go, gdy chcesz odczytywać dane z generycznej struktury:

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));        // Lista obiektów Integer
sumNumbers(Arrays.asList(1.5, 2.5));       // Lista obiektów Double

Wieloznacznik z dolnym ograniczeniem ? super Type akceptuje określony typ lub dowolny z jego nadtypów. Używaj go, gdy musisz zapisywać do struktury generycznej:

public static void addIntegers(List<? super Integer> list) {
    list.add(1);
    list.add(2);
}

List<Number> numbers = new ArrayList<>();
addIntegers(numbers);  // Działa - Number jest nadtypem Integer

Pomocna wskazówka: używaj extends, gdy tylko odczytujesz (producent), i używaj super, gdy tylko zapisujesz (konsument). Jest to znane jako zasada PECS: producent używa extends, konsument — super.

challenge icon

Wyzwanie

Łatwy

Stwórzmy system karmienia w zoo, który pokazuje wszystkie trzy typy symboli wieloznacznych! Utworzysz hierarchię zwierząt i pożywienia, a następnie napiszesz metody pomocnicze, które używają symboli wieloznacznych bez ograniczeń, z górnym ograniczeniem i z dolnym ograniczeniem do obsługi różnych scenariuszy karmienia.

Rozmieścisz kod w czterech plikach:

  • Food.java: Utwórz prostą hierarchię klas dla pożywienia. Zacznij od klasy bazowej Food, która ma prywatne pole name (String), konstruktor do jego inicjalizacji oraz metodę getName(). Następnie utwórz w tym samym pliku dwie podklasy: Meat rozszerza Food (konstruktor wywołuje super(name)) oraz Vegetable rozszerza Food (ten sam schemat).
  • Animal.java: Utwórz klasę bazową Animal z prywatnym polem species (String), konstruktorem i metodą getSpecies(). Utwórz dwie podklasy: Carnivore i Herbivore, z których każda rozszerza Animal i wywołuje konstruktor klasy nadrzędnej.
  • ZooFeeder.java: To tutaj symbole wieloznaczne pokazują swoje możliwości! Utwórz klasę pomocniczą z trzema metodami statycznymi:

    printInventory(List<?> items) - Używa symbolu wieloznacznego bez ograniczeń, aby wypisać dowolną listę. Przejdź przez nią i wypisz każdy element za pomocą toString(), po jednym w każdym wierszu.

    calculateTotalFood(List<? extends Food> foods) - Używa symbolu wieloznacznego z górnym ograniczeniem, aby odczytywać listę dowolnego typu Food. Zwróć liczbę elementów jako int. To demonstruje odczyt z producenta.

    addMeatToStock(List<? super Meat> stock, String meatName) - Używa symbolu wieloznacznego z dolnym ograniczeniem, aby zapisywać do listy, która może przechowywać Meat. Utwórz nowy obiekt Meat o podanej nazwie i dodaj go do zapasów. To demonstruje zapis do konsumenta.

  • Main.java: Połącz wszystkie elementy swojego systemu karmienia w zoo! Otrzymasz dwa dane wejściowe: nazwę mięsa (String) i nazwę warzywa (String).

    Najpierw utwórz ArrayList<Food> i dodaj obiekt Meat o nazwie mięsa oraz obiekt Vegetable o nazwie warzywa. Wypisz Food inventory:, a następnie wywołaj printInventory, przekazując tę listę.

    Następnie wypisz pusty wiersz, a potem wypisz Total food items: [count], używając calculateTotalFood na liście pożywienia.

    Na koniec utwórz nowy ArrayList<Food> o nazwie meatStock. Wypisz pusty wiersz, a następnie Adding to meat stock.... Wywołaj addMeatToStock, przekazując tę listę i ciąg znaków "Beef". Następnie wypisz Meat stock after adding: i wywołaj printInventory dla zapasów mięsa.

Otrzymasz dwa dane wejściowe w podanej kolejności: nazwę mięsa i nazwę warzywa.

Nadpisz toString() w klasie Food, aby zwracała nazwę pożywienia, dzięki czemu wypisywanie będzie działać poprawnie. Zwróć uwagę, jak każdy typ symbolu wieloznacznego służy innemu celowi: ? zapewnia maksymalną elastyczność, gdy wystarczy odczytać elementy jako Object, ? extends służy do odczytu określonych typów, a ? super do zapisu do kolekcji!

Spróbuj swoich sił

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: Utwórz ArrayList<Food> i dodaj obiekty Meat oraz Vegetable
        
        // TODO: Wypisz "Food inventory:", a następnie wywołaj printInventory
        
        // TODO: Wypisz pusty wiersz, a następnie "Total food items: [count]", używając calculateTotalFood
        
        // TODO: Utwórz nowy ArrayList<Food> o nazwie meatStock
        
        // TODO: Wypisz pusty wiersz, a następnie "Adding to meat stock..."
        
        // TODO: Wywołaj addMeatToStock z argumentami meatStock i "Beef"
        
        // TODO: Wypisz "Meat stock after adding:", a następnie wywołaj printInventory dla meatStock
    }
}
quiz iconSprawdź się

Ta lekcja zawiera krótki quiz. Zacznij lekcję, żeby na niego odpowiedzieć i śledzić swoje postępy.

Wszystkie lekcje w sekcji Programowanie obiektowe

Poćwicz samodzielnie: Kompilator Java online