原创

Java 泛型怎么写:泛型方法与 extends、super 的常见用法

Integer 是 Number,为什么 List<Integer> 不能交给接收 List<Number> 的方法?这不是泛型故意增加限制。接收 List 的方法可以往里面放 Double;如果它实际拿到的是一份整数列表,就会把原来的类型约束破坏掉。

一个方法只读商品价格,另一个方法往目标清单添加图书,两者需要的参数权限不同。下面先让编译器拒绝一个错误赋值,再用完整商品清单说明泛型类、泛型方法、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 ,不能赋给左侧 List 。同样,Book 即使继承 Product,也不能据此把 List 赋给 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 后,value 的静态类型就是 Book,读取时不需要把 Object 强转回来。appendAll 前面的 <T> 则属于方法:本次调用推断一组能连接 source 与 target 的类型关系,下一次调用可以使用另一种类型,方法所在的类不需要声明成泛型类。

extends 让方法读共同类型,不等于把集合冻结

total(List<? extends Product>) 接受 Product 或其子类型的列表。因此 List 能传进来,List 也能;方法读取每项时,把它作为 Product 使用就足够了,不关心究竟是哪一个具体子类。

为什么不能直接往这个参数里加一支 Pen?因为“某个 Product 子类型”还没有确定。实际传入的可能是 List ,并不接受 Pen。即使想添加一份普通 Product,也无法证明它属于这份未知列表的元素类型。

用一个独立文件核对同样的规则,保存为 BadUpperWrite.java:

import java.util.List;
class BadUpperWrite {
    static void add(List<? extends Number> values) {
        values.add(1);
    }
}

这个文件也应编译失败。values 可能来自 List ,不能因为上界是 Number 就允许加入 Integer。上界描述的是能安全读取的共同类型,不是给所有子类元素开通写权限。

不过 ? extends Product 不会让底层集合变成不可修改对象。它仍可能允许 clear、移除当前元素,原列表也可能被其他引用修改;Java 的类型规则并没有把这些操作统统封掉。若要交付不可修改快照,应另用适合的复制或包装方式。total 的“不修改输入”来自它的实现只遍历读取,不能只凭签名就推断所有接收上界通配符的方法都不改数据。

super 让目标接收一种值,不保证取出来还是它

addGift 接收 List<? super Book>,意味着这份列表的元素类型是 Book 或 Book 的某个父类型。往 List 、List 、List 里放 Book 都合法,所以它们都可以作为目标。

读取则需要保守。gifts 在加入赠品之前,里面已经有一支 Pen;不能因为方法允许往目标放 Book,就认定从目标拿到的都是 Book。对这个通配符引用而言,读取通常只能先得到 Object。

保存为 BadLowerRead.java:

import java.util.List;
class BadLowerRead {
    static Number first(List<? super Integer> values) {
        return values.get(0);
    }
}

分别编译后面两个错误示例:

javac -encoding UTF-8 BadUpperWrite.java
javac -encoding UTF-8 BadLowerRead.java

两条都应失败。第二条的目标可能是 List,其中本来就允许存字符串,所以不能把 get 的结果直接认成 Number。

appendAll 的签名把这两层关系接在一起:从 ? extends T 中读取 T,往 ? super T 中加入 T。books 合并进 all 时不需要先把图书清单改造成商品清单,也没有破坏原来的 List 约束。标准库 Collections.copy 也采用类似的源与目标方向,不过它要求目标已有足够位置;本例方法使用 add 追加,不是替换目标原有槽位。

泛型仍然不负责所有运行时条件。target 若是 List.of 创建的不可修改列表,add 会抛异常;把同一份可变列表同时作为 source 与 target,也不在这个练习的调用约定里。这里没有事务式复制保证,追加到一半失败不会替你恢复目标旧内容。先用类型参数保证元素能否流向目标,再由方法合同约定可变性、输入关系和失败后的行为,调用才是完整的。

正文到此结束
评论插件初始化中...
Loading...