API 中的通配符
设计灵活的泛型方法
API 中的通配符 是 CoddyKit 上的免费 Java Academy 课时。 这是第 4 节课,共 4 节。 你可以在下方免费阅读本课时的完整内容 — 然后在浏览器中使用内置代码编辑器和全天候 AI 导师进行实践。 这是 Java Academy 学习路径的一部分,你的进度在网页和 CoddyKit 应用中同步。 Java Academy 课程共包含 4 节课。
设计灵活的方法
通配符主要是为API 设计者准备的工具。放置得当的通配符可以让调用者传入更多类型,而无需进行类型转换。
目标是:为调用者提供最大的灵活性,同时在内部保证完整的类型安全。
import java.util.List;
public class Main {
static double total(List<? extends Number> items) {
double sum = 0;
for (Number n : items) sum += n.doubleValue();
return sum;
}
public static void main(String[] args) {
System.out.println(total(List.of(1, 2, 3)));
System.out.println(total(List.of(1.5, 2.5)));
}
}无界通配符
List<?> 是无界通配符,表示某种未知类型的列表。
当您的方法不关心元素类型时可以使用它,例如统计元素数量或检查列表是否为空。
import java.util.List;
public class Main {
static int size(List<?> any) {
return any.size();
}
public static void main(String[] args) {
System.out.println(size(List.of("a", "b")));
System.out.println(size(List.of(1, 2, 3)));
}
}List<?> 与 List<Object> 的区别
两者并不相同。List<Object> 只接受声明为 Object 的列表,而 List<?> 接受任意类型的列表。
您可以将 List<String> 传给 List<?> 参数,但不能传给 List<Object> 参数。
import java.util.List;
public class Main {
static void printAll(List<?> list) {
for (Object o : list) System.out.println(o);
}
public static void main(String[] args) {
List<String> names = List.of("Ada", "Linus");
printAll(names); // works; List<Object> would reject this
}
}何时使用类型参数更清晰
如果需要多次引用元素类型,使用命名类型参数比使用通配符更易读。
请比较 void swap(List<?> l, int i, int j)(需要私有辅助方法)和简洁的泛型版本。
import java.util.ArrayList;
import java.util.List;
public class Main {
static <T> void swap(List<T> list, int i, int j) {
T tmp = list.get(i);
list.set(i, list.get(j));
list.set(j, tmp);
}
public static void main(String[] args) {
List<String> list = new ArrayList<>(List.of("a", "b", "c"));
swap(list, 0, 2);
System.out.println(list);
}
}辅助方法捕获技巧
如果公共 API 必须使用 List<?>,您可以委托给私有泛型辅助方法来捕获通配符。
辅助方法为类型 T 命名,这样就可以写回列表。
import java.util.ArrayList;
import java.util.List;
public class Main {
public static void reverse(List<?> list) { reverseHelper(list); }
private static <T> void reverseHelper(List<T> list) {
for (int i = 0, j = list.size() - 1; i < j; i++, j--) {
T tmp = list.get(i);
list.set(i, list.get(j));
list.set(j, tmp);
}
}
public static void main(String[] args) {
List<Integer> list = new ArrayList<>(List.of(1, 2, 3, 4));
reverse(list);
System.out.println(list);
}
}真实 API 中的生产者参数
设计一个能够从任意兼容源中提取数据的 addAll 方法,需要为源指定上界。
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
public class Main {
static <T> void addAll(List<T> target, Collection<? extends T> source) {
for (T t : source) target.add(t);
}
public static void main(String[] args) {
List<Number> nums = new ArrayList<>();
addAll(nums, List.of(1, 2, 3));
System.out.println(nums);
}
}真实 API 中的消费者参数
写入结果的接收端 API 会为目标指定下界,这样调用者就可以提供更宽泛的容器。
import java.util.ArrayList;
import java.util.List;
public class Main {
static <T> void repeat(T value, int times, List<? super T> sink) {
for (int i = 0; i < times; i++) sink.add(value);
}
public static void main(String[] args) {
List<Object> out = new ArrayList<>();
repeat("hi", 3, out);
System.out.println(out);
}
}嵌套泛型中的通配符
嵌套泛型通常需要使用通配符。接受任意类型列表的方法可以使用 List<? extends List<?>>。
import java.util.List;
public class Main {
static int countAll(List<? extends List<?>> lists) {
int total = 0;
for (List<?> inner : lists) total += inner.size();
return total;
}
public static void main(String[] args) {
System.out.println(countAll(List.of(List.of(1, 2), List.of("a", "b", "c"))));
}
}避免过度泛型化
并非每个方法都需要通配符。如果调用者始终只传入一种类型,使用普通参数会更清晰。
当您发现实际调用者受到不变性的限制时,再添加通配符,而不要预先添加。
import java.util.List;
public class Main {
// Simple and clear; no wildcard needed for this internal use
static String joinStrings(List<String> parts) {
return String.join(", ", parts);
}
public static void main(String[] args) {
System.out.println(joinStrings(List.of("a", "b", "c")));
}
}记录通配符的意图
通配符表达了设计意图。? extends T 告诉读者“我只读取它”。? super T 表示“我只写入它”。
选择正确的通配符是一种自解释代码形式,同时还能强制执行约定。
import java.util.ArrayList;
import java.util.List;
public class Main {
// src: read-only producer; dest: write-only consumer
static <T> void transfer(List<? extends T> src, List<? super T> dest) {
for (T t : src) dest.add(t);
}
public static void main(String[] args) {
List<Number> dest = new ArrayList<>();
transfer(List.of(1, 2), dest);
System.out.println(dest);
}
}泛型 Stack API
Bloch 著名的栈示例:pushAll 从生产者中消费数据(extends),popAll 将数据产生到消费者中(super)。
import java.util.ArrayDeque;
import java.util.Collection;
import java.util.Deque;
import java.util.ArrayList;
import java.util.List;
public class Main {
static class Stack<E> {
private final Deque<E> items = new ArrayDeque<>();
void pushAll(Collection<? extends E> src) { for (E e : src) items.push(e); }
void popAll(Collection<? super E> dst) { while (!items.isEmpty()) dst.add(items.pop()); }
}
public static void main(String[] args) {
Stack<Integer> s = new Stack<>();
s.pushAll(List.of(1, 2, 3));
List<Number> out = new ArrayList<>();
s.popAll(out);
System.out.println(out);
}
}快速检查
测试您设计 API 的直觉。
回顾
您已经学习了如何使用通配符设计 API:
- 对与类型无关的只读方法使用
List<?>。 - 在参数上应用 PECS 以提高灵活性。
- 必须通过通配符写入时,使用私有的辅助方法捕获技巧。
- 避免在返回类型中使用通配符,也不要过度泛型化。
您已经完成了通配符与 PECS 课程。
public class Main {
public static void main(String[] args) {
System.out.println("Wildcards in APIs course complete");
}
}常见问题解答
「API 中的通配符」课时是免费的吗?
是的 — 「API 中的通配符」的完整文本可在网页上免费阅读。要进行交互式练习(内置代码编辑器和全天候 AI 导师)并解锁 Java Academy 课程的其余内容,请升级到 CoddyKit PRO。 Java Academy 课程共包含 4 节课。
「API 中的通配符」这节课中我会学到什么?
设计灵活的泛型方法 你通过在浏览器中直接运行的动手代码来练习 Java Academy,全天候 AI 导师会在你学习这节课的过程中回答你的问题。
学习 Java Academy 需要有经验吗?
无需任何先前经验。CoddyKit 上的 Java Academy 课程适合初学者到高级学习者,你可以从这里开始或从头开始,按照自己的节奏学习。 这是第 4 节课,共 4 节。
「API 中的通配符」课时需要多长时间?
大多数 CoddyKit 课程大约需要 5–10 分钟。每节课都很精短且互动,所以你能稳步进步,并在网页和应用中从离开的地方继续。
我能在这节 Java Academy 课中编写并运行代码吗?
能。每节 Java Academy 课都包含内置代码编辑器,你可以在浏览器中直接编写并运行真实代码,并获得即时 AI 反馈 — 无需本地设置。