Java 泛型怎么写:泛型方法与 extends、super 的常见用法
Integer 是 Number,为什么 List<Integer> 不能交给接收 List<Number> 的方法?这不是泛型故意增加限制。接收 List
一个方法只读商品价格,另一个方法往目标清单添加图书,两者需要的参数权限不同。下面先让编译器拒绝一个错误赋值,再用完整商品清单说明泛型类、泛型方法、extends 与 super 怎样配合。示例使用 JDK 17,不涉及反射,也不通过原始类型和强制转换绕开检查。
先看清编译器在保护什么
单独保存为 BadInvariant.java:
import java.util.List;
class BadInvariant {
List<Number> numbers = List.<Integer>of(1, 2);
}
执行:
javac -encoding UTF-8 BadInvariant.java
这次预期编译失败。右侧已经明确是 List
但业务通常不是想给图书列表塞一支笔,只是想统一统计价格。参数写得过窄,会误把这个合法需求挡住;这时才需要上界通配符,而不是删除泛型参数,把问题推迟到运行时。
两份子类清单,合并成一份商品清单
保存为 ProductGenericsDemo.java。图书和笔都提供商品编号与价格分值,统计方法只读这些共同信息;appendAll 则把一份清单追加到另一个容器。金额使用整数分值,输出不是性能测量。
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
public class ProductGenericsDemo {
static abstract class Product {
private final String sku;
private final long priceCents;
Product(String sku, long priceCents) {
if (sku == null || sku.isBlank() || priceCents < 0) {
throw new IllegalArgumentException("invalid product");
}
this.sku = sku;
this.priceCents = priceCents;
}
String sku() { return sku; }
long priceCents() { return priceCents; }
}
static final class Book extends Product {
Book(String sku, long priceCents) { super(sku, priceCents); }
}
static final class Pen extends Product {
Pen(String sku, long priceCents) { super(sku, priceCents); }
}
record Box<T>(T value) {
Box { Objects.requireNonNull(value, "box value is required"); }
}
static long total(List<? extends Product> products) {
long result = 0;
for (Product product : products) result = Math.addExact(result, product.priceCents());
return result;
}
static List<String> skus(List<? extends Product> products) {
return products.stream().map(Product::sku).toList();
}
static <T> void appendAll(List<? extends T> source, List<? super T> target) {
Objects.requireNonNull(source);
Objects.requireNonNull(target);
for (T value : source) target.add(value);
}
static void addGift(List<? super Book> target) {
target.add(new Book("GIFT", 0));
}
static void equal(Object expected, Object actual) {
if (!Objects.equals(expected, actual)) {
throw new AssertionError("expected=" + expected + " actual=" + actual);
}
}
public static void main(String[] args) {
List<Book> books = new ArrayList<>(List.of(new Book("B1", 1990), new Book("B2", 2590)));
Box<Book> featured = new Box<>(books.get(0));
equal("B1", featured.value().sku());
equal(4580L, total(books));
System.out.println("BOX=" + featured.value().sku());
System.out.println("BOOK_TOTAL=" + total(books));
List<Product> all = new ArrayList<>();
appendAll(books, all);
appendAll(List.of(new Pen("P1", 500)), all);
equal(List.of("B1", "B2", "P1"), skus(all));
equal(5080L, total(all));
System.out.println("ALL_SKUS=" + skus(all));
System.out.println("ALL_TOTAL=" + total(all));
List<Object> objects = new ArrayList<>();
appendAll(books, objects);
equal(2, objects.size());
System.out.println("OBJECT_SINK_SIZE=" + objects.size());
List<Product> gifts = new ArrayList<>(List.of(new Pen("P2", 500)));
addGift(gifts);
equal(List.of("P2", "GIFT"), skus(gifts));
System.out.println("GIFT_TARGET=" + skus(gifts));
System.out.println("ALL_CHECKS_PASSED");
}
}
构建并运行这个成功示例,别把前面的错误文件一起交给 javac *.java:
javac -encoding UTF-8 ProductGenericsDemo.java
java ProductGenericsDemo
应输出:
BOX=B1
BOOK_TOTAL=4580
ALL_SKUS=[B1, B2, P1]
ALL_TOTAL=5080
OBJECT_SINK_SIZE=2
GIFT_TARGET=[P2, GIFT]
ALL_CHECKS_PASSED
Box 的 T 属于这个泛型类型。创建 Box
<T> 则属于方法:本次调用推断一组能连接 source 与 target 的类型关系,下一次调用可以使用另一种类型,方法所在的类不需要声明成泛型类。
extends 让方法读共同类型,不等于把集合冻结
total(List<? extends Product>) 接受 Product 或其子类型的列表。因此 List
为什么不能直接往这个参数里加一支 Pen?因为“某个 Product 子类型”还没有确定。实际传入的可能是 List
用一个独立文件核对同样的规则,保存为 BadUpperWrite.java:
import java.util.List;
class BadUpperWrite {
static void add(List<? extends Number> values) {
values.add(1);
}
}
这个文件也应编译失败。values 可能来自 List
不过 ? extends Product 不会让底层集合变成不可修改对象。它仍可能允许 clear、移除当前元素,原列表也可能被其他引用修改;Java 的类型规则并没有把这些操作统统封掉。若要交付不可修改快照,应另用适合的复制或包装方式。total 的“不修改输入”来自它的实现只遍历读取,不能只凭签名就推断所有接收上界通配符的方法都不改数据。
super 让目标接收一种值,不保证取出来还是它
addGift 接收 List<? super Book>,意味着这份列表的元素类型是 Book 或 Book 的某个父类型。往 List