Java泛型详解(对比Kotlin)

### 为什么需要泛型

我们如果需要一个动物园(Zoo)然后里面会生活着很多小动物(animals),如果没有泛型的情况下去实现这样一个类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public interface Animal {
}
public class Cat implements Animal {
}
public class Dog implements Animal {
}
public class Zoo {
private Animal[] animals;

public void add(Animal e) {

}
public Animal get(int index) {
return animals[index];
}
}

因为animals有很多种,小狗小猫猴子等,所以我们用Animal的数组,这样可以满足我们的需求,如下代码

1
2
3
4
Zoo zoo = new Zoo();
zoo.add(new Cat());
zoo.add(new Dog());
Animal animal = zoo.get(0);

然后我需要一个全是小狗的动物园

1
2
3
4
5
6
Zoo zoo = new Zoo();
zoo.add(new Dog());
zoo.add(new Dog());
zoo.add(new Cat());//(1)不符合要求但是可以加进来,不会报错
Dog dog = (Dog) zoo.get(0);
Dog dog = (Dog) zoo.get(2);//(2)此时得到了错误的类型,强转会出错

当我们变换需求为全是小狗的动物园后,发现这里会有两个问题

1、这里是需要强制转换类型的;2、我们没办法去校验加入的类型是不是我们需要的类型,有可能是错误的,比如代码中的(1),会导致代码中(2)的错误

如果要解决上面这个问题我们可以写一个叫做DogZoo,全是小狗的动物园

1
2
3
4
5
6
7
8
9
10
public class DogZoo {
private Dog[] dogs;

public void add(Dog e) {

}
public Dog get(int index) {
return dogs[index];
}
}

但是这样就会写一堆的Zoo,比如CatZoo,MonkeyZoo等等。

所以就诞生的泛型,有了泛型之后我们就可以写出如下的代码

1
2
3
4
5
6
7
8
9
10
public class Zoo<T> {
private T[] animals;

public void add(T e) {

}
public T get(int index) {
return animals[index];
}
}

这样我就可以拥有我想拥有的动物园了:

1
2
3
4
5
6
7
8
9
10
11
// 搞一个各种小动物的动物园
Zoo<Animal> animalZoo = new Zoo<Animal>();
animalZoo.add(new Dog());
animalZoo.add(new Cat());
Animal animal = animalZoo.get(1);
// 搞一个只有小狗的动物园
Zoo<Dog> dogZoo = new Zoo<Dog>();
Dog dog = dogZoo.get(1);
// 搞一个只有小猫的动物园
Zoo<Cat> catZoo = new Zoo<Cat>();
Cat cat = catZoo.get(1);

泛型怎么用

泛型类

1
2
3
4
5
6
7
8
9
10
public class Zoo<T> {
private T[] animals;

public void add(T e) {

}
public T get(int index) {
return animals[index];
}
}

Zoo类其实就是一个带泛型的类,声明类的时候就需要给类一个类型,例如下面

1
2
Zoo<Dog> dogZoo = new Zoo<Dog>();
dogZoo.add(new Dog());

泛型接口

1
2
3
4
public interface Food<T> {

public void eat(T food);
}
1
2
3
4
5
6
public class Cat extends Animal implements Food<String> {
@Override
public void eat(String food) {

}
}

声明类使用接口泛型的时候需要把泛型类型一块定义好

泛型方法

1
2
3
4
public <E> void addE(E e){
}
// 使用
dogZoo.addE(dog1);

这里有个容易迷惑的地方,如下面所示

1
2
3
4
5
6
7
8
9
public class Zoo<T> {
T[] animals;

public T get(int index){
return animals[index];
}
public <T> void addT(T t){
}
}

这里的addT方法的T和类Zoo上的T其实代表的不是一个类型,方法上的类型是优先级是高于类上面的泛型,在我们正常使用过程中,最好不要出现类泛型和方法泛型使用同样的字母表示。

泛型静态方法

如下例子

1
2
3
4
5
6
7
8
9
10
public class Zoo<T> {
private T[] animals;

public void add(T e) {

}
public static T staticT(T t){// 1 报错
return t;
}
}

这时代码行1是报错的,类中的静态方法使用泛型的话,要自己定义一个类型,如下面所示:

1
2
3
4
5
6
7
8
9
10
public class Zoo<T> {
private T[] animals;

public void add(T e) {

}
public static <E> E staticE(E e){// 1 正常不报错
return e;
}
}

多个泛型类型

1
2
3
4
public class Zoo<T, E> {
private T[] animals;
private E[] Zookeepers;
}

协变,逆变是个啥?

