返回 登录
0

Java核心技术点之泛型

阅读131

Why ——引入泛型机制的原因
假如我们想要实现一个String数组,并且要求它可以动态改变大小,这时我们都会想到用ArrayList来聚合String对象。然而,过了一阵,我们想要实现一个大小可以改变的Date对象数组,这时我们当然希望能够重用之前写过的那个针对String对象的ArrayList实现。

在Java 5之前,ArrayList的实现大致如下:

1 public class ArrayList {
2   public Object get(int i) { ... }
3   public void add(Object o) { ... }
4    ...
5   private Object[] elementData;
6 }

从以上代码我们可以看到,用于向ArrayList中添加元素的add函数接收一个Object型的参数,从ArrayList获取指定元素的get方法也返回一个Object类型的对象,Object对象数组elementData存放这ArrayList中的对象, 也就是说,无论你向ArrayList中放入什么类型的类型,到了它的内部,都是一个Object对象。

基于继承的泛型实现会带来两个问题:第一个问题是有关get方法的,我们每次调用get方法都会返回一个Object对象,每一次都要强制类型转换为我们需要的类型,这样会显得很麻烦;第二个问题是有关add方法的,假如我们往聚合了String对象的ArrayList中加入一个File对象,编译器不会产生任何错误提示,而这不是我们想要的。

所以,从Java 5开始,ArrayList在使用时可以加上一个类型参数(type parameter),这个类型参数用来指明ArrayList中的元素类型。类型参数的引入解决了以上提到的两个问题,如以下代码所示:

ArrayList<String> s = new ArrayList<String>();
s.add("abc");String s = s.get(0); //无需进行强制转换
s.add(123); //编译错误,只能向其中添加String对象

在以上代码中,编译器“获知”ArrayList的类型参数String后,便会替我们完成强制类型转换以及类型检查的工作。

泛型类
所谓泛型类(generic class)就是具有一个或多个类型参数的类。例如:

public class Pair<T, U> {
  private T first;
  private U second;
  public Pair(T first, U second) {
   this.first = first;
   this.second = second;
  } 
  public T getFirst() { 
    return first; 
  } 
  public U getSecond() { 
    return second; 
  } 
  public void setFirst(T newValue) { 
    first = newValue; 
  } 
  public void setSecond(U newValue) { 
    second = newValue; 
  }
}

上面的代码中我们可以看到,泛型类Pair的类型参数为T、U,放在类名后的尖括号中。这里的T即Type的首字母,代表类型的意思,常用的还有E(element)、K(key)、V(value)等。当然不用这些字母指代类型参数也完全可以。
实例化泛型类的时候,我们只需要把类型参数换成具体的类型即可,比如实例化一个Pair

Pair<String, Integer> pair = new Pair<String, Integer>();

泛型方法
所谓泛型方法,就是带有类型参数的方法,它既可以定义在泛型类中,`也可以定义在普通类中。例如:

public class ArrayAlg { 
  public static <T> T getMiddle(T[] a) { 
    return a[a.length / 2]; 
  }
}

以上代码中的getMiddle方法即为一个泛型方法,定义的格式是类型变量放在修饰符的后面、返回类型的前面。我们可以看到,以上泛型方法可以针对各种类型的数组调用,在这些数组的类型已知切有限时,虽然也可以用过重载实现,不过编码效率要低得多。调用以上泛型方法的示例`代码如下:

String[] strings = {"aa", "bb", "cc"};
String middle = ArrayAlg.getMiddle(names);

类型变量的限定
在有些情况下,泛型类或者泛型方法想要对自己的类型参数进一步加一些限制。比如,我们想要限定类型参数只能为某个类的子类或者只能为实现了某个接口的类。相关的语法如下:

<T extends BoundingType> //BoundingType是一个类或者接口

其中的BoundingType可以多于1个,用“&”连接即可。
深入理解泛型的实现
实际上,从虚拟机的角度看,不存在“泛型”概念。比如上面我们定义的泛型类Pair,在虚拟机看来(即编译为字节码后),它长的是这样的:

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 newValue) { 
    first = newValue; 
  } 
  public void setSecond(Object newValue) { 
    second = newValue; 
  }
}

上面的类是通过类型擦除得到的,是Pair泛型类对应的原始类型(raw type)。类型擦除就是把所有类型参数替换为BoundingType(若未加限定就替换为Object)。
我们可以简单地验证下,编译Pair.java后,键入“javap -c -s Pair”可得到:
图片描述
上图中带“descriptor”的行即为相应方法的签名,比如从第四行我们可以看到Pair构造方法的两个形参经过类型擦除后均已变为了Object。

由于在虚拟机中泛型类Pair变为它的raw type,因而getFirst方法返回的是一个Object对象,而从编译器的角度看,这个方法返回的是我们实例化类时指定的类型参数的对象。实际上, 是编译器帮我们完成了强制类型转换的工作。也就是说编译器会把对Pair泛型类中getFirst方法的调用转化为两条虚拟机指令:

  • 第一条是对raw type方法getFirst的调用,这个方法返回一个Object对象;
  • 第二条指令把返回的Object对象强制类型转换为当初我们指定的类型参数类型。

我们通过以下的代码来直观的感受下:

1 public class Pair<T, U> { 
2 //请见上面贴出的代码 
3  
4   public static void main(String[] args) { 
5     String first = "first", second = "second"; 
6     Pair<String, String> p = new Pair<String, String>(first, second); 
7     String result = p.getFirst(); 
8    } 
9 
10 }

