美文网首页Java基础知识java
Java泛型的类型擦除

Java泛型的类型擦除

作者: 五十米_深蓝 | 来源:发表于2018-07-10 17:36 被阅读36次

    泛型是什么?

    泛型的英文是 generics,generic 的意思是通用,而翻译成中文,泛应该意为广泛,型即是类型。所以泛型就是能广泛适用的类型。

    但泛型还有一种较为准确的说法就是为了参数化类型,或者说可以将类型当作参数传递给一个类或者是方法。

    那么,如何解释类型参数化呢?

    public class Test {
       Object value;
       public Object getValue() {
           return value;
       }
       public void setValue(Object value) {
           this.value = value;
       }
    }
    

    上面的Object为java.lang包中的所有类的根类,于是,我们可以这样使用它。

    Test test = new Test ();
    test.setValue('x');//Character包装类的自动装箱
    String value1 = (String) test.getValue();
    test.setValue(19950904);//Integer包装类自动装箱
    int value = (int) test.getValue();
    

    Java八种基本数据类型对应的包装类,以下以int和char型数据对应的包装类为例:

    Character ch = new Character('x');//等价于test.setValue('x');
    test.setValue(ch )
    Integer in = new Integer("19950904");//等价于test.setValue(19950904);
    test.setValue(in )
    

    上述方法简单粗暴,只要我们做一个正确的类型强制转换就OK了
    但是通过在JDK1.5版本引入泛型之后,它给我们带来了完全不一样的编程体验

    public class Test<T> {
       T value;
       public Object getValue() {
           return value;
       }
       public void setValue(T value) {
           this.value = value;
       }
    }
    

    这就是泛型,它将Test类的成员变量value这个属性的类型也参数化了,这就是所谓的参数化类型。以下是它的使用方法。

    Test<Character> test1 = new Test<Character>();
    test1 .setValue('x');
    char value1 = test1 .getValue();
    Test<Integer> test2 = new Test<Integer>();
    test2.setValue(19950904);
    int value2 = test2 .getValue();
    

    上述代码最显而易见的好处就是不用再对取出来的结果进行强制的类型转换了。除此之外,还有另外一个特点,那就是,泛型除了可以将参数类型化外,当参数一旦确定好,如果参数类型不匹配,编译器在编译的时候就会因为参数类型不匹配而报错不通过,如下代码在校验泛型指定数据类型是否匹配时就会报错:

    Test<String> test1 = new Test<String>();
    test1.setValue('liupeng');
    String value1 = test1 .getValue();
    test1.setValue(19950904);//泛型指定类型不匹配导致报错
    

    因此,综合上述信息,我们可以得出以下结论。
    1、与通过Object根类来代替一切类型这样简单粗暴的方式而言,泛型的引入使得数据的类别可以像参数一样由外部传递进来。它提供了一种扩展能力。它更符合面向抽象的编程思想。
    2、当具体的类型确定后,泛型又提供了一种类型检测的机制,只有相匹配的数据才能正常的赋值,否则编译器就不通过。所以说,它是一种类型安全检测机制,一定程度上提高了软件的安全性。
    3、泛型提高了程序代码的可读性,不必要等到运行的时候才去进行数据类型的强制转换,在定义或者实例化阶段,因为泛型类型显示话的效果,程序员能够一目了然猜测出代码要操作的数据类型。

    接下来我们介绍下泛型的相关知识
    泛型的分类,按照使用情况可以分为三类:泛型类、泛型方法和泛型接口
    1、泛型类
    泛型类的定义如下:

    public class Test<T> {
       private T field1;
    }
    

    尖括号 <> 中的 T 被称作是类型参数,用于指代任何类型。事实上,T只是一种习惯性写法,如果你愿意。你可以写成任意的字符串,如下:

    public class Test<LiuPeng> {
       private LiuPeng Field1;
    }
    

    但出于规范的目的,Java还是建议我们用单个大写字母来代表类型参数。常见的如:
    (1) T 代表一般的任何类。
    (2) E 代表 Element 的意思,或者 Exception 异常的意思。
    (3) K 代表 Key 的意思。
    (4) V 代表 Value 的意思,通常与 K 一起配合使用。
    (5) S 代表 Subtype 的意思,文章后面部分会讲解示意。
    以下是泛型类的使用:

    Test<String> test1 = new Test<>();//泛型指定具体的显示化数据类型为String
    Test<Integer> test2 = new Test<>();//泛型指定具体的显示化数据类型为Integer
    

    注意:泛型指定的具体数据类型不能为基本数据类型,必须为其对应的包装类
    在对泛型类创建实例对象的时候,只需要在尖括号中赋上具体的数据类型(可以是任何类)即可,T就会被替换成对应的类型,如String 或者是 Integer,泛型类被创建具体实例对象的时候,内部自动扩展成以下代码:

    public class Test<String> 
       String field1;               
    }
    

    当然,泛型类也可以同时接受多个类型参数,如下定义所示:

    public class MultiType <E,T>{
       E filed1;
       T filed2;
       public E getFiled1(){
           return filed1;
       }
       public T getFiled2(){
           return filed2;
       }
    }
    

    2、泛型方法
    泛型方法的定义如下:

    public class Test {
       public <T> void testMethod(T t){
       }
    }
    

    泛型方法与泛型类稍有不同的地方是,类型参数也就是尖括号那一部分是写在返回值前面的。<T> 中的 T 被称为类型参数,而方法中参数中的 T 被称为参数化类型,它不是运行时真正的参数。
    当然,声明的类型参数,其实也是可以当作返回值的类型的。

    public  <T> T testMethod(T t){
           return null;
    }
    

    泛型类与泛型方法是可以同时出现在一个类中的,如下所示:

    public class Test<T>{
       public  void testMethod1(T t){
           System.out.println(t.getClass().getName());//取的是类的全路径名称
       }
       public  <T> T testMethod2(T t){
           return t;
       }
    }
    

    上面代码中,Test<T> 是泛型类,testMethod1 是泛型类中的普通方法,而 testMethod2 是一个泛型方法。而泛型类中的类型参数与泛型方法中的类型参数是没有相应的联系的,泛型方法始终以自己定义的类型参数为准。
    针对上面的代码,我们可以这样编写测试代码。

    Test<String> t = new Test();
    t.testMethod1("liupeng");
    Integer i = t.testMethod2(new Integer(19950904));
    

    泛型类的实际类型参数是 String,而传递给泛型方法的类型参数是 Integer,两者互不相干。
    但是,为了避免混淆,如果在一个泛型类中存在泛型方法,那么两者的类型参数最好不要同名。比如,Test<T> 代码最好写成以下形式:

    public class Test<T>{
       public  void testMethod1(T t){
           System.out.println(t.getClass().getName());//取的是类的全路径名称
       }
       public  <E> E testMethod2(E e){
           return e;
       }
    }
    

    3、泛型接口
    泛型接口的定义如下:

    public interface Interable<T> {
    }
    

    泛型接口与泛型类相似,可以在泛型接口中声明相应的泛型方法:

    public interface Interable<T>{
       public void testMethod1(T t);
       public <E> E testMethod2(E e);
    }
    

    通配符 ?
    除了用<T>表示泛型外,还有<?>这种形式。?被称为通配符。

    class Father{}
    class Son extends Father{}
    Son son = new Son ();
    Father father = son;
    

    如上代码显示,Father是Son的父类,它们之间是继承关系,所以Son的实例可以给一个 Father引用赋值,那么以下代码会编译通过吗?

    List<Son> sonList = new ArrayList<>();
    List<Father> fatherList = sonList ;
    

    答案是肯定不行的。
    编译器不会让它通过的。Son是Father的子类,不代表 List<Son> 和 List<Father> 有继承关系。
    但是,现实的编码中,确实存在此类需求,希望泛型能够处理某一范围内的数据类型,比如某个类和它的子类,对此 Java 引入了通配符这个概念。
    所以,通配符的出现是为了指定泛型中的类型范围。
    通配符有 下一三种形式。
    1、 <?> 被称作无限定的通配符。
    无限定通配符

    public void testMethod(Collection<?> collection){
    }
    

    上面的代码中,方法内的参数是被无限定通配符修饰的 Collection 对象,它隐略地表达了一个意图或者可以说是限定,那就是 testMethod() 这个方法内部无需关注 Collection 中的真实类型,因为它是未知的。所以,你只能调用 Collection 中与类型无关的方法。

    pubilc class Tset{
       public void testMethod(Collection<?> collection){
          collection.add(1995);//编译出错,无法确认真实数据类型
          collection.add("liupeng");//编译出错,无法确认真实数据类型
          collection.add(new Object());//编译出错,无法确认真实数据类型-正常
          collection.iterator().next();//迭代器遍历,与数据类型无关
          collection.size();//容器容量,与数据类型无关-正常
       }
    }
    

    有人说,<?> 提供了只读的功能,也就是它删减了增加具体类型元素的能力,只保留与具体类型无关的功能。它不管装载在这个容器内的元素是什么类型,它只关心元素的数量、容器是否为空等。
    2、 <? extends T> 被称作有上限的通配符。
    <?> 代表着类型未知,但是我们的确需要对于类型的描述再精确一点,我们希望在一个范围内确定类别,比如类型 A 及 类型 A 的子类都可以。
    那么我们就可以采用如下的定义方式:

    public void test(Collection<? extends Father> param){
    }
    

    上面代码中,para 这个 Collection 接受 Father及 Father的子类的类型。
    但是,它仍然丧失了写操作的能力。也就是说以下代码仍然编译不通过。

    param.add(new Son());
    param.add(new Father());
    

    没有关系,我们不知道具体类型,但是我们至少清楚了类型的范围。
    3、 <? super T> 被称作有下限的通配符。
    这个和 <? extends T> 相对应,代表 T 及 T 的超类。

    public void test(Collection<? super Son> param){
    }
    

    <? super T> 神奇的地方在于,它拥有一定程度的写操作的能力。

    public void test(Collection<? super Son> param){
       param.add(new Son());//编译通过
       param.add(new Father());//编译不通过
    }
    

    <类型擦除>
    泛型是Java 1.5版本才引进的概念,在这之前是没有泛型的概念的,但显然,泛型代码能够很好地和之前版本的代码很好地兼容。
    这是因为,泛型信息只存在于代码编译阶段,在进入JVM之前,与泛型相关的信息会被擦除掉,专业术语叫做类型擦除。
    通俗地讲,泛型类和普通类在java虚拟机内是没有什么特别的地方。

    List<String> list1 = new ArrayList<String>();
    List<Integer> list2 = new ArrayList<Integer>();
    System.out.println(list1.getClass() == list.getClass());
    

    输出的结果为true是因为List<String>和 List<Integer>在jvm中的Class都是 List.class。泛型信息被擦除了。
    有人可能会问,那类型String和Integer怎么办?答案是泛型转译。

    public class Test<T>{
       T object;
       public Test(T object) {//通过构造方法给成员变量object传递值
           this.object = object;
       }
    }
    

    Test是一个泛型类,我们查看它在运行时的状态信息可以通过Java反射机制。

    Test<String> test = new Test<String>("liupeng");
    System.out.println("test class is:" + test.getClass().getName());
    

    输出的结果是:

    test class is:com.hundsun.lp.Test
    

    Class 的类型仍然是Test并不是Test<T>这种形式,那我们再看看泛型类中T的类型在 jvm 中是什么具体类型。

    Field[] fd = test.getClass().getDeclaredFields();
    for (Field f:fd) {
       System.out.println("Field name "+f.getName()+" type:"+f.getType().getName());
    }
    

    输出结果是

    Field name object type:java.lang.Object
    

    我们更改下上述代码,再看下测试结果

    public class Test<T extends String>{
    //  public class Test<T>{
       T object;
       public Erasure(T object) {
           this.object = object;
       }
    }
    

    输出结果是

    Field name object type:java.lang.String
    

    综上所述,我们可以得出以下结论:
    在泛型类被类型擦除的时候,之前泛型类中的类型参数部分如果没有指定上限,如<T>则会被转译成普通的Object 类型,如果指定了上限如<T extends String>则类型参数就被替换成类型上限。
    所以,在反射中。

    public class Test<T>{
       T object;
       public Test(T object) {
           this.object = object;
       }
       public void add(T object){
       }
    }
    

    add() 这个方法对应的Method的签名应该是 Object.class。
    实际结果如下:

    Test <String> test = new Test <String>("hello");
    System.out.println("erasure class is:"+ test.getClass().getName());
    Method[] methods =  test.getClass().getDeclaredMethods();
    for ( Method m:methods ){
       System.out.println(" method:"+m.toString());
    }
    

    输出结果是:

    method:public void com.hundsun.lp.Test.add(java.lang.Object)
    

    也就是说,如果你要在反射中找到add对应的 Method,你应该调用 getDeclaredMethod("add",Object.class) 否则程序会报错,提示没有这么一个方法,原因就是类型擦除的时候,T被替换成Object 类型了。

    类型擦除带来的局限性
    类型擦除,是泛型能够与之前的java版本代码兼容共存的原因。但也因为类型擦除,它会抹掉很多继承相关的特性,这是它带来的局限性。

    public class Test{
        public static void mian(){
          List<Integer> list1 = new ArrayList<>();
          list1.add(19950904);//正常
          list1.add("liupeng");//类型不匹配导致报错
        }
    }
    

    正常情况下,因为泛型的限制,编译器不让最后一行代码编译通过,因为类似不匹配,但是,基于对类型擦除的了解,利用反射,我们可以绕过这个限制。

    对泛型方法的困惑

    public <T> T test(T t){
       return null;
    }
    

    有的同学可能对于连续的两个T感到困惑,其实<T>是为了说明类型参数,是声明,而后面的不带尖括号的T是方法的返回值类型。
    你可以相像一下,如果test()这样被调用

    test("liupeng");
    

    那么实际相当于

    public String test(String t);
    

    Java 不能创建具体类型的泛型数组,因为类型擦除,程序无法分辨一个数组中的元素类型具体是什么类型。

    List<Integer>[] li2 = new ArrayList<Integer>[];//List<Integer>在 jvm 中等同于List<Object>
    List<Boolean> li3 = new ArrayList<Boolean>[];//List<Boolean>在 jvm 中等同于List<Object>
    

    但是,

    List<?>[] li3 = new ArrayList<?>[10];
    li3[1] = new ArrayList<String>();
    List<?> v = li3[1];
    

    借助于无限定通配符却可以,前面讲过 ? 代表未知类型,所以它涉及的操作都基本上与类型无关,因此 jvm 不需要针对它对类型作判断,因此它能编译通过,但是,只提供了数组中的元素,因为通配符原因,它只能读,不能写。比如,上面的 v 这个局部变量,它只能进行 get() 操作,不能进行 add() 操作。

    最后,还是要建议大家使用泛型,如官方文档所说的,如果可以使用泛型的地方,尽量使用泛型。

    毕竟它抽离了数据类型与代码逻辑,
    本意是提高程序代码的简洁性和可读性,
    并提供可能的编译时类型转换安全检测功能。
    

    相关文章

      网友评论

        本文标题:Java泛型的类型擦除

        本文链接:https://www.haomeiwen.com/subject/sftqpftx.html