当前位置:网站首页>泛型
泛型
2022-07-27 14:12:00 【James-Tom】
1、定义
泛型是java1.5版本引进的概念。
有两种定义:
1、在程序编码中一些包含类型参数的类型,也就是说泛型的参数只可以代表类,不能代表个别对象。
2、在程序编码中一些包含参数的类。其参数可以代表类或对象等等。
不论使用哪个定义,泛型的参数在真正使用泛型时都必须作出指明。
上面两种定义不是很好理解,个人理解的话,想象成可以在定义时替代整型、字符串类型、对象类的一个集合代表类。
2、常用术语
2.1、类型擦除
下面这段代码,会输出什么?
List<String> sList = new ArrayList<>();
List<Integer> iList = new ArrayList<>();
System.out.println(sList.getClass() == iList.getClass());
输出结果:
true
泛型信息只存在代码编译阶段,在进入JVM之前,与泛型相关的信息会被擦除掉,专业术语叫做类型擦除。
对于List 和 List而言它们的Class类型在 jvm 中的 Class 都是 List.class。泛型类型信息被擦除了。
指定的 String 和 Integer 到底体现在哪里呢?
答案是体现在类编译的时候。当JVM进行类编译时会进行泛型检查,如果一个集合被声明为String类型,那么它往该集合存取数据的时候就会对数据进行判断,从而避免存入或取出错误的数据。
也就是说:泛型只存在与编译阶段,而不存在于运行阶段。在编译后的class文件中,是没有泛型这个概念的。
2.2、通配符
Rider<? extends Animal> rider1 = new Rider<Horse>();
Rider<? super Horse> rider4 = new Rider<Animal>();
上段代码中 extends 和 super关键字就是泛型通配符。
在介绍泛型通配符之前,需要对编译时类型和运行时类型有一个基本的了解,才能更好的理解通配符的使用。
先定义一个父类(Animal) 和一个子类(Horse) :
public class Animal {
}
public class Horse extends Animal {
}
再创建一个Horse对象
Horse horse = new Horse();
上面这段代码中,horse实例指向的对象,在编译时类型和运行时类型都是Horse类型。
但是我们有时候可以使用下面的这种写法
Animal horse1 = new Horse();
使用Animal类型的变量指向了一个Horse对象 ,因为在java中允许把一个子类的对象直接赋值给一个父类的引用变量,我们称之为[向上转型]。
但是问题来了,此时horse1变量所指向的对象,在编译时类型和运行时类型是什么呢?
答案是:horse1变量所指向的对象,其在编译时的类型就是Animal类型,而在运行时的类型就是Horse类型。
这是为什么呢?
因为在编译的时候,JVM只知道Animal类变量指向了一个对象,并且这个对象是Animal的之类的对象或自身对象,其具体的类型并不确定,有可能是Horse,也有可能是Mule类型。而为了安全方面考虑,JVM此时将horse1变量指向的对象定义为Animal类型。因为无论其实Horse类型还是Mule类型,他们都可以安全转为Animal类型。
而在运行时阶段,JVM通过初始化知道了它指向一个Horse对象,所以在其运行时的类型就是Horse类型。
2.2.1、向上转型
上面有了一个父类Animal和一个子类Horse,这时候再增加一个骑手类,Rider类。Rider类定义了一些基本的行为:
public class Rider<T> {
private List<T> list;
public Rider() {
list = new ArrayList<T>();
}
public void ride(T item) {
list.add(item);
}
public T get() {
return list.get(0);
}
}
骑手类Rider类定义了一个T泛型类型,可以接受任何类型,如骑手可以骑马,骑一些动物等,主要是具备骑这个行为。
例如定义一个骑手,代码片段如下:
Rider<Animal> rider=new Rider<Animal>();
例如骑手骑乘一些动物,代码片段如下:
rider.ride(new Animal());
rider.ride(new Horse());
按照java向上转型的原则,我们可以这样定义一个骑手:
Rider<Animal> rider1=new Rider<Horse>();
但上面这段代码在编译的时候会报错:Incompatible types.。
按理说,这种写法应该没问题,因为java支持向上转型。
错误的原因就是:java并不支持泛型的向上转型,所以不能够使用上面的写法。
但是可以采用泛型通配符来解决这个问题。
正确的代码片段如下:
Rider<? extends Animal> rider1 = new Rider<Horse>();
上面这行代码标识:Rider可以指向任何Animal类对象,或者任何Aimal的子类对象。
Horse是Animal的之类,所以可以正常编译通过。
2.2.2、extends通配符的缺陷
虽然通过extends通配符支持了java泛型的向上转型,但是这种方式是有缺陷的,那就是:其无法向Rider中添加任何对象,只能从中读取对象。
Rider<? extends Animal> rider1 = new Rider<Horse>();
rider1.ride(new Horse());//编译报错
rider1.ride(new Mule());//编译报错
Animal animal1 = rider1.get();//编译成功
可以看到,当我们让骑手骑马和骡子时,会发现编译错误。但是我们可以从中取出被骑的动物。
那为什么我们会无法让骑手骑动物呢?
这个还得从这还得从我们对骑手的定义说起。
Rider<? extends Animal> rider = new Rider<XXX>();
在上面对骑手的定义种,rider可以指向任何Animal对象,或者是Animal子类对象。也就是说,plate属性指向的对象其在运行时可以是Horse类型,也可以是Mule类型。
如下面的代码片段都是正确的:
Rider<? extends Animal> rider1 = new Rider<Horse>();
Rider<? extends Animal> rider2 = new Rider<Mule>();
在还没有具体运行时,JVM并不知道我们要让骑手骑什么动物,到底是马还是骡子,完全不知道。既然不能确定要骑的什么动物,那么JVM就干脆什么都不给放,避免出错。
鉴于此,所以当使用extends通配符时,无法向其中添加任何东西。
但是为什么可以取出数据呢?
因为无论是取出马还是骡子,都可以通过向上转型用Animal类型的变量指向它,这在java中都是允许的。
Animal animal1 = rider1.get();//编译成功
Horse horse1 = rider1.get();//编译错误
在上面的代码片段中,当使用一个Horse类型的变量指向一个从骑手类里取出待骑对象马时,是会提示错误的。
所以当使用extends通配符时,可以取出所有东西。
总结,通过 extends 关键字可以实现向上转型。但是却失去了部分的灵活性,即不能往其中添加任何东西,只能取出东西。
2.2.3、super通配符的缺陷
与extends通配符相似的而另一个通配符是:super通配符,其特性与extends完全相反。同样的也有缺陷:super通配符可以存入对象,但是取出对象的时候受到限制。
使用示例片段如下:
Rider<? super Horse> rider3 = new Rider<Horse>();
Rider<? super Horse> rider4 = new Rider<Animal>();
Rider<? super Horse> rider5 = new Rider<Object>();
上面代码表示:Rider 变量可以指向一个特定类型的 Rider 对象,只要这个特定类型是 Horse 或 Horse 的父类。Object也是Animal的父类,同理关系可以继承。
也就是说rider5指向的具体类型可以是Horse的父级,JVM在编译的时候肯定无法判断具体是哪个类型。但是JVM能确定的是,任何Horse的子类都可以转为Horse类型,但任何Horse的父类都无法转换为Horse类型。
所以对于使用了super通配符的情况,我们只能存入T类型及T类型的子类对象。
rider4.ride(new Horse());
rider4.ride(new Warhorse());
rider4.ride(new Animal());//编译错误
当我们传入Horse的父类Animal时就会报编译错误。
而当我们取出数据的时候,也是类似的,JVM在编译的时候知道,我们具体的运行时类型可以是任何Horse的父类,那么为了安全起见,我们就用一个最顶层的父级来指定取出的数据,这样就可以避免发生强制类型转换异常了。
Object object = rider4.get();
Horse object = rider4.get();//编译错误
从上面的代码可以知道,当使用Horse类型的变量指向Rider取出的对象,会出现编译错误,而使用Objec类型的变量指向Rider取出的对象,则可以正常通过。
也就是说对于使用了super通配符的情况,我们取出的时候只能用Object类型的变量指向取出的对象。
3、总结
1、当你的情景是生产者类型,需要获取资源以供生产时,我们建议使用 extends 通配符,因为使用了 extends 通配符的类型更适合获取资源。
2、当你的场景是消费者类型,需要存入资源以供消费时,我们建议使用 super 通配符,因为使用 super 通配符的类型更适合存入资源。
3、如果你既想存入,又想取出,那么你最好还是不要使用 extends 或 super 通配符。

