本文收录专栏《深入理解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
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呢❓
⭐️原来,在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; } }
child2
原来如此,编译器为我们自动生成了桥接方法,为父类和子类之间架起了一座桥梁。
那么同理,原来的我们也通过反编译查看:
parent
⭐️因为父类的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,那么两个方法的签名就一模一样了,编译器就会直接报错。
⭐️原理同上一条
还有许多都是因为擦除机制带来的问题,博主在此就不一一举例了,只需明白在使用泛型时要考虑到擦除机制带来的影响。
这篇博客的分享就到此结束了。下一篇博主将就泛型来详细介绍。