【JAVA进化论】LV3-3:Object&泛型

泛型,即“参数化类型”。一提到参数,最熟悉的就是定义方法时有形参(方法定义里括号里的参数声明),然后调用此方法时传递实参(实际传给方法的参数)。那么参数化类型怎么理解呢?顾名思义,就是将类型参数化,类似于方法中的变量参数,此时类型也定义成参数形式(可以称之为类型形参),然后在使用/调用时传入具体的类型(类型实参)。泛型的本质是为了参数化类型(在不创建新的类型的情况下,通过泛型指定的不同类型来控制形参具体限制的类型)。也就是说在泛型使用过程中,操作的数据类型被指定为一个参数,这种参数类型可以用在类、接口和方法中,分别被称为泛型类泛型接口泛型方法

一、一个例子

1.1:使用Object实现

假设现在让我们开发一个用于在应用中传递对象的容器。但对象类型并不总是相同。因此,需要开发一个能够存储各种类型对象的容器。下面来试着开发一下:

前面的章节我们提到过,Object是所有类的父类,那么意味着可以用Object类型的参数变量来接收任何类型的对象:

代码块1
1
2
3
4
5
6
7
8
9
10
11
12
public class ObjectContainer {

private Object obj;

public void setObj(Object obj) { //只要你喜欢,你可以放任何类的对象进来
this.obj = obj;
}

public Object getObj(){ //通过此方法获取到当前容器内的对象
return obj;
}
}

我们来测试下看看:

代码块2
1
2
3
4
5
6
7
public static void main(String[] args){

ObjectContainer container = new ObjectContainer(); //先把容器对象创建出来
container.setObj(new Cat()); //存
Cat cat = (Cat) container.getObj(); //取,之前讲过,小类型接大类型需要强转

}

这段代码是没有任何问题的,现在让我们改下代码:

代码块3
1
2
3
4
5
6
7
public static void main(String[] args){

ObjectContainer container = new ObjectContainer(); //先把容器对象创建出来
container.setObj(new Cat()); //存
Dog dog = (Dog) container.getObj(); //取,这里强转成Dog型

}

这段代码在编译阶段不会有任何报错,但是在运行时,就会报类型转换错误。事实上,我们设计的这个对象容器就面临着这种问题,它确实可以接收并储藏任何类型的对象,但是对于使用方来说,他并不知道你这里面存的到底是什么类型,只知道它是个Object,使用方在强转时也不会有任何提示说存在类型转换错误的风险,这样稀里糊涂上了线,就会在运行期加大报错的风险。

1.2:利用泛型改造

我们现在利用泛型改造下代码:

代码块4
1
2
3
4
5
6
7
8
9
10
11
12
public class ObjectContainer<T> { //尖括号内的字母就是类型声明

private T obj; //类里已经声明了的类型,在这里可以直接替代Object

public void setObj(T obj) { //现在只能接收被声明的类型
this.obj = obj;
}

public T getObj() { //通过此方法获取到当前容器内的对象,返回的变成了被声明的类型
return obj;
}
}

上面的泛型声明放到了类上面,这个类被称为泛型类,现在来测试代码就可以在实例化容器对象的同时声明容器内存储的类型了:

图1

可以看到,泛型类在实例化的时候,可以指定声明的具体类型,被指定后,意味着该容器存放的对象类型必须是Cat或者其子类型的对象,因为有了这个特性,下面getObj方法返回的是Cat类型现在也是已知的,因此强转成Dog会在编译期就报错,IDE的语法检查也过不了。

泛型的好处就在于这里,它可以触发更加严谨的语法检查,让程序员编写的类似容器的代码在允许接收任何类型对象的同时不容易发生类型转换错误。

二、泛型的分类

2.1:泛型类

即声明在类上的泛型,上面的例子就是,我们在类名后面加上泛型声明,那么这个类就是一个泛型类,既然泛型是一种类似方法里形参的一种声明性的东西,那么它就应该支持声明多个:

代码块5
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
public class ObjectContainer<X, Y, Z> { //该泛型类声明了三个类型(这个类似方法的形参,理论上可以无限声明)

private X x; //X类型的对象

private Y y; //Y类型的对象

private Z z; //Z类型的对象

public void setX(X x) { //现在该方法只能接收X的类型的对象
this.x = x;
}

public X getX() { //通过此方法只能获取X类型的对象
return x;
}

public void setY(Y y) { //现在该方法只能接收Y的类型的对象
this.y = y;
}

public Y getY() { //通过此方法只能获取Y类型的对象
return y;
}

public void setZ(Z z) { //现在该方法只能接收Z的类型的对象
this.z = z;
}

public Z getZ() { //通过此方法只能获取Z类型的对象
return z;
}
}

这是一个声明了三种类型的泛型类。

你也可以控制泛型的取值范围,比如你希望所有的X类型必须是Animal的子类,你就可以把声明改成这样:

代码块6
1
public class ObjectContainer<X extends Animal, Y, Z> //这样你传入X对应的类型就必须是Animal或者它的子类们(值得注意的是,不可以通过implements关键词限定接口)

2.2:泛型方法

泛型除了可以声明在类的开头,也可以用来放到方法的声明当中去:

代码块7
1
2
3
4
public <K, V> V getValue(K k) { //声明该方法有俩泛型变量,K和V
V value = (V) mc.getValue(k);
return value;
}

此外,需要注意,方法上声明的泛型,即便命名和类里声明的一致,它们所代表的的含义也完全不一样:

