前言:上文我们了解了接下来学习的路线, 明白了如何计算时间复杂度和空间复杂度, 最后还留下来 两个题目, 这里就先来解答上文留下来的两个题目, 在来学习本文介绍的泛型。
题目一:消失的数字
解法:


如过我们 数据量不是像力扣这样 给你一坨我们 是可以这样写的。

好 了 上文留的题目完成了 我们来 学习一下本文内容 泛型
再学习泛型前我们先来学习一下包装类。
啥是包装类呢?
包装类(Wrapper Class): Java是一个面向对象的编程语言,但是Java中的八种基本数据类型却是不面向对象的,为了使用方便和解决这个不足,在设计类
时为每个基本数据类型设计了一个对应的类进行代表,这样八种基本数据类型对应的类统称为包装类(Wrapper Class),包装类均位于java.lang包。
同时没有包装类 我们接下来学习的 泛型 , 对于基础类型 也是不能够使用的 。
| 基本数据类型 | 包装类 |
|---|---|
| byte | Byte |
| short | Short |
| int | Integer |
| long | Long |
| float | Float |
| double | Double |
| char | Character |
| boolean | Boolean |
包装类存在的意义:
演示: 将字符串 转化为 整形数据

除了上面这两个方法, 我们的包装类还有很多方法, 如果感兴趣可以 通过Alt + 7 去查看, 或者百度查找也是ok的。

先来看一段代码

有没有疑问 不是说我们的 Integer 不是一个类吗 为啥 不通过 new 来创建 而是 直接 给了 一个 整形数据, 另外 还有一个疑问, a 明明 是一个对象,为啥能直接赋值给 b 这个整形数据类型 ?
小小的疑惑大大的脑袋, 其实 这里跟我们下面要将的 装箱和拆箱有关 (这里也可以 称为 装包 和 拆包)
先看概念:
装箱 / 装包 :把简单类型数据 变为 包装类类型数据
拆箱 / 拆包 : 把包装类类型数据 变为 简单类型数据
知道了 概念 , 我们就可以解释上面的 代码 了

另外 我们 也可通过 反汇编来 看底层是如何实现的
第一步: 找到我们的 字节码文件 ( 再此之前 Build 一下, 或者 运行一下,产生字节码文件)

第二步: 找到我们的 字节码文件(.class 后缀的 )

第三步 : 输入 javap -c 查看的类名

第四步: 观察字节码文件 装箱 过程 (装箱:把简单类型数据 变为 包装类类型数据 )

下面就来看一下拆箱(把包装类类型数据 变为 简单类型数据):这里 前面都差不多就直接来看 字节码文件

学完了装箱 和 拆箱 , 这里 来 问一个问题 : 请问下面输出什么(注: 阿里的面试题)

A: true, true B: false , false , C true , false
答案: C
解释: 这里我们想要回答正确 ,就需要 去观察 源码, 这里我们 装包是会调用 valueOf ,这里我们就点这个方法看源码 。

注意:泛型的算 java 里面 比较难的语法,这里我们的学习目标只需要能看懂java集合源码即可
一般的类和方法,只能使用具体的类型: 要么是基本类型,要么是自定义的类。如果要编写可以应用于多种类型的代码,这种刻板的限制对代码的束缚就会很大。----- 来源《Java编程思想》对泛型的介绍。
泛型是在JDK1.5引入的新的语法,通俗讲,泛型:就是适用于许多许多类型。从代码上讲,就是对类型实现了参数化。
先来看一个问题 : 实现一个类,类中包含一个数组成员,使得数组中可以存放任何类型的数据,也可以根据成员方法返回数组中某个下标的值?
补充: object类是所有类的父类。
根据上面的补充,那么我们能不能 new 一个 object数组来存放我们的数据呢?
这里就来演示:

可以 看到我们的 object 数组存放了我们的 字符串类型数据,整形数据, 浮点类型数据, 还有我们的自定义数据,是不是非常方便呀, 啥都能存。
但这里 是有问题的, 啥问题呢?
这里我们来取数据就能够知道了,开始演示:
因为我们是 object 数组, 那么返回的元素 也应该是 一个 object 类型的 元素, 这里就需要使用 我们的 object来接收。


可以发现 这里就拿出来我们的数据, 但是有一个问题, 我们使用 object 这个类型来接收,那么能不能使用我们存放的类型呢?
演示:

发现这里爆红线了, 发现不能, 其实是可以的这里就需要强转 (父类对象给子类引用【Obejct 是 所有类的 父类】,需要进行强转, 向下转型)。

