Java泛型的要点

由于长时间没有使用泛型库,我正在创建一个大量使用泛型的库。我忘记了一些东西并陷入了一些困境,所以现在我将它们作为备忘录总结起来。

“用語” 可以被漢語化為 “术语” 。

鉴于泛型存在类似的术语,很容易引起混淆,因此首先要确切地理解这些术语。
以下内容摘自《Effective Java》的第23条。

用語例ジェネリック型(generic type)List<E>仮型パラメータ(type parameter)Eパラメータ化された型(parameterized type)List<String>実型パラメータ (actual type parameter)String原型(raw type)List境界型パラメータ(bounded type parameter)<E extends Number>非境界ワイルドカード型(unbounded wildcard type)List<?>境界ワイルドカード型(bounded wildcard type)List<? extends Number>

变性

在泛型类型X中,当类型A是类型B的子类型时,如果X是X的子类型,则X在类型参数T上是协变的(covariant)。相反地,如果X是X的子类型,则X被称为反变的(contravariant)。如果两者都不成立,则称为不变的(invariant)。泛型类型具有这些变异之一。以下是示例。

変性例共変  List<Integer>List<Number> のサブタイプとなる反変List<Number>List<Integer>のサブタイプとなる非変List<Number>List<Integer>の継承関係はない

Java的泛型是不变的。

在Java中,泛型是不变的。因此,虽然Integer是Number的子类型,但List不是List的子类型。

在以下的示例代码中展示了一个例子。

    public void hoge() {
        List<Integer> intList = new ArrayList<>();
        List<Number> numList = new ArrayList<>();
        List<Number> anotherNumList = new ArrayList<>();

        numList = anotherNumList; //OK
        numList = intList; //コンパイルエラー       
    }

数组是协变的。

相比之下,Java的数组具有共变性。因此,可以编写出以下这种危险的代码。

    public static void foo() {
        Object[] objects;
        Integer[] integers = new Integer[]{1,2,3};
        objects = integers;
        objects[0] = "error"; //実行時例外 ArrayStoreExceptioin
    }

在编译时,类型信息被擦除

参数化类型和类型参数的类型信息将被编译器消除。这被称为类型擦除。

例如,考虑一个具有以下类型变量T的泛型类型。
Container是一个容器类,它仅保持Number类型的值,如Integer或BigDecimal。

class Container<T extends Number> {
    private T value;    
    public Container(T value) {
        this.value = value;
    }   
    public T get() {
        return value;
    }
}

编译器会从上述类中清除类型信息,并生成与下面的类等效的类。

class Container {
    private Number value;
    public Container(Number value) {
        this.value = value;
    }
    public Number get() {
        return value;
    }
}

尽管我们称之为类型信息,但实际上并不会被真正删除,类型参数会被替换为其上限边界的类型(在上述例子中为Number)。
因此,由于类型信息被编译器删除,无法在运行时获取类型信息。

无法创建 new T()

由于类型参数会被擦除,所以无法创建类型为T的实例。

class Illegal<T> {
    public T create() {
        return new T();
    }
}

如果非做不可的话,只有以下的方式可选。

class Legal<T> {
    public T create(Class<T> clazz) throws Exception {
        return clazz.newInstance();
    }
}

无法创建新的T[]。

对于泛型类型,可以根据以下方式生成一个以T为类型的实例。

class Sample<T> {
    public List<T> createList(int size) {
        //TのArrayListを生成できる
        return new ArrayList<T>(size);
    }
}

一方,无法创建一个以类型参数T作为元素的数组。

class Sample<T> {   
    public T[] createArray(int size) {
        return new T[size];
    }   
}

就像之前解释过的那样,类型参数T的类型信息会在运行时被擦除。在泛型类型的情况下,会生成不包含类型信息的原型,因此在运行时不需要类型信息。然而,在生成数组时需要运行时的类型信息(确切地说是生成数组的字节码需要类型信息),所以无法生成以类型参数T为元素的数组。

List<?> 表示具有任意元素类型的列表。

List<?> 表示具有某种类型元素的列表。这种类型被称为非边界通配符类型。List<?>的变量可以赋值为任何列表。因此,可以编写以下代码。

    public void wildcardHasAnyType() {
        List<String> stringList = new ArrayList<>();
        List<Integer> integerList = new ArrayList<>();

        boolean b1 = contains(stringList, "a"); //List<String>
        boolean b2 = contains(integerList, 3); //List<Integer>
    }

    public boolean contains(List<?> list, Object o) {
        return list.contains(o);
    }

这与由于协变属性,无法将List赋值给List的情况形成鲜明对比。通过使用通配符类型,可以放宽泛型的不变约束。

