文章目录
- 第二十章 泛型
- 5. 泛型擦除
- 5.1 泛型擦除
- 5.2 迁移兼容性
- 5.3 擦除的问题
- 5.4 边界处的动作
- 6. 补偿擦除
- 7. 边界
- 8. 通配符
- 8.1 通配符
- 8.2 逆变
- 9. 问题
- 10. 动态类型安全
- 11. 泛型异常
第二十章 泛型
普通的类和方法只能使用特定的类型:基本数据类型或类类型。如果编写的代码需要应用于多种类型,这种严苛的限制对代码的束缚就会很大。
多态是一种面向对象思想的泛化机制。你可以将方法的参数类型设为基类,这样的方法就可以接受任何派生类作为参数,包括暂时还不存在的类。
拘泥于单一的继承体系太过局限,如果方法以接口而不是类作为参数,限制就宽松多了,只要实现了接口就可以。
即便是接口也还是有诸多限制。一旦指定了接口,它就要求你的代码必须使用特定的接口。而我们希望编写更通用的代码,能够适用“非特定的类型”,而不是一个具体的接口或类。
这就是泛型的概念,是 Java 5 的重大变化之一。在很多情况下,它可以使代码更直接更优雅。
5. 泛型擦除
5.1 泛型擦除
package generics;import java.util.ArrayList;public class ErasedTypeEquivalence {public static void main(String[] args) {Class c1 = new ArrayList<String>().getClass();Class c2 = new ArrayList<Integer>().getClass();System.out.println(c1 == c2);}
}
输出:
true
ArrayList 和 ArrayList 应该是不同的类型。不同的类型会有不同的行为。例如,如果尝试向 ArrayList 中放入一个 Integer ,所得到的行为(失败)和向ArrayList 中放入一个 Integer 所得到的行为(成功)完全不同。然而上面的程序认为它们是相同的类型。
下面的例子是对该谜题的补充:
package generics;import java.util.*;class Frob {
}class Fnorkle {
}class Quark<Q> {
}class Particle<POSITION, MOMENTUM> {
}public class LostInformation {public static void main(String[] args) {List<Frob> list = new ArrayList<>();Map<Frob, Fnorkle> map = new HashMap<>();Quark<Fnorkle> quark = new Quark<>();Particle<Long, Double> p = new Particle<>();// Class.getTypeParameters() “返回一个 TypeVariable 对象数组,// 表示泛型声明中声明的类型参数...”System.out.println(Arrays.toString(list.getClass().getTypeParameters()));System.out.println(Arrays.toString(map.getClass().getTypeParameters()));System.out.println(Arrays.toString(quark.getClass().getTypeParameters()));System.out.println(Arrays.toString(p.getClass().getTypeParameters()));}
}
输出:
[E]
[K, V]
[Q]
[POSITION, MOMENTUM]
残酷的现实是:
在泛型代码内部,无法获取任何有关泛型参数类型的信息。
因此,你可以知道如类型参数标识符和泛型边界这些信息,但无法得知实际的类型参数从而用来创建特定的实例。
Java 泛型是使用擦除实现的。这意味着当你在使用泛型时,任何具体的类型信息都被擦除了,你唯一知道的就是你在使用一个对象。因此, List 和 List 在运行时实际上是相同的类型。它们都被擦除成原生类型 List 。
package generics;public class HasF {public void f() {System.out.println("HasF.f()");}
}
package generics;
class Manipulator<T> {private T obj;public Manipulator(T obj) {this.obj = obj;}public void manipulate() {obj.f(); // 报错,没有 f()}
}
public class Manipulation {public static void main(String[] args) {HasF hf = new HasF();Manipulator<HasF> manipulator = new Manipulator<>(hf);manipulator.manipulate();}
}
因为擦除,Java 编译器无法将 manipulate() 方法中调用 obj 的 f() 方法这一需求映射到HasF 的 f() 方法。为了调用 f() ,我们必须协助泛型类,给定泛型类一个边界,以此告诉编译器只能接受遵循这个边界的类型。这里重用了 extends 关键字。由于有了边界,下面的代码就能通过编译:
package generics;public class Manipulator2<T extends HasF> {private T obj;Manipulator2(T x) {obj = x;}public void manipulate() {obj.f();}
}
边界 声明 T 必须是 HasF 类型或其子类。
你可能认为泛型在 Manipulator2.java 中没有贡献任何事。你可以很轻松地自己去执行擦除,生成没有泛型的类:
package generics;public class Manipulator3 {private HasF obj;Manipulator3(HasF x) {obj = x;}public void manipulate() {obj.f();}
}
泛型只有在类型参数比某个具体类型(以及其子类)更加“泛化”——代码能跨多个类工作时才有用。
如果某个类有一个返回 T 的方法,那么泛型就有所帮助,因为它们之后将返回确切的类型:
package generics;public class ReturnGenericType<T extends HasF> {private T obj;public ReturnGenericType(T x) {this.obj = x;}public T get(){return obj;}
}
5.2 迁移兼容性
擦除不是一个语言特性,它是 Java 实现泛型的一种妥协,因为泛型不是 Java 语言出现时就有的。泛型在 Java 中仍然是有用的,只是不如它们本来设想的那么有用,而原因就是擦除。
擦除的核心动机是你可以在泛化的客户端上使用非泛型的类库,反之亦然。这经常被称为“迁移兼容性”。在理想情况下,所有事物将在指定的某天被泛化。在现实中,即使程序员只编写泛型代码,他们也必须处理 Java 5 之前编写的非泛型类库。这些类库的作者可能从没想过要泛化他们的代码,或许他们可能刚刚开始接触泛型。
因此 Java 泛型不仅必须支持向后兼容性——现有的代码和类文件仍然合法,继续保持之前的含义——而且还必须支持迁移兼容性,使得类库能按照它们自己的步调变为泛型,当某个类库变为泛型时,不会破坏依赖于它的代码和应用。在确定了这个目标后,Java 设计者们和从事此问题相关工作的各个团队决策认为擦除是唯一可行的解决方案。擦除使得这种向泛型的迁移成为可能,允许非泛型的代码和泛型代码共存。
—PS:擦除虽然有弊端,但它是解决泛型向后兼容性的唯一可行方案
5.3 擦除的问题
因此,擦除主要的正当理由是从非泛化代码到泛化代码的转变过程,以及在不破坏现有类库的情况下将泛型融入到语言中。
泛型不能用于显式地引用运行时类型的操作中,例如转型、instanceof 操作和 new 表达式。因为所有关于参数的类型信息都丢失了,当你在编写泛型代码时,必须时刻提醒自己,你只是看起来拥有有关参数的类型信息而已。
public class Foo<T> {T var;
}
看上去当你创建一个 Foo 实例时:
Foo<Cat> f = new Foo<>();
class Foo 中的代码应该知道现在工作于 Cat 之上。泛型语法也在强烈暗示整个类中所有 T 出现的地方都被替换,就像在 C++ 中一样。但是事实并非如此,当你在编写这个类的代码时,必须提醒自己:“不,这只是一个 Object“。
另外,擦除和迁移兼容性意味着,使用泛型并不是强制的:
package generics;class GenericBase<T> {private T element;public void set(T arg) {element = arg;}public T get() {return element;}
}class Derived1<T> extends GenericBase<T> {
}class Derived2 extends GenericBase {
}// Derived3 产生的错误意味着编译器期望得到一个原生基类
//class Derived3 extends GenericBase<?> {}public class ErasureAndInteritance {public static void main(String[] args) {Derived2 d2 = new Derived2();Object obj = d2.get();d2.set(obj);}
}
5.4 边界处的动作
边界:即对象进入和离开方法的地点。这些正是编译器在编译期执行类型检查并插入转型代码的地点。这就告诉我们泛型的所有动作都发生在边界处——对入参的编译器检查和对返回值的转型。这有助于澄清对擦除的困惑,记住:“边界就是动作发生的地方”。
6. 补偿擦除
因为擦除,我们将失去执行泛型代码中某些操作的能力。无法在运行时知道确切类型:
package generics;public class Erased<T> {private final int SIZE = 100;public void f(Object arg) {if (arg instanceof T) {} // 报错T var = new T(); // 报错T[] array = new T[SIZE]; // 报错T[] array2 = (T[]) new Object[SIZE]; // 书上说这个地方应该有警告,但是没有,不知道为啥}
}
—PS:上文有:泛型不能用于显式地引用运行时类型的操作中,例如转型、instanceof 操作和 new 表达式。
有时,我们可以对这些问题进行编程,但是有时必须通过引入类型标签来补偿擦除。这意味着为所需的类型显式传递一个 Class 对象,以在类型表达式中使用它。
例如,由于擦除了类型信息,因此在上一个程序中尝试使用 instanceof 将会失败。类型标签可以使用动态 isInstance() :
package generics;class Building {
}class House extends Building {
}public class ClassTypeCapture<T> {Class<T> kind;public ClassTypeCapture(Class<T> kind) {this.kind = kind;}public boolean f(Object arg) {return kind.isInstance(arg);}public static void main(String[] args) {ClassTypeCapture<Building> ctt1 =new ClassTypeCapture<>(Building.class);System.out.println(ctt1.f(new Building()));System.out.println(ctt1.f(new House()));ClassTypeCapture<House> ctt2 =new ClassTypeCapture<>(House.class);System.out.println(ctt2.f(new Building()));System.out.println(ctt2.f(new House()));}
}
输出:
true
true
false
true
7. 边界
边界允许我们对泛型使用的参数类型施加约束。尽管这可以强制执行有关应用了泛型类型的规则,但潜在的更重要的效果是我们可以在绑定的类型中调用方法。
由于擦除会删除类型信息,因此唯一可用于无限制泛型参数的方法是那些 Object 可用的方法。但是,如果将该参数限制为某类型的子集,则可以调用该子集中的方法。为了应用约束,Java 泛型使用了extends 关键字。
重要的是要理解,当用于限定泛型类型时, extends 的含义与通常的意义截然不同。此示例展示边界的基础应用:
package generics;import java.awt.*;interface HasColor {java.awt.Color getColor();
}class WithColor<T extends HasColor> {T item;WithColor(T item) {this.item = item;}T getItem() {return item;}java.awt.Color color() {return item.getColor();}
}class Coord {public int x, y, z;
}// 类需要放接口前面
// class WithColorCoord<T extends HasColor & Coord> {}
class WithColorCoord<T extends Coord & HasColor> {T item;WithColorCoord(T item) {this.item = item;}T getItem() {return item;}java.awt.Color color() {return item.getColor();}int getX() {return item.x;}int getY() {return item.y;}int getZ() {return item.z;}
}interface Weight {int weight();
}// 泛型只能 extends 一个类,但是可以 extends 多个接口
class Solid<T extends Coord & HasColor & Weight> {T item;Solid(T item) {this.item = item;}T getItem() {return item;}java.awt.Color color() {return item.getColor();}int getX() {return item.x;}int getY() {return item.y;}int getZ() {return item.z;}int weight() {return item.weight();}
}class Bounded extends Coord implements HasColor, Weight {@Overridepublic Color getColor() {return null;}@Overridepublic int weight() {return 0;}
}public class BasicBounds {public static void main(String[] args) {Solid<Bounded> solid = new Solid<>(new Bounded());solid.color();solid.getY();solid.weight();}
}
你可能会观察到 BasicBounds.java 中似乎包含一些冗余,它们可以通过继承来消除。在这里,每个继承级别还添加了边界约束:
package generics;class HoldItem<T> {T item;HoldItem(T item) {this.item = item;}T getItem() {return item;}
}class WithColor2<T extends HasColor> extends HoldItem<T> {WithColor2(T item) {super(item);}java.awt.Color color() {return item.getColor();}
}class WithColorCoord2<T extends Coord & HasColor> extends WithColor2<T> {WithColorCoord2(T item) {super(item);}int getX() {return item.x;}int getY() {return item.y;}int getZ() {return item.z;}
}class Solid2<T extends Coord & HasColor & Weight> extends WithColorCoord2<T> {Solid2(T item) {super(item);}int weight() {return item.weight();}
}public class InheritBounds {public static void main(String[] args) {Solid2<Bounded> solid2 = new Solid2<>(new Bounded());solid2.color();solid2.getY();solid2.color();}
}
HoldItem 拥有一个对象,因此此行为将继承到 WithColor2 中,这也需要其参数符合 HasColor。
WithColorCoord2 和 Solid2 进一步扩展了层次结构,并在每个级别添加了边界。现在,这些方法已被继承,并且在每个类中不再重复。
—PS:边界由 T extends 限定,每个类或接口中有独有的方法,在最后的继承类中都能引用
8. 通配符
8.1 通配符
起始示例要展示数组的一种特殊行为:将派生类的数组赋值给基类的引用:
package generics;class Fruit {
}class Apple extends Fruit {
}class Jonathan extends Apple {
}class Orange extends Fruit {
}public class CovariantArrays {public static void main(String[] args) {Fruit[] fruit = new Apple[10];fruit[0] = new Apple();fruit[1] = new Jonathan();try {// 编译不报错fruit[0] = new Fruit(); // 运行报错-ArrayStoreException} catch (Exception e) {System.out.println(e);}try {// 编译不报错fruit[0] = new Orange(); // 运行报错-ArrayStoreException} catch (Exception e) {System.out.println(e);}}
}
输出:
java.lang.ArrayStoreException: generics.Fruit
java.lang.ArrayStoreException: generics.Orange
看起来就像数组对它们持有的对象是有意识的,因此在编译期检查和运行时检查之间,你不能滥用它们。
数组的这种赋值并不是那么可怕,因为在运行时你可以发现插入了错误的类型。但是泛型的主要目标之一是将这种错误检测移到编译期。所以当我们试图使用泛型集合代替数组时,会发生什么呢?
package generics;import java.util.ArrayList;
import java.util.List;public class NonCovariantGenerics {public static void main(String[] args) {List<Fruit> fruit = new ArrayList<Apple>();}
}
阅读这段代码时会认为“不能将一个 Apple 集合赋值给一个 Fruit 集合”。记住,泛型不仅仅是关于集合,它真正要表达的是“不能把一个涉及 Apple 的泛型赋值给一个涉及 Fruit 的泛型”。
与数组不同,泛型没有内建的协变类型。这是因为数组是完全在语言中定义的,因此可以具有编译期和运行时的内建检查,但是在使用泛型时,编译器和运行时系统不知道你想用类型做什么,以及应该采用什么规则。
但是,有时你想在两个类型间建立某种向上转型关系。通配符可以产生这种关系。
package generics;import java.util.ArrayList;
import java.util.List;public class GenericsAndCovariance {public static void main(String[] args) {List<? extends Fruit> flist = new ArrayList<>();
// flist.add(new Apple());
// flist.add(new Fruit());
// flist.add(new Object());flist.add(null);Fruit f = flist.get(0);}
}
展示一个简单的 Holder 类:
package generics;import java.util.Objects;public class Holder<T> {private T value;public Holder() {}public Holder(T value) {this.value = value;}public T get() {return value;}public void set(T value) {this.value = value;}@Overridepublic boolean equals(Object o) {return o instanceof Holder && Objects.equals(value, ((Holder) o).value);}@Overridepublic int hashCode() {return Objects.hashCode(value);}public static void main(String[] args) {Holder<Apple> apple = new Holder<>(new Apple());Apple d = apple.get();apple.set(d);
// Holder<Fruit> fruit = apple; // 不能向上转型Holder<? extends Fruit> fruit = apple;Fruit p = fruit.get();d = (Apple) fruit.get();try {Orange c = (Orange) fruit.get(); // No warning} catch (Exception e) {System.out.println(e);}
// fruit.set(new Apple());
// fruit.set(new Fruit());System.out.println(fruit.equals(d));}
}
输出:
java.lang.ClassCastException: generics.Apple cannot be cast to generics.Orange
false
以上得到的信息:
1、创建了一个 Holder ,就不能将其向上转型为 Holder ,但是可以向上转型为 Holder<? extends Fruit> 。
2、如果调用 get() ,只能返回一个 Fruit——这就是在给定“任何;额扩展自 Fruit 的对象”这一边界后,它所能知道的一切了。如果你知道更多的信息,就可以将其转型到某种具体的 Fruit 而不会导致任何警告,但是存在得到ClassCastException 的风险。
3、set() 方法不能工作在 Apple 和 Fruit 上,因为 set() 的参数也是"?
extends Fruit",意味着它可以是任何事物,编译器无法验证“任何事物”的类型安全性。
8.2 逆变
还可以走另外一条路,即使用超类型通配符。这里,可以声明通配符是由某个特定类的任何基类来界定的,方法是指定 <?super MyClass> ,或者甚至使用类型参数: <?super T> 。
package generics;import java.util.List;public class SuperTypeWildcards {static void writeTo(List<? super Apple> apples) {apples.add(new Apple());apples.add(new Jonathan());
// apples.add(new Fruit()); // 报错}
}
参数 apples 是 Apple 的某种基类型的 List,这样你就知道向其中添加 Apple 或 Apple 的子类型是安全的。但是因为 Apple 是下界,所以你知道向这样的 List 中添加 Fruit 是不安全的,因为这将使这个List 敞开口子,从而可以向其中添加非 Apple 类型的对象,而这是违反静态类型安全的。 下面的示例复习了一下逆变和通配符的的使用:
package generics;import java.util.Arrays;
import java.util.List;public class GenericReading {static List<Apple> apples = Arrays.asList(new Apple());static List<Fruit> fruits = Arrays.asList(new Fruit());static <T> T readExact(List<T> list) {return list.get(0);}static void f1() {Apple a = readExact(apples);Fruit f = readExact(fruits);f = readExact(apples); // 向上转型}// 内部类static class Reader<T> {T readExact(List<T> list) {return list.get(0);}}static void f2() {Reader<Fruit> fruitReader = new Reader<>();Fruit f = fruitReader.readExact(fruits);
// Fruit a = fruitReader.readExact(apples);}static class CovariantReader<T> {T readCovariant(List<? extends T> list) {return list.get(0);}}static void f3() {CovariantReader<Fruit> fruitReader = new CovariantReader<>();Fruit f = fruitReader.readCovariant(fruits);Fruit a = fruitReader.readCovariant(apples);}public static void main(String[] args) {f1();f2();f3();}
}
看到这里有点费劲,还积攒了几个问题,没关系,带着疑惑找答案才能有收获,推荐大佬文章:Java 中的泛型(两万字超全详解)
看完大佬文章,下面的别看了,我是随便摘了几个。
9. 问题
任何基本类型都不能作为类型参数
Java 泛型的限制之一是不能将基本类型用作类型参数。因此,不能创建ArrayList 之类的东西。
实现参数化接口
一个类不能实现同一个泛型接口的两种变体,由于擦除的原因,这两个变体会成为相同的接口。下面是产生这种冲突的情况:
package generics;interface Payable<T> {}class Employee implements Payable<Employee> {}// Hourly 不能编译,因为擦除会将 Payable<Employe> 和 Payable<Hourly> 简化为相同的类
// Payable,这样,上面的代码就意味着在重复两次地实现相同的接口。
class Hourly extends Employee implements Payable<Hourly> {}public class MultipleInterfaceVariants {
}
转型和警告
使用带有泛型类型参数的转型或 instanceof 不会有任何效果。
重载
package generics;public class UseList {void f(List<T> v) {}void f(List<W> v) {}
}
因为擦除,所以重载方法产生了相同的类型签名。
基类劫持接口
package generics;public class ComparablePet implements Comparable<ComparablePet>{@Overridepublic int compareTo(ComparablePet o) {return 0;}
}class Cat extends ComparablePet implements Comparable<Cat> {}
—PS:就是上面提到的 擦除 引起的
自限定的类型
class SelfBounded<T extends SelfBounded<T>> { // ...
这就像两面镜子彼此照向对方所引起的目眩效果一样,是一种无限反射。SelfBounded 类接受泛型参数T,而 T 由一个边界类限定,这个边界就是拥有 T 作为其参数的 SelfBounded。
当你首次看到它时,很难去解析它,它强调的是当 extends 关键字用于边界与用来创建子类明显是不同的。
10. 动态类型安全
因为可以向 Java 5 之前的代码传递泛型集合,所以旧式代码仍旧有可能会破坏你的集合。Java 5 的java.util.Collections 中有一组便利工具,可以解决在这种情况下的类型检查问题,它们是:静态方法 checkedCollection() 、 checkedList() 、 checkedMap() 、 checkedSet()、 checkedSortedMap() 和 checkedSortedSet() 。这些方法每一个都会将你希望动态检查的集合当作第一个参数接受,并将你希望强制要求的类型作为第二个参数接受。
11. 泛型异常
由于擦除的原因,catch 语句不能捕获泛型类型的异常,因为在编译期和运行时都必须知道异常的确切类型。泛型类也不能直接或间接继承自 Throwable(这将进一步阻止你去定义不能捕获的泛型异常)。 但是,类型参数可能会在一个方法的 throws 子句中用到。
(图网,侵删)