前言
??大家好,我是小窝,今天来为大家介绍擦除机制在java中的应用。 说到擦除机制,我们不得不提Java中的泛型,因为擦除机制可以说就是为Java泛型而构造出来的。在这篇博客里我们先简单了解下泛型,关于泛型的详细知识,博主之后将会再写一篇博客带你领略泛型的妙处所在。🔔因为完全理解泛型得首先了解擦除机制,而初步认识现在就够理解了。
📖博客主页:海绵宝宝养的小窝 👏欢迎关注🔥点赞💓收藏?评论💬 ??看前先三连🙉养成好习惯🙈 📅首发时间:🌴 2022.01.08 🌴 ??生活不止眼前的苟且,还有诗和远方💫 🔑参考书籍:📚《Effective Java》、📚《深入理解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){
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(){
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同一个泛型异常类的多个实例
??原理同上一条
💡还有许多都是因为擦除机制带来的问题,博主在此就不一一举例了,只需明白在使用泛型时要考虑到擦除机制带来的影响。
📌结语
这篇博客的分享就到此结束了。下一篇博主将就泛型来详细介绍。
如果觉得文章写的不错的话,可以给个三连不🙈谢谢支持了
|