泛型的概念
泛型的魅力在于:程序具有更好的可读性(避免出现过多的强制转化)和安全性(可以很清晰的看出类型,避免存取不一致导致的错误)
同时泛型又是一个编译器的概念,虚拟机没有泛型类型的对象,只有普通类和方法
泛型的使用
泛型不仅可以定义在泛型类上,也可以在普通类里面定义泛型方法,类型变量\<T>要放到修饰符后面,返回参数前面
对于泛型方法,调用的时候可以根据参数省略掉类型变量,如:
ArrayAlg.getMiddle("a","b"); //全等于
ArrayAlg.<String>getMiddle("a","b");
编译器可以根据参数确定类型变量为string,如果传参的比较模糊,如:
ArrayAlg.getMiddle(1.27,5200);
当编译器收到一个Double和一个Integer时候,就会寻找这些类的共同超类型,也就是Number和Comparable,所以无论用哪个形参接受都行,但最好统一一下
类型变量的限定
通过extends关键字,可以限定泛型属于哪一个类的子类
而super关键字可以限定泛型属于哪一个类的超类
<T extends Employee> or <T extends Comparable & Serializable>
<T extends Manager>
注:类型变量可以拥有多个接口的限定,但只能拥有一个类的限定,并且这个类的限定必须写在第一个
类型擦除
无论何时定义一个泛型类型,都自动提供一个原始类型(raw type),原始类型的名字就是删去类型参数后的泛型类型名,擦除(erased)类型变量,并替换为限定类型(无限定类型则为Object,如果有多个限定类型则用第一个替换)
注:如果存在多个限定类型,尽量将标签(tagging)接口(即没有方法的接口)放在后面,因为如果方法中调用限定中的接口的方法,而第一个限定类型又不是该接口,那么会多一步强行转化。
然而类型擦除带来一个棘手的问题:
如果一个非泛型类继承自一个泛型类,并且重写了其中一个带有泛型的参数的方法,如下:
public class DateInterval extends Pair<LocalDate>{
...
@Override
public void setSecond(LocalDate second) {
if (second.compareTo(getFirst()) > 0){
super.setSecond(second);
}
}
}
public class Pair<T>{
...
public void setSecond(T second){
this.second = second;
}
}
在类型消除后类Pair的setSecond方法的参数变为Object second,如此一来类DateInterval变有了两个setSecond方法,一个类型为Object,另一个类型为LocalDate,这是不合理的。对于以下代码执行:
DateInterval interval = new Datelnterval(. . .);
Pair<Loca1Date> pair = interval; // OK assignment to superclass
pair.setSecond(aDate);
这里的pair调用setSecond调用具有多态性,由于是Pair引用的Datelnterval实例,Pair的引用方法在擦除泛型后为setSecond(Object second),那么应该调用所以应该调用Datelnterval.setSecond(Object second)方法,但是Datelnterval中只有setSecond(T second)方法,所有编译器替Datelnterval类生成如下一个桥方法(bridge method)
public class DateInterval{
public void setSecond(Object second) { setSecond((Date) second);
}
另一个问题:
如果DateInterval也实现了 getSecond()方法,那么桥方法会和原始方法拥有同一个方法签名
public LocalDate getSecond(){...}
public Object getSecond(){return (LocalDate)getSecond()}
虽然以上方法在java编译器中不合法,但是在虚拟机中可以存在返回值不同但方法签名相同的方法
约束与局限性
局限性:
- 不能用于基本类型,如:Pair<int>
- 不能用于类型检测,如:if (a instanceof Pair<String>) //error
- 不能创建数组实例,如:Pair<String>[] table = new Pair<String>[10]; // Error
- 不能实例化类型变量,如:new T();但可以通过函数式接口实现
- 泛型类的静态上下文中类型变量无效,原因也很简单,静态方法和域是跟着类走的,只有一份,类型擦除后,类只能携带一份静态资源。
- 泛型消除的一个原则:要想支持擦除的转换,就要强行限制一个类或者类型变量不能同时成为两个接口类型的子类,并且这两个接口类型是同一接口的不同参数化。如以下泛型就是非法的:因为消除变量后,compareTo方法不知该强转成哪个类型
class Employee implements Comparable<Emp1oyee> { . . . }
class Manager extends Employee implements Comparable<Manager>
{ . . . } // Error
</br>
通配符类型
泛型有时候并不能确定某一个类型,但是可以确定他们都有某些特性,或者继承自某些接口,又或者是某些类的超类
通配符子类限定
Pair<? extends Employee>
通配符超类限定(supertype bound)
Pair<? super Manager>
总结:
带有超类限定的通配符可以向泛型对象写入,带有子类型限定的通配符可以从泛型对象读取
解释一下:
首先明确一点,java的多态,是指父类引用可以引用子类实现
当使用子类通配符时, setFirst(? extends Employee)时,编译器只知道传递Employee子类,但是不知道具体类型,但是如果使用getFirst()的时候,无论什么类型的实例都可以用Employee类型作为引用参数。
而当使用超类通配符时,setFirst(? super Manager)时,Pair类的first参数的引用只可能是Manager及其超类,那么传递Manager或是其子类的实例都可以,但是getFirst()并不能知道得到哪个类型,只能用Object去接收
超类的另一种应用:
public static <T extends Comparable> T min(T[] a){
...
}
如上所示,该方法就是调用compareTo方法来确定最小值,由于Comparable也是一个泛型,我们可以通过如下方式提升效率,减少强转
public static <T extends Comparable<T>> T min(T[] a){
...
}
上面这个对于string是够用了,但是如果传入LocalDate数组,由于LocalDate实现了ChronoLocalDate接口,而ChronoLocalDate又继承自Comparable<ChronoLocalDate>,所以LocalDate实现的是Comparable<ChronoLocalDate>而不是Comparable<LocalDate>,所以返回值变成了ChronoLocalDate,还需要用户进行强行转化才行,如下代码可以进行正确的限制。
public static <T extends Comparable<? super T>> T min(T[] a){
...
}
由于ChronoLocalDate 是LocalDate 的超类Comparable<ChronoLocalDate>是Comparable<? super LocalDate>的子类,所以返回值可以为LocalDate
无限定通配符
Pair<?>
无限定通配符和原始类的区别在于,无限定通配符不可以set泛型域(除了null),主要用于一些简单方法的使用,如:
public static boolean hasNulls(Pair<?> p)
{
return p.getFirst() == null || p.getSecond() == null;
}
</br>