Menu
CoddyTech

通配符(?、extends、super)

本课属于 Coddy Java 学习旅程的“面向对象编程”单元。第 57 课,共 87 课。

有界类型参数会将泛型限制为特定类型,但如果你使用现有的泛型类型,却不知道或不关心确切的类型参数,该怎么办?这时就要用到通配符。

无界通配符 ?表示未知类型。当你想接受任意参数化类型时,它很有用:

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 原则:生产者使用 Extends,消费者使用 Super。

challenge icon

挑战

简单

让我们构建一个动物园喂食系统,展示通配符的全部三种类型!你将创建动物和食物的类层次结构,然后编写实用方法,使用无界、上界和下界通配符来处理不同的喂食场景。

你需要将代码分放在四个文件中:

  • 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。这展示了从生产者中读取数据。

    addMeatToStock(List<? super Meat> stock, String meatName) - 使用下界通配符向能够容纳 Meat 的列表写入数据。使用给定名称创建一个新的 Meat 对象,并将其添加到库存中。这展示了向消费者写入数据。

  • Main.java:把动物园喂食系统整合起来!你将收到两个输入:肉类名称(String)和蔬菜名称(String)。

    首先,创建一个 ArrayList<Food>,并添加一个名称为肉类名称的 Meat 对象,以及一个名称为蔬菜名称的 Vegetable 对象。打印 Food inventory:,然后使用这个列表调用 printInventory。

    接下来,打印一个空行,然后使用 calculateTotalFood 处理食物列表,并打印 Total food items: [count]。

    最后,创建一个新的 ArrayList<Food>,命名为 meatStock。打印一个空行,然后打印 Adding to meat stock...。使用这个列表和字符串 "Beef" 调用 addMeatToStock。然后打印 Meat stock after adding:,并对肉类库存调用 printInventory。

你将依次收到两个输入:肉类名称和蔬菜名称。

在 Food 类中重写 toString(),让它返回食物名称,这样打印才能正常工作。注意,每种通配符类型都有不同的用途:只需将内容作为 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:先输出一个空行,再使用 calculateTotalFood 输出 "Total food items: [count]"
        
        // TODO:创建一个名为 meatStock 的新 ArrayList<Food>
        
        // TODO:先输出一个空行,再输出 "Adding to meat stock..."
        
        // TODO:以 meatStock 和 "Beef" 为参数调用 addMeatToStock
        
        // TODO:输出 "Meat stock after adding:",然后对 meatStock 调用 printInventory
    }
}
quiz icon自我测试

这节课包含一个小测验。开始学习即可作答,并记录你的进度。

面向对象编程中的所有课

自己练习: Java 在线编译器