泛型的初步认识(2)

前言~🥳🎉🎉🎉

hellohello~,大家好💕💕,这里是E绵绵呀✋✋ ,如果觉得这篇文章还不错的话还请点赞❤️❤️收藏💞 💞 关注💥💥,如果发现这篇文章有问题的话,欢迎各位评论留言指正,大家一起加油!一起chin up!👍👍

💥个人主页:E绵绵的博客**
💥所属专栏:JAVA知识点专栏 JAVA题目练习 c语言知识点专栏 c语言题目练习**
这篇文章我们将继续介绍泛型,相比于上一篇文章,这篇文章内容更深,更难理解。还请好好阅读消化。
参考文章:Java 中的泛型(两万字超全详解)_java 泛型-CSDN博客

🎯🎯泛型绝对要注意的一点

🎯🎯在java中,我们无法直接实例化泛型的类型参数对象. 如存在<T>,我们就不能new T()或者new T[]等等,凡是牵扯到创建T相关的对象都会报错。

而之所以该行为会报错是因为它牵扯了类型擦除这个很深层的知识点,那么我们来看下类型擦除是什么吧。

类型擦除

类型擦除的定义

在Java中,类型擦除是指在编译时期对泛型类型进行擦除,将泛型类型转换为原始类型。(原始类型大部分情况下都是Object类)

❤️❤️换而言之,泛型信息只存在于代码编译阶段,在代码编译结束后,与泛型相关的信息会被擦除掉替换为原始类型,专业术语叫做类型擦除。也就是说,成功编译过后的 class 文件中不包含任何泛型信息,泛型信息不会进入到运行时阶段。这样做的目的是为了保持与旧版本的Java代码的兼容性。
这有一个例子能验证编译时泛型会进行类型擦除,假如我们给 ArrayList 集合传入两种不同的数据类型,并比较它们的类信息:

复制代码
public class GenericType {
    public static void main(String[] args) {  
        ArrayList<String> arrayString = new ArrayList<String>();   
        ArrayList<Integer> arrayInteger = new ArrayList<Integer>();   
        System.out.println(arrayString.getClass() == arrayInteger.getClass());// true
    }  
}

在这个例子中,我们定义了两个 ArrayList 集合,不过一个是 ArrayList< String>,只能存储字符串。一个是 ArrayList< Integer>,只能存储整型对象。我们通过 arrayString 对象和 arrayInteger 对象的 getClass() 方法获取它们的对象信息并比较,发现结果为true。

明明我们在 <> 中传入了两种不同的数据类型,按照上文所说的,它们的类型参数 T 不是应该被替换成我们传入的数据类型了吗,那么结果应该是不同的,那为什么它们的对象信息还是相同呢? 这是因为在编译期间,所有的泛型信息都会被擦除变为原始类型, 所以这两个对象信息在运行时就完全相同,结果就为true。
我们还可以通过观察编译之后生成的的字节码发现一个现象,所有的T编译后都变为Object。
那么是不是所有的类型参数被擦除后都以 Object 类进行替换呢?

答案是否定的,大部分情况下,类型参数 T 被擦除后都会以 Object 类进行替换;而有一种情况则不是,那就是使用到了 extends 和 super 语法的有界类型参数。

当为上界时,假设定义一个泛型类如下:

复制代码
public class Caculate<T extends Number> {
	private T num;
}

将其反编译:

复制代码
public class Caculate {
	public Caculate() {}// 默认构造器,不用管

	private Number num;
}

可以发现,使用到了 extends (上界)语法的类型参数 T 被擦除后会替换为 Number 而不再是 Object。

复制代码
public class Example<T super Number> {
    private T value;
    
    public Example(T value) {
        this.value = value;
    }
    
    public T getValue() {
        return value;
    }
}

同理对于下限虽然我们没学,但是我们也要知道在类型擦除上其跟上限差不多,泛型类Example使用super关键字限定了泛型类型参数T的下界为Number,在编译时期,T会被擦除成为Number类型。

类型擦除的原理

假如我们定义了一个 ArrayList< Integer > 泛型集合,若向该集合中插入 String 类型的对象,不需要运行程序,编译器就会直接报错。这里可能有小伙伴就产生了疑问:

不是说泛型信息在编译的时候就会被擦除掉吗?那既然泛型信息被擦除了,如何保证我们在集合中只添加指定的数据类型的对象呢?换而言之,我们虽然定义了 ArrayList< Integer > 泛型集合,但其泛型信息最终被擦除后就变成了 ArrayList< Object > 集合,那为什么不允许向其中插入 String 对象呢?

Java 是如何解决这个问题的?

  • 其实在创建一个泛型类的对象时, Java 编译器是先检查代码中传入 < T > 的数据类型,并记录下来,然后再对代码进行编译,编译的同时进行类型擦除;如果需要对被擦除了泛型信息的对象进行操作,编译器会自动将对象进行强制类型转换。
    我们可以把泛型的类型安全检查机制和类型擦除想象成演唱会的验票机制:以 ArrayList< Integer> 泛型集合为例。

1.当我们在创建一个 ArrayList< Integer > 泛型集合的时候,ArrayList 可以看作是演唱会场馆,而< T >就是场馆的验票系统,Integer 是验票系统设置的门票类型;
2.当验票系统设置好为< Integer >后,只有持有 Integer 门票的人才可以通过验票系统,进入演唱会场馆(集合)中;若是未持有 Integer 门票的人想进场,则验票系统会发出警告(编译器报错)。
3.在通过验票系统时,门票会被收掉(类型擦除),但场馆后台(JVM)会记录下观众信息(泛型信息)。
4.进场后的观众变成了没有门票的普通人(原始数据类型)。但是,在需要查看观众的信息时(操作对象),场馆后台可以找到记录的观众信息(编译器会自动将对象进行类型转换)。

