Java 之中的泛型和类型擦除

Posted by Haiming on December 19, 2019

参考:https://blog.csdn.net/xiangwanpeng/article/details/77896340

https://blog.csdn.net/s10461/article/details/53941091

本文的顺序是先讲泛型,然后讲关于类型擦除的问题。

泛型

概述

首先要解决的问题:什么是泛型?为什么要使用泛型?

这是网上一段内容的引用:

泛型,就是“参数化类型”。 我们在讲到参数的情况时,一般讲的都是在定义方法的时候定义的形参,然后在使用的时候传入实参。

那么”参数化类型“就很好理解了: 将类型由原来的具体类型进行参数化, 类似于方法之中的变量参数,将类型也定义成变量形式。只是在使用方法的时候传入具体的类型(类型实参)

泛型本质是参数化类型,在不创建新的类型的情况下,通过泛型指定的不同类型来控制形参具体限制的类型。这种参数类型可以用在类型,接口和方法之中,分别称为泛型类,泛型接口和泛型方法。

举2个例子

下面两个例子的输出不同,下面会详细叙述其原因。

首先这个例子可以说明泛型的作用:

package GenericStudy;

import java.util.ArrayList;
import java.util.List;

public class GenerateTesting {
    public static void main(String[] args) {
        List arrayList=new ArrayList();
        arrayList.add("abcd");
        arrayList.add(1234);

        for(int i =0;i<arrayList.size();i++){
            String item = arrayList.get(i);
            System.out.println("Testing generic: "+item);
        }

    }
}

输出为:

Testing generic: abcd
Exception in thread "main" java.lang.ClassCastException: java.lang.Integer incompatible with java.lang.String
	at GenericStudy.GenerateTesting.main(GenerateTesting.java:13)

首先,此处的错误的确显而易见:将arrayList之中第二个Integer对象cast成String,那么肯定是会出问题的。但是这处在编译阶段并没有报错。而为了对于的 List<> 这样类型的对象,希望可以在编译阶段就解决,那么泛型就应运而生了。

下面是我自己改过的代码:

package GenericStudy;

import java.util.ArrayList;
import java.util.List;

public class GenerateTesting {
    public static void main(String[] args) {
        List arrayList=new ArrayList();
        arrayList.add("abcd");
        arrayList.add(1234);

        for(int i =0;i<arrayList.size();i++){
            Object item = arrayList.get(i);
            System.out.println("Testing generic: "+item);
        }

    }
}

结果是:

Testing generic: abcd
Testing generic: 1234

此处运行正常,原因是使用Object类来接住遍历的对象,然后对其直接进行操作。

特性

泛型只在编译阶段有效,下面的代码为例:

CheckArrayListType.java

package GenericStudy;

import java.util.ArrayList;

public class CheckArrayListType {
    public static void main(String[] args) {
        ArrayList<String> arrayList1 = new ArrayList<>();
        arrayList1.add("abcde");
        ArrayList<Integer> arrayList2 = new ArrayList<>();
        arrayList2.add(12345);
        ArrayList<Object> arrayList3 = new ArrayList<>();
        arrayList3.add(123433);
        arrayList3.add("hahahaha");
        System.out.println(arrayList3);
        /*
        Here will show that below is always true
         */
        System.out.println(arrayList1.getClass() == arrayList2.getClass());
    }
}

在最下面这一行,会出现这样的提示:

Screenshot 2019-12-27 at 6.19.28 PM

程序在编译之后,会采取去泛型化的特征,也就是Java 之中所说的泛型,只在编译阶段有效。

在编译过程中,正确检验泛型结果之后,会将泛型相关的信息全部擦除,并且在对象进入和厉害方法的边界处添加类型检查类型转换的方法。也就是说,泛型信息不会进入到运行时阶段。

总结:泛型类型在逻辑上可以看成是多个不同的类型,实际都是相同的类型。

泛型的使用

泛型有三种使用方式:泛型类,泛型接口,泛型方法。

泛型类

泛型类型用于类的定义之中,被称为泛型类。通过泛型可以完成对 一组 类的操作对外开放相同的接口。最典型的就是各种容器类,比如 List,Set,Map。

基本写法:

class 类名称 <泛型标识可以随便写任意标识号标识指定的泛型的类型>{
  private 泛型标识 /*(成员变量类型)*/ var; 
  .....

  }
}

下面是自己的几个例子:

GenericClassExample.java

package GenericStudy.GenericClass;

public class GenericClassExample<SB> {
    private SB key;

    public GenericClassExample(SB key){
        this.key=key;
    }

    public SB getKey(){
        return key;
    }
}


在这个例子之中,我们首先定义了一个类型 SB,用来作为泛型的指定参数。

GenericClassTesting.java

package GenericStudy.GenericClass;


public class GenericClassTesting {
    public static void main(String[] args) {
        GenericClassExample<Integer> genericInteger = new GenericClassExample<Integer>(123);
        GenericClassExample<String> genericString =  new GenericClassExample<String>("Hello");

        System.out.println("genericInteger is "+genericInteger.getKey());
        System.out.println("genericString is "+genericString.getKey());

        System.out.println("********Below is without real parameter of type*******");
        GenericClassExample generic = new GenericClassExample(123);
        GenericClassExample generic2 = new GenericClassExample("String");

        System.out.println("Generic is "+generic.getKey());
        System.out.println("Generic2 is "+generic2.getKey());
    }

}