编译后我们通过javap查看下生成的字节码:
图片描述
我们重点关注下上面标着”17:”的那行,根据后面的注释,我们知道这是对getFirst方法的调用,可以看到他的返回类型的确是Object。
我们再看下标着“20:”的那行,是一个checkcast指令,字面上我们就可以知道这条指令的含义是检查类型转换是否成功,再看后面的注释,我们这里确实存在一个到String的强制类型转换。
类型擦除也会发生于泛型方法中,如以下泛型方法:

public static <T extends Comparable> T min(T[] a)

编译后经过类型擦除会变成下面这样:

public static Comparable min(Comparable[] a)

方法的类型擦除会带来一些问题,考虑以下的代码:

1 public class DateInterval extends Pair<Date, Date> { 
2   public DateInterval(Date first, Date second) { 
3     super(first, second); 
4   } 
5  
6  
7   public void setSecond(Date second) { 
8     if (second.compareTo(getFirst()) >= 0) { 
9       super.setSecond(second);
10    }
11  }
12 
13}

以上代码经过类型擦除后,变为:

1 public class DateInterval extends Pair { 
2  
3   ... 
4   public void setSecond(Date second) { 
5     if (second.compareTo(getFirst()) >= 0) { 
6       super.setSecond(second); 
7     } 
8   } 
9 
10}

而在DateInterval类还存在一个从Pair类继承而来的setSecond的方法(经过类型擦除后)如下:

public void setSecond(Object second)

现在我们可以看到,这个方法与DateInterval重写的setSecond方法具有不同的方法签名(形参不同),所以是两个不同的方法,然而这两个方法之前却是override的关系。考虑以下的代码:

DateInterval interval = new DateInterval(...);
Pair<Date, Date> pair = interval;
Date aDate = new Date(...);
pair.setSecond(aDate);

由以上代码可知,pair实际引用的是DateInterval对象,因此应该调用DateInterval的setSecond方法,这里的问题是类型擦除与多态发生了冲突。

我们来梳理下为什么会发生这个问题:pair在之前被声明为类型Pair

public void setSecond(Object second) { setSecond((Date) second);}

我们再来通过javap来感受下:
图片描述
我们可以看到,在DateInterval类中存在两个setSecond方法,第一个setSecond方法(即我们定义的setSecond方法)的形参为Date,第二个setSecond方法的形参是Object,第二个方法就是编译器为我们生成的桥方法。我们可以看到第二个方法中存在到Date的强制类型转换,而且调用了第一个setSecond方法。

综合以上,我们知道了泛型机制的实现实际上是编译器帮我们分担了一些麻烦的工作。一方面通过使用类型参数,可以告诉编译器在编译时进行类型检查;另一方面,原本需要我们做的强制类型转换的工作也由编译器为我们代劳了。

注意事项
不能用基本类型实例化类型参数
也就是说,以下语句是非法的:

Pair<int, int> pair = new Pair<int, int>();

不过我们可以用相应的包装类型来代替。

不能抛出也不能捕获泛型类实例
泛型类扩展Throwable即为不合法,因此无法抛出或捕获泛型类实例。但在异常声明中使用类型参数是合法的:

public static <T extends Throwable> void doWork(T t) throws T {
  try {
    ...
  } catch (Throwable realCause) {
    t.initCause(realCause);
    throw t;
  }
}

参数化类型的数组不合法
在Java中,Object[]数组可以是任何数组的父类(因为任何一个数组都可以向上转型为它在定义时指定元素类型的父类的数组)。考虑以下代码:

String[] strs = new String[10];
Object[] objs = strs;
objs[0] = new Date(...);

在上述代码中,我们将数组元素赋值为满足父类(Object)类型,但不同于原始类型(Pair)的对象,在编译时能够通过,而在运行时会抛出ArrayStoreException异常。

基于以上原因,假设Java允许我们通过以下语句声明并初始化一个泛型数组:

Pair<String, String>[] pairs = new Pair<String, String>[10];

那么在虚拟机进行类型擦除后,实际上pairs成为了Pair[]数组,我们可以将它向上转型为Object[]数组。这时我们若往其中添加Pair

Pair<String, String>[] pairs = (Pair<String, String>[]) new Pair[10];

不能实例化类型变量
不能以诸如“new T(…)”, “new T[…]”, “T.class”的形式使用类型变量。Java禁止我们这样做的原因很简单,因为存在类型擦除,所以类似于”new T(…)”这样的语句就会变为”new Object(…)”, 而这通常不是我们的本意。我们可以用如下语句代替对“new T[…]”的调用:

arrays = (T[]) new Object[N];

泛型类的静态上下文中不能使用类型变量
注意,这里我们强调了泛型类。因为普通类中可以定义静态泛型方法,如上面我们提到的ArrayAlg类中的getMiddle方法。关于为什么有这样的规定,请考虑下面的代码:

public class People<T> { 
  public static T name; 
  public static T getName() { 
    ... 
  }
}

我们知道,在同一时刻,内存中可能存在不只一个People类实例。假设现在内存中存在着一个People对象和People对象,而类的静态变量与静态方法是所有类实例共享的。那么问题来了,name究竟是String类型还是Integer类型呢?基于这个原因,Java中不允许在泛型类的静态上下文中使用类型变量。

学习Java的同学注意了!!!
学习过程中遇到什么问题或者想获取学习资源的话,欢迎加入Java学习交流群,群号码:184625948【长按复制】 我们一起学Java!

评论