如下是一个例子:

复制代码
public class GenericType {
    public static void main(String[] args) {  
        ArrayList<Integer> arrayInteger = new ArrayList<Integer>();// 设置验票系统   
        arrayInteger.add(111);// 观众进场,验票系统验票,门票会被收走(编译时会进行类型擦除)
        Integer n = arrayInteger.get(0);// 获取观众信息,编译器会自动进行强制类型转换
        System.out.println(n);
    }  
}

擦除 ArrayList< Integer > 的泛型信息后,泛型类型参数都变为Object,get() 方法的返回值将返回 Object 类型,但编译器会自动插入 Integer 的强制类型转换。也就是说,编译器把 get() 方法调用翻译为两条字节码指令:

对原始方法 get() 的调用,返回的是 Object 类型;
将返回的 Object 类型强制转换为 Integer 类型;

代码如下:

复制代码
	Integer n = arrayInteger.get(0);// 这条代码底层如下:
	
	//(1)get() 方法的返回值返回的是 Object 类型
	Object object = arrayInteger.get(0);
	//(2)编译器自动插入 Integer 的强制类型转换
	Integer n = (Integer) object;

类型擦除小结

1.泛型信息(包括泛型类、接口、方法)只在代码编译阶段存在,在代码成功编译后,其内的所有泛型信息都会被擦除,并且类型参数 T 会被统一替换为其原始类型(默认是 Object 类,若有 extends 或者 super 则另外分析);

2.在泛型信息被擦除后,若还需要使用到对象相关的泛型信息,编译器底层会自动进行类型转换(从原始类型转换为未擦除前的数据类型)。

泛型绝对要注意的一点 (续写)

❤️❤️所以我们可以得出原因,在Java中,不能直接使用new关键字创建泛型对象。这是因为Java的泛型是在编译时期进行类型擦除的,即在运行时泛型信息被擦除,只保留原始类型,我们不清楚其原本的具体类型。因此,编译器不允许直接创建泛型对象。
因此如T[] ts = new T[5];是会报错的,那有人这样思考,既然这样的代码不行,那么我们将其修改成这样的代码:T[] array = (T[])new Object[10]; 是否就足够好,答案是未必的。

T[] array = (T[])new Object[10]; 在大部分情况下都是能正常使用的,但是在一些特殊情况下如以下代码是不能正常使用

复制代码
class MyArray<T> {
    public T[] array = (T[])new Object[10];
//编译之后类型擦除变为 object[] array=(Object)new Object[10],所以运行时成立
 //如果编译之后不会进行类型擦除,则会发生类型转换错误
    public T getPos(int pos) {
        return this.array[pos];
   }
    public void setVal(int pos,T val) {
        this.array[pos] = val;
   }
    public T[] getArray() {
        return array;
   }
}
 
 public static void main(String[] args) {
     MyArray<Integer> myArray1 = new MyArray<>();
 
     Integer[] strings = myArray1.getArray();
       //因为array的对象是以Object为实例创建的,所以返回出来也是Object类
       //如果是返回出Integer,则直接报错,所以编译器此时不会自动强制类型转换
       //而前面都没报错,我们却在返回出Object时,用Integer接收,所以报错
 }

所以在这情况下报错了,通俗讲就是:返回的Object数组直接转给Integer类型的数组,编译器认为是不安全的,直接报错。

正确方式应该是 但对我们来说,这个又太过复杂了,所以对于这种类似形式的代码:T[] array = (T[])new Object[10]; 一般是不采用的。我们大可看一下源码是怎么创建类数组的:
而在我们的源码中类数组的创建都是用 Object[] array = new Object[n];该种形式去创建的,而不是T[] array = (T[])new Object[n];

❤️❤️所以以后我们类数组的创建都是直接 Object[] array = new Object[n]; ,一切向源码看齐,源码也是这么用的,我们最好不要用T[] array = (T[])new Object[n];

总结

对于这篇文章的内容大家可能看的云里雾里,的确本人也觉得牵涉的很深,很绕。所以其实对于第二部分内容你只要了解清楚类型擦除这个机制和不能用new 实例化泛型对象就行了,其他的内容看的懂就看,看不懂也就算了。

所以我们的泛型的初步认识就这样结束啦,对于其泛型的进阶我们会在java数据结构快完结的时候讲。还希望各位大佬们能给个三连,点点关注,点点赞,发发评论呀,感谢各位大佬~❤️❤️💕💕🥳🎉🎉🎉

相关推荐
2401_859049083 分钟前
Git使用
arm开发·git·stm32·单片机·mcu·算法
是三好3 分钟前
并发容器(Collections)
java·多线程·juc
jian1105810 分钟前
java项目实战、pom.xml配置解释、pojo 普通java对象
java·开发语言·python
油头少年_w27 分钟前
Python 爬虫之requests 模块的应用
开发语言·爬虫·python
述雾学java28 分钟前
Spring Boot是什么?MybatisPlus常用注解,LambdaQueryWrapper常用方法
java·spring boot·后端
jinhuazhe201328 分钟前
maven 3.0多线程编译提高编译速度
java·maven
冠位观测者34 分钟前
【Leetcode 每日一题】2942. 查找包含给定字符的单词
算法·leetcode·职场和发展
小镇学者35 分钟前
【JS】Vue 3中ref与reactive的核心区别及使用场景
前端·javascript·vue.js
xosg1 小时前
HTMLUnknownElement的使用
java·前端·javascript
yi个名字1 小时前
C++继承:从生活实例谈面向对象的精髓
开发语言·c++·链表