【JAVA进化论】LV3-3:Object&泛型
泛型
,即“参数化类型
”。一提到参数,最熟悉的就是定义方法时有形参(方法定义里括号里的参数声明),然后调用此方法时传递实参(实际传给方法的参数)。那么参数化类型怎么理解呢?顾名思义,就是将类型参数化,类似于方法中的变量参数,此时类型也定义成参数形式(可以称之为类型形参),然后在使用/调用时传入具体的类型(类型实参)。泛型的本质是为了参数化类型(在不创建新的类型的情况下,通过泛型指定的不同类型来控制形参具体限制的类型)。也就是说在泛型使用过程中,操作的数据类型被指定为一个参数,这种参数类型可以用在类、接口和方法中,分别被称为泛型类
、泛型接口
、泛型方法
。
一、一个例子
1.1:使用Object实现
假设现在让我们开发一个用于在应用中传递对象的容器。但对象类型并不总是相同。因此,需要开发一个能够存储各种类型对象的容器。下面来试着开发一下:
前面的章节我们提到过,Object是所有类的父类,那么意味着可以用Object类型的参数变量来接收任何类型的对象:
1 | public class ObjectContainer { |
我们来测试下看看:
1 | public static void main(String[] args){ |
这段代码是没有任何问题的,现在让我们改下代码:
1 | public static void main(String[] args){ |
这段代码在编译阶段不会有任何报错,但是在运行时,就会报类型转换错误。事实上,我们设计的这个对象容器就面临着这种问题,它确实可以接收并储藏任何类型的对象,但是对于使用方来说,他并不知道你这里面存的到底是什么类型,只知道它是个Object,使用方在强转时也不会有任何提示说存在类型转换错误的风险,这样稀里糊涂上了线,就会在运行期加大报错的风险。
1.2:利用泛型改造
我们现在利用泛型改造下代码:
1 | public class ObjectContainer<T> { //尖括号内的字母就是类型声明 |
上面的泛型声明放到了类上面,这个类被称为泛型类,现在来测试代码就可以在实例化容器对象的同时声明容器内存储的类型了:
可以看到,泛型类在实例化的时候,可以指定声明的具体类型,被指定后,意味着该容器存放的对象类型必须是Cat或者其子类型的对象,因为有了这个特性,下面getObj方法返回的是Cat类型现在也是已知的,因此强转成Dog会在编译期就报错,IDE的语法检查也过不了。
泛型的好处就在于这里,它可以触发更加严谨的语法检查,让程序员编写的类似容器的代码在允许接收任何类型对象的同时不容易发生类型转换错误。
二、泛型的分类
2.1:泛型类
即声明在类上的泛型,上面的例子就是,我们在类名后面加上泛型声明,那么这个类就是一个泛型类,既然泛型是一种类似方法里形参的一种声明性的东西,那么它就应该支持声明多个:
1 | public class ObjectContainer<X, Y, Z> { //该泛型类声明了三个类型(这个类似方法的形参,理论上可以无限声明) |
这是一个声明了三种类型的泛型类。
你也可以控制泛型的取值范围,比如你希望所有的X类型必须是Animal的子类,你就可以把声明改成这样:
1 | public class ObjectContainer<X extends Animal, Y, Z> //这样你传入X对应的类型就必须是Animal或者它的子类们(值得注意的是,不可以通过implements关键词限定接口) |
2.2:泛型方法
泛型除了可以声明在类的开头,也可以用来放到方法的声明当中去:
1 | public <K, V> V getValue(K k) { //声明该方法有俩泛型变量,K和V |
此外,需要注意,方法上声明的泛型,即便命名和类里声明的一致,它们所代表的的含义也完全不一样:
跟普通的声明一样,声明位置不同,作用域也不同。方法里声明的泛型跟类里一样,也可以通过extends
关键词来约束接收对象的范围。
让我们来加深一下方法泛型的理解,重新定义一下上面的类:
1 | public class ObjectContainer2<K, V> { //这里声明的K,V是类全局的,跟方法里的不一样 |
然后写个测试类:
1 | public static void main(String[] args) { |
这个过程会发生什么?来看下分析图:
2.3:泛型接口
即在接口里加上泛型声明,比如我们现在定义一个泛型接口,如下:
1 | public interface DataOp<ID, DATA> { //声明一个泛型类型,ID表示的是数据ID的类型,DATA表示的是数据类型 |
然后我们就可以利用不同的实现类来定义泛型具体的类型:
1 | public class CatDataOp implements DataOp<Integer, Cat> { //指定这个实现类是针对id为Integer类型且数据为Cat类型的数据操作实现 |
当然你也可以选择不指定泛型,继续让其实现类也是个泛型类:
1 | public class DataOpImpl<ID, DATA> implements DataOp<ID, DATA> { |
然后写一个子类继承该类时再指定具体类型也可以:
1 | public class DogDataOp extends DataOpImpl<Integer, Dog> { |
2.4:通配符泛型
跟前面的有关系,但不太大,我们来通过一个例子来说下什么是通配符泛型。
首先定义一个”窝“类,每个窝盛放一个动物:
1 | //窝,可以容纳一种动物,利用泛型规范自己所接收的对象 |
然后我们定义一个动物园类,动物园可以容纳多个窝:
1 | public class Zoo { |
我们设想的是,新建好的存放着某种动物的窝,通过setDen建到动物园里,我们期待着setDen可以接收Den
可以看到,Den<Cat>和Den<Dog>无法被Den<Animal>所接收,这并不是java的多态出了问题,而是这种泛型类里的泛型类型,根本就没有任何类的特性,我们前面说过,它们仅仅是类似于方法声明里的形参一样的参数,在编译器眼里,Den<Cat>、Den<Dog>、Den<Animal>是三个不同的类定义,它们三个一个里面放的是Cat对象,一个是Dog对象,一个是Animal对象,这又怎么可以称之为一种类定义呢?
我们再来改造下setDen方法:
1 | public void setDen(Den<?> animal) { |
泛型类作为参数接收时,定义<?>通配符可以表示,你可以传入任意Den泛型对象,这个时候再次按照图4里的传参方式,Den<Cat>和Den<Dog>就都可以被接收了。
但是,我们说了,<?>意味着你可以传任意符合Den泛型的对象,如果我想要搞一个只接收猫族特权的方法,该怎么定义呢?如下:
1 | public void setCatDen(Den<? extends Cat> cat) { //通配符仍然可以控制接收泛型类参数的范围,但相比Den原类,范围只能缩小 |
看到了吗?就算是<?>通配符,你仍然可以定制它的接收范围,当然,这个范围肯定不可以比Den本身大,例如例子里的Den<? extends Cat>,它确实只能接收Cat及其子类的猫窝,但是它也符合Den定义的Den<T extends Animal>这个规则。