协变,逆变,不型变,通俗理解

**invariance(不型变)**:就是上述例子中的Zoo< Animal>和Zoo< Dog>没有关系

covariance(协变): Zoo 是 Zoo 的子类型

contravariance(逆变):Zoo 是 Zoo 的子类型

当然上面的说法在Java中是不准确的,在Java中Zoo和Zoo总是没有关系的,但是为了方便理 解先这么表示,正确的应该是Zoo 是 Zoo<?extend Animal> 的子类型,Zoo 是 Zoo<? super Dog> 的子类型(子类型和子类也不是一个概念)。

Java中泛型为什么是不型变的

假如Java的泛型不是不型变的,看下面的代码

1
2
3
Zoo<Dog> dogZoo = new Zoo<Dog>(); // 1
Zoo<Animal1> animalZoo = dogZoo; // 2 报错
animalZoo.add(new Cat()); //3

在Java中代码行2是不被编译器允许的,假如他被允许,我们都知道在Java中代码行1中的dogZoo其实是指向new Zoo()对象的一个引用,同理在代码行2中animalZoo也相当于是一个指向了new Zoo()对象的引用,所以此时animalZoo按理说只能存Dog才对,如果代码行2被允许,那么代码行3也要被允许,但是这明显是不对的,因为此时animalZoo只能存Dog类型才对。所以如果代码行2允许的话,那么代码行3就不能被允许,也就是animalZoo的add方法就不能被允许,如果这样的话所有的泛型类都不被允许使用add方法了,那指定是不可以的。所以最理想的状态是有一个泛型类是没有add方法的,也就衍生出了协变。

协变

Java中的协变通过给泛型类加? extend来实现,这样编译器就会允许如代码行1中那样写了,为了避免上面的问题,同时禁用了add方法。这样可以保证不会出错,并且animalZoo可以代表此泛型类及其字类泛型。

1
2
3
4
Zoo<? extends Animal> animalZoo = dogZoo;//1 
animalZoo.add(new Cat()); // 2报错
animalZoo.add(new Dog());// 3报错
Animal animal = animalZoo.get(1);// 4

代码行2和代码行3都不行,即使代码行3在此处应该可以,但是对于animalZoo来说,此时它可以指向任何它本身以及其字类的泛型类。

其实说到这里大家虽然懂了这些,但不禁有个疑惑,上面的情况完全没有必要使用代码行1那样的操作,因为实际写代码的时候根本不会这么用,那就是这么做的意义是什么呢?看下面的两个需求:

1.需要获取不同类型动物园的符合某种动物特性的动物

没有使用? extend写法,有重复代码

1
2
3
4
5
6
for (Dog dog: dogZoo.animals){
// 符合某种条件代码
}
for (Animal animal: animalZoo.animals){
// 符合某种条件代码
}

改进,使用? extend特性:

1
2
3
4
5
6
7
8
9
10
public Animal getAnimal(Zoo<? extends Animal> zoo){
for (Animal animal : zoo.animals){
// 符合某种条件
return animal;
}
return null;
}
getAnimal(dogZoo);
getAnimal(animalZoo);
getAnimal(catZoo);

方便

2.Animal动物园需要一群一群的添加小动物

没有? extend的写法:

1
2
3
public void addAll(Collection<T> animals) {

}

这会带来一个问题,animalZoo不能添加dogZoo的小动物,也不能添加catZoo的小动物,明明是可以的,所以修改一下,改成下面:

1
2
3
public void addAll(Collection<? extend T> animals) {

}

这样就可以实现上面的需求了。

逆变

与协变相反的是逆变,在Java中使用? super来表示,即把泛型类型限定到了此类以及它的父类上,如下:

1
2
3
4
5
6
7
Zoo<? super Animal> animalZoo1 = dogZoo; // 1 报错
Zoo<? super Animal> animalZoo1 = animalZoo; // 2
animalZoo1.add(new Dog());// 3
animalZoo1.add(new Cat());// 4
Zoo<? super Dog> dogZoo1 = animalZoo;// 5
dogZoo1.add(new Dog());// 6
dogZoo1.add(new Animal());// 7 报错

从这个例子中我们可以发现被? super 修饰后只能指向本身及其父类,例如代码行2和代码行5,此时只能向里面添加super修饰符修饰的类型,因为此时指向的是本身的泛型类或者父类的泛型类,对于代码行7,在此时按理说应该是正确的,因为dogZoo1指向了Animal类型的对象,但是对于dogZoo1来说也有可能指向Dog类型的对象,所以编译器就做了一个限制,只能添加super后面类型的对象,对于然后?super修饰的类型指向范围被限定为本身及其父类。