无法在List<?>中使用add方法添加元素。

List<?> 是一个具有任意元素的列表,但具体元素类型是不确定的。因此,除了 null 值之外,不能传递任何其他值。因此,以下代码会报错。

    public void foo(List<?> anyList) {
        anyList.add("error"); //コンパイルエラー
    }

一般而言,无法调用具有类型参数作为参数的非限定通配符类型的方法。

interface UnaryFunction<T> {
    public T apply(T value);
}

class Illegal {
    public void foo(UnaryFunction<?> f) {
        f.apply("a"); //コンパイルエラー
    }
}

从List<?>中获取的元素是Object类型的。

非境界的通配符可以是任何类型,因此获取的元素类型是最通用的类型Object。

    public void bar(List<?> list) {
        Object o = list.get(0);
        String s = list.get(0); //コンパイルエラー
    }

List<? extends T> 具备协变性

List<? extends T>是一种带有上界限定通配符的类型,表示一种包含类型T的某个子类型元素的列表。通过使用上界通配符类型,可以像以下示例代码一样,在实际上是不变的泛型中引入协变性。

    public void foo() {
        List<Number> numList = new ArrayList<>();
        List<? extends Number> wildCardNumList = new ArrayList<>();
        List<Integer> intList = new ArrayList<>();

        wildCardNumList = intList; //OK
        numList = intList; //コンパイルエラー
    }

List<? super T> 具有逆变性

List<? super T> 是一种带有下限边界通配符的类型。它表示一个包含类型T的某个超类型元素的列表。使用下限边界通配符类型,可以像下面的示例代码一样,在本来是不变的泛型中引入逆变性。

    public void bar() {
        List<Integer> intList = new ArrayList<>();
        List<? super Integer> wildCardIntList = new ArrayList<>();
        List<Number> numList = new ArrayList<>();

        wildCardIntList = numList; //OK
        intList = numList; //コンパイルエラー
    }

放置/獲取原則

在《Effective Java》的第28项中,PECS也被称为以下缩写。

PECS表示生产者(producer)扩展,消费者(consumer)超越。

Put/Get原则或PECS是一个由以下两个通配符基本原则组成的概念。

    型変数Tでパラメータ化された型が、プロデューサーであれば、<? extends T> を利用する。型変数Tでパラメータ化された型が、コンシューマであれば、<? super T> を利用する。

以下是通过示例代码展示Put/Get原则的有用性,这个原则是为了实现灵活性的API。首先,考虑以下没有应用Put / Get原则的接口。必要的代码摘录如下。

//型Tの値を消費する関数
interface Consumer<T> {
    void apply(T value);
}

//型Tの値をもつコンテナ
interface Box<T> {
    //保持する値を返す
    T get();
    //値を設定する
    void put(T element);
    //別のコンテナの値を設定する
    void put(Box<T> box);
    //Consumer関数を適用する
    void applyConsumer(Consumer<T> function);
}

在这里,以下的代码将会产生编译错误。

class InvariantSample {
    public void foo(Box<Number> numBox, Box<Integer> intBox) {
         numBox.put(1); //OK
         numBox.put(intBox); //コンパイルエラー      
    }
}

Box 的 put 方法接受 Box 作为参数,但由于是非变性,Box 不是 Box 的子类型,因此将 Box 作为参数应用会产生错误。尽管可以使用put(1)方法将整数类型的元素设置进去,但由于缺乏API的灵活性,无法将元素设置进Box中。接下来,考虑以下代码。

    public void foo(Box<Integer> intBox, Consumer<Integer> intConsumer, Consumer<Number> numConsumer) {
        intBox.applyConsumer(intConsumer); //OK
        intBox.applyConsumer(numConsumer); //コンパイルエラー
    }

尽管可以应用于消耗 Integer 元素的 Consumer 函数,但不能将消耗 Number 元素的 Consumer 函数应用于其中,这与直觉相违背。尝试将上述界面应用于Put/Get原则。可以应用于以下两个方法。

    void put(Box box) の引数 Box は値のプロデューサーである。void applyConsumer(Consumer function) の引数 Consumer は値のコンシューマーである

因此,当将Put/Get原则应用于这两个方法时,结果如下。

//型Tの値をもつコンテナ
interface Box<T> {
    //保持する値を返す
    T get();
    //値を設定する
    void put(T element);
    //別のコンテナの値を設定する
    void put(Box<? extends T> box);
    //Consumer関数を適用する
    void applyConsumer(Consumer<? super T> function);
}

經過這個結果,應用Put/Get原則之前的編譯錯誤都被解決了。

广告
将在 10 秒后关闭
bannerAds