结果是:

genericInteger is 123
genericString is Hello
********Below is without real parameter of type*******
Generic is 123
Generic2 is String

在第二个例子之中,我们对两种类型分别做了对比,一种是指定传入的泛型类型 GenericClassExample<Integer>GenericClassExample<String>,一种直接使用泛型,没有对其具体的传入类型进行指定。下面的输出,可以看出两种情况都可以正常输出结果。但是第二种情况IDEA会有如下的提示:

Screenshot 2020-01-02 at 5.10.39 PM

其提示为:这里的使用方法是 unchecked call,IDEA默认会建议将其中的类型参数补上。

如果没有指定具体的传入类型,那么这里的泛型就没法起到真正的检查作用。其传入方法的类型可以为任何的类型。

注意:

  1. 泛型的类型参数只可以为类类型,不可以为简单类型

Screenshot 2020-01-02 at 5.14.41 PM

  1. 不可以对确定的泛型类型做 instanceof 操作,比如下面的操作就是非法的,编译就会出错:
if(ex_num instanceof Generic<Number>){   
} 

泛型接口

泛型接口和泛型类的定义和使用基本相同。泛型接口经常被用在各种类的生产器之中,下面这个例子:

public interface GenericInterface<SB>{
    public SB next();
}

定义了一个泛型接口。

当实现泛型接口的类没有传入泛型实参的时候:

在没有传入泛型实参的时候,和泛型类的定义相同,在声明类的时候,要把泛型的声明也一起加到类之中。

例如:class GenericClassImpl<SB> implements GenericInterface<SB>

如果不声明泛型,那么编译器会报错:”Unknown class”

下面是例子:

package GenericStudy.GenericClass;

public class GenericClassImpl<SB> implements GenericInterface<SB> {
    @Override
    public SB next(){
        return null;
    }
}

当实现泛型接口的类,传入泛型实参的时候,要将所有的泛型类型都替换成具体的泛型实参。比如我们以 SB 作为泛型类型,那么要在 implement 的过程之中将所有的 SB 都换成 String或者其他要使用的类型。

package GenericStudy.GenericClass;

import java.util.Random;

public class ClassUseGenericInterface implements GenericInterface<String> {
    private String[] fruits = new String[]{"Apple","Banana","Pear"};

    @Override
    public String next() {
        Random rand=new Random();
        return fruits[rand.nextInt(3)];
    }
}

泛型通配符(Wildcard)

package GenericStudy.GenericClass;


public class GenericClassTesting {
    public static void main(String[] args) {
        GenericClassExample<Integer> genericInteger = new GenericClassExample<Integer>(123);
        GenericClassExample<String> genericString = new GenericClassExample<String>("Hello");

        System.out.println("genericInteger is " + genericInteger.getKey());
        System.out.println("genericString is " + genericString.getKey());

        System.out.println("********Below is without real parameter of type*******");
        GenericClassExample generic = new GenericClassExample(123);
        GenericClassExample generic2 = new GenericClassExample("String");

        System.out.println("Generic is " + generic.getKey());
        System.out.println("Generic2 is " + generic2.getKey());

        System.out.println("*********Below is Wildcard example***************");
        GenericClassExample<Integer> gInteger = new GenericClassExample<>(123);
        GenericClassExample<Number> gNumber = new GenericClassExample<>(456);
        showValue(gInteger);
    }

    public static void showValue(GenericClassExample<Number> obj){
        System.out.println("Key value is "+obj.getKey());
    }

}

编译之后会返回给我们下面的信息:

Error:(22, 19) java: incompatible types: GenericStudy.GenericClass.GenericClassExample<java.lang.Integer> cannot be converted to GenericStudy.GenericClass.GenericClassExample<java.lang.Number>

提示信息是其不可以被直接转换,那么可以看出,不同版本的泛型类实例是互不兼容的

那么如何解决这个办法呢?毕竟如果为了Integer的情况再写一个 showValue(GenericClassExample<Integer> obj) ,那么与java的多态理念是相悖的。因此,我们需要一个在逻辑上面可以同时表示Generic<Integer>Generic<Number> 父类的引用类型,由此,类型通配符应运而生。

将上面的方法略改一下:

 public static void showValue(GenericClassExample<?> obj){
        System.out.println("Key value is "+obj.getKey());
    }

 此处的类型通配符,一般是使用 ? 来代替具体的类型实参。注意,此处’?’ 是类型实参,而不是类型形参! 直白一些说,此处的 ‘?’ 就是和 Number, String 等等一样的实际类型,可以将其看做是所有类型的父型。

当操作类型时,不需要使用类型的具体功能时,只使用Object类之中的功能,那么就可以用 ? 通配符来表示未知类型。

泛型方法

首先一句话总结:泛型方法是在调用方法的时候指明泛型的具体类型的方法

总结一下,所有需要使用的泛型都需要在<> 之中进行声明,以下面的泛型方法举例:

    public static <SB> SB genericMethod(Class<SB> sbClass) throws InstantiationException, IllegalAccessException {
        SB instance = sbClass.newInstance();
        return instance;
    }

在使用这个泛型方法的时候,首先在<SB> 之中声明了此方法之中涉及的泛型变量,然后在返回类型和传入类型之中都加入了这个<SB>

泛型类之中使用了泛型的成员方法并不是泛型方法。

下面是自己的例子和输出。

package GenericStudy.GenericClass;


public class GenericClassTesting {
    public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException {
        GenericClassExample<Integer> genericInteger = new GenericClassExample<Integer>(123);
        GenericClassExample<String> genericString = new GenericClassExample<String>("Hello");

        System.out.println("genericInteger is " + genericInteger.getKey());
        System.out.println("genericString is " + genericString.getKey());

        System.out.println("********Below is without real parameter of type*******");
        GenericClassExample generic = new GenericClassExample(123);
        GenericClassExample generic2 = new GenericClassExample("String");

        System.out.println("Generic is " + generic.getKey());
        System.out.println("Generic2 is " + generic2.getKey());

        System.out.println("*********Below is Wildcard example***************");
        GenericClassExample<Integer> gInteger = new GenericClassExample<>(123);
        GenericClassExample<Number> gNumber = new GenericClassExample<>(456);
        showValue(gInteger);

        System.out.println("************");
//        GenericClassExample gExample = new GenericClassExample();
        Object obj = genericMethod(Class.forName("GenericStudy.GenericClass.GenericClassImpl"));
        System.out.println(obj);
    }

    public static void showValue(GenericClassExample<?> obj) {
        System.out.println("Key value is " + obj.getKey());
    }

    public static <SB> SB genericMethod(Class<SB> sbClass) throws InstantiationException, IllegalAccessException {
        SB instance = sbClass.newInstance();
        return instance;
    }

}

输出为:

genericInteger is 123
genericString is Hello
********Below is without real parameter of type*******
Generic is 123
Generic2 is String
*********Below is Wildcard example***************
Key value is 123
************
GenericStudy.GenericClass.GenericClassImpl@ce3af2b1

Process finished with exit code 0

类型擦除

Java的泛型都是伪泛型,这是什么意思呢?因为在编译期间,所有的泛型信息都会被直接擦除掉。那么就涉及到了我们这一章的标题:类型擦除

具体证明在我们之前的:

Screenshot 2019-12-27 at 6.19.28 PM

之中所看到的代码标黄的部分。

如果我们这个地方使用两个类的 getClass 方法,那么我们也可以发现其结果为 true,这样意味着泛型都被擦除掉了,最后剩下的只是原始类型。

下面是个例子:

package GenericStudy.GenericClass;

import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;

public class Test4 {
    public static void main(String[] args) throws IllegalArgumentException, SecurityException, IllegalAccessException, InvocationTargetException, NoSuchMethodException, InvocationTargetException {
        ArrayList<Integer> arrayList3 = new ArrayList<Integer>();
        arrayList3.add(1);//这样调用add方法只能存储整形,因为泛型类型的实例为Integer  
        arrayList3.getClass().getMethod("add", Object.class).invoke(arrayList3, "asd");
        for (int i = 0; i < arrayList3.size(); i++) {
            System.out.println(arrayList3.get(i));
        }
    }
}

结果为:

1
asd

可见此处使用反射,我们可以在已经指定了泛型类型为 Integer 的 List 之中赋入 String。

这说明了Integer泛型在编译之后已经擦除了,只剩下了原始类型。

类型擦除之后的原始类型

首先我们要知道:什么是原始类型?

原始类型,是擦除了泛型信息,最后在字节码之中的类型变量的真实类型。

比如将所有泛型替换成 Object 之后的类。

在编译的时候进行检查的注意事项

public class Test10 {  
    public static void main(String[] args) {  
          
        //  
        ArrayList<String> arrayList1=new ArrayList();  
        arrayList1.add("1");//编译通过  
        arrayList1.add(1);//编译错误  
        String str1=arrayList1.get(0);//返回类型就是String  
          
        ArrayList arrayList2=new ArrayList<String>();  
        arrayList2.add("1");//编译通过  
        arrayList2.add(1);//编译通过  
        Object object=arrayList2.get(0);//返回类型就是Object  
          
        new ArrayList<String>().add("11");//编译通过  
        new ArrayList<String>().add(22);//编译错误  
        String string=new ArrayList<String>().get(0);//返回类型就是String  
    }  
}  

上面这个例子就是编译时候的检查方式。解释如下:

类型检查,是在编译的时候完成的。new ArrayList()只是在内存之中开辟一个内存空间而已,其内部可以存储各种类型的对象,真正有类型检查的是其引用,也就是等号左边的部分,因为我们是使用等号左边的部分,比如arrayList1 来进行的方法调用,所以 arrayList1 引用就可以完成泛型类型的检查。但是arrayList2 就没有使用泛型,所以不可以。

上面这个例子,我们就可以看出来,类型检查就是针对引用的,而无关其真正引用的对象。