图2

跟普通的声明一样,声明位置不同,作用域也不同。方法里声明的泛型跟类里一样,也可以通过extends关键词来约束接收对象的范围。

让我们来加深一下方法泛型的理解,重新定义一下上面的类:

代码块8
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class ObjectContainer2<K, V> { //这里声明的K,V是类全局的,跟方法里的不一样

private K k;
private V v;

private Mc mc = new Mc(); //辅助类,先别关注

public <K, V> V getValue(K k) { //这里声明的KV是方法自己的
return getValue2(k);
}

public <K, V> V getValue2(K k) { //这里声明的KV是方法自己的
V value = (V) mc.getValue(k);
return value;
}

}

然后写个测试类:

代码块9
1
2
3
4
5
6
7
8
public static void main(String[] args) {

ObjectContainer2<Cat, Dog> catDogObjectContainer2 = new ObjectContainer2<>();

String key = "myKey";
Cat cat = catDogObjectContainer2.getValue(key);

}

这个过程会发生什么?来看下分析图:

图3

2.3:泛型接口

即在接口里加上泛型声明,比如我们现在定义一个泛型接口,如下:

代码块10
1
2
3
4
5
6
7
8
9
public interface DataOp<ID, DATA> { //声明一个泛型类型,ID表示的是数据ID的类型,DATA表示的是数据类型

boolean save(DATA data); //存储数据

boolean del(ID id); //根据id删除数据

DATA selectById(ID id); //根据id查询数据

}

然后我们就可以利用不同的实现类来定义泛型具体的类型:

代码块11
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class CatDataOp implements DataOp<Integer, Cat> { //指定这个实现类是针对id为Integer类型且数据为Cat类型的数据操作实现
@Override
public boolean save(Cat cat) { // 实现方法这里必须是指定类型
//业务代码
return false; //处理结果
}

@Override
public boolean del(Integer integer) {
return false;
}

@Override
public Cat selectById(Integer integer) {
return null;
}
}

当然你也可以选择不指定泛型,继续让其实现类也是个泛型类:

代码块12
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class DataOpImpl<ID, DATA> implements DataOp<ID, DATA> {
@Override
public boolean save(DATA data) {
return false;
}

@Override
public boolean del(ID id) {
return false;
}

@Override
public DATA selectById(ID id) {
return null;
}
}

然后写一个子类继承该类时再指定具体类型也可以:

代码块13
1
2
3
4
5
6
7
public class DogDataOp extends DataOpImpl<Integer, Dog> {

@Override
public boolean save(Dog dog) { //重写父类方法时,由于该子类指定了具体类型,所以这里参数就变成具体的类型了
return super.save(dog);
}
}

2.4:通配符泛型

跟前面的有关系,但不太大,我们来通过一个例子来说下什么是通配符泛型。

首先定义一个”窝“类,每个窝盛放一个动物:

代码块14
1
2
3
4
5
6
7
8
9
10
11
12
13
14
//窝,可以容纳一种动物,利用泛型规范自己所接收的对象
public class Den<T extends Animal> {

private T animal;

public void setAnimal(T animal) {
this.animal = animal;
}

public T getAnimal() {
return animal;
}

}

然后我们定义一个动物园类,动物园可以容纳多个窝:

代码块15
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class Zoo {

private Den[] dens = new Den[10];

private int i = 0;

public void setDen(Den<Animal> animal) { //接收Den对象,泛型就用动物们的父类Animal赋值好了~
this.dens[i++] = animal;
}

public void forEachAllDensAnimalCry() { //打印出来动物园内每个窝里动物的叫声
for (Den den : dens) {
if (den != null) {
Animal animal = den.getAnimal();
animal.cry();
}
}
}

}

我们设想的是,新建好的存放着某种动物的窝,通过setDen建到动物园里,我们期待着setDen可以接收Den和Den等泛型类对象,但是:

图4

可以看到,Den<Cat>和Den<Dog>无法被Den<Animal>所接收,这并不是java的多态出了问题,而是这种泛型类里的泛型类型,根本就没有任何类的特性,我们前面说过,它们仅仅是类似于方法声明里的形参一样的参数,在编译器眼里,Den<Cat>、Den<Dog>、Den<Animal>是三个不同的类定义,它们三个一个里面放的是Cat对象,一个是Dog对象,一个是Animal对象,这又怎么可以称之为一种类定义呢?

我们再来改造下setDen方法:

代码块16
1
2
3
public void setDen(Den<?> animal) {
this.dens[i++] = animal;
}

泛型类作为参数接收时,定义<?>通配符可以表示,你可以传入任意Den泛型对象,这个时候再次按照图4里的传参方式,Den<Cat>和Den<Dog>就都可以被接收了。

但是,我们说了,<?>意味着你可以传任意符合Den泛型的对象,如果我想要搞一个只接收猫族特权的方法,该怎么定义呢?如下:

代码块17
1
2
3
public void setCatDen(Den<? extends Cat> cat) { //通配符仍然可以控制接收泛型类参数的范围,但相比Den原类,范围只能缩小
this.dens[i++] = cat;
}

看到了吗?就算是<?>通配符,你仍然可以定制它的接收范围,当然,这个范围肯定不可以比Den本身大,例如例子里的Den<? extends Cat>,它确实只能接收Cat及其子类的猫窝,但是它也符合Den定义的Den<T extends Animal>这个规则。