此时我们就 拿到了我们的数据, 可以发现这样是非常麻烦的,如果采用这样存储数据,不仅需要对这个数组存放的数据非常了解, 还要对取出来的数据进行强转,是真的麻烦, 那么有什么方法,能解决呢?
这里就需要我们的 泛型。
使用Object 的 缺点:
这里我们的Obejct 不能将类型参数化导致什么类型都能放入Object数组中.
这里我们的泛型就能让我们传一个类型。

这里我们的 T相当于一个占位符,(这里 E F G H I K 等等都可以, 名字而已) 。
常用的名称
E 表示 Element
K 表示 Key
V 表示 Value
N 表示 Number
T 表示 Type
S, U, V 等等 - 第二、第三、第四个类型
下面就来改我们的 代码

这里发现我们的 T 报错了, 这里的原因比较复杂,这里先不解释后面在解释。
这里我们先写成这样, 但是这样写也不太好。

使用:类我们创建好了,这里我们就来使用

这里我们指定 当前 MyArray ,接收的类型为 Integer(我们在尖括号里面传的是Integer类型),这里就我们 就只能存放整形数据(这里就会类型检查,如果不是Integer 就会报错), 如果看不懂Integer 没有关系,后面会讲到包装类, 这里就 将 Integer 当作 int 的 升级版即可。

此时我们 拿数据 就不需要想 上面一样需要强转了。
下面我们就在存放我们的字符串数据。

通过上面这里就能得出 泛型存在的意义
注意: 上面这两步都是在编译的时候完成的,运行时就没有泛型的概念。
正因为 泛型主要是编译时期的一种机制,这种机制会有一种概念叫做 擦除机制。
看到这里, 我们就基本能够看的懂 后面java 集合源码。
上面展示完泛型, 这里我们 来看一下语法(其实就是上面的)。
class 泛型类名称<类型形参列表> {
// 这里可以使用类型参数
}
一个泛型的参数列表可以指定多个类型
class ClassName<T1, T2, ..., Tn> {
}
class 泛型类名称<类型形参列表> extends 继承类/* 这里可以使用类型参数 */ {
// 这里可以使用类型参数
}
class ClassName<T1, T2, ..., Tn> extends ParentClass<T1> {
// 可以只使用部分类型参数
}
一个泛型的参数列表可以指定多个类型:

下面继续 、
作为一个刚刚知道如何定义泛型的小白玩家,肯定会有下面的操作
高高兴兴的写好泛型类:

使用泛型类,没有带<>

如果此时取数据,发现是需要强转的。

如果我们 这里再加上一些其他的类型。

此时 就会发现我靠这里回到了使用Object , 发现啥类型都能存放,取数据也是需要强转。
此时这种情况就好比我们幸幸苦苦的做好了一个工具,然而我们需要使用结果脑袋一抽忘记了使用这个工具导致我们需要完成很多步多余的工作。
这里没有加<> 就称为裸类型 (Raw Type)
大家想一想为啥我们没有加上<> ,编译器没有报错呢?
这里是因为 我们的泛型是在 1.5之后引入的一种的新语法,如果我们这里报错,那么之前版本就不能使用吗?
这里我们就需要考虑兼容性问题, 所以这里就没有严格的限制(编译器没有报错)。
这里我们可以小结一下泛型:
这里主要看我 演示 即可 :
在 idea 中有一个 插件

这里我们 通过 这个插件可以观察我们的 .class文件 ,这里就通过这个插件进行演示, 如果感兴趣可以 自行了解。

这里我们 看到 我们的方法,点击 setObjects ,这里 I 代表是 int ,后面 这个Object 就是我们的 T类型。

在来看一下 getobjects 这个方法

(I) 代表 括号里的参数类型是 int , <()java/lang/Object> 代表返回值的类型是Object。
这我们就能发现 我们的 T 在编译的时候都变成了Object ,这里就是擦除机制在编译的时候 将T擦成 Object类型。
那么这里 有一个问题:我们在编译的时候将 T 全部擦成Object,那么为啥我们 还只能存放 我们指定的类型呢?
答案: 上面我们不是说过 使用泛型语法会在编译时自动进行类型检查和转换,那么就会通过我们指定的类型,对我们传入的参数进行检查如果不符合就报错。
这里如果想要更深层次的 学习擦除机制这里有一篇文章可以学习一下:Java泛型擦除机制
这里我们讲完了擦除机制, 来解答一下上面留下来的问题 。
这里按照我们的 擦除机制, T 会擦成 Object 那么我们 new 一个Object 为啥不行呢?

