之前学Java时对泛型没有好好学习和理解,但是很多源码都涉及到泛型程序设计,所以借着书在整理记录下。
以下内容借鉴于 《Java核心技术 卷Ⅰ》
目录
1.为什么使用泛型程序设计
泛型程序设计(Gernric Programing)意味着编写的代码可以被很多不同类型的对象所重用。
例如,我们并不希望为聚集String和File对象分别设计不同的类。 实际上,用ArrayList就可以聚集任何类型的对象,因为ArrayList就是一个泛型程序设计的实例。
1.1 类型参数的好处
public class ArryList{
private Object[] elementData;
...
public Object get(int i){...}
public void add(Object o){...}
}
上面 是Java还没增加泛型前的ArrayList实现,通过维护一个Object数组来实现功能。
如果要储存一串String时,每次都要通过强制类型转换然后进行赋值,这样很不方便而且也不安全,经常没办法转换成功而报错。
泛型提供了一个更好的解决方案:类型参数(type parameter)。例如:
ArrayList<String> files = new ArrayList<>();
上面的代码具有更好的可读性,一看就知道是String数组。
而且使用get方法时不需要进行类型转换,编译器知道返回String。使用add方法时,同样如此,而且编译器会检查是否插入了错误的类型对象,会无法通过编译的。相比于之前的方法,编译错误比运行时出现转换异常要安全得多。
类型参数的魅力在于:使得程序具有更好的可读性和安全性。
1.2 谁想成为泛型程序员
实现一个泛型类并没有那么容易,对于类型参数,使用这段代码的程序员可能想内置(plug in)所有的类。他们希望在没有过多的限制以及混乱的错误消息的状态下,做所有的事情。
因此,一个泛型程序员的任务就是预测出所用类的未来可能有的所有用途。
这个任务是相当困难的,为此Java语言设计者发明了一个具有独创性的新概念,通配符类型(wildcard type),它们能让库的构建者编写出尽可能灵活的方法。
所以,泛型能够带给我们的:重用性、更方便、更安全。
后面会介绍更多的书中知识。
2. 定义简单泛型类
一个泛型类(generic class)就是具有一个或多个类型变量的类。使用一个简单的Pair类作为例子。(只关注泛型)
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 T getSecond() { return second; }
public void setFirst(T newValue) { first = newValue; }
public void setSecond(T newValue) { second = newValue; }
}
Pair类引入了一个类型变量T,用尖括号(<>)扩起来,并放在类名的后面。泛型类可以有多个类型变量。
例如可以定义Pair类,第一个域与第二个域不同:
public class Pair<T,U> {...}
在Java库中,使用变量E表示集合的元素类型,K和V分别表示表的关键字与值的类型,T表示“任意类型”。
用具体的类型替换类型变量就可以实例化泛型类型。例如:
Pair<String>()
Pair<String>(String,String)
String getFirst()
void setFirst(String)
...
换句话说,泛型类可看作普通类的工厂。
3.泛型方法
除了上述定义的泛型类方法,实际上还可以定义一个带有类型参数的简单方法。
public class ArrayAlg {
public static <T> T getMiddle(T... a){
return a[a.length/2];
}
}
这个方法是在普通类中定义的,而不是在泛型类中定义的。然而,这是一个泛型方法,可以从尖括号和类型变量看出一点。
注意,类型变量放在修饰符(这里是public static)的后面,返回类型的前面。
泛型方法可以定义在普通类中,也可以定义在泛型类中。
当调用一个泛型方法是,在方法名前的尖括号中放入具体的类型:
String middle = ArrayAlg.<String>getMiddle("John","Q.","Public");
上述情况下,方法调用中可以省略<String>类型参数,编译器通过后面的String[]可以推测T一定为String。
String middle = ArrayAlg.getMiddle("John","Q.","Public");
当然也有一些情况,编译器也会报错,比如下面这种:
double middle = ArrayAlg.getMiddle(3.14,110,0);
上面这种就没办法确定是什么具体类型参数,因为后两个参数是Integer类型,除了写上<Double>外,还可以将后面两个参数改成doble值。
4.类型变量的限定
有时,类或方法需要对类型变量加以约束。比如对泛型数组进行最小比较时,需要确定数组中的类型都实现了Comparable接口。
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;
}
上面的代码就需要将T限制为实现了Comparable接口(其实Comparable本身也是一个泛型类型),可以对T设置限定(bound)实现这一点:
public static <T extends Comparable> T min(T[] a){...}
这里可能会有困惑的是为什么写extends,不写成implements,毕竟Comparable是个接口。<T extends BoundingType>表示T应该是绑定类型的子类型(subtype),选择extends是因为更接近子类的概念,与其他无关,而且Java设计者们也不打算在语言中再添加新的关键字。比较重要的一点,一个类型变量或通配符可以有多个限定,例如:
T extends Comparable & Serializable
5. 泛型代码和虚拟机
虚拟机没有泛型类型对象——所有对象都属于普通类。
5.1 类型擦除
无论何时定义一个泛型类型,都自动提供了一个相应的原始类型(raw type)。原始类型的名字就是删去类型参数后的泛型泪姓名。擦除(erased)类型变量,并替换为限定类型(无限定的变量用Object)。比如说之前写了的Pair对象:
public class Pair{
private Object first;
private Object second;
public Pair() { first = null;second = null; }
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 newValue) { first = newValue; }
public void setSecond(Object newValue) { second = newValue; }
}
因为T是一个无限定的变量,所以直接用Object替换。
如果有限定类型变量,则选择第一个限定的类型变量来替换,例如:
public class Interval<T extends Comparable & Serializable> implements Serializable{
private T lower;
private T upper;
//...
public Interval(T first,T upper){
//...
}
}
则原始类型Interval如下:
public class Interval implements Serializable{
private Comparable lower;
private Comparable upper;
//...
public Interval(Comparable first,Comparable upper){
//...
}
}
5.2 翻译泛型表达式
当程序调用泛型方法时,如果擦除返回类型,编译器插入强制类型转换。例如:
Pair<Employee> buddies = ...
Employee buddy = buddies.getFirst();
编译器会把这个方法翻译成两条虚拟机指令
- 对原始方法Pair.getFirst调用
- 将返回的Object类型强制转换为Employee
当存取泛型域时也要插入强制类型转换,例如:
Employee buddy = biddies.first;
5.3 翻译类型方法
这部分会有点绕。
方法的擦除带来了两个复杂问题。看下面的示例:
class DateInterval extends Pair<LocalDate>{
public void setSecond(LocalDate second){
if(second.compareTo(getFist())>=0)
super.setSecond(second);
}
//...
}
一个日期区间是一对LocalDate对象,并且需要覆盖这个方法来确保第二个值永远不小于第一个值。这个类擦除后:
class DateInterval extends Pair{
public void setSecond(LocalDate second){
//.....
}
//...
}
这时候还存在另一个从Pair继承的setSecond(Object second)方法。
这时候希望调用最合适的方法,编译器就需要在DateInterval中生成一个桥方法(bridge method):
public void setSecond(Object second){
setSecond((Date) second);
}
第二个问题是桥方法有时候会变得很奇怪。例如DateInterval中有个函数是getSecond():
class DateInterval extends Pair<LocalDate>{
public localDate getSecond(){
return (Date) super.getSecond().clone();
}
}
这时候也会产生另一个函数 Object getSecond(),这时候具有相同参数的两个方法,这样编写java代码是不合法的(虽然他们的返回类型不一样)。但是,在虚拟机中,用参数类型和返回类型确定一个方法,因此,虚拟机可能产生仅两个返回类型不同的字节码。
总之,需要记住有关Java泛型转换的事实:
- 虚拟机中没有泛型,只有普通的类和方法
- 所有的类型参数都用他们的限定类型替换
- 桥方法被合成来保持多态
- 为保持类型安全性,必要时插入强制类型转换
5.4 调用遗留代码
设计Java泛型类型时,主要目标是允许泛型代码和遗留代码之间能够互操作。
6. 约束与局限性
下面将会阐述使用Java泛型时需要考虑的一些限制。大多数限制都是由类型擦除引起的。
6.1 不能用基本类型实例化类型参数
如小标题所说,所以没有Pair<double>,只有Pair<Double>。原因是类型擦除,擦出之后,Pair类含有Object类型的域,而Object不能存储double值。
6.2 运行时类型查询只适用于原始查询
虚拟机中的对象总有一个特定的非泛型类型。因此所有的类型查询只产生原始类型。例如:
if(a instanceof Pair<String>) //Error
if(a instanceof Pair<T>) //Error
同样的道理,getClass方法总是返回原始类型。例如:
Pair<String> stringPair = ... ;
Pair<Employee> employeePair = ... ;
if(stringPair.getClass()==employee.getClass()) //they are eqaul
上面的两次调用getClass都将返回Pair.class。
6.3 不能创建参数化类型的数组
不能实例化参数化类型的数组,例如:
Pair<String>[] table = new Pair<String>[10];//ERROR
擦除后table的类型是Pair[],可以把它转换为Object[]。数组会记住它的元素类型,如果试图存储其他类型的元素,就会抛出一个ArrayStoreException异常。
需要说明的是,只是不允许创建这些数组,而生命类型为Pair<String>[]的变量鞥是合法的。不过不能用new Pair<String>[10]初始化这个变量。
可以声明统配类型的数组,然后进行类型转换:
Pair<String>[] table = (Pair<String>[]) new Pair<?>[10];
但是上面的方法不安全,如果table[0]存储一个Pair<Employee>,然后对table[0].getFirst() 调用一个String方法,会得到一个ClassCastException异常。
如果需要收集参数化类型对象,只有一种安全而有效的方法:使用ArrayList:ArrayList<Pair<String>>
6.4 Varags 警告
上一节我们已经了解到,Java不支持泛型类型的数组
这节讨论一个问题:像参数个数可变的方法传递一个泛型类型的实例。
public static <T> void addAll(Collection<T> coll,T... ts){
for(t : ts) coll.add(t);
}
Collection<Pair<String>> table=...;
Pair<String> pair1=...;
Pair<String> pair2=...;
addAll(table,pair1,pair2);
为了调用这个方法,Java虚拟机必须建立一个Pair<String>数组,这样违反了之前的规则,不过这种情况,规则有所放松,你会得到一个警告而不是错误。可以对调用了addAll的方法加注解@SuppressWarnings("unchecked")或者对addAll加上注解@SafeVarargs。
6.5 不能实例化类型变量
不能使用像new T(...),new T[...]或T.class这样的表达式中的类型变量。
Java SE 8之后,最好的解决办法是让调用者提供一个构造器表达式。例如:
Pair<String> p=Pair.makePair(String::new);
上面涉及到1.8新特性lambda表达式,makePair方法接收一个Supplier<T>,这是个函数式接口,表示一个无参数而且返回类型为T的函数:
public static <T> Pair<T> makePair(Supplier<T> constr)
{
return new Pair<>(constr.get(),constr.get());
}
也可以像下面这样:
public static <T> Pair<T> makePair(Class<T> c1){
try{
return new Pair<>(c1.newInstance(),c1.newInstance());
}catch (Exception e){
return null;
}
}
调用的话:Pair<Stirng> p = Pair.makePair(String.class);
注意,Class本身是泛型。例如String.class是一个Class<String>的实例,因此makePair能判断出pair的类型。
6.6 不能构造泛型数组
//持续更新中........