Core-Java的第8章 泛型程序设计
一、为什么引入泛型程序设计
泛型意味着代码对多种不同类型的对象重用。
1.1 类型参数的好处
在没有泛型之前,泛型都是通过继承来实现。如ArrayList
类维护一个Object
引用数组。
1 | public class ArrayList { |
这里存在的问题在于。当获取一个值时必须强制类型转换为需要的类型。并且,这里没有类型检查,可以添加任何类的值。泛型为此提供了一个更好的类型参数(type parameter)。使代码可读性变高。
并让编译器知道方法需要传入何种类型的参数,让编译器可以为我们完成检查工作,防止插入错误类型的对象。
1.2 通配符类型
详见Java类库。
二、定义简单泛型类
泛型类就是有一个或者多个类型变量的类。这个类只关心泛型,不为存储的细节而分心。
1 | public class Pair<T> { |
Pair
类使用了类型变量T
,使用尖括号<>
包裹,放于类名后。
如果有多个类型,可以用逗号分隔即可。
1 | public class Pair<T, U> { ... } |
一般使用E
代表集合元素类型,K
和V
代表键和值的类型,T、U 、S
代表任意类型。
也可以使用具体类型来替换类型变量来实例化(instantiate)泛型变量。
三、泛型方法
类型变量需放置修饰符后、返回类型前
1 | class ArrayAlg { |
调用时,可以将具体类型包围在尖括号中、方法名前。
1 | String middle = ArrayAlg.<String>getMiddle("John", "Q.","Public"); |
这种情况下、方法调用可以省略<String>
类型参数。(大多数情况下都省略)
四、类型变量的限定
有时需要对类型变量加以约束。
1 | public static <T extends Comparable> T min(T[] a) {...} |
约束T
只能是实现了Comparable
接口的类。
表示限定类型时一定是使用extends
而非implements
。
1 | <T extends BoundingType> |
表示T应该是限定类型的子类型。T
和限定类可以是类,也可以是接口,使用extends
是因为它更接近子类型的概念。
有多个限定时:
1 | T extends Comparable & Serializable |
用&
分隔限定类型,用,
分隔类型变量。
五、泛型代码和虚拟机
在虚拟机中没有泛型类型对象,所有对象都最终属于普通类。编译器会为我们完成泛型擦除这一步的工作。
5.1 泛型擦除
无论何时定义一个泛型类型,都会自动提供一个相应的原始类型(raw type)。这个类型的名字就是去掉类型参数后的泛型类型名。类型变量就会被擦除(erased),并替换为其限定类型。(或者,对于无限定类型则替换为object)。
5.2 转换泛型表达式
编写一个泛型方法调用时,如果擦除了返回类型,编译器会插入强制类型转换。
5.3 转换泛型方法
类型擦除也会出现再泛型方法中。
1 | public static <T extends Comparable> T min(T[] a) |
并不是一组方法,在擦除类型后,就只剩下一个方法。
1 | public static Comparable min(Comparable[] a) |
虚拟机中没有泛型,只有普通的类和方法。
所有的类型参数都会替换为他们的限定类型。
会合成桥方法来维持多态。
为保持类型安全性,必要时会插入强制类型转换。
5.4 调用遗留代码
1 |
关闭对方法中所有代码的检查。
六、限制与局限性
6.1 不能用基本数据类型实例化类型参数
不能用基本数据类型代替类型参数,只能用其包装类或单独的类。
1 | Pair<double>// error |
类型擦除后,Pair
类只含有Object
类型的字段,而Object
不能存储double
值。
6.2 运行时类型查询只适用于原始类型
虚拟机中的对象总有一个特定的非泛型类型。因此,所有的类型查询只产生原始类型。
instanceof
关键字、getClass
等方法只能获得原始类型。
1 | if(a instanceof Pair<String>) // error |
以上都只可以检查a是否是一个Pair
类的对象。
1 | Pair<String> stringPair = ...; |
其比较结果为true
,因为两次getClass
都会返回Pair.class
。
6.3 不能创建参数化类型的数组
不能实例化参数化类型的数组
1 | var table = new Pair<String>[10];// error |
类型擦除后,table的类型为Pair[]
。可以转换为Object[]
。数组会记住它的元素类型,如果试图存储其他类型,则会抛出ArrayStoreException
异常
1 | var table = new Pair<String>[10]; |
尽管存储一个Pair
类型可以,但仍会导致一个类型错误。出于这个原因,不允许创建参数化类型的数组
1 | objArray[0] = new Pair<Employee>(); |
只是不允许创建这些数组,但是允许声明类型为参数化类型的数组。
6.4 Varargs警告
若向参数个数可变的方法传递一个泛型类型的实例。
1 | public static <T> void addAll(Collection<T> coll, T... ts) { |
如上述方法,参数个数可变,实际上为一个数组,包含所有实参。
以下调用过程:
1 | Collection<Pair<String>> table = ...; |
此时,jvm必须要创建一个Pair<String>
类型的数组,虽然违反了6.3,但是对于这种情况,规则会有所放松。
将会得到一个警告,而非错误。
可以采用@SuppressWarnings("unchecked")
抑制这个警告。或Java7中的@SafeVarargs
直接注解addAll
方法。
@SafeVarargs
只能用于声明为static
、final
或(Java9中)private
的构造器和方法。因为其他方法可能被覆盖,使得这个注解没有什么意义。
6.5 不能实例化类型变量
即不能在类似 new T(...)
表达式中使用类型变量。
1 | public Pair() {first = new T(); second = new T();} |
类型擦除将T
变成Object
。在Java8后,最好让调用者提供一个构造器表达式。
1 | Pair<String> p = Pair.makePair(String::new); |
makePair
方法接受一个Supplier<T>
的函数式接口参数,表示一个无参数且返回值类型为T的方法。
1 | public static <T> Pair<T> makePair(Supplier<T> constr) { |
或者比较传统的方式使用反射调用Constructor.newInstance
方法构造泛型对象。
1 | public static <T> Pair<T> makePair(Class<T> cl) { |
这个方法就可以如下调用:
1 | Pair<String> p = Pair.makePair(String.class); |
注意,class类本身就是泛型的。如String.class
就是Class<String>
的一个实例。
Class<T>
有且仅有一个实例就是T.class
。
因此在这里,使用makePair
方法就可以推断出Pair
的类型。
6.6 不能构造泛型数组
就像不能实例化泛型实例一样,也不能实例化数组。
不过原因不同,虽然数组可以填充null
值,但是数组本身也带有类型,如果带有泛型,则泛型会被擦除。
1 | public static <T extends Coparable> T[] minmax(T... a) { |
类型擦除总是会构造Comparable[2]
数组。
若数组仅为私有实例字段,可以将元素类型声明为擦除的类型并使用强制类型转换。
如ArrayList
类可以如下实现:
1 | public class ArrayList<T> { |
但是实际,ArrayList
的设计者采用了如下设计:
1 | public class ArrayList<E> { |
这里,强制类型转换E[]
只是一个假象,而类型擦除使其无法察觉。
6.7 泛型类的静态上下文中类型变量无效
不能在静态字段或方法中引用类型变量。因为类型将会被擦除。
1 | /** |
6.8 不能抛出或捕获泛型类的实例
既不能抛出也不能捕获泛型类的对象。泛型类也不能扩展Throwable
。
catch
子句也不能使用类型变量。在异常规范中,可以使用类型变量。
1 | /*以下定义不能通过编译*/ |
6.9 可以取消对检查类异常的检查
Java中对于检查型异常的机制是,必须为所有的检查型异常提供处理器。但是可以使用泛型来取消这个机制。
1 |
|
若上述方法位于接口Task
中,且有一个检查型异常e,调用了Task.<RuntimeException>throwAs(e);
编译器就会认为e是一个非检查型异常。以下代码能够将所有异常转换为编译器认为的非检查型异常。
1 | try { |
若有以下需求:我们知道要在一个线程中运行代码,需要把代码放在一个实现了Runnable
接口的类的run
方法中。
不过我们知道这个方法不允许抛出检查型异常,只能在内部try-catch
处理掉,不能往外抛。因为线程是一个独立运行的代码片段,它的问题不能影响到其他线程。我们将提供一个从Task
到Runnable
的适配器,它的run
方法可以抛出任何异常。
1 | interfact Task { |
此时,有如下程序运行了一个线程,它会抛出了一个检查型异常。
1 | public class Test { |
sleep
方法声明会抛出一个InterruptedException
,我们不再需要捕获这个异常。由于我们没有中断这个线程,这个异常不会被抛出。不过,程序会抛出一个检查型异常。
这里的意义在于,在正常情况下,必须捕获run
方法中的所有检查型异常,把它们保证到非检查异常里。
在此处我们没有采取包装,而是直接抛出异常,“哄骗”编译器,让其“相信”这不是一个检查型异常、
6.10 注意类型擦除后的冲突
当泛型类型被擦除后,不允许创建引发冲突的条件。
1 | public class Pair<T> { |
则对于Pair
类,就有两个equals
方法,一个参数类型为T
,实际类型为擦除后的类型。
另一个为Object
,继承自Object
类。
这时,由于类型擦除,这两个方法就会发生方法冲突。补救的方法就是重新给方法命名。
为支持擦除转换,要施加一个限制:两个接口类型是同一接口的不同参数化,一个类或类型变量就不能同时作为两个接口类型的子类。
1 | class Employee implements Comparable<Employee> { ... } |
Manager
会实现接口,其中的类型参数不同,这就是同一接口的不同参数化。
七、泛型类型的继承规则
对于类型参数T
、S
,无论S
与T
有什么关系,Pair<T>
和Pair<S>
都没有任何关系。
可以将参数化的类型转换为原始类型。如Pair<Employee>
是原始类型Pair
的一个子类型。
泛型类可以扩展或实现为其他的泛型类。如ArrayList<T>
可以转换为List<T>
。
八、通配符类型
8.1 通配符概念
即允许类型参数发生变化。如Pair<? extends Employee>
代表泛型Pair
类型,且类型参数必须为Employee
的子类。如Pair<Manager>
但不是Pair<String>
。
8.2 通配符的超类型限定
通配符限定与类型参数限定十分类似,但还可以指定一个超类型限定。
? super Manager
限制了类型都要为Manager
的超类型。
即带有超类型限定的通配符允许写入一个泛型对象,而带有子类型限定的通配符允许读取一个泛型对象。
8.3 无限定通配符
还可以使用根本无限定通配符,如Pair<?>。
8.4 通配符捕获
通配符不是类型变量,所以不能在编写代码时使用“?”作为一种类型。
1 | public static void swap(Pair<?> p) { |
以上代码用于交换元素是不能成功的。但是,可以借助一个辅助方法。
1 | public static <T> void swapHelper(Pair<T> p) { |
若使用swap来调用swapHelper
1 | public static void swap(Pair<?> p) { |
这种情况下称swapHelper方法的参数T捕获通配符。
九、 反射与泛型
9.1 泛型Class类
Class
类也是泛型的。String.class
实际上是一个Class<String>
类的对象,也是唯一的对象。
T newInstance()
返回无参数构造器构造的一个新实例。
T cast(Object obj)
如果obj为null或有可能转换成类型T,则返回obj;
否则抛出BadCastException异常。
T[] getEnumConstants()
如果T是枚举类型,返回所有值组成的数组,否则返回null。
Class<? super T> getSuperClass()
返回这个类的超类。如果T不是一个类或者为Object,返回null。
Constructor<T> getConstructor(Class... parameterTypes)
Constructor<T> getDeclaredConstructor(Class... parameterTypes)
获得公共构造器或有给定参数类型的构造器。
T newInstance(Object... parameters)
返回用指定参数构造的新实例。
9.2 使用Class参数进行类型匹配
上文提到的makePair(Class<T> cl)
方法
9.3 虚拟机中的泛型类型信息
Class
类,描述具体类型TypeVariable
接口,描述类型变量(如T extends Comparable<? super T>
)WildcardType
接口,描述通配符(如? super T
)ParameterizedType
接口,描述泛型类或接口类型(如Comparable<? super T>
)GenericArrayType
接口,描述泛型数组如(T[]
)