目录
10、泛型
10.1、概念
在泛型没有引入的时候,Java有一个缺点,那就是把对象保存到集合中后,集合不会记住保存数据的类型,当后续再次使用时,这些数据的编译类型会变成Object。这样在使用时,往往都需要进行类型转换,这样不仅增加了复杂程度,在转换时还容易出现ClassCastException异常。
所以JDK1.5版本后引入了泛型这个概念。泛型的本质是把元素的类型设计成一个参数,允许程序在传递参数时指定传递的类型
。它就像是一个标签(比如超市的货柜上贴上标签后,该位置就只放和标签对应的商品),泛型就是告诉我们传递进去的数据是什么类型的,并且会对传递的参数进行约束,这样就能将参数进行统一,再获取时就不会出现ClassCastException异常。
泛型允许在定义类、接口时通过一个标识表示类中某个属性的类型或者是某个方法的返回值及参数类型,这个类型参数将在使用时是确定的。
泛型最大的好处有两点:
1、使用泛型能让我们在使用前就可以明确知道传递或返回的数据类型
2、泛型具有限制作用,一旦规定了一种数据类型,就只会接收这一种数据类型
10.2、泛型的使用
10.2.1、在集合中使用
比方说:我们使用ArrayList时,没有泛型。我们甚至不知道会保存进去什么类型的参数,这样使用起来显然很不方便。大家想想,我们获取的数据是什么类型的都不知道,那我们后续要怎么样去处理数据呢?
![image-20230218150225067](https://yudejava.oss-cn-hangzhou.aliyuncs.com/typora/202302181502196.png)
而在集合中添加了泛型对传递的数据进行约束后,我们在后续获取中就能明确的知道该集合保存的数据类型了,只需要在类后面添加一个<>,在<>内注明要传递的类型即可。
![image-20230218150542667](https://yudejava.oss-cn-hangzhou.aliyuncs.com/typora/202302181505799.png)
10.2.2、自定义泛型
泛型允许在定义类、接口、方法时使用,它将在声明变量、创建对象、调用方法时动态地指定(即传入实际的类型参数,也可称为类型实参)。<>中的字母是通配符。但是静态方法、静态初始化块或者静态变量的声明和初始化中不允许使用泛型形参。
![image-20230218153057433](https://yudejava.oss-cn-hangzhou.aliyuncs.com/typora/202302181530511.png)
<>中的字母在Java中常用的有:?
, T
,K
, V
, E
-
?
表示不确定的Java类型(通配符) -
T
为type
, 表示一种具体的Java类型 -
K
为key
, 表示Java键值对中的键 -
V
取词为value
, 表示Java键值对中的值 -
E
取词为element
, 表示元素
比如使用 List传递的泛型定位 时,可以想象成 E被全部替换成String了。这样通过一个List接口生成无数个不同参数类型的 List 接口。
创建泛型的注意事项:
1、泛型的<>内是能有多个参数的
2、泛型类的构造器不能带<>
3、泛型不同的引用不能相互赋值。ArrayList和ArrayList虽然语法上说是两种不同的类型,但是运行时只加载一个ArrayList到JVM中。
4、泛型如果不指定,将被擦除,泛型对应的类型均按照Object处理,但不等价于Object。
5、泛型无法使用基本数据类型,需要使用对应的包装类
6、 异常类不能是泛型的
10.2.3、泛型的继承
需要注意的是当父类、接口声明了泛型时,子类在继承父类或实现接口时,需要分不同的情况去讨论:
-
父类定义了泛型,则子类也定义成泛型,完全继承父类
-
父类定义泛型,子类也定义成泛型,但是实现父类一部分泛型
-
父类是泛型,子类不是泛型,需要实现父类的泛型
-
父类是泛型,子类不是泛型,直接忽略父类的泛型,会被默认为Object
10.2.4、菱形语法
在Java 7以前,如果使用带泛型的接口、类定义变量,那么调用构造器创建对象时构造器的后面也必须带泛型。
![image-20230218151014700](https://yudejava.oss-cn-hangzhou.aliyuncs.com/typora/202302181510759.png)
这样会显得代码有些多余,而从JDK7之后,Java允许在构造器后需要带完整的泛型信息,只需要一对<>即可,Java可以通过前面的泛型推断出泛型信息。这种写法被称为菱形语法,它没有对泛型做任何改变,只是做了一些简化。
![image-20230218151039420](https://yudejava.oss-cn-hangzhou.aliyuncs.com/typora/202302181510473.png)
而在 JDK9 中对菱形语法又做了进一步的强化,允许我们在创建匿名内部类时使用菱形语法,Java通过上下文来推断泛型的信息。
![image-20230218152625463](https://yudejava.oss-cn-hangzhou.aliyuncs.com/typora/202302181526650.png)
泛型并不是一个类,它只是做一个参数的类型约束,比如List在系统上不不会被当成一个新类的,在内存中也只占用一块内存,因此在静态方法、静态初始化块或者静态变量的声明和初始化中不允许使用泛型形参。
![]()
10.3、通配符
在数组的定义中,子类和父类是可以协变的,比如说 Dog extends Animal,那么Animal[] 与dog[]是兼容的。
![image-20230218162417522](https://yudejava.oss-cn-hangzhou.aliyuncs.com/typora/202302181624569.png)
但是如果在集合中是无法这样用的,而且泛型也不是一个类,List 和 List也并没有父子类的关系,它们本质上都是List,所以这时候就需要使用通配符来实现。
通配符其实就是<?>
它表示不确定的Java类型。
具体的使用主要有三种:
- 无边界的通配符:
- 固定上边界通配符
- 固定下边界通配符
10.3.1、无边界的
无边界的通配符只有一个 <?>:
![image-20230218162858185](https://yudejava.oss-cn-hangzhou.aliyuncs.com/typora/202302181628236.png)
这种方式主要作用就是让泛型能够接受未知类型的数据,这种方式可以使用任何类型的List 来调用,其元素类型是Object的。
![image-20230218163714763](https://yudejava.oss-cn-hangzhou.aliyuncs.com/typora/202302181637949.png)
但是需要注意的是这种写法仅仅表示是各种泛型的父类,不能向其中添加元素。
![image-20230218163830678](https://yudejava.oss-cn-hangzhou.aliyuncs.com/typora/202302181638739.png)
10.3.2、固定上边界
我们知道泛型是不协变的。比如我定义动物类,其子类有狗和猫:
abstract class Animal{
abstract void say();
}
class Dog extends Animal{
@Override
void say() {
System.out.println("是一条狗");
}
}
class Cat extends Animal{
@Override
void say() {
System.out.println("是一只猫");
}
}
如果我定义一个方法用来遍历 List集合,那么List和List是传递不进去的,即便内部的数据有几成的关系,但是保存它们的容器并没有继承的关系
![image-20230218170135176](https://yudejava.oss-cn-hangzhou.aliyuncs.com/typora/202302181701296.png)
所以为了解决这种情况,有一种上边界的通配符,写法是:<? extends T>
,这样能在泛型的层面上规定它们的容器也有继承关系。其中通配符的上限是Object
![image-20230218170457956](https://yudejava.oss-cn-hangzhou.aliyuncs.com/typora/202302181704203.png)
这种通配符的写法和无边界的类似,也是不能添加元素的。指定通配符上限的集合,能从集合中取元素(因为取出的元素总是上限的类型)但是不能向集合中添加元素(因为编译器没法确定集合元素实际是哪种子类型)
。
为什么能取不能存呢?通俗讲就是:List<? extends Dog>,它存放的数据肯定是一个动物,不管穿过来的是List 还是List,它取的都是一个动物,所以是可以取的。但是存放的时候,它不知道是该存猫还是该存狗,因为传递过来可能是List 和List中的任何一个。
10.3.3、固定下边界
在Java中除了指定上边界外,还能指定下边界,写法是:<? super T>
。它的作用和上边界是相反的,指定通配符的下限就是为了支持类型型变,这种型变方式被称为逆变。这种情况下编译器只知道集合元素是下限的父类型,但具体是哪种父类型则不确定,所以下边界只能添加元素(因为实际赋值的集合元素总是逆变声明的父类),不能取元素(取元素时只能被当成Object类型处理)
。如果想取元素只能当成Object类来处理了
![image-20230218172947390](https://yudejava.oss-cn-hangzhou.aliyuncs.com/typora/202302181729528.png)
为什么能存不能取,通俗讲:List<? super Dog>,表示可以存放的数据是Dog类及其父类,其父类是Animal,而Animal 有两个子类Dog和Cat,按类的转型来说,Cat至少都是一个Animal,所以他存放的数据可能存在Cat对象,这样在取的时候,它就会出现错误,因为 <? super Dog> 的边界内是不包含Cat的,所以最后只能都当成Object处理了。而存放时候,我们明确存放进行的类型,所以存放数据是不会出现错误的。