什么是泛型程序设计
泛型程序设计是指编写的代码可以被很多不同类型的对象重用。比如,不需要给存储String的集合和存储File的集合分别设计不同的类。
类型参数的好处
在没有泛型类型之前,Java泛型程序设计是利用继承实现的。比如一个ArrayList,它内部维护一个Object数组,get出来的也是Object类型的对象,add进去的也是Object类型的对象,这样会导致get值出来的时候要手动做下类型转换,而且add东西的时候没有类型检查,可能会导致add不同类型的东西进去而导致业务出错。
通过类型参数,可以指定ArrayList里的元素的类型,比如一个存储String的ArrayList,其创建就是这样:
ArrayList<String> stringArrayList = new ArrayList<>();
可读性更好了,而且编译器据此能知道,get出来的东西是一个String,add的时候是add的String而不是其他类型的对象,省去了类型转换,增加了类型校验。
因此类型参数带来的好处是:可读性和安全性。
泛型类
泛型类就是具有一个或多个类型变量的类。
例如:
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 this.first; }
public T getSecond() { return this.second; }
public void setFirst(T first) { this.first = first; };
public void setSecond(T second) { this.second = second; }
}
public class Pair<T, U>{ //多个类型变量
// …
}
泛型方法
泛型方法就是带有类型参数的方法。
例如:
class ArrayAlg{
public static <T> getMiddle(T… a){
return a[a.length / 2];
}
}
注意:类型变量放在修饰符(public和static)的后面,返回类型的前面。
调用泛型方法:
String middle = ArrayAlg.<String>getMiddle(“john”, “lucy”, “tom”);
根据类型推断,也可以简化为:
String middle = ArrayAlg.getMiddle(“john”, “lucy”, “tom”);
类型变量的限定
可以限定类型变量是指定类/接口的子类/子接口。具体的,格式为:
<T extends BoundingType>
可以有多个限定,如:
<T extends Comparable & Serializable>
注意,如果用类作为限定,那它必须是限定列表的第一个。
示例代码,计算泛型数组的最大值和最小值:
class ArrayAlg{
pubic static <T extends Comparable> Pair<T> minmax(T[] a){
if (a == null || a.length == 0) return null;
T min = a[0];
T max = a[0];
for(int i = 1; i <= a.length -1; i++){
if(min.compareTo(a[i]) > 0) min = a[i];
if (max.compareTo(a[i]) < 0) max = a[i];
}
return new Pair<>(min, max);
}
}
在这个例子中,通过对类型变量T设置限定,确保了T一定具有compareTo方法。如果传入的类型没有实现Comparable,那么在编译期就会报错。
类型擦除
泛型机制其实是一种语法糖。
语法糖(Syntactic sugar),也译为糖衣语法,是由英国计算机科学家彼得·约翰·兰达(Peter J. Landin)发明的一个术语,指计算机语言中添加的某种语法,这种语法对语言的功能并没有影响,但是更方便程序员使用。通常来说使用语法糖能够增加程序的可读性,从而减少程序代码出错的机会。
在字节码层面上,并没有泛型类型、泛型方法,所有的类和方法都还是原始类型和方法。Java编译器通过类型擦除,将源码里的泛型类型、泛型方法编译为原始类型和方法。
类型擦除,就是擦除类型变量,替换为限定类型。具体的,通过第一个限定的类型变量来替换,如果没有限定就用Object来替换。
例如,几个泛型类代码如下:
// 泛型类型变量无限定
public class Pair<T> {
private T first;
private T second;
public Pair() {
}
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;
}
}
// 泛型类型变量,一个限定
public class Pair2<T extends Comparable> {
private T first;
private T second;
public Pair2() {
}
public Pair2(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;
}
}
// 泛型类型变量,多个限定
import java.io.Serializable;
public class Pair3<T extends Serializable & Comparable> {
private T first;
private T second;
public Pair3() {
}
public Pair3(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;
}
}
编译生成class文件,然后反编译查看其内容:
public class Pair
{
private Object first;
private Object second;
public Pair()
{
}
public Pair(Object first, Object second)
{
this.first = first;
this.second = second;
}
public Object getFirst()
{
return first;
}
public void setFirst(Object first)
{
this.first = first;
}
public Object getSecond()
{
return second;
}
public void setSecond(Object second)
{
this.second = second;
}
}
public class Pair2
{
private Comparable first;
private Comparable second;
public Pair2()
{
}
public Pair2(Comparable first, Comparable second)
{
this.first = first;
this.second = second;
}
public Comparable getFirst()
{
return first;
}
public void setFirst(Comparable first)
{
this.first = first;
}
public Comparable getSecond()
{
return second;
}
public void setSecond(Comparable second)
{
this.second = second;
}
}
import java.io.Serializable;
public class Pair3
{
private Serializable first;
private Serializable second;
public Pair3()
{
}
public Pair3(Serializable first, Serializable second)
{
this.first = first;
this.second = second;
}
public Serializable getFirst()
{
return first;
}
public void setFirst(Serializable first)
{
this.first = first;
}
public Serializable getSecond()
{
return second;
}
public void setSecond(Serializable second)
{
this.second = second;
}
}
可以看到,类型变量不存在限定的时候,通过Object替换了类型变量;存在类型限定,通过第一个限定类型替换了类型变量。
使用泛型类的地方也是同样,经过类型擦除后,只有原始类型。
例如对上述三个泛型类型的调用如下:
Pair<String> pair = new Pair<>();
pair.setFirst("first value");
String tmp = pair.getFirst();
Pair2<Integer> pair2 = new Pair2<>();
pair2.setFirst(new Integer(20));
Integer tmp2 = pair2.getFirst();
Pair3<Long> pair3 = new Pair3<>();
pair3.setFirst(new Long(20));
Long tmp3 = pair3.getFirst();
反编译class,其内容如下:
Pair pair = new Pair();
pair.setFirst("first value");
String tmp = (String)pair.getFirst();
Pair2 pair2 = new Pair2();
pair2.setFirst(new Integer(20));
Integer tmp2 = (Integer)pair2.getFirst();
Pair3 pair3 = new Pair3();
pair3.setFirst(new Long(20L));
Long tmp3 = (Long)pair3.getFirst();
可以看到,class中只有原始类型。在返回类型变量型值的时候,编译器自动帮我们生成了强制类型转换代码。
桥方法
当我们覆盖具体的泛型类型的方法的时候,情况会变得有些特殊。
以下面这段代码为例:
public class PairSon extends Pair<String> {
@Override
public void setSecond(String second){
System.out.println("PairSon's setSecond(String)");
super.setSecond(second);
}
}
这里PairSon类覆盖了Pair<String>的setSecond(String)方法。由于类型擦除,它会继承到来自父类Pair的setSecond(Object)方法。那么,当我们将PairSon的引用赋给Pair<String>引用变量,再通过Pair<String>引用调用setSecond(String)方法时,由于类型擦除,Pair类只有setSecond(Object)方法,所以最终会调用PairSon的setSecond(Object)方法,与传统多态的预期调用子类PairSon的setSecond(String)不相符合,因此这里类型擦除和多态发生了冲突。
Java是如何解决这个问题的呢?
我们编译PairSon,反编译其class文件:
public class PairSon extends Pair
{
public PairSon()
{
}
public void setSecond(String second)
{
System.out.println("PairSon's setSecond(String)");
super.setSecond(second);
}
public volatile void setSecond(Object obj)
{
setSecond((String)obj);
}
}
我们看到PairSon的class文件里多了一个setSecond(Object)方法,覆盖了父类的setSecond(Object),并且该方法将输入强转为String,然后直接调用setSecond(String)。这样就使得虽然调用了PairSon的setSecond(Object),但是起到的效果和调用PairSon的setSecond(String)是一样的。
这里的PairSon里编译器自动合成的setSecond(Object)就是桥方法。
泛型类型的继承规则
以上面的Pair<T>类为例,即使S是F的子类,Pair<S>也不是Pair<F>的子类。
泛型类可以扩展或实现其他泛型类。例如,ArrayList<E>继承自AbstractList<E>,同时实现了List<E>。这一点和普通的类没有什么两样。
public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable {
// ....
}
通配符类型
通配符类型表示允许类型参数变化,具体的,又分为:上界通配符;下界通配符;无限定通配符。
上界通配符
上界通配符形如:
<? extends className>
例如:Pair<? extends Comparable>。任何其他的Pair<T>,T实现了Comparable,都是Pair<? extends Comparable>的子类,对应的实例引用都可以赋值给Pair<? extends Comparable>引用变量。
Pair<? extends Comparable> pair_upper_bound = new Pair<String>(); // OK
不能对上界通配符型参数传除了null以外的任何值,否则都将引起类型错误,但是上界通配符可以作为方法的返回类型。
pair_upper_bound.setFirst(""); // 编译期异常
Comparable tmp_upper_bound = pair_upper_bound.getFirst(); // OK,返回值可以赋给Comparable及其基类
上述代码的class文件反编译后结果如下:
Pair pair_upper_bound = new Pair();
Comparable tmp_upper_bound = (Comparable)pair_upper_bound.getFirst();
下界通配符
下界通配符形如:
<? super className>
例如:Pair<? super ArrayList>。任何其他的Pair<T>,T是ArrayList的超类或者是其实现的接口,都是Pair<? super ArrayList>的子类,对应的实例引用都可以赋值给Pair<? super ArrayList>引用变量。
Pair<? super ArrayList> pair_lowner_bound = new Pair<List>(); // OK
与上界通配符相反,下界通配符可以做为方法参数,但是若做为返回类型,则只能赋给Object类型引用。
pair_lower_bound.setFirst(new ArrayList()); // OK
Object tmp_lower_bound = pair_lower_bound.getFirst(); // OK
反编译上述代码的class文件,结果如下:
Pair pair_lower_bound = new Pair();
pair_lower_bound.setFirst(new ArrayList());
Object tmp_lower_bound = pair_lower_bound.getFirst();
PECS原则
PECS原则,意为Producer Extends Consumer Super,是泛型程序设计中的一个好的实践规则。即如果泛型类型表示一个生产者,就用<? extends className>;如果泛型类型表示一个消费者,就用<? super className>。结合上面所讲,其原因很好理解。
无限定通配符
无限定通配符就是一个直接的<?>,例如Pair<?>。其他的具体的Pair类型,都可以赋给Pair<?>引用,即其他的Pair<String>、Pair<LocalDate>...都是Pair<?>的子类型。
无限定通配符类型做为方法返回值时,只能用Object类型引用来承载;做为方法参数时,只能给它传null,其他任何类型都会导致编译错误。