泛型的理解

泛型的由来

在多态中,我们知道,我们可以通过方法重载来实现通过同一函数名不同的参数来实现不同的方法。在这里,我们有另一种方法同样可以达到这个目的,并且不需要编写多个方法。我们知道Object类是所有类的父类,我们来看下面的代码。
泛型的理解

定义了一个tt类,并对成员变量进行了封装,注意它们的类型都是Object类。
在main函数里面,我们如果没有对23行等号右边加上(Integer)进行向下类型转换的话,是不能编译通过的。如下图:
泛型的理解
泛型的理解

加上之后就可以进行正常编译运行了。
当然,我们不进行赋值,直接输出也是可以编译运行的。
泛型的理解

通过上面的栗子,我们可以发现,这样每次在获取类的成员变量并赋值给另一个与其基本类型一致的变量时,我们都需要对它们做相应的向下转换,麻烦的同时,还很容易出错。
在来看一个栗子:
泛型的理解
运行之后,会出现如下错误(抛出类型转换异常):
泛型的理解
但是这个过程在编译的时候,编译器不会检测出这个错误,只有运行的时候才会检测到。
如果我们把类型实参(这里是Integer)加入到list和arraylist里面,编译器就会在编译阶段检测出错误,就不能编译通过了。
泛型的理解
泛型的理解
第九行代码的意思是,创建一个ArrayList的只含有整数类以及它子类类型(其实可以自己尝试把Integer换成Number,这样就可以add(浮点数),这也说明了可以把子类添加进去的)的线性表对象,被其父类List接口的引用调用其实现的add()方法。类类型(类型实参)用一对尖括号<>括起来。括起来的参数Integer其实是一个类型实参,我们通过思考,可以推断出它里面会有一个可以接受任意的类(自定义类或系统提供的类)的类型的一个类型形参的。这个就是“泛型”,它是一种“参数化类型”,即将类型由原来具体的类型参数化。在java的collection集合框架中,我们查看源码可以看到它们就是用泛型的方式来编写的。

泛型的主要用途

泛型可以分为泛型类泛型接口泛型方法这三种。

泛型类

在第一个栗子中,我们采用泛型来编写泛型类的话,就是下面的代码。
泛型的理解
T1,T2代表不同的类类型(类型实参),在这里我们把id看成是整型,name看成是字符串类型。在main函数里面,获取它们的时候,就不需要进行向下转型了,但是在实例化这个泛型类的时候,我们需要把类型实参(在这里是Integet和String)传进泛型类的两个类型形参T1,T2里面去。如图:
泛型的理解

泛型的符号我们可以自己设定(只要是合法的),不一定要是T,但是有4个常用的标识符,它们是T(表示数据类型)、K(键)、V(值)、E(表示异常或错误)。其中K和V在map接口里面会用到它们(可以去看一下map的源码)。
值得注意的是,在我们编写了泛型类,但是在为实例化泛型类的时候,并没有传入泛型实参,则系统会自动进行类型擦除,而为泛型类传入数据的过程中数据会自动向上转型变成Object,但是这个时候如果在想赋值给上述栗子的id和name变量就必须手动进行向下转型了(和一开始的例子一样)。
一个知识点的扩展:在我们为泛型类的类型参数传入不同的类型实参时,并没有真正意义上生成不同的泛型类,传入不同类型实参的泛型类在内存中只有一个。原因是在编译阶段,系统在检测泛型信息正确之后,会将泛型信息擦除,也就是说泛型信息在编译阶段就已经完成了。

泛型接口

语法定义如下
泛型的理解

定义一个类实现该接口
泛型的理解

语法规则:当实现该接口时,接口没有传入类型实参的话,必须要在实现类的后面加入,如果传入了类型实参,比如String, 则上面的(矩形框着的)可以省略,而且要将类里面的T全部手动改成String,如下图。
泛型的理解
泛型接口除了以上语法规则的区别以外,和平常的接口没有多大的区别了。

泛型方法

首先需要声明的是,泛型方法与泛型类并没有多大的联系,也就是说,泛型方法不一定只能在泛型类里面,即泛型方法也可以定义在泛型类的外部。
泛型方法定义在泛型类外部的时候,当是静态方法时,因为静态方法不可以访问类上定义的泛型(不过泛型不是在编译阶段就完成了吗),所以静态方法需要定义成泛型方法。如下图
泛型的理解
其中位于static以及返回类型T之间的不可以省略,在主函数main里面调用
泛型的理解
就可以调用这一静态方法。
当不是静态方法也是一样的,同样要求在修饰符(如public)与返回类型之间必须有,当然不止可以有一个T,多个可以用逗号分隔(与泛型类、泛型接口是一样的)。它们之间的是这一方法是否是泛型方法的唯一标志。

泛型通配符‘?’

泛型通配符‘?’是一个类型“实参”,不是一个类型“形参”。它可以被看作成所有类型(如Integer、String、Number)的父类,即 泛型类名称与同一泛型类名称并不是子父类的关系。具体来说就是,当我们想实现一个方法,方法的参数是 泛型类名称的时候,如果我们还想实现同一泛型类名称的时候,只能重新编写一个参数为 同一泛型类名称的方法,但这不符合java的多态思想,多态思想要求我们用一个逻辑引用类型就可以同时表示它们,这时候泛型通配符‘?’就排上了用场,把<>里面变成‘?’就把上面的问题解决了。

泛型的上下边界(即规定类型实参的传入范围)

通常与泛型通配符‘?’一起使用,将通配符改成’T’其实也可以。
比如:<? extends Number>则规定传入的类型实参只允许是Number类或其子类,这是规定泛型的上边界。
如果想规定泛型的下边界,则可以使用<? super Number>,这规定的是传入的类型实参只能是Number类以及其父类,便是规定泛型的上边界了。