那么同样对于? super的使用又有哪些地方用到呢,

我想对两条条小狗做一些每个小狗入园都要做的操作(比如洗澡打针)后再加入不同的动物园

不优雅的做法

1
2
3
4
5
6
Dog dog1 = new Dog();
Dog dog2 = new Dog();
// 省略dog1洗澡打针代码
// 省略dog2洗澡打针代码
dogZoo.add(dog1);
animalZoo.add(dog2);

这里就出现了重复代码就是省略的那部分,然后修改一下

1
2
3
4
5
6
addDog(dogZoo,dog1);
addDog(animalZoo,dog2);
public void addDog(Zoo<?super Dog> canAdddogZoo,Dog dog){
// 省略dog洗澡打针代码
canAdddogZoo.add(dog);
}

是不是好了很多。

协变逆变总结

对于? extend ? super的使用,我们只需要记住当需要返回T的时候就用?extend,是一个生产者;当需要写入一个T的时候使用?super,他是一个消费者。

这一部分是比较难以理解的,主要是不清楚为什么和怎么用,我也是搞了很久才搞明白,查阅了好多文章,总感觉少了点什么,所以我也就把我自己的理解写了下,有些表达应该是欠严谨,发现问题的朋友可以给我留言,我们一块探讨。

泛型在Java中的实现(类型擦除)

Java是通过类型擦除来实现泛型的:也就是说我们写出来的泛型代码跟虚拟机去执行的代码并不是一样的代码

比如我们上面的例子中:

1
2
3
4
5
6
7
8
9
10
public class Zoo<T> {
private T[] animals;

public void add(T e) {

}
public T get(int index) {
return animals[index];
}
}

在要执行的虚拟机的眼里是这个样子的:

1
2
3
4
5
6
7
8
9
10
public class Zoo {
private Object[] animals;

public void add(Object e) {

}
public Object get(int index) {
return animals[index];
}
}

再比如我们写的代码是这个样子的:

1
2
3
Zoo<Dog> dogZoo = new Zoo<Dog>();

Dog dog = dogZoo.get(1);

虚拟机里看到是这个样子的:

1
2
3
Zoo dogZoo = new Zoo();

Dog dog = (Dog) dogZoo.get(1);

带来的问题

  1. 泛型T不可以使用基本数据类型如int,double等,因为擦除后是Object嘛

  2. 无法获得泛型的Class

  3. 不能实例化泛型,例如T(),但是可以通过反射来实例化T的类型,如下

    1
    2
    3
    public Zoo(Class<T> tClass){
    tClass.newInstance();// 会提示需要捕获异常
    }

Kotlin中的泛型有啥不同

Kotlin与Java在实现泛型的底层原理是一致的,毕竟Kotlin也是要转换成Java的,但是Kotlin在Java的基础上做了一些比较方便的改变:

替换? extendout ? superin

Java对型变的处理都放到了使用处,导致有一些类只是生产者也需要在使用处多次定义

还是上面那个动物园的例子,此时的动物园只有get方法,没有add方法,说明只是一个生产者

1
2
3
4
5
6
7
8
public class ZooProducer<T> {
private T[] animals;

public T get(int index) {
return animals[index];
}
}
ZooProducer<Animal1> animal1Zoo = new ZooProducer<Dog>();// 1 报错

此时代码行1是报错的,这不意外,虽然这不太合适,毕竟我只是生产者,但是编译器不知道,但是这个问题在Kotlin中得到了解决,Kotlin可以在类声明处进行限制,如下代码

1
2
3
4
5
6
7
8
class ZooProducer1<out T> {
private val animals: Array<out T> = TODO()

fun get(): T {
return animals[1]
}
}
var animalsZooProducer: ZooProducer1<Animal1> = ZooProducer1<Dog>()// 1正常

这就带来了极大的便利,对于? superin也是同理

当一个类不能唯一确定一定是生产者或者一定是消费者的时候,和Java一样,在使用处型变就可以。其他的好像就没啥不一样的,总体而言,Kotlin的泛型是更简洁,灵活而且严格的,这也是Kotlin的特点。

这篇博客写了好久才写完,写到后面有点后劲不足,但是我相信我还是把泛型从一个新的角度表达了出来,希望研究泛型的小伙伴能够有一点点收获,泛型在实际编码过程中也是很有用的。

参考:

泛型 - 廖雪峰的官方网站 (liaoxuefeng.com)

Java和Kotlin中泛型的协变、逆变和不变 - 简书 (jianshu.com)