这里如果我们 这样写 就会 回到我们 最初的问题,这样写数组什么元素都能存放没有限制, 而且我们需要对存入的数据十分了解,才能拿到数据,而且每次拿数据都需要强转非常麻烦。
这里我们继续来看
假设这样创建能够创建数组,那么我们是不是可以创建返回一个T[]类型的方法。
这里 public T[] objects = new T[10] ; 在编译器上报错, 我们拿下面这种创建方式来演示,返回T[] 数组
(注意这里我们是假设 T[] obkects = new T[] 能够创建数组)

使用:返回 一个Integer 数组, 使用 Integer 数组接收

这里发现没有报错, 这里运行一下。(这里没有报错的原因是 我们的指定的类型是Integer, 泛型会进行类型检查,认为 getT 返回的类型会是 Integer[] 类型, 所以这里就不会报错)

发现报错了,这里的错误原因就是我们的 擦除机制会将T类型 擦除成 Object 类型, 返回的类型就是 Object 那么我们使用 Integer 数组接收是不是就 有问题。
最后我们还说过 : public T[] objects = (T[])new Object[10];也不太好
主要问题 也是一样的 数组的元素 也有可能存储不同类型的元素。
这里我们要 创建数组 最正确的写法是使用 反射,反射是什么 ,不要急这里先埋下一个坑,等后面 会讲到的。
这里我们只需要了解一下如何使用反射来创建这个数组即可。

下面继续 : 这里通过一个题目引出我们接下来学习的知识点。
题目: 写一个泛型类,类中有一个方法,求一个数组当中的最大值。
这里看到题目是不是 , 都笑出了声,这不贼简单,直接比较不就好了,那么我们就来写代码。

发现这里 报错了, 眉头一紧 ,我靠为啥这里会报错。
想一想:
第一: 这里我们传入的类型是不是未知的我们如果我们就这样使用 < > = 这些符号比较是不是就 非常草率了,
这里补充一个细节: 我们的 泛型 尖括号内部 必须是引用类型。 如:Integer, String , Double 这些都是后面要讲 的 包装类型(包装类型是一个引用类型)。
第二: 根据上面的补充我们能知道 我传入的类型是 引用类型,那么我们的引用类型能 通过< = > 直接比较吗?
(之前讲过的对象比较需要实现Comparator 或者 Comparable 接口,通过 compareTo 方法 来比较。,注意: equals 在这种情况下是不可以的,因为 equals 是 比较是否相等的, 返回的 是 true 和 false)
如果不懂可以看看下面的文章
当我们想要通过 compareTo方法进行比较的时候,发现这并没有与 Comparable 和 Comparator 相关的功能。

解释: 为啥没有compareTo这个方法 , 是因为我们的 T 在编译的时候都擦除成 Object类,这里我们进入源码一看会发现我们的Object类是没有实现我们的Comparable 接口的,所以他是不会具备我们的 compareTo方法的

那么此时需要如何解决呢?
来看

这里我们就不报错了,为啥要这样呢?
不要急慢慢来,保持一个学者的心。
这里 就是泛型的上界 --》 T一定是实现了这个接口的。

先来使用一下这个 Alg类

下面举个反例
自定义一个类 Person , 传入 Alg 这个类中,此时你会发现报错了。

这里我们想要不报错 ,这里需要让 Person 实现 Comparable这个接口然后重写compareTo这个方法

可以看到这里不报错了。
下面通过 Alg 来 找到我们的 People 数组中的最大值。

这里有一个小细节: 我们的 包装类 都实现了 Comparable 重写了compareTo方法。

这里就不在多讲了, 回到我们的泛型上界

这里是特殊的一个上界,这里我们 T的上界是一个接口, 这个T一定是实现这个接口的。
下面这里就来看一下更简单的上界

这里的 E 一定是 Number的子类或 Number 本身。
通过 java的帮助手册查询 Num的子类

演示:

那么 这里 与 没有加上 extends Number有啥区别呢?

再来看:

上界看完是不是有预言家 预言我们接下来要学习泛型的下界,抱歉这里我们的泛型只有上界没有下届的哦。
接下我们来学习一下泛型的方法
方法限定符 <类型形参列表> 返回值类型 方法名称(形参列表) { ... }
非静态: 就那刚刚的题目来举例。

这里就是一个非静态的泛型方法。
动动我们聪明的小脑袋瓜想一想, 是否能想到 static 呢?
在类和对象那里我们 讲过 static 他表示静态的, 这里被static修饰的方法 是存放在方法去的, 不需要 new 对象就能够调用 ,那么我们是不是就可以将我们的 findMax方法写成我们的静态方法从而直接通过类名进行调用。
开始尝试: 发现加上 static 报错了


有问题多思考才能进步是不是, 那么这里我们在来想一想 。
那么这里没有加上 , 那么 T 不就更确定不了了吗?

这里我们就需要对上面的代码进行修改、
这里就没有报错了,这里我们的方法是静态的必须要在 static 加上一个 (这里可以指定泛型上界)
这里来使用一下:注意这里我们不是没有传入参数类型,这里会省略掉 , 它会根据我们传入数组的类型来推到我们传入的类型。

看完泛型的方法, 我们来学习一下通配符
先来看一段代码
class Message<T>{
private T message;
public T getMessage(){
return message;
}
public void setMessage(T message) {
this.message = message;
}
}
public class Main {
public static void main(String[] args) {
Message<String> message = new Message<>();
message.setMessage("小葵花幼儿园欢迎你");
fun(message);
}
public static void fun(Message<String> temp){
System.out.println(temp.getMessage());
}
}
这里我们指定 传入Message这个类的参数类型是 String , 然后通过 fun 打印 我们传入的参数 小葵花幼儿园欢迎你。
(这个代码是不是非常简单, 下面我们改变一下)。
将 Message 传入的参数类型改为 Integer

此时发现报错了,我们不能使用 这个fun 方法,理由很简单,我们fun 的 Message的类型是 String , 这里我们指定的类型是Integer ,这里的类型就不匹配。

如果我们 再写一个 fun 函数, 就会 感觉差点意思,如果我们传入的 是Double 这个类型呢? 你再写一个吗 ,这就非常的 憨憨了。
细节扩展:

这里我们 将 Message 改为 ? 我们的代码就可以运行了

运行:

这里我们通配符主要解决的问题:
通配符是用来解决泛型无法协变的问题。
协变 指的就是如果 Student 是 Person 的子类,那么List 也应该是 List的子类。
但是泛型是不支持这样样子父子类关系的。
1、泛型 T 是指定的类型,一旦你传给了我就定下了。而通配符则更为灵活或者说是不确定,更多的是用于扩充参数的范围。
2、或者我们可以这样理解:泛型 T 就是一个变量,等着你将来传给它一个具体的类型;而通配符则是一种规定:规定你只传某一个范围的参数类型。【比如说整形 short、int 都是整形范围里的类型】
下面继续:
语法:
<? extends 上界>
<? extends Number>//可以传入的实参类型是Number或者Number的子类
根据下面这个图 来举例子:

class Food {
}
class Fruit extends Food {
}
class Apple extends Fruit {
}
class Banana extends Fruit {
}
class plate<T> { // 设置泛型上限
private T plate;
public T getPlate() {
return plate;
}
public void setPlate(T plate) {
this.plate = plate;
}
}
public class Test {
public static void main(String[] args) {
plate<Apple> plate1 = new plate<>();
plate<Banana> plate2 = new plate<>();
fun(plate1);
plate1.setPlate(new Apple());
plate2.setPlate(new Banana());
fun(plate2);
}
public static void fun(plate<? extends Fruit> temp){
}
}
请问: 能不能 再 temp(盘子) 放 东西

尝试放苹果 和 香蕉

这里都报错了, 此时都不可以 ,站在 temp 的角度 来看, 它不知道是接收 Apple 还是 Banana , 一般对于 统配符的上界来说存放数据一般是不会成功的, 它 不能具体的知道你要存放的类 , 这里 一般用于取数据, 这里我们取出的数据都是 Fruit的子类或 Fruit (这里是上面这个代码的 Fruit)

补充: 往 tmp里面 添加 一个 Fruit类

这里同样是不行的, 我们 new 一个 Fruit , 假设我们 传入 fun的 参数 类型是 Apple (temp 的参数类型 是 Apple) 那么 new Fruit 相当于 向下转型 ,之前文章说过, 向下转型本身 就不是一个安全的操作 。
语法:
<? super 下界>
<? super Integer>//代表 可以传入的实参的类型是Integer或者Integer的父类类型
根据下面这个图 来举例子:

class Food {
}
class Fruit extends Food {
}
class Apple extends Fruit {
}
class Banana extends Fruit {
}
class plate<T> { // 设置泛型上限
private T plate;
public T getPlate() {
return plate;
}
public void setPlate(T plate) {
this.plate = plate;
}
}
public class Test {
public static void main(String[] args) {
plate<Fruit> plate1 = new plate<>();
plate1.setPlate(new Fruit());
fun(plate1);
plate<Food> plate2 = new plate<>();
plate2.setPlate(new Food());
fun(plate2);
}
public static void fun(plate<? super Fruit> temp){
}
尝试 : 传入 Fruit 的子类

演示:

这里我们想要读一个数据是不能的 , 还是那个原因, 我们不能够知道是使用啥来接收。

泛型的学习就到这里了, 为主要了解即可, 重点能看懂后面的源码即可。