通配符(?、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。
挑战
简单让我们构建一个动物园喂食系统,展示通配符的全部三种类型!你将创建动物和食物的类层次结构,然后编写实用方法,使用无界、上界和下界通配符来处理不同的喂食场景。
你需要将代码分放在四个文件中:
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
}
}
这节课包含一个小测验。开始学习即可作答,并记录你的进度。
面向对象编程中的所有课
自己练习: Java 在线编译器