泛型代码和虚拟机

虚拟机没有泛型类型对象(所有对象都属于普通类)

类型擦除

无论何时定义一个泛型代码,都会自动提供一个相应的原始类型

这个原始类型的名字就是去掉类型参数之后的泛型类型名。

类型变量会被擦除,并替换为其限定类型(或者,对于无限定的变量则替换为Object)。

例如,Pair的原始类型如下所示:

// 因为T是一个无限定的变量,所以直接用Object替换
// 结果是一个普通的类,就像Java在引入泛型之前实现的类一样
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,例如,pari或Pair

不过擦除类型之后,它们都会变成原始的Pair类型。

原始类型用第一个限定来替换类型变量,如果没有给定限定,就替换成Object。

public class Interval<T extends Comparable & Serializable> implements Serializable{
      private T lower;
      private T upper;

      public Interval(T first, T second){
            ...
      }
}

// 原始类型的Interval如下所示:
public class Interval implements Serializable{
      private Comparable lower;
      private Comparable upper;

      public Interval(Comparable first, Comparable second){...}
}

// 如果切换为class Interval<T extends Serializable & Comparable>
// 那么,原始类型会用Serializable替换T,而编译器会在必要时要向Comparable插入强制类型转换。
// 为提高效率,应该将标签接口(即没有方法的接口)放在限定列表的末尾

转换泛型表达式

编写一个泛型方法调用时,如果擦除了返回类型,编译器会插入强制类型转换。
例如

Pair<Employee> buddies = ...;
Employee buddy = buddies.getFirst(); 

getFirst擦除类型之后是Object。编译器自动插入转换到Employee的强制类型转换。

也就是说,编译器把这个方法调用转换为两条虚拟机指令:

  • 对原始方法Pair.getFirst的调用。
  • 将返回的Object类型强制转换为Employee类型。

当访问一个泛型字段时也要插入强制类型转换。

假设Pair类的first字段和second字段都是公共的。

表达式Employee buddy = buddies.first;也会在结果字节码中插入强制类型转换。

转换泛型方法

类型擦除也会出现在泛型方法中。

通常认为类似这样的泛型方法public static <T extends Comparable>是整个一组方法,

而擦除类型之后,只剩下一个方法:public static Comparable min(Comparable[] a)

注意,类型参数T已经被擦除了,只留下了限定类型Comparable

方法的擦除带来了两个复杂问题。
如下示例:

class DateInterval extends Pair<LocalDate>{
      public void setSecond(LocalDate second){...}
}

// 这个类擦除之后变成
class DateInterval extends Pair{
      public void setSecond(LocalDate second){...}
      
      public void setSecond(Object second){...} // 这显然是一个不同的方法,因为它有一个不同类型的参数
}

// 参考如下语句序列
var interval = new DateInterval{...}
Pair<LocalDate> pair = interval;
pair.setSecond(aDate);

我们希望setSecond调用具有多态性,会调用最适合的方法。
由于pair引用了一个DateInterval对象。所以应该调用DateInterval.setSecond.
但是,类型擦除与多态发生了冲突。
为了解决这个问题,编译器在DateInterval类中生成了一个桥方法public void setSecond(Object second){...}
因为变量pair已经声明为类型Pair,并且这个类型只有一个名为setSecond的方法,即setSecond(Object)。
虚拟机在pair引用的对象上调用这个方法。这个对象是DateInterval类型,所以将会调用DateInterval.setSecond(Object)方法。这个方法是合成的桥方法。
它会调用DateInterval.setSecond(LocalDate),这正是我们想要的。

桥方法可能会变得很奇怪,假如DateInterval类也覆盖了getSecond方法:

class DateInterval extends Pair<LocalDate>{
      public LocalDate getSecond(){...}
}

// 在DateInterval类中,有2个get方法
LocalDate getSecond(){...}
Object getSecond(){...}
// 不能这样编写Java代码(两个方法有相同的参数类型是不合法的,在这里,两个方法都没有参数)
// 但是,在虚拟机中,会由参数类型和返回类型共同指定一个方法。
// 因此,编译器可以为两个仅返回类型不同的方法生成字节码,虚拟机能够正确处理这种情况。

桥方法不仅仅用于泛型类型。
一个方法覆盖另一个方法时,可以指定一个更严格的返回类型,这是合法的。

Java泛型转换总结

  • 虚拟机中没有泛型,只有普通的类和方法
  • 所有的参数类型都会替换为它们的限定类型
  • 会合成桥方法来保持多态
  • 为保持类型安全性,必要时会插入强制类型转换
posted @ 2021-01-29 23:58  张三丰学Java  阅读(132)  评论(0编辑  收藏  举报