• 【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核心卷一》

  • 相关阅读:
    2018-div-matrix
    cf663div2
    生成树
    Call to your teacher
    并查集总结
    分组背包
    被3整除的子序列
    多重背包
    12.05
    django生命周期图
  • 原文地址:https://www.cnblogs.com/66ccffly/p/13523126.html
Copyright © 2020-2023  润新知