微信公众号:一粒尘埃的漫旅
里面有很多想对大家说的话,就像和朋友聊聊天。
写代码,做设计,聊生活,聊工作,聊职场。
我见到的世界是什么样子的?
搜索关注我吧。
边栏推荐
猜你喜欢

于不确定中见“安全感” 沃尔沃2022年中问道

Graphic SQL of giant image

周鸿祎:数字安全能力落后也会挨打

移动端使用vantUI的list组件,多个tab项来回切换时,列表加载多次导致数据无法正常展示

终于有人把面试必考的动态规划、链表、二叉树、字符串全部撸完了

Tencent two sides: @bean and @component are used in the same class, what will happen?

Construction of knowledge map of financial securities and discovery of related stocks from the perspective of knowledge relevance

@Bean 与 @Component 用在同一个类上,会发生什么?

谷歌团队推出新Transformer,优化全景分割方案|CVPR 2022
![[work] about technical architecture](/img/24/f3402c04157ce9a8846580f017f472.png)
[work] about technical architecture
随机推荐
什么是Tor?Tor浏览器更新有什么用?
Automatically configure SSH password free login and cancel SSH password free configuration script
网络设备硬核技术内幕 路由器篇 13 从鹿由器到路由器(上)
Jmeter录制接口自动化
Research on Chinese idiom metaphorical knowledge recognition and relevance based on transfer learning and text enhancement
两阶段提交与三阶段提交
Unity3D学习笔记10——纹理数组
一些二进制位操作
LeetCode 1143. 最长公共子序列 动态规划/medium
数据仓库项目从来不是技术项目
NEFU118 n! How many zeros are there after [basic theorem of arithmetic]
mysql保存数据提示:Out of range value for column错误
aac 和 h264等的时间戳
周鸿祎:数字安全能力落后也会挨打
炒黄金开户平台有没有正规,安全的
正则表达式:邮箱匹配
事务_基本演示和事务_默认自动提交&手动提交
OBS advanced DXGI acquisition screen process, and how to modify it to its own cursor
How to do well in enterprise system vulnerability assessment
网络设备硬核技术内幕 路由器篇 5 汤普金森漫游网络世界(上)