泛型
看了《Java 核心技术 卷1》泛型以及多篇博客之后,感受颇深,所以写下了这篇心得体会。
1 泛型程序说明
泛型程序意味着编写的代码可以对多种不同类型的对象重用
ArrayList类可以收集任何类的对象,里面的方法可以对String对象生效也可以对File对象生效,这就是泛型程序的强大之处。
2 泛型类
泛型类:就是有一个或多个类型变量的类。
定义一个简单的泛型类:
public class Pair<T> {
private T first;
private T second;
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,用<>括起来,放在类名的后面。
一个泛型类可以拥有多个类型变量,例如:
public class Pair<T,U>{}
pulbic class Pair<T,U,I>{}
类型变量在整个类定义中用于指定方法的 返回类型 以及 字段 和 局部变量的类型
2.1 实例化一个泛型类
用具体的类型替换掉类型变量来实例化泛型类型,例如
Pair<String> pair = new Pair<String>("firstStr","secondStr")
3 泛型方法
类型变量T用<>括起来并放在方法修饰符之后(这里的修饰符是public static),方法返回类型之前。例如:
public class ArrayAlg{
public static <T> T getMiddle(T[] a){
return a[a.length / 2]
}
}
泛型方法既可以定义在普通类中也可以定义在泛型类中
4 类型变量的限定
看下面的这个方法
public class ArrayAlg {
public static <T> T getMin(T[] a){
if (a == null || a.length == 0){
return null;
}
T min = a[0];
for (int i = 0; i < a.length; i++) {
if (min.compareTo(a[i]) > 0){ //这行代码会报错
min = a[i];
}
}
return min;
}
}
这里有一个问题,如何能保证T这个类型拥有compareTo方法?
可以给T增加一个限定,即:
public static <T extends Comparable> T getMin(T[] a){
if (a == null || a.length == 0){
return null;
}
T min = a[0];
for (int i = 0; i < a.length; i++) {
if (min.compareTo(a[i]) > 0){ //这行代码不再报错
min = a[i];
}
}
return min;
}
区别在于换成了这就意味着类型变量T不能再表示任意类型了,而是必须实现了Comparable接口的任意类型。
类型变量T可以有多个限定类型,例如:
<T extends Comparable & Serializable>
多个限定类型之间用“&”来分隔,但是只能拥有一个类类型限定且类必须在第一个,可以拥有多个接口类型限定。
5 类型擦除
虚拟机没有泛型类型对象—所有对象属于普通类。泛型程序的类型变量会被编译器“擦除”。
无论何时定义个泛型类型,都会自动提供一个相应的原始类型。这个原始类型的名字就是去掉类型参数后的泛型类型名。
类型变量会被擦除,并替换为其限定类型,如果没有限定类型用Object替代
例如上面所定义的泛型类Pair,类型擦除之后的原始类型为:
public class Pair {
private Object first;
private Object second;
public Object getFirst(){return first};
public void setFirst(Object first){this.first = first}
}
上面定义的泛型方法,类型擦除之后为:
public static Comparable getMin(Comparable[] a)
来看一下这个程序
Pair<String> pair = new Pair<String>("fir","sec");
String fir = pair.getFirst();
类型擦除之后调用Pair.getFirst()方法实际上分为两步:
- 调用getFirst()方法
- 将返回的Object强制替换为String类型
5.1 桥方法
假设上面的泛型类Pair有一个子类:
public class DateInterval extends Pair<LocalDate> {
public void setSecond(LocalDate second){// 子类重写了setSecond()
...
}
}
Pair类代码:
public class Pair<T> {
private T first;
private T second;
...
public void setSecond(T second) {
...
}
...
}
根据刚才所说的类型擦除,那么Pair类中的setSecond()会变成:
public void setSecond(Object second){...}
那么在DateInterval类中就会有两个方法:
public void setSecond(Object second){...}; //从父类继承的
public void setSecond(LocalDate second){...};// 重写的
首先,我们在一个类中编写这样的代码是不合法的!但是在虚拟机中,确认一个方法是由参数类型和返回类型来指定的。因此在虚拟机中这是两个方法。
当我们书写下面的代码:
Pair<LocalDate> pair = new DateInterval();// 这是多态,父类引用指向子类实例
Pair中只有一个setSecond(Object second)的方法,虚拟机在pair引用上调用setSecond()方法时,由于这个对象是DateInterval类型,因此将会调用DateInterval.setSecond(Object second)方法,但是DateInterval的setSecond(Object second)方法是一个桥方法,它的方法体是:
public void setSecond(Object second){
this.setSecond((LocalDate)second)// 会把Object类型的second转为LocalDate类型并调用另一个setSecond(LocalDate second)方法
}
总结
- 虚拟机中没有泛型,只有普通的类和方法
- 所有的类型参数都会替换为他们的限定类型
- 会合成桥方法来保持多态
- 为保持类型安全性,必要时会插入强制类型转换
6 限制与局限性
6.1 不能用基本类型实例化类型参数
即不能这样做:
Pair<double>,Pair<int>...
可以使用他们的包装类
Pair<Double>,Pair<Integer>...
6.2 运行时类型查询只适用于原始类型
虚拟机中的对象总有一个特定的非泛型类型。因此所有的类型查询只产生原始类型,例如:
if(a instanceof Pair<String>) //error
if(a instanceof pair<T>)// error
Pair<String> strPair = ...;
Pair<Double> douPair = ...;
if(strPair.getClass() == douPair.getClass())// true
调用getClass()都是返回Pair.class
6.3 不能创建参数化类型的数组
即不能new Pair[10]…
6.4 不能实例化类型变量
即不能T t = new T();
6.5 不能构造泛型数组
即不能T[] t = new T[2]
6.6 泛型类的静态上下文中类型变量无效
下面的做法是错误的
public class Singleton<T> {
public static T singleInstance;// error
public static T getSingleInstance(){return singleInstance;};//error
}
6.7 不能抛出或捕获泛型类的实例
try{...}
catch(T t) {...} // error
throw t //error
6.8 注意擦除后的冲突
若两个接口类型是同一接口的不同参数化,一个类或类型变量就不能同时作为这两个接口类型的子类
下面的代码是非法的
public class Person implements Comparable<Person>{...}
public class Student extends Person implements Comparable<Student>{...}//error
Student会实现Comparable和Comparable,这是同一接口Comparable的不同参数化。
7 泛型类型的继承规则
Student和Person是继承关系,但是Pair和Pair没有任何关系!
泛型类可以继承其他泛型类或者实现其它泛型接口
ArrayList和List是实现关系,ArrayList和List是实现关系;因为ArrayList类 实现了泛型接口 List。ArrayList和List没有关系。
8 通配符概念
通配符"?"允许类型参数发生变化。例如,通配符类型
Pair<? extends Person> 表示类型参数只能是Person类或它的子类。
并且 Pair,Pair都是Pair<? extends Person>的子类。
8.1 通配符的子类型限定和父类型限定
子类型限定:Pair<? extends Person> 表示:类型参数只能是Person及其子类;子类型限定又称为 上界限定
父类型限定:Pair<? super Student> 表示:类型参数只能是Student和其父类;父类型限定又称为超类型限定或下界限定。
8.2 父类型限定的使用
上面类型变量的限定中有一个例子:
public static <T extends Comparable> T getMin(T[] a){
if (a == null || a.length == 0){
return null;
}
T min = a[0];
for (int i = 0; i < a.length; i++) {
if (min.compareTo(a[i]) > 0){
min = a[i];
}
}
return min;
}
为了保证min变量有compareTo方法,因此个类型变量T进行了限制,限制它为Comparable类型。
Comparable本身也是一个泛型:Comparable。
上面的程序在类型变量T为String时是没有任何问题的,因为String类实现了Comparable。
但是如果上面的getMin()方法的入参是一个LocalDate数组时,存在这样的问题:LocalDate实现了ChronoLocalDate接口,而ChronoLocalDate继承了Comparable,因此LocalDate实际上实现了Comparable而不是Comparable,此时就可以使用父类型限定来解决:
public static <T extends Comparable<? super T>> T getMin(T[] a){
if (a == null || a.length == 0){
return null;
}
T min = a[0];
for (int i = 0; i < a.length; i++) {
if (min.compareTo(a[i]) > 0){
min = a[i];
}
}
return min;
}
这样写的话CompareTo方法的入参就可以传入一个T类型或者T类型的超(父)类型。
另一个常见的用法是作为一个函数式接口的参数类型。例如Collection接口有一个方法:
defatlt boolean removeIf(Predicate<? super E> filter);
这个方法会删除满足过滤条件的元素。
ArrayList<Person> list = ...;
Predicate<Object> filter = obj -> obj.hasCode() % 2 != 0;
list.removeIf(filter);// 使用?super E就可以让这里不止可以传入Predicate<Person>,还可以传入Predicate<Object>
在生活中,生产力有一个上限,消费也有一个消费下限。
为了获得最大限度的灵活性,要在表示生产者或者消费者的输入参数上使用通配符,那么规则就是:生产者有上界,消费者有下界,即:
- 如果参数化类型表示一个T的生产者,使用<?extends T>;
- 如果它表示一个T的消费者,使用 <? super T>;
- 如果既是消费者又是生产者,那么通配符就失去了意义,应该用精确地参数类型。
8.3 生产有上限,消费有下限的理解
看一下这些常见的泛型接口:Comparable,Predicate,Function<T,U>,Consumer;
Comparable:这个接口通常是把T用来进行比较,可以理解为消费,消费有下限,所以用下界限定,所以经常在程序中看到Comparable<? super T>;
Predicate:四大函数式接口之一,断言接口,主要是对T进行判断,同样可以理解为消费,消费有下限,所以用下界限定,所以经常在程序中看到某些方法的入参是:Predicate<? super T>;
Function<T,U>:四大函数式接口之一,通常传入T,返回U,那么可以理解为T是用来消费,U是生产而来的,所以经常在程序中看到某些方法的入参是这样的:Function<? super T,? extends U>;
Consumer:四大函数式接口之一,消费者接口,消费T,所以经常看到:Consumer<? super T>;
Supplier:四大函数式接口之一,生产者接口,生产T,所以经常看到:Supplier<? extends T>