• Java基础:泛型及其擦除性、不可协变性


    转载请注明出处:jiq•钦's technical Blog 


    1泛型语法:

    泛型类: class ClassName<T>{}

    泛型方法:public <T> void f(T x){}

    基本指导原则:假设使用泛型方法能够代替将整个类泛型化,那么就应该使用泛型方法,由于它能够让事情更加清楚。

     

    2为什么使用泛型?

    在Java SE1.5之前,没有泛型的情况的下,通过对类型Object的引用来实现參数的“随意化”,“随意化”带来的缺点是要做显式的强制类型转换,而这样的转换是要求开发人员对实际參数类型能够预知的情况下进行的。对于强制类型转换错误的情况,编译器可能不提示错误,在执行的时候才出现异常。这是一个安全隐患。

    泛型的优点是:

    (1)在编译时检查类型安全

    (2)而且全部的强制转换都是自己主动和隐式的,提高代码的重用率

    促成泛型出现最引人注目的一个原因就是为了创造容器类。

     

    优先考虑泛型!!!

    使用泛型比使用须要在client代码中进行转换的类型来的更加安全,也更加easy。

    在设计新类型的时候,要确保它们不须要这样的转换就能够使用。这通常意味着要把类做成是泛型的。

    比方


    client代码使用Stack的时候,假设不使用泛型,在取用Stack中的对象时还须要进行不安全的类型推断。类型转换等,并且代码还比較复杂。假设使用带泛型的Stack,client代码中就不须要进行类型转换了,直接使用并且不会复杂。

     

    3泛型数组:

    无法创建泛型数组,一般的解决方案是在不论什么想要使用泛型数组的地方使用ArrayList。

    public <T> voidtest()
    {        
    //Cannotcreate a generic array of T
    T[]tList = new T[10];
    int[]iList = new int[10];
     
    //useArrayList<T>
    ArrayList<T>sList = new ArrayList<T>();
    }

    4类型擦除:

    一、概念

    类型擦除:将泛型类型中类型參数信息去除,仅仅映射为一份字节码。在必要时进行类型检查和类型转换。

    编译时通过两个步骤来对泛型类型的类型进行擦除:

    1.将全部的泛型參数用其最左边界(最顶级的父类型)类型替换。

    2.移除全部的类型參数。

     

    备注:擦除也是为什么泛型必须编译时进行类型检查的原因。由于执行时类型信息被擦除了。

     

    二、擦除举例

    (1)容器泛型类型擦除:

    ArrayList<Integer>l1 = new ArrayList<Integer>();
    ArrayList<String> l2= new ArrayList<String>();
    LinkedList<Integer>l3 = new LinkedList<Integer>();
    List<String> l4 =new LinkedList<String>();
     
    System.out.println(l1.getClass().getName());
    System.out.println(l2.getClass().getName());
    System.out.println(l3.getClass().getName());
    System.out.println(l4.getClass().getName());
     
    //output
    java.util.ArrayList
    java.util.ArrayList
    java.util.LinkedList
    java.util.LinkedList
    能够看到上面四个泛型类型的类型信息均被擦出掉了,比方ArrayList<Integer>和ArrayList<String>编译后生成的字节码是一份。

     (2)自己定义泛型类型擦除:

    再看一个自己定义的泛型类:

    class TObject<T>
    {
    privateT obj;
    publicvoid Set(T object)
    {
    this.obj= object;
    System.out.println("T:"+object.getClass().getName());
    }        
    }
    编译擦除后,生成的类是这样:

    class TObject
    {
    privateObject obj;
    publicvoid Set(Object object)
    {
    this.obj= object;
    }        
    }
    首先泛型參数T被向上替换为自身的顶级父类Object,然后将类型參数T去除。

     (3)自己定义继承关系泛型类型擦除:

    class Manipulator<Textends SuperClass>
    {
     
      private T obj;
      public Manipulator(T x){
        obj = x;
      }
      public void doSomething(){
        obj.f();
       System.out.println(obj.getClass().getName());
      }
    }
    首先将泛型參数T向上替换为上边界。然后去除泛型參数:

    class Manipulator
    { 
      private SuperClass obj;
      public Manipulator(SuperClass x){
        obj = x;
      }
      public void doSomething(){
        obj.f();
       System.out.println(obj.getClass().getName());
      }
    }

    三、擦除原因:

    泛型不是在java一開始就有的。擦除是java中泛型实现的一种折中手段

    详细来说两个原因使得泛型代码须要类型擦除:

    (1)引入泛型代码不能对现有代码类库产生影响,所以须要将泛型代码擦除为非泛型代码;

    (2)当泛型代码被当做类库使用时。为了兼容性,不须要知道泛型代码是否使用泛型,所以须要擦除;

     

    5不可协变:

    (1)数组和泛型对照

    数组是可协变的、泛型是不可协变的。

    什么是可协变性?举个样例说明:

    数组可协变(covariant)是指假设类Base是类Sub的基类。那么Base[]就是Sub[]的基类。

    泛型不可变的(invariant)是指List<Base>不会是List<Sub>的基类,两者压根没关系。

     

    (2)泛型为什么不可协变

    泛型“编译时进行类型检查(类型安全)”特性决定了其不可协变。

    ArrayList<Object> objList = new ArrayList<Long>();

    //can't compile pass

    类型安全检查

    Object[] objArray = new Long[10];

    //compile OK

     

    假设ArrayList<Long>类型对象能够赋值给ArrayList<Object>类型引用,那么就违反了泛型类型安全的原则。

    由于假设编译器你这样做,会导致能够往容器中放置非Long型的对象。

    可是数组就无所谓,他不是类型安全的。

     

    再看看以下代码。第一行编译错误是由于不可协变性,那么为什么第二行能够呢?

    List<Type> listt = new ArrayList<SubType>();        

    //can't compile pass

     

    List<? extends Type> listt = new ArrayList<SubType>();

    //OK

    參考泛型通配符,这就是其作用

     

    不可协变并不代表不能在泛型代码中将父类出现的地方使用子类取代,如以下代码是合法的:

    ArrayList<Type> list = new ArrayList<Type>();

    //Type is SuperClass

    list.add(new SubType());

    //SubType is SubClass

    Type[] tt = new Type[3];

    tt[0] = new SubType();

     

    (3)数组可协变带来的问题:

    数组的协变性可能会导致一些错误,比方以下的代码:

    public static voidmain(String[] args) {  
       Object[] array = new String[10];  
        array[0] = 10;  
    }     
    它是能够编译通过的,由于数组是协变的,Object[]类型的引用能够指向一个String[]类型的对象。可是执行的时候是会报出例如以下异常的:

    Exception in thread"main" java.lang.ArrayStoreException: java.lang.Integer

    可是对于泛型就不会出现这样的情况了:

    public static voidmain(String[] args) {  
        List< Object> list = newArrayList< String>();  
        list.add(10);  
    } 
    这段代码连编译都不能通过。

     

    6通配符:

    通配符在类型系统中的作用部分来自其不会发生协变(covariant)这一特性。即通配符产生一部分原因来自突破不可协变的限制

    能够觉得通配符使得List<?>是List<AnyType>的基类。List<? extends  Type>是List<SubType>的基类。

    // collection1能够存放不论什么类型
    Collection<?

    >collection1 = new ArrayList<String>(); collection1 = newArrayList<Integer>(); collection1 = newArrayList<Object>(); //collection3表示它能够存放Number或Number的子类 Collection<?extends Number> collection3 = null; collection3 = newArrayList<Number>(); collection3 = newArrayList<Double>(); collection3 = newArrayList<Long>(); //collection4表示它能够存放Integer或Integer的父类 Collection<? superInteger> collection4 = null; collection4 = newArrayList<Object>();


  • 相关阅读:
    论文阅读笔记(四十五)【CVPR2020、AAAI2020】:Relation Feature 的应用
    论文阅读笔记(四十四)【ICCV2017】:Deeply-Learned Part-Aligned Representations for Person Re-Identification
    论文阅读笔记(四十三)【AAAI2020】:Rethinking Temporal Fusion for Video-based Person Re-identificationon Semantic and Time Aspect
    论文阅读笔记(四十二)【AAAI2019】:STA:Spatial-Temporal Attention for Large-Scale Video-based Person Re-Identification
    论文阅读笔记(四十一)【CVPR2017】:Learning Deep Context-aware Features over Body and Latent Parts for Person Re-identification
    论文阅读笔记(四十)【CVPR2017】:Human Semantic Parsing for Person Re-identification
    论文阅读笔记(三十九)【CVPR2017】:Spindle Net Person Re-identification with Human Body Region Guided Feature Decomposition and Fusion
    论文阅读笔记(三十八)【AAAI2020】:Semantics-Aligned Representation Learning for Person Re-identification
    学习笔记_SpringBoot2
    学习笔记_SpringBoot1
  • 原文地址:https://www.cnblogs.com/zfyouxi/p/5132566.html
Copyright © 2020-2023  润新知