前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Java基础系列(三十五):泛型进阶

Java基础系列(三十五):泛型进阶

作者头像
山禾说
发布2019-01-21 10:12:39
5760
发布2019-01-21 10:12:39
举报
文章被收录于专栏:Vi的技术博客

类型变量的限定

有时,类或方法需要对类型变量加以约束,就像下面这样:

代码语言:javascript
复制
class ArrayAlg {
   public 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; i++)
      {
         if (min.compareTo(a[i]) > 0) min = a[i];
         if (max.compareTo(a[i]) < 0) max = a[i];
      }
      return new Pair<>(min, max);
   }}

下面来解释一下,为什么我们要这么做?

因为变量minmax使用了compareTo方法,那么我们应该如何去保证一个泛型类具有这样的方法呢?解决方案就是对类型变量T进行限定,将T限制为实现了Comparable接口的类:<T extends Comparable>

现在泛型的minmax方法只能被实现了Comparable接口的类的数组调用,而没有实现Comparable接口的数据组调用的话会产生一个错误。

一个类型变量或通配符可以有多个限定,例如:

代码语言:javascript
复制
T extends Comparable & Serializable

限定类型使用“&”分隔,而逗号用来分隔类型变量。 在Java的继承中,可以根据需要拥有多个接口超类型,但是限定中至多有一个类。如果用一个类作为限定,它必定是限定列表中的第一个。

泛型代码与虚拟机

虚拟机没有泛型类型对象,所有的对象都属于普通类。 无论何时定义一个泛型类型,都自动提供了一个相应的原始类型。原始类型的名字就是删去类型参数后的泛型类型名。擦除类型变量,并替换成限定类型(没有限定的变量用Object)。这样做的目的是为了让非泛型的Java程序在后续支持泛型的 jvm 上还可以运行(向后兼容)。比如,Pair<T>的原始类型如下所示:

代码语言:javascript
复制
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替换。 结果就是一个普通的类,就好像泛型引入Java语言之前已经实现的那样。 在程序中可以包含不同类型的Pair,例如,Pair<String>Pair<LocalDate>。而擦除类型后就变成原始的Pair类型了。

原始类型用第一个限定的类型变量来替换,如果没有给定限定就用Object替换。例如,类Pair<T>中的类型变量没有显式的限定,因此,原始类型用Object替换T。假定声明了一个不同的类型。

代码语言:javascript
复制
public class Interval<T extends Comparable & Seriailizable> implements Serializable {

    private T lower;
    private T upper;
    public Interval(T first, T second) {
        if (first.compareTo(second) <= 0) {
            lower = first;
            upper = second;
        } else {
            lower = second;
            upper = first;
        }
    }}

原始类型Interval如下显示:

代码语言:javascript
复制
public class Interval implements Serializable {
    
    private Comparable lower;
    private Comparable upper;
    public Interval(Comparable first, Comparable second) {
        if (first.compareTo(second) <= 0) {
            lower = first;
            upper = second;
        } else {
            lower = second;
            upper = first;
        }
    }}

当程序调用泛型方法时,如果擦除返回类型,编译器插入强制类型转换。

代码语言:javascript
复制
Pair<Employee> ems = ...;Employee em = ems.getFirst();

擦除getFirst的返回类型后将返回Object类型。编译器自动插入Employee的强制类型转换。也就是说,编译器把这个方法调用编译为两条虚拟机指令:

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

当存取一个公有泛型域时也要插入强制类型转换。

代码语言:javascript
复制
//我们写的代码Employee em = ems.first;//编译器做的事情Employee em = (Employee)ems.first;

类型擦除也会出现在泛型方法中,程序员通常认为下述的泛型方法

代码语言:javascript
复制
public static <T extends Comparable> T min(T[] a)

是一个完整的方法族,而擦除类型之后,只剩下一个方法:

代码语言:javascript
复制
public static Comparable min(Comparable[] a)

这个时候类型参数T已经被擦除了,只留下了限定类型Comparable

但是方法的擦除会带来一些问题:

代码语言:javascript
复制
class DateInterval extends Pair<LocalDate> {
    public void setSecond(LocalDate second) {
        if (second.compareTo(getFirst()) >= 0) {
            super.setSecond(second);
        }
    }}

擦除后:

代码语言:javascript
复制
class DateInterval extends Pair {
    public void setSecond(LocalDate second) {...}}

这时,问题出现了,存在另一个从Pair类继承的setSecond方法,即:

代码语言:javascript
复制
public void setSecond(Object second)

这显然是一个不同的方法,因为它有一个不同类型的参数(Object),而不是LocalDate

代码语言:javascript
复制
DateInterval interval = new DateInterval(...);Pair<LocalDate> pair = interval;pair.setSecond(aDate);

这里,希望对setSecond的调用具有多态性,并调用最合适的那个方法。由于pair引用DateInterval对象,所以应该调用DateInterval.setSecond。问题在于类型擦除与多态发生了冲突。要解决这个问题,就需要编译器在DateInterval类中生成一个桥方法:

代码语言:javascript
复制
public void setSecond(Object second) {
    setSecond((Date)second);}

变量pair已经声明为类型Pair<LocalDate>,并且这个类型只有一个简单的方法叫setSecond,即setSecond(Object)。虚拟机用pair引用的对象调用这个方法。这个对象是DateInterval类型的,所以会调用DateInterval.setSecond(Object)方法。这个方法是合成的桥方法。它会调用DateInterval.setSecond(Date),这也正是我们所期望的结果。

所以,我们要记住关于Java泛型转换的几个点:

虚拟机中没有泛型,只有普通的类和方法 所有的类型参数都用它们的限定类型替换 桥方法被合成来保持多态 为保持类型安全性,必要时插入强制类型转换

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2018-09-25,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 Vi的技术博客 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档