Sorry, your browser cannot access this site
This page requires browser support (enable) JavaScript
Learn more >

基本用法

Java提供了泛型,可以在编译期做一些类型检查。以集合类List为例,如果我们这样用:

1
2
3
4
5
6
7
8
9
10
11
12
public class GenericApp {
public static void main(String[] args) {
List list = new ArrayList();
list.add(1);
list.add("a");

for (Object value : list) {
System.out.println((int)value);
}
}
}

虽然也不会报错,但是由于这个list什么类型都能添加,在运行期如果要获取其中元素并做一些强制类型转换的话,一定会报错。

有了泛型,可以帮助我们在编译器就发现程序的一些问题:

1
2
3
4
5
6
7
public class GenericApp {
public static void main(String[] args) {
List<Integer> list = new ArrayList<>();
list.add(1);
list.add("a");
}
}

上述代码在编译器就会报错,因为泛型限制了当前list只能添加整型。

需要注意的是,泛型只存在于编译期,编译完成之后会进行类型擦除, 将上述代码编译后再反编译,你会看到list的元素类型其实是Object

image

当然了,如果使用的是有上限通配符的泛型,那反编译后泛型会被替换成上界的类型。

自限定类型

在Java泛型中,有一种比较奇特的用法,叫自限定泛型(Self Bounded Generic)。写法是这样的:

1
class SelfBounded<T extends SelfBounded<T>>{}

这种自限定泛型初次看起来可能会比较懵逼,SelfBounded接收的一个泛型参数,并且这个泛型的上界是它自己?看起来有点递归调用的意思?

先说结论:

它的作用常常体现在继承中,用于限定子类中泛型的类型上界,当父类的某个方法想要返回的子类的类型,可以采用自限定类型这种方式

下面我们通过一个建造者模式的例子来加深对它的理解。

实现一个建造者模式

有一个披萨类,是个抽象类,具体实现有芝士披萨和牛肉披萨,这两种披萨都有一些共有特征,比如都可以往上面加一些小料(topping),同时各自又有一些特性,比如芝士披萨需要指定尺寸,而牛肉披萨需要指定是否加酱料。

我们使用Builder模式来实现这个需求,首先将共有属性抽象到父Builder

1
2
3
4
5
6
7
8
9
10
11
public abstract class Pizza {

public static abstract class Builder {
abstract Pizza build();

public Builder addTopping(Topping topping){
System.out.println("topping added");
return this;
}
}
}

芝士披萨

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
public class CheesePizza extends Pizza {
private String size;

public CheesePizza(){}

public CheesePizza(CheesePizzaBuilder builder){
CheesePizza cheesePizza = new CheesePizza();
cheesePizza.size = builder.size;
}

public static Builder builder(){
return new CheesePizzaBuilder();
}


public static class CheesePizzaBuilder extends Builder {
private String size;

@Override
Pizza build() {
return new CheesePizza(this);
}

public CheesePizzaBuilder size(String size){
this.size = size;
return this;
}
}
}

测试类:

1
2
3
4
5
public class PizzaApp {
public static void main(String[] args) {
Pizza cheesePizza = CheesePizza.builder().size("small").addTopping(new Topping()).build();
}
}

这时候问题就出现了,CheesePizzabuilder()方法返回Builder类,Builder类是没有size方法的,因为size是子类CheesePizzaBuilder特有的。

image

这好办,我把CheesePizzabuilder()方法返回具体子类不就好了?也不是不能用,但是不优雅,这个改动意味着每次build时必选先调用子类Builder中的特有方法,如size, 然后才能调用父BuilderaddTopping方法,这样好吗?这样不好,很不优雅。

带泛型的建造者模式

冷静分析上面的问题,你就会发现根因在于父BuilderaddTopping方法返回的类型和子Buildersize方法返回的类型不一致。理论上,既然是子Builder进行build, 我们自然希望子Builder里的每个方法都能返回子Builder,这样既能调用子Builder自己的独有的方法,也能调用父Builder的方法。所以,我们可以这么改造:

Pizza

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public abstract class Pizza {

public static abstract class Builder<T> {
public Builder(){}

abstract Pizza build();

public T addTopping(Topping topping){ // 这里做了改变
System.out.println("topping added");
return self();
}

protected abstract T self();
}
}

CheesePizza

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class CheesePizza extends Pizza {
...
public static class CheesePizzaBuilder extends Builder<CheesePizzaBuilder> {
private String size;

public CheesePizzaBuilder(){}

@Override
Pizza build() {
return new CheesePizza(this);
}

@Override
protected CheesePizzaBuilder self() { //这里做了改变
return this;
}

public CheesePizzaBuilder size(String size){
this.size = size;
return this;
}
}
}

考虑到Builder中的每个具体的build方法(如size)都应该返回具体的Builder, 我们为父Builder加入了泛型,该泛型表示的是具体的Builder, 并在addTopping后返回该泛型。如此一来,我们就可以快乐的build了。

还能再优化吗

从上面的讨论可以知道,我们引入的泛型其实是有上界的,泛型T一定是继承自Builder的,所以我们可以更精简一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public abstract class Pizza {

public static abstract class Builder<T extends Builder<T>> { //这里做了修改
public Builder(){}

abstract Pizza build();

public T addTopping(Topping topping){
System.out.println("topping added");
return self();
}

protected abstract T self();
}
}

子类中的写法保持不变:

1
public static class CheesePizzaBuilder extends Builder<CheesePizzaBuilder> {}

那这时候就出现了我们前文提到的自限定泛型,它在这里的语义是:传给Builder的泛型一定是一个继承自Builder的类型。这么做有什么好处呢?它可以在编译器尽可能地帮我们检查出一些错误,如果这时候子类的Builder接收了一个非继承自Builder的类型,那么编译器就会直接报错。

总结一下

这篇文章主要介绍了自限定泛型,并通过建造者模式加深了对它的理解。自限定泛型,常用于传入的类型参数需要和类本身继承自同一父类的场景,说白了,它的作用常常体现在继承中,用于限定子类中泛型的类型上界,当父类的某个方法想要返回的子类的类型,可以采用自限定类型这种方式,加强编译期校验。

如果你要问只用个泛型,不要自我限定行不行,答案是也行,只不过前者更“细”,前者会在编译器做更多的类型校验。

评论