Java泛型的要点
由于长时间没有使用泛型库,我正在创建一个大量使用泛型的库。我忘记了一些东西并陷入了一些困境,所以现在我将它们作为备忘录总结起来。
“用語” 可以被漢語化為 “术语” 。
鉴于泛型存在类似的术语,很容易引起混淆,因此首先要确切地理解这些术语。
以下内容摘自《Effective Java》的第23条。
变性
在泛型类型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
public void foo(Box<Integer> intBox, Consumer<Integer> intConsumer, Consumer<Number> numConsumer) {
intBox.applyConsumer(intConsumer); //OK
intBox.applyConsumer(numConsumer); //コンパイルエラー
}
尽管可以应用于消耗 Integer 元素的 Consumer
- void put(Box
因此,当将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原則之前的編譯錯誤都被解決了。