美文网首页
java泛型详解

java泛型详解

作者: 勇敢地追 | 来源:发表于2019-06-12 18:50 被阅读0次

    先来一道经典的测试题。

    List<String> l1 = new ArrayList<String>();
    List<Integer> l2 = new ArrayList<Integer>();
    System.out.println(l1.getClass() == l2.getClass());
    

    输出是啥?正确答案是true。上面的代码中涉及到了泛型,而输出的结果缘由是类型擦除。

    1.泛型是什么?

    泛型的英文是 generics,较为准确的说法就是为了参数化类型,或者说可以将类型当作参数传递给一个类或者是方法。
    那么,如何解释类型参数化呢?
    比如我们需要在Cache里面可以存取任何值,通常我们会这么做

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

    但是这么做缺点就是每次getValue以后必须要强制转化才行.但是如果用泛型的话就是另外的画面

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

    测试代码

    Cache<String> cache1 = new Cache<String>();
    cache1.setValue("aaa");
    // cache1.setValue(1); 这么写会报错,默认类型只能是string
    String aString = cache1.getValue();
    

    这就是泛型,它将 value 这个属性的类型也参数化了,这就是所谓的参数化类型
    所以,综合上面信息,我们可以得到下面的结论。

    • 1.与普通的 Object 代替一切类型这样简单粗暴而言,泛型使得数据的类别可以像参数一样由外部传递进来。它提供了一种扩展能力
    • 2.当具体的类型确定后,泛型又提供了一种类型检测的机制,只有相匹配的数据才能正常的赋值,否则编译器就不通过。所以说,它是一种类型安全检测机制
    • 3.泛型提高了程序代码的可读性,在定义或者实例化阶段,因为 Cache<String>这个类型显化的效果,程序员能够一目了然猜测出代码要操作的数据类型。

    当然,并不意味着 cache1.setValue(1) 这种操作完全不能执行,后面会说明

    2.通配符 ?

    在讲类型擦除前先介绍一下通配符 ?
    除了用 <T>表示泛型外,还有 <?>这种形式。?被称为通配符。

    class Base{}
    class Sub extends Base{}
    Sub sub = new Sub();
    Base base = sub;        
    List<Sub> lsub = new ArrayList<>();
    List<Base> lbase = lsub;
    

    最后一行代码成立吗?编译会通过吗?答案是否定的。
    编译器不会让它通过的。Sub 是 Base 的子类,不代表 List<Sub>和 List<Base>有继承关系。
    但是,在现实编码中,确实有这样的需求,希望泛型能够处理某一范围内的数据类型,比如某个类和它的子类,对此 Java 引入了通配符这个概念。
    所以,通配符的出现是为了指定泛型中的类型范围。
    通配符有 3 种形式。

    • <?>被称作无限定的通配符。
    • <? extends T>被称作有上限的通配符。
    • <? super T>被称作有下限的通配符。

    2.1无限定通配符 <?>

    无限定通配符经常与容器类配合使用,它其中的 ? 其实代表的是未知类型,所以涉及到 ? 时的操作,一定与具体类型无关。

    ArrayList<?> list = new ArrayList<>();
    list.size();
    list.isEmpty();
    list.get(0);
    list.add("aa")//这句要报错
    

    <?>提供了只读的功能,也就是它删减了增加具体类型元素的能力,只保留与具体类型无关的功能。它不管装载在这个容器内的元素是什么类型,它只关心元素的数量、容器是否为空
    如果你看到分函数的参数有无限定通配符的list,而你此时又在查bug,那么这个分函数可以直接跳过,因为这个函数里面,这个list只读,不会对其进行修改

    2.2有上限的通配符<? extends T>

    主要特征是add受限

    List<? extends Number> list = null;//list类型是Number子类,一定能get到数据
    list = new ArrayList<Integer>();
    // list.add(new Integer(1));//报错,因为list不能确定实例化的对象具体类型导致add()方法受限
    list.get(0);//类型是Number,和无限定通配符的区别就是返回值的类型,无限定通配符返回object
    

    2.3有下限的通配符<? super T>

    主要特征是get受限

    List<? super Integer> list = null;//list类型是Integer父类,一定能add int型数据
    list = new ArrayList<Number>();
    list.add(1);
    Number number = list.get(0);//报错,get获取到的值不确定
    

    3.类型擦除

    回顾文章开始时的那段代码,打印的结果为 true 是因为 List<String>和 List<Integer>在 jvm 中的 Class 都是 List.class。
    为啥?这是因为,泛型信息只存在于代码编译阶段,在进入 JVM 之前,与泛型相关的信息会被擦除掉,专业术语叫做类型擦除。
    那么类型 String 和 Integer 怎么办?答案是泛型转译
    还是以上面Cache<T>为例,看一下Cache类型

    Cache<String> cache = new Cache<>();
    System.out.println(cache.getClass());
            
    Field[] fs = cache.getClass().getDeclaredFields();
    for ( Field f:fs) {
        System.out.println("Field name "+f.getName()+" type:"+f.getType().getName());
    }
    

    打印的结果是Cache和Object
    明显,并不是 Cache<T>这种形式,同时所谓的T也不是我们所想要的String.为什么呢?我们有必要来看一下Cache<T>的class文件
    MAC下选中对应的Cache,然后cmd+shift+r就可以查看字节码

    // Signature: <T:Ljava/lang/Object;>Ljava/lang/Object;
    public class fanxing.Cache {
      
      // Field descriptor #6 Ljava/lang/Object;
      // Signature: TT;
      java.lang.Object value;
      
      // Method descriptor #10 ()V
      // Stack: 1, Locals: 1
      public Cache();
      
      // Method descriptor #21 ()Ljava/lang/Object;
      // Signature: ()TT;
      // Stack: 1, Locals: 1
      public java.lang.Object getValue();
      
      // Method descriptor #26 (Ljava/lang/Object;)V
      // Signature: (TT;)V
      // Stack: 2, Locals: 2
      public void setValue(java.lang.Object value);
    }
    

    看到了没?getValue返回的是Object,无论T是什么,字节码都是Object,进入JVM自然都是Object咯
    顺带说一句,前文提到 cache1.setValue(1) 可以执行的奥秘就在这里
    那怎么样获取T的具体类型呢?改一下,改成Cache<T extends String>.再看一下字节码

    // Signature: <T:Ljava/lang/String;>Ljava/lang/Object;
    public class fanxing.Cache {
      
      // Field descriptor #6 Ljava/lang/String;
      // Signature: TT;
      java.lang.String value;
      
      // Method descriptor #10 ()V
      // Stack: 1, Locals: 1
      public Cache();
      
      // Method descriptor #21 ()Ljava/lang/String;
      // Signature: ()TT;
      // Stack: 1, Locals: 1
      public java.lang.String getValue();
      
      // Method descriptor #26 (Ljava/lang/String;)V
      // Signature: (TT;)V
      // Stack: 2, Locals: 2
      public void setValue(java.lang.String value);
    }
    

    getValue返回string,Signature也变了
    我们现在可以下结论了,在泛型类被类型擦除的时候,之前泛型类中的类型参数部分如果没有指定上限,如 <T>则会被转译成普通的 Object 类型,如果指定了上限如 <T extends String>则类型参数就被替换成类型上限。
    (字节码中的descriptor表示返回值,Signature表示泛型信息)

    4.类型擦除带来的局限性

    • 利用类型擦除的原理,用反射的手段就绕过了正常开发中编译器不允许的操作限制
    • 当泛型遇见重载

    4.1反射

    还是Cache<T>

            Cache<String> cache = new Cache<>();
            cache.setValue("aaa");
            // cache.setValue(123);正常不允许
            try {
                Method method = cache.getClass().getDeclaredMethod("setValue", Object.class);// 字节码是object
                method.invoke(cache, 123);
            } catch (NoSuchMethodException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            } catch (SecurityException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            } catch (IllegalAccessException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            } catch (IllegalArgumentException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            } catch (InvocationTargetException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
    
            System.out.println(cache.getValue());//这句话会报错,int无法转化为string,内部已经存在123这个int值,但无法输出
    

    4.2重载

    一下4个方法能同时存在么?

        public void method(List<String> list) {
        }
    
        public void method(List<Integer> list) {
        }
    
        public int method(List<String> list) {
            return 0;
        }
    
        public String method(List<Integer> list) {
            return "";
        }
    

    先看一下字节码

      // Method descriptor #19 (Ljava/util/List;)V
      // Signature: (Ljava/util/List<Ljava/lang/String;>;)V
      // Stack: 0, Locals: 2
      public void method(java.util.List list);
    
      // Method descriptor #19 (Ljava/util/List;)V
      // Signature: (Ljava/util/List<Ljava/lang/Integer;>;)V
      // Stack: 0, Locals: 2
      public void method(java.util.List list);
    
      // Method descriptor #19 (Ljava/util/List;)I
      // Signature: (Ljava/util/List<Ljava/lang/String;>;)I
      // Stack: 1, Locals: 2
      public int method(java.util.List list);
    
      // Method descriptor #19 (Ljava/util/List;)Ljava/lang/String;
      // Signature: (Ljava/util/List<Ljava/lang/Integer;>;)Ljava/lang/String;
      // Stack: 1, Locals: 2
      public java.lang.String method(java.util.List list);
    

    方法重载要求方法具备不同的特征签名,返回值并不包含在方法的特征签名之中,所以返回值不参与重载选择.但是在 Class 文件格式之中,只要描述符不是完全一致的两个方法就可以共存。
    从方法重载的要求来看,看Signature一行,首先把返回值去掉,然后类型擦除,整个Signature就剩下相同的Ljava/util/List,所以以上四个方法都不能共存
    但是在class文件格式中,3和4的Method descriptor不同,导致在低版本的jdk里面可以共存.后两个方法jdk1.6是警告,jdk1.8更严格,直接爆红(警告和爆红的文字信息都是一样的)

    参考文章

    https://blog.csdn.net/briblue/article/details/76736356
    https://blog.csdn.net/itmyhome1990/article/details/78872403

    相关文章

      网友评论

          本文标题:java泛型详解

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