title: 泛型程序设计
tag: 标签名
categories: 分类
comment: 是否允许评论(true or false)
description: 描述
top_img: https://z3.ax1x.com/2021/10/06/4xq2s1.png
cover: https://z3.ax1x.com/2021/10/06/4xq2s1.png
为什么要使用泛型程序设计
泛型程序设计意味着编写的代码可以对多种不同类型的对象重用。如,你并不希望收集String和File对象分别编写不同的类。实际上,也不需要这样做,因为一个ArrayLsit类就可以收集任何类的对象。
类型参数的好处
在Java中增加泛型类之前,泛型程序设计是用继承实现的。ArrayList类只维护一个Object引用的数组:
public class ArrayList{
private Object[] elementData;
...
public Object get(int i){
...
}
public void add(Object 0){
...
}
}
// 这种类型存在两种问题。当获取一个值时必须进行强制类型转换。
ArrayList files = new ArrayList();
...
String filename = (String)files.get(0);
// 此外,这里没有错误检查。可以向数组列表中添加任何类型的值
// 对于这个调用,在编译和运行都不会出错,不过在其他地方,如果将get的结果强制类型转换为String类型,就会产生一个错误。
泛型提供了一个更好的解决方案:类型参数。ArrayList类有一个类型参数用来指示元素的类型。
var files = new ArrayList<String>();
// 这样使得代码具有可读性,一看就知道数组里面的是String类型的对象
// 如果用一个明确的类型而不是用var声明一个变量,则可以使用“菱形”语法省略构造器中的类型参数:
ArrayList<String> files = new ArrayList<>();
编译器也可以充分利用这个类型信息。调用get的时候,不需要进行强制类型转换。编译器知道返回值类型为String,而不是Object。
String filename = files.get(0);
编译器还知道ArrayList的add方法有一个类型为String的参数,这比有一个Object类型的参数要安全得多。现在,编译器可以检查,防止比插入错误类型的对象。如以下语句:
files.add(new File(". . .")); // can only add String objects to ArrayList<String>
// 是无法通过编译的。不过,出现编译错误要比运行时出现类的强制类型转换异常好得多。
// 它们会让你的程序更易读,也更加安全
谁想成为泛型程序员
ArrayList类有一个方法addAll,用来添加另一个集合的全部元素。一个程序员可能想要将一个ArrayList中的所有元素添加到一个ArrayList中去。不过,反过来就不可以,如何允许前一个调用,而不允许后一个调用。Java语言的设计者发明了一个具有独创性的新概念来解决这个问题,即通配符类型。通配符类型非常抽象,不过,利用通配符类型,构建类库的程序员可以编写尽可能灵活的方法。
定义简单泛型类
泛型类就是有一个或多个类型变量的类。
public class Pair<T>{
private T first;
private T second;
public Pair() {
first = null;
second = null;
}
public Pair(T first, T second) {
this.first = first;
this.second = second;
}
public T getFirst() {
return first;
}
public void setFirst(T first) {
this.first = first;
}
public T getSecond() {
return second;
}
public void setSecond(T second) {
this.second = second;
}
}
Pair类中引入了一个类型变量T,用尖括号(<>)括起来,放在类名的后面。泛型类可以有对个类型变量。如,可以定义Pair类,其中第一个字段和第二个字段使用不同的类型:
public class Pair<T,U>{
...
}
类型变量在整个类定义中用于指定方法的返回类型以及字段和局部变量的类型。如
private T first; // uses the type variable
可以用具体的类型替换类型变量来实例化泛型类型,如:
Pair<String>
可以把结果想象成一个普通的类,它有以下构造器:
Pair<String>
Pair<String>(String, String)
以及以下方法:
String getFirst()
String getSecond()
void setFirst(String)
void setSecond(String)
换句话说,泛型类相当于普通类的工厂。
泛型方法
我们还可以定义一个带有类型参数的方法。
class ArrayAlg{
public static<T> T getMiddle(T ... a){
return a[a.length/2];
}
}
这个方法在普通类中定义的,而不是在泛型类中。不过,这是一个泛型方法,可以从尖括号和类型变量看出这一点。注意,类型变量放在修饰符的后年,并在返回类型的前面。
泛型方法可以在普通类中定义,也可以在泛型类中定义。
当调用一个泛型方法时,可以把具体类型包围在尖括号中,放在方法名前面:
String middle = ArrayAlg.<String> getMiddle("John","Q.","Public");
在这种情况下,方法调用中可以省略类型参数。编译器有足够的信息推断出你想要的方法。它将参数的类型与泛型类型T进行匹配,推断出T一定是String。也就是说,可以简单地写为:
String middle = ArrayAlg.getMiddle("John","Q.","Public");
类型变量的限定
有时,类或方法需要对类型变量加以约束。
class ArrayAlg{
public static<T> T min(T[] a){
if(a == null || a.length == 0)return null;
T smallest = a[0];
for(int i = 1; i < a.length; i++){
if(smallest.compareTo(a[i]) > 0)smallest = a[i];
}
return smallest;
}
}
上述代码时有一个问题的,变量smallest的类型为T,这意味着它可以是任何一个类的对象。如何知道T所属的类有一个compareTo方法呢?
解决这个问题的方法是限制T只能实现了Compareable接口的类。可以通过对类型变量T设置一个限定类实现这一点:
public static<T extends Compareable> T min(T[] a)...
现在min只能在实现了Compareable接口的类的数组上调用。由于Rectangle类没有实现Comparable接口,所以在Rectangle数组上调用min将会产生一个编译错误。
为什么使用关键字extends而不是implements?毕竟,Comparable是一个接口。下面的记法:
<T extends BoundingType>
// T表示限定类型的子类型。T和限定类型可以是类,也可以是接口。选择关键字extends的原因是它更接近子类型的概念。
一个类型变量或通配符可以有多个限定,例如:
T extends Comparable & Serializable
// 限定类型用 "&"分隔,而逗号用来分隔类型变量。
泛型代码和虚拟机
类型擦除
无论何时定义一个泛型类型,都会自动提供一个相应的原始类型。这个原始类型的名字就是去掉类型参数后的泛型类型名。类型变量会被擦除,并替换为其限定类型。
public class Pair{
private Object first;
private Object second;
public Pair(Object first, Object second) {
this.first = first;
this.second = second;
}
public Object getFirst() {
return first;
}
public Object getSecond() {
return second;
}
public void setFirst(Object first) {
this.first = first;
}
public void setSecond(Object second) {
this.second = second;
}
}
因为T是一个无限定的变量,所以直接用Object替换。
结果是一个普通类,就好像Java语言中引入泛型之前实现的类一样。
在程序中可以包含不同类型的Pair,如Pair。但是擦除类型后,它们都会变成原始的Pair类型。
原始类型用第一个限定来替换类型变量,或者,如果没有给定限定,就替换为Object。如,类Pair中的类型变量没有显示的限定,因此,原始类型用Object替换T。假定我们声明了一个稍有不同的类型:
public class Interval<T extends Comparable & Serializable> implements Serializable{
private T lower;
private T upper;
...
public Interval(T first, T second){
if(first.compareTo(second) <= 0){
lower = first;
upper = second;
}
}
}
// 原始类型Interval如下所示
public class Interval implements Serializable{
private Comparable lower;
private Comparable upper;
...
public Interval(Comparable first,Comparable second){
...
}
}
转换泛型表达式
编写一个泛型方法调用时。如果擦除了返回类型,编译器会插入强制类型转换。如
Pair<Employee> buddies = ...;
Employee buddy = buddies.getFirst();
getFirst擦除类型后的返回类型是Object。编译器自动插入转换到Employee的强制类型转换。编译器把这个方法调用转换成为两条虚拟机指令:
- 对原始方法Pair.getFirst的调用
- 将返回的Object类型强制转换为Employee类型。
转换泛型方法
类型擦除也会出现在泛型方法中。
public static<T extends Comparable> T min(T[] a);
// 是整个一组方法,而擦除类型之后,只剩下一个方法
public static Comparable min(Comparable[] a)
// 注意,类型参数T已经被擦除了,只留下限定类型Comparable.
对于Java泛型的转换,需要记住以下几个事实:
- 虚拟机中没有泛型,只有普通的类和方法
- 所有的类型参数都会替换为它们的限定类型
- 会合成桥方法来保存多态
- 为保持类型安全性,必要时会插入强制类型转换
限制与局限性
不能用基本类型实例化类型参数
不能用基本类型代替类型参数。无Pair,只有Pair。当然,其原因就在于类型擦除。擦除之后,Pair类含有Object类型的字段。而Object不能存储double值。
运行时类型查询只使用于原始类型
虚拟机中的对象总有一个特定的非泛型类型。因此,所有的类型查询只产生原始类型。如
if(a instanceof Pair<String>) // ERROR
// 实际上仅仅测试a是否是任意类型的一个Pair.
if(a instanceof Pair<T>) // ERROR
// 同样的道理,getClass方法总是返回原始类型。
Pair<String> stringPair = ...;
Pair<Employee> employeePair = ...;
if(stringPair.getClass() == employeePair.getClass()); // they are equal
// 其比较结果是true,这是因为两次getClass调用都返回Pair.class
不能创建参数化类型的数组
不能实例化参数化类型的数组,如
var table = new Pair<String>[10];// ERROR
这有什么问题呢?擦除之后,table的类型是Pair[]。可以把它转换为Object[]:
Object[] objarray = table;
数组会记住它的元素类型,如果试图存储其他类型的元素,就会抛出一个ArrayStoreException异常:
objarray[0] = "Hello";
尽管能够通过数组存储的检查,但仍会导致一个类型错误。出于这个原因,不允许创建参数化类型的数组。
需要说明的是,只是不允许创建这些数组,而声明类型为Pair[]的变量仍然是合法的。
不能实例化类型变量
不能在类型new T(…)的表达式中使用类型变量。如,下面的Pair构造器就是非法的:
public Pair(){
first = new T();
second = new T();
} // ERROR
// 类型擦除将T变成Object,而你肯定不希望调用new Object();
在Java8之后,最好的解决办法是让调用者提供一个构造器表达式。如
Pair<String> p = Pair.makePair(String::new);
makePair方法接收一个Supplier,这是一个函数式接口,表示一个无参数而且返回类型为T的函数。
public static <T> Pair<T> makePair(Supplier<T> constr){
return new Pair<>(constr.get(),constr.get());
}
传统的解决方法是通过反射调用Constructor.newInstance方法来构造泛型对象。
此时必须适当地设计API以便得到一个Class对象,如下:
public static <T> Pair<T> makerPair(Class<T> cl){
try{
return new Pair<>(
cl.getConstructor().newInstacnce(),
cl.getConstructor().newInstance()
);
}
catch(Exception e){
return null;
}
}
这个方法可以如下调用:
Pair<String> p = Pair.makePair(String.class);
不能构造泛型数组
就像不能实例化泛型实例一样,也不能实例化数组。不过原因有所不同,毕竟数组可以填充null值,看上去好像可以安全地构造,不过,数组本身也带有类型,用来监控虚拟机中的数组存储。这个类型会被擦除:
public static<T extends Comparable> T[] minmax(T... a){
T[] mm = new T[2]; // ERROR
...
}
类型擦除会让这个方法总是构造Comparable[2]数组。
如果数组仅仅作为一个类的私有的实例字段,那么可以将这个数组的元素类型声明为擦除的类型并使用强制类型转换。
public class ArrayList<E>{
private Object[] elements;
...
@SupperWarning("unchecked")
public E get(int n){
return (E)elements[n];
}
public void set(int n,E e){
elements[n] = e;+
}
}
泛型类的静态上下文中类型变量无效
不能在静态字段或方法中引用类型变量。如,下面的做法是行不同的
public class Singleton<T>{
private static T singleInstance; //ERROR
public static T getSingleInstance(){
if(singleInstance == null)
construct new instance of T
return singleInstance;
}
}
如果这样可行,程序就可以声明一个Singleton共享一个随机数生成器,另外声明一个Singleton共享一个文件选择器对话框。但是,这样是行不通的。类型擦除之后,只剩下Singleton类,它只包含一个singleInstance字段。因此,禁止使用带有类型变量的静态字段和方法。
不能抛出或捕获泛型类的实例
既不能抛出也不能捕获泛型类的对象。
catch子句中不能使用类型变量。如,以下方法将不能编译:
public static <T extends Throwable> void doWork(Class<T> t){
try{
do work
}
catch(T e){ // ERROR
Logger.global.info(...);
}
}
不过,在异常规范中使用类型变量时允许的。以下方法是合法的:
public static<T extends Throwable> void d{
{
do work
}
catch(Throwable realCause){
t.initCause(realCause);
throw t;
}
}
可以取消对检查型异常的检查
Java异常出处理的一个基本原则是,必须为所有检查型异常提供一个处理器。不过可以利用泛型取消这个机制。关键在于以下方法:
@SupperessWarnings("unchecked")
static <T extends Throwable> void throwAs(Throwable t)throws T{
throw(T) t;
}
要在一个线程中运行代码,需要把代码放在一个实现了Runnable接口的类的run方法中。不过这个方法不允许抛出检查型异常。我们将提供一个Task到Runnable的适配器。它的run方法可以抛出任意异常。
interface Task{
void run() throws Exception;
static <T extends Throwable> void throwAs(Throwable t) throws T{
throw(T) t;
}
static Runnable asRunnable(Task task){
return() ->{
try{
task.run();
}
catch(Exception e){
Task.<RuntimeException>throwAs(e);
}
};
}
}
例如,以下运行了一个线程,它会抛出一个检查型异常。
public class Test{
public static void main(String[] args){
var thread = new Thread(Task.asRunnable(() ->{
Thread.sleep(1000);
System.out.println("Hello,World");
throw new Exception("Check this out!");
}));
thread.start();
}
}
注意擦除后的冲突
当泛型类型被擦除后,不允许创建引发冲突的条件。
假定为Pair类增加一个equals方法。如下所示:
public class Pair<T>{
public boolean equals(T value){
return first.equals(value) && second.equals(value);
}
}
考虑一个Pair 。从概念上讲,它有两个equals方法
boolean equals(String) // defined in Pair<T>
boolean equals(Object) // inherited from Object
但是直觉会将我们引入歧途。方法
booelan equals(T)
擦除之后就是
boolean equals(Object)
补救的方法就是重写命名引发冲突的方法。
泛型规范说明还引用了另外一个原则:“为了支持擦除转换,我们要施加一个限制:倘若两个接口类型是同一接口的不同参数化,一个类或类型变量就不能同时作为这两个接口类型的子类”,例如以下代码时不合法的:
class Employee implements Comparable<Employee>{
...
}
class Manager extends Employee implements Comparable<Manager>{
...
} //ERROR
泛型类型的继承规则
我们考虑一个类和一个子类,如Employee和Manager。Pair是Pair的一个子类吗。答案是不是。下面的代码将不能成功编译:
Manager[] topHonchos = ...;
Pair<Employee> result = ArrayAlg.minmax(topHonchos); // ERROR
minmax方法返回Pair,而不是Pair,并且这样的赋值是不合法的。
无论S和T有什么关系。通常,Pair<S>和Pair<T>都没有任何关系。
泛型类可以扩展或实现其他的泛型类。就这一点而言,它们与普通的类没有什么区别。如,ArrayList类实现了List接口。这意味着,一个ArrayList可以转换为List.
通配符类型
通配符概念
在通配符类型中,允许类型参数发生变化。如,通配符类型
Pair< ? extends Employee>
// 表示任何泛型Pair类型,它的类型参数是Employee的子类。如Pair<Manager>,但不是Pair<String>.
假设要编写一个打印员工对的方法,如下所示:
public static void printBuddies(Pair<Employee> p){
Employee first = p.getFirst();
Employee second = p.getSecond();
System.out.println(first.getName() + "and" +second.getName()+ "are buddies.");
}
不能将Pair传递给这个方法。不过解决方法很简单,可以使用一个通配符类型:
public static void printBuddies(Pair<? extends Employee> p)
// 类型Pair<Manager>是Pair<? extends Employee>的子类型。
使用通配符会通过Pair<? extends Employee>的引用破坏Pair吗?
var managerBuddies = new Pair<Manager>(ceo,cfo);
Pair<? extends Employee> wildcardBuddies = managerBuddies; //OK
wildcardBuddies.setFirst(lowlyEmployee); // compile-time error
对setFirst的调用有一个类型错误。请仔细看一看类型Pair<? extends Employee>
? extends Employee getFirst()
void setFirst(? extends Employee)
这样将不可能调用setFirst方法。编译器只知道需要Employee的某个子类型,但不知道具体是是什么类型。它拒绝传递任何特定的类型。毕竟?不能匹配。
使用getFirst就不存在这个问题:将getFirst的返回值赋给一个Employee引用是完全合法的。
通配符的超类型限定
通配符限定与类型变量限定十分相似,但是有一个附加的能力,可以指定一个超类型限定,如下所示:
? super Manager
这个通配符限制为Manager的所有超类型。
可以为方法提供参数,但不能使用返回值。如
void setFirst(? super Manager)
? super Manager getFirst()
编译器无法知道setFirst方法的具体类型,因此不能接受参数类型为Employee或Object的方法调用。只能传递Manger类型的对象,或者某个子类型的对象。如果调用getFirst,不能保证返回对象的类型。只能把它赋给一个Object。
public static void minmaxBonus(Manager[] a,Pair<? super Manager> result){
if (a.length == 0) return;
Manager min = a[0];
Manager max = a[0];
for(int i = 0; i < a.length; i++){
if(min.getBonus() > a[i].getBonus()) min = a[i];
if(max.getBonus() < a[i].getBonus()) max =a[i];
}
result.setFirst(min);
result.setSecond(max);
}
直观地讲,带有超类型限定的通配符允许你写入一个泛型对象,而带有子类型限定的通配符允许你读取一个泛型对象。
无限定通配符
还可以使用根本无限定的通配符。如Pair<?>.这好像与原始的Pair类型一样。实际上,这两种类型有很大的不同。类型Pair<?>有以下方法:
? getFirst()
void setFirst(?)
// getFirst的返回值只能赋给一个Object。SetFirst方法不能被调用,甚至不能用Object调用。Pari<?>和Pair的本质的不同在于:可以用任意Object对象调用原始Pair类的setFirst方法。
t){
if (a.length == 0) return;
Manager min = a[0];
Manager max = a[0];
for(int i = 0; i < a.length; i++){
if(min.getBonus() > a[i].getBonus()) min = a[i];
if(max.getBonus() < a[i].getBonus()) max =a[i];
}
result.setFirst(min);
result.setSecond(max);
}
[[外链图片转存中...(img-icpeGZMy-1644393623461)]](https://imgtu.com/i/TKI7lj)
直观地讲,带有超类型限定的通配符允许你写入一个泛型对象,而带有子类型限定的通配符允许你读取一个泛型对象。
### 无限定通配符
还可以使用根本无限定的通配符。如Pair<?>.这好像与原始的Pair类型一样。实际上,这两种类型有很大的不同。类型Pair<?>有以下方法:
```Java
? getFirst()
void setFirst(?)
// getFirst的返回值只能赋给一个Object。SetFirst方法不能被调用,甚至不能用Object调用。Pari<?>和Pair的本质的不同在于:可以用任意Object对象调用原始Pair类的setFirst方法。