Bootstrap

一文带你深入了解Java擦除机制

在这里插入图片描述
本文收录专栏📓《深入理解Java虚拟机》.

📌什么是擦除机制

🍊Java在编译后的字节码(.class)文件中是不包含泛型中的类型信息的,使用泛型的时候加上的类型参数,会在编译的时候被擦除,这个过程就叫做类型擦除机制。

🍺泛型

关于泛型,我们先来简单了解下。
⭐️泛型可以理解为对类型的抽象。以前我们定义一个属性或者方法的时候,我们都会明确具体的类型,比如int、String、void等等,但泛型不同,泛型是一个参数化类型,即不明确类型,只有在具体调用对象的时候,才传递实际类型实参。指定了泛型参数的类型就是一个具体化了的类型。

🌰举个栗子:

    List arrayList = new ArrayList();
         arrayList.add("aaa");
         arrayList.add(100);
         for (int i = 0; i < arrayList.size(); i++) {
             String item = (String) arrayList.get(i);
             System.out.println("泛型测试,item = "+item);
         } 

毫无疑问,程序的运行结果会以崩溃结束
在这里插入图片描述
ArrayList可以存放任意类型,例子中添加了一个String类型,添加了一个Integer类型,在使用时都以String的方式使用,因此程序崩溃了。为了解决类似这样的问题(在编译阶段就可以解决),泛型就应运而生了。
我们将ArrayList的初始化改一下:
List<String> arrayList = new ArrayList();
那么编译器将会直接在编译阶段就报错提醒我们只能存放String类型的数据。
在这里插入图片描述
这个<>里括起来的就是一种参数化的类型(也就是泛型),例如String、Integer、Float等。记住要用包装类,不能用int、char等基础数据类型。

正如我们前面所说泛型的类型参数会在编译的时候擦除,怎么证明呢?很简单,但我们写了如下代码后:
👇


> public class Test {
    public static void main(String[] args) {
        Map<String,String> map = new HashMap<String,String>();
        map.put("三国演义","罗贯中");
        map.put("西游记","吴承恩");
        System.out.println(map.get("三国演义"));
        System.out.println(map.get("西游记"));
    }
}

因为泛型只在编译期就会被擦除,我们可以用反编译查看代码内的泛型是否被擦除。

我们使用命令反编译后:
👇在这里插入图片描述
发现泛型果然被擦除了,并且还将类型参数全都擦成了Object这个具体的类型。

为什么会被擦成object呢
⭐️原来,在JDK5.0之前,容器存储的对象都只有具有Java的通用类型:Object。单根继承结构意味着所有的东西都是Object类型,所以该容器可以存储任何东西(就像我们最开始写的代码一样,可以存,但取会报错),但是由于容器只存储Object,所以当将对象引入容器时,他必须被向上转型成Object。

📌擦除机制的过程

⭐️擦除机制过程可以理解为将泛型类变成普通Java代码的过程。一般包括两个步骤:
🍊1:将所有的泛型参数用其最左边界(最顶级的父类型)类型替换,默认则是Object。
🍊2:擦除泛型

🌰举个栗子:

public class MyClass < T extends TestB > {
    private T object;
    public void setObject (T object){
       this.object = object;
    }
    public T getObject(){
       return object;
    }
    public static void main(String[] args) {
      MyClass<TestC> testCMyClass = new MyClass<TestC>();
      testCMyClass.setObject(new TestC());
      TestC testC = testCMyClass.getObject();
      System.out.println(testC);
    }
}

在擦除之后,你可以把它理解为
👇

public class MyClass  {
   private TestB object;
   public void setObject (TestB object){
      this.object = object;
   }
   public TestB getObject(){
      return object;
   }
   public static void main(String[] args) {
     MyClass testCMyClass = new MyClass();
     testCMyClass.setObject(new TestC());
     TestC testC = testCMyClass.getObject();
     System.out.println(testC);
   }
}

擦除机制过程真有这么简单
我们再来看一个栗子🌰

public class Parent<T> {
    public Number get(T key){//number几乎包含了所有数据类型
        return 0;
    } 
}

class child1 extends Parent<String>{
    public Number get(String key){
        return 1;
    } 
}

class child2 extends Parent<String>{
    public Integer get(String key){
        return 2;
    }
}

我们知道有擦除机制在,上述代码的泛型都会被擦除,并且类型参数会被擦成Object,那么子类的重写方法的参数应该是Object的,但我们写成上述也并没有报错,这是怎么回事呢?

🍺桥接方法

看到标题你们应该猜出来了吧,没错,这是因为编译器为我们自动生成了桥接方法
那什么是桥接方法呢❓

栗子🌰

public class Parent {
    public Number get(){//number几乎包含了所有数据类型
        return 0;
    }
}

class child1 extends Parent{
    public Number get(){
        return 1;
    } 
}

class child2 extends Parent {
    public Integer get(){
        return 2;
    } 
}

我们先将代码转换成非泛型来瞧瞧,通过反编译
🍊child1
在这里插入图片描述

🍊child2
在这里插入图片描述
原来如此,编译器为我们自动生成了桥接方法,为父类和子类之间架起了一座桥梁。
🔑那么同理,原来的我们也通过反编译查看:
🍊parent
在这里插入图片描述

🍊child1
在这里插入图片描述
🍊child2
在这里插入图片描述

⭐️因为父类的T被擦成了object,所以编译器为我们先重写了父类的参数类型为object的方法,再通过该桥接方法区调用自己的重写的方法。

📌擦除机制带来的影响

上面也说了类型擦除机制的实现原理-类型擦除指的是通过类型参数合并,将泛型实例关联到同一份字节码上,在运行期间类型参数丢失。就单单只有一份字节码这个事上就会出现许多匪夷所思的问题。

🍊不能用同一个泛型类的实例区分方法签名

栗子🌰

public class Test {
    public void test(List<String> a){
        System.out.println("String");
    }
    public void test(List<Integer> b){
        System.out.println("Integer");
    }
}

⭐️这样是不行的,因为List< String >和List< Integeer >在类型擦除后都变成了List,那么两个方法的签名就一模一样了,编译器就会直接报错。

🍊不能同时catch同一个泛型异常类的多个实例

⭐️原理同上一条

💡还有许多都是因为擦除机制带来的问题,博主在此就不一一举例了,只需明白在使用泛型时要考虑到擦除机制带来的影响。

📌结语

这篇博客的分享就到此结束了。下一篇博主将就泛型来详细介绍。

;