Java泛型探究及泛型擦除机制和如何跳过编译阶段

2022-12-02 09:34:13 浏览数 (1)

在工作闲暇之余,开始了对Java本身的探究,首先研究的便是日常使用的泛型

  • 泛型的原理: Java泛型是jdk5引入的一种机制。为了向下兼容,所以Java虚拟机是不支持泛型的,也就是说Java泛型是一种伪泛型机制。
  • 泛型的本质: 参数化类型

我们先来看泛型的使用,然后再看泛型的擦除机制

泛型的使用

要想使用好泛型,首先要对其基本的定义有所了解

泛型通配符的介绍
  1. 无边界通配符 举例:<?> 通用的类型
  2. 上边界通配符 举例:< ? extends Number > 代表从Number往下的子类或孙类对象都可以使用
  3. 下边界通配符 举例:<? super Integer> 代表从Integer 到Object所有的对象都可以

泛型的具体的使用

规则

必须先声明再使用

泛型的声明是通过"<>"实现

约定泛型可以使用单个大写字母来表示 K E T V 等

常见的泛型使用:

泛型类

代码语言:javascript复制
public class PersonNew <T> {

    private T t;

    public T getT() {
        return t;
    }

    public void setT(T t) {
        this.t = t;
    }

    public PersonNew(T t) {
        this.t = t;
    }
}

增加我们代码的灵活度

泛型方法

代码语言:javascript复制
public class Demo07 <K,V> {

    /**
     * 普通方法 可以使用 类中定义的泛型
     * @param k
     * @param v
     * @return
     */
    public K method1(K k,V v){
        return (K)null;
    }

    /**
     * 普通方法  使用方法中定义的泛型
     * @param t
     * @param v
     * @param <T>
     * @return
     */
    public <T> T method2(T t,V v){
        return (T)null;
    }

    /**
     * 在静态方法中我们没法使用 类中定义的泛型
     * @return
     */
    public static <K> K method3(){
        return null;
    }

泛型接口

代码语言:javascript复制
public interface CalGeneric <T> {

    T add(T a,T b);

    T sub(T a,T b);

    T mul(T a,T b);

    T div(T a,T b);
}

泛型的擦除机制

  • 泛型的擦除机制: 伪泛型机制就是说:在编译期间把泛型的信息全部擦除掉了, 泛型只在编译阶段有效,编译之后JVM会采取去泛型化的措施.所以泛型最终都变成了最原始的类型(Object); 在运行期就不存在泛型的信息。

在这里贴上两句百度上对泛型的解释:

  Java 泛型的参数只可以代表类,不能代表个别对象。   由于 Java泛型的类型参数之实际类型在编译时会被消除,所以无法在运行时得知其类型参数的类型。Java编译器在编译泛型时会自动加入类型转换的编码,故运行速度不会因为使用泛型而加快

  泛型是Java SE 1.5的新特性,泛型的本质是参数化类型,也就是说所操作的数据类型被指定为一个参数。这种参数类型可以用在类、接口和方法的创建中,分别称为泛型类、泛型接口、泛型方法。 Java语言引入泛型的好处是安全简单。   泛型的好处是在编译的时候检查类型安全,并且所有的强制转换都是自动和隐式的,以提高代码的重用率。 ————百度百科

  也就是说,我们日常使用的泛型,JVM并不知道它的存在,因为泛型在编译阶段就已经被处理成普通的类和方法

那么编译期是怎么擦除泛型的呢? 通过研究发现,其实就是将泛型指定为限定类型而已

编译器怎么擦除泛型的?

获取泛型的类型,再转换罢了

  • 如果没有指定类型,使用Object作为原始类型;
  • 若有限定类型< T exnteds X >,使用X作为原始类型;
  • 如果有多个限定类型(<T extends A & B & C >),则用第一个边界A作为原始类型

在必要时还会插入类型转换以保持类型安全

怎么证明编译器擦除了泛型?

举个栗子:

代码语言:javascript复制
        List<String> arrList = new ArrayList<>();
        List<Integer> intList = new ArrayList<>();
        System.out.println(arrList.getClass()==intList.getClass());

我们创建两个指定不同类型的泛型的List,然后进行比较

呐,两个List其实就是一个实例 ArrayList 和 ArrayList 在编译的时候是完全不同的类型,但是运行结果却是true,这就Java泛型的类型擦除造成的。 我们再获取一下ClassName,输出一下看看

代码语言:javascript复制
System.out.println(arrList.getClass().getName() "n" intList.getClass().getName());

结果如下图:

因为不管是 ArrayList 还是 ArrayList,在编译完成后都会被编译器擦除成了 ArrayList。

Java 泛型擦除是 Java 泛型中的一个重要特性,其目的是避免过多的创建类而造成的运行时的过度消耗。所以,想 ArrayList 和 ArrayList 这两个实例,其类实例是同一个。

那么使用泛型的副作用有哪些呢?

  1. 使用泛型后,不能使用基本数据类型(byte,short,int ,long,float,double,boolean,char); 至于原因嘛,上面也讲了,擦除后变成Object,而Object无法存放int类型
  2. 不能使用 instanceof运算符 原因:因为擦除后只剩下原始类型,泛型信息不存在。

额外的思考: 上面已经分析过,泛型是编译阶段有效的,如果我们插入数据时想要跳过编译阶段,应该怎么做呢? 没错,可以通过反射的方式达成我们的目的

代码语言:javascript复制
        List<String> arrList = new ArrayList<>();
        arrList.add("1");
        arrList.add("2");
        Class<? extends List> aClass = arrList.getClass();
        Method method = aClass.getDeclaredMethod("add",Object.class);
        method.invoke(arrList,new Object());
        System.out.println(arrList);

0 人点赞