【Java学习笔记七】——一文教你读懂泛型的约束与局限性

声明:本文章内容主要摘选自尚硅谷宋红康Java教程、《Java核心卷一》、廖雪峰Java教程,示例代码部分出自本人,更多详细内容推荐直接观看以上教程及书籍,若有错误之处请指出,欢迎交流。

一、擦拭法

泛型是一种类似“模板代码”的技术,不同语言的泛型实现方式不一定相同。Java语言的泛型实现方式是擦拭法(Type Erasure)

所谓擦拭法是指,虚拟机对泛型其实一无所知,所有的工作都是编译器做的。

//例如,我们编写了一个泛型类Pair<T>,这是编译器看到的代码:
public class Pair<T> {
    private T first;
    private T last;
    public Pair(T first, T last) {
        this.first = first;
        this.last = last;
    }
    public T getFirst() {
        return first;
    }
    public T getLast() {
        return last;
    }
}

//而虚拟机根本不知道泛型。这是虚拟机执行的代码:
public class Pair {
    private Object first;
    private Object last;
    public Pair(Object first, Object last) {
        this.first = first;
        this.last = last;
    }
    public Object getFirst() {
        return first;
    }
    public Object getLast() {
        return last;
    }
}

因此,Java使用擦拭法实现泛型,导致了:

  • 编译器把类型<T>视为Object;
  • 编译器根据<T>实现安全的强制转型。
//使用泛型的时候,我们编写的代码也是编译器看到的代码:
Pair<String> p = new Pair<>("Hello", "world");
String first = p.getFirst();
String last = p.getLast();

//而虚拟机执行的代码并没有泛型:
Pair p = new Pair("Hello", "world");
String first = (String) p.getFirst();
String last = (String) p.getLast();

所以,Java的泛型是由编译器在编译时实行的,编译器内部永远把所有类型T视为Object处理,但是,在需要转型的时候,编译器会根据T的类型自动为我们实行安全地强制转型

二、泛型的约束与局限性

了解了Java泛型的实现方式——擦拭法,我们就知道了Java泛型的局限,大多数限制都是由类型擦除引起的

  • 1.<T>不能是基本类型,例如int,因为实际类型是Object,Object类型无法持有基本类型

    Pair<int> p = new Pair<>(1, 2); // compile error!
    
  • 2.无法取得带泛型的Class

Pair<string>p1=new Pair<>("Hello","world"); 
Pair<Integer>p2=new Pair<>(123,456); 
Class c1=p1.getClass();
Class c2=p2.getClass();
System.out.println(c1==c2);//true 
System.out.println(c1==Pair.class);//true
/*因为T是Object,我们对Pair<String>和Pair<Integer>类型获取Class时,获取到的是同一个Class,也就是Pair类的Class。
  换句话说,所有泛型实例,无论T的类型是什么,getClass()返回同一个Class实例,因为编译后它们全部都是Pair<Object>。
*/
  • 3.无法判断带泛型的Class
Pair<Integer> p = new Pair<>(123, 456);
// Compile error:
if (p instanceof Pair<String>.class) {
}
//原因和前面一样,并不存在Pair<String>.class,而是只有唯一的Pair.class。
  • 4.不能创建参数化类型的数组
Pair<Integer>[] table = new Pair<>[10];//Error
/*需要说明的是,只是不允许创建这些数组,而声明类型为Pair<String>[]的变量仍是合法的。不过不能用new Pair<String>[10]初始化这个变量。
  这有什么问题呢?擦除之后,table的类型是Pair[]。可以把它转换为Object[]:  Object[] objarray = table;
  数组会记住它的元素类型,如果试图存储其他类型的元素,就会抛出一个Array-StoreException异常: */
      objarray[0]="Hel1o";//Error--component type is Pair
//不过对于泛型类型,擦除会使这种机制无效。以下赋值:
      objarray[0]=new Pair<Employee>();
//能够通过数组存储检查,不过仍会导致一个类型错误。出于这个原因,不允许创建参数化类型的数组。

提示:如果需要收集参数化类型对象,只有一种安全而有效的方法:使用ArrayList: ArrayList<Pair>。

  • 5.不能实例化T类型
public class Pair<T> {
    private T first;
    private T last;
    public Pair() {
        // Compile error:
        first = new T();
        last = new T();
    }
}
//上述代码无法通过编译,因为构造方法的两行语句:
first = new T();
last = new T();
//擦拭后实际上变成了:
first = new Object();
last = new Object();
//这样一来,创建new Pair<String>()和创建new Pair<Integer>()就全部成了Object,显然编译器要阻止这种类型不对的代码。

//要实例化T类型,我们必须借助额外的Class<T>参数:
public class Pair<T> {
    private T first;
    private T last;
    public Pair(Class<T> clazz) {
        first = clazz.newInstance();
        last = clazz.newInstance();
    }
}
//上述代码借助Class<T>参数并通过反射来实例化T类型,使用的时候,也必须传入Class<T>。例如:
Pair<String> pair = new Pair<>(String.class);
  • 6.不能在静态域或方法中引用类型变量
public class Singleton<T>
{
      private static T singlelnstance;//Error 
      public static T getSingleInstanceO//Error 
      {
            if(singleInstance ==null)
            return singleInstance;
      }
}
/*如果这个程序能够运行,就可以声明一个Singleton<Random>共享随机数生成器,声明一个Singleton<JFileChooser>共享文件选择器对话框。
但是,这个程序无法工作。类型擦除之后,只剩下Singleton类,它只包含一个singlelnstance域。因此,禁止使用带有类型变量的静态域和方法。*/
  • 7.不能抛出或捕获泛型类的实例

既不能抛出也不能捕获泛型类对象。实际上,甚至泛型类扩展Throwable都是不合法的。
例如,以下定义就不能正常编译:

public static <T extends Throwable> void dolork(Class<T>t)
{
      try
      {
            ... 
      }catch(T e)//Error--can't catch type variable
      {
            Logger.global.info(.…)
      }
}

//不过,在异常规范中使用类型变量是允许的。以下方法是合法的:
public static <T extends Throwable> void doWork(T t)throws T//0K 
{
      try
      {
           ...
      }catch(Throwable realCause)
      {
            t.nitCause(realCause); 
            throw t;
      }
}

三、不恰当的覆写方法

有些时候,一个看似正确定义的方法会无法通过编译。例如:

public class Pair<T> {
    public boolean equals(T t) {
        return this == t;
    }
}

这是因为,定义的equals(T t)方法实际上会被擦拭成equals(Object t),而这个方法是继承自Object的,编译器会阻止一个实际上会变成覆写的泛型方法定义。

换个方法名,避开与Object.equals(Object)的冲突就可以成功编译:

public class Pair<T> {
    public boolean same(T t) {
        return this == t;
    }
}

此笔记仅针对有一定编程基础的同学,且本人只记录比较重要的知识点,若想要入门Java可以先行观看相关教程或书籍后再阅读此笔记。

最后附一下相关链接:
Java在线API中文手册
Java platform se8下载
尚硅谷Java教学视频
《Java核心卷一》

原文地址:https://www.cnblogs.com/66ccffly/p/13523126.html