Java 基础 之 泛型
一:什么是泛型
泛型在我们的代码中使用非常广泛的一部分知识,今天就系统的把泛型总结一下,并记录自己的学习历程。
泛型,即“参数化类型”。顾名思义,就是将类型由原来的具体的类型也定义成参数形式。然后在使用/调用时传入具体的类型(实参)。这种参数类型可以用在类、接口和方法中,分别被称为泛型类、泛型接口、泛型方法。
二:泛型的引入
首先,让我们先来设计一个加法计算的类,可以计算int, double。
如果有一天我们需要再计算char,string的加法,那我们还得新增这样类很多个,显然不符合软件设计的规范,下面我就来更新一下我们的代码,把所有的参数都变成object.
好了,让我们来测试一下,我们的这个新类能不能完成,int, double,String 的加法计算
通过试验发现,将参数抽象成object类型 后,发现只需要定义一个类就可以搞定感觉这样挺完美的了。但是每次都需要强制转换,这样的代码设计太麻烦了!下面让我们再升级一次我们的code,见证一下泛型的好处,不需要我们每次都强制类型转换。
三:泛型类
泛型类型用于类的定义中,被称为泛型类。
泛型类的定义格式:class类名称<泛型标识>,
1:泛型标识可以随便写为任意标识,常见的如T、E、K、V等形式的参数常用于表示泛型,
2:泛型的类型参数只能是类类型,不能是简单类型,
3:在实例化泛型类时,必须指定T的具体类型
下面来定义一个泛型类,让他满足各种类型的加法操作
通过上面的试验,是不是发现如果利用泛型来实现就只需要创建一个泛型类,然后实例化时传入参数类型,不需要进行强制类型转化就可以正确计算出结果!
但是有人就会提出来,可不可以设计成一套标准啊,我们定义这样的一个泛型接口,让具体的操作人员来实现这个标准,答案是当然可以!
四:泛型接口
泛型接口与泛型类的定义及使用基本相同
泛型接口的定义格式:publicinterface Generator<T>
定义一个泛型接口(定义参数的设定和获取)
定义一个泛型类来实现这个泛型接口
但是有些人说我们可能只需要做int或String或Double的计算,不需要其他的计算,那能不能做成定制化的那?当然也是可以的,看下面的定义:
是不是发现泛型接口,更加的灵活,如果我们只需要做String的加法那就把T换成具体的数据类型String即可
总结:
如果一个类实现了一个泛型接口,那么该类也必须是泛型的:
GenericAdd<T> implements GenericInterface<T>
如果一个类实现了一个特定类型的泛型接口,那么该类不是泛型的:
GenericAdd implements GenericInterface<Integer>
五:泛型方法
有泛型类,泛型接口,那一定也有泛型方法。泛型类,是在实例化类的时候指明泛型的具体类型;泛型方法,是在调用方法的时候指明泛型的具体类型 。泛型方法指返回值和参数都用泛型表示的方法。
泛型方法的定义格式:void doTest<T t>
1:普通类中的泛型方法
通过上面的测试发现在普通类的中的定义一个泛型方法用T,V…来参数化,使用也很简单,只需要在调用时传入具体的参数类型。
2:泛型类中的泛型方法
在上面的泛型类中定义了三个泛型方法,在我们的测试中看到226行编译出错了,说明了第一个泛型方法只能传入和泛型类实例化时一样的类型,第2个和第3个泛型方法可以处理任意的类型。
在泛型类(<T>)中定义泛型方法时如果不在返回值前面指定<T>泛型类型,那这个泛型方法就只能处理与泛型类实例化时声明的类型,如上面的第1个泛型方法 getClassInfo1
在泛型类(<T>)中声明了一个使用泛型E的泛型方法,这种泛型E可以为任意类型。可以与泛型类实例化时声明的T相同,也可以不同。如上面的第2个泛型方法 getClassInfo2
在泛型类(<T>)中声明了一个泛型方法,使用泛型T,其实这个T是一种全新的类型,可以与泛型实例化时声明的T不是同一种类型。如上面的第3个泛型方法 getClassInfo3可以处理People类
3:静态泛型方法
在泛型类或泛型接口中,静态函数无法使用泛型,所以,若要static方法需要使用泛型能力,必须使其成为泛型方法(静态泛型方法)
静态泛型方法也很简单,就是在普通泛型方法前面加上一个static,使用方式也和普通的静态方法一样,直接类名.方法名就可以了!
4:泛型方法的重载
普通方法有重载,当然泛型方法也是有重载的,是因为java编译器在编译时会对泛型做擦除功能,所以T就会变成Object, <T extends Ape>转成Ape, <T extends People> People。当我们这样定义泛型方法:
public void doTest(T t)
public void doTest(T t)
public void doTest(T t)
编译后就变成了:
publicvoid doTest(Object t)
publicvoid doTest(People t)
public void doTest(Ape t)
编译后的三个方法就是我们很熟悉的doTest重载,下面就用code来见证一下:
使用也很简单和普通的泛型方法一样
s, pe, a分别是三个类类型。
六:泛型通配符
前面我们介绍了泛型的基本用法、类型擦除。在泛型的使用中,还有个重要的东西叫通配符,下面就介绍通配符的使用。
我们先定义三个类:Ape, People继承自Ape,Student继承自People
我们再定义一个泛型类
完成了基础的class建设后,我们来讲通配符的使用
1:<? extends T>
<? extends T>表示上界通配符,它表示T以及T的子类, 类型最高是T。
在上面的的code中我们定义了 <? extends People>的泛型类,理论上,他可以指向和存放自己以及所有所有属于他的子类。但是第141,142,143却都报错了,编译器却告诉我们gp不能存放任何元素,这好像和我们前面的解释相反:这是因为编译器在编译时要做类型安全检查,当我们这样定义gp时,就是告诉编译器gp存的是People以及People的子类可能是Student,可能是People,也可能是其他的子类,编译器没办法保证gp的下限是什么,就不知道到底该指向什么类型。
总结: extends不能用于参数类型限定。
2:<? super T>
<? super T>表示下界通配符: 表示T以及T的超类
在上面的的code中我们定义了 <? super People>的泛型类,理论上,他可以指向和存放自己以及People父类。但是我们的测试结果却是:他可以添加People以及People的子类Student,不能添加他的父类。这好像和我们前面的解释相反:这是因为当我们这样定义gp1时,就是告诉编译器gp1存的是People以及People的父类可能会放People,可能会放Ape,这样编译器可以确定,gp1最低的标准就是People,这样编译器就把下限设置为people,所以我们可以存放Student,People。
总结:super可用于参数类型限定
7: 泛型实践
写这么多,当然有点乏味,下面就写个获取最大值和遍历数组的工具泛型来实践一下:
1:创建一个泛型接口,把类型参数限制成实现了Comparable接口的类
2:再编写一个泛型类继承我们的泛型接口,实现获取最大值接口,在定义一个泛型方法,完成数组的遍历
3:在main 函数中实例化泛型类并定义2个数组来测试获取最大值功能,2个类数组来测试遍历功能