泛型的精髓--类型擦除

Java语言的泛型采用的是擦除法实现的伪泛型,泛型信息(类型变量、参数化类型)在编译的时候去掉。使用擦除法的好处就是实现简单、非常容易Backport,运行期也能够节省一些类型所占的内存空间。而擦除法的坏处就是,通过这种机制实现的泛型远不如真泛型灵活和强大。Java选取这种方法是一种折中,因为Java最开始的版本是不支持泛型的,为了兼容以前的库而不得不使用擦除法。

泛型类型只有在静态类型检查期间才出现,在此之后,程序中的所有泛型类型都将被擦除,替换成它们非泛型上界。

看下面一个列子,代码如下:

public class Test01 {

    public static void main(String[] args) {

    }
    static void a(ArrayList<String> strings){

    }
    static void a(ArrayList<Integer> integers){

    }
}

编译器会报出错误信息:a(ArrayList<String>)’ clashes with ‘a(ArrayList)’; both methods have same erasure ,翻译出来就是两种方法冲突,擦除相同
从上面代码可以看出 Java 编译后的字节码中已经没有泛型的任何信息,在编译后所有的泛型类型都会做相应的转化,转化如下:
<? super A> 擦除后变为Object
<T>擦除后变为Object<? extends E> 擦除后的类型为 E.
List<String>、List 擦除后的类型为 List。
List<String>[]、List<T>[] 擦除后的类型为 List[]。
List<? extends E>、List<? super E> 擦除后的类型为 List<E>。
List<T extends Serialzable & Cloneable> 擦除后类型为 List<Serializable>

按照我们所来理解的,我们看看下面的 ArrayList<String>和ArrayList<Integer>有没有被擦除,如果擦除了,就是ArrayList,我们试试下面的代码运行结果是否为true。

public class Test01 {
    public static void main(String[] args) {
        ArrayList<String> strings = new ArrayList<>();
        ArrayList<Integer> integers = new ArrayList<>();
        System.out.println(strings.getClass() == integers.getClass());
    }
}
true

哟,还真的是,O(∩_∩)O
在JDK1.5后Signature属性被增加到了Class文件规范中,它是一个可选的定长属性,可以出现在类、字段表和方法表结构的属性表中。在JDK1.5中大幅度增强了Java语言的语法,在此之后,任何类、接口、初始化方法或成员的泛型签名如果包含了类型变量(Type Variables)或参数化类型(Parameterized Types),则Singature属性会为它记录泛型签名信息。Signature属性就是为了弥补擦除法的缺陷而增设的,Java可以通过反射获得泛型类型,最终的数据来源也就是这个属性。

public class Test01 {
    public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
        ArrayList<String> list = new ArrayList<>();
        list.add("luo");
        list.getClass().getMethod("add",Object.class).invoke(list,22);
        System.out.println(list.get(0));
        System.out.println(String.valueOf(list.get(1)));//指定了泛型为String 但是可以通过反射插入Integer类型
    }
}
luo
22

要区分原始类型和泛型变量的类型

在调用泛型方法的时候,可以指定泛型,也可以不指定泛型。
在不指定泛型的情况下,泛型变量的类型为 该方法中的几种类型的同一个父类的最小级,直到Object。
在指定泛型的时候,该方法中的几种类型必须是该泛型实例类型或者其子类。

public class Test2{
      public static void main(String[] args) {
           /**不指定泛型的时候*/
          int i=Test2.add(1, 2); //两参数都是Integer,所以T为Integer类型
          Number f=Test2.add(1 , 1.2);//参数是Integer和Float,取同一父类的最小级Number
          Object o=Test2.add(1, "asd"); //参数是Integer和String,取同一父类的最小级Object
        
          /**指定泛型的时候*/
          int a=Test2.<Integer>add(1, 2);//指定了Integer,所以只能为Integer类型或者其子类
          int b=Test2.<Integer>add(1 , 2.2);//编译错误,指定了Integer,不能为Float
          Number c=Test2.<Number>add(1,  2.2);  //指定为Number,所以可以为Integer和Float
     }  

     //这是一个简单的泛型方法
      public static <T> T add(T x,T y){
              return y;
      }  
  }

注意
在使用泛型的时候可以遵循一些基本的原则,从而避免一些常见的问题。
1.在代码中避免泛型类和原始类型的混用。比如List和List不应该共同使用。这样会产生一些编译器警告和潜在的运行时异常。当需要利用JDK 5之前开发的遗留代码,而不得不这么做时,也尽可能的隔离相关的代码。
2.在使用带通配符的泛型类的时候,需要明确通配符所代表的一组类型的概念。由于具体的类型是未知的,很多操作是不允许的。
3.泛型类最好不要同数组一块使用。你只能创建new List<?>[10]这样的数组,无法创建new List[10]这样的。这限制了数组的使用能力,而且会带来很多费解的问题。因此,当需要类似数组的功能时候,使用集合类即可。
4.不要忽视编译器给出的警告信息。

猜你喜欢

转载自blog.csdn.net/qq_42224683/article/details/107552580