"递归类型限定"在泛型中是什么意思?

35

我正在阅读《Effective Java[第27项]》中的有关泛型的章节。

书中有这样一段话:

允许某个类型参数被某个表达式所限制,而该表达式涉及到该类型参数本身,这种情况虽然相对较少,但也是可以的。这就是所谓的递归类型限定(recursive type bound)。

还有这句话:

// Using a recursive type bound to express mutual comparability
public static <T extends Comparable<T>> T max(List<T> list) {...}

“递归类型绑定”是什么,上述代码如何帮助实现相互可比性?
3个回答

40
为了理解递归类型边界的概念,让我们解决一个简单的问题。通过解决实际问题来更容易地理解这个概念。在理解概念后,我会在最后提供递归类型边界的定义,因为这样更有意义。
问题:
假设我们必须按大小对水果进行排序。我们被告知只能比较相同类型的水果。例如,我们不能将苹果与橙子进行比较(双关语)。所以,我们创建一个简单的类型层次结构,如下所示:
Fruit.java
interface Fruit {
    Integer getSize();
}

Apple.java

class Apple implements Fruit, Comparable<Apple> {
    private final Integer size;

    public Apple(Integer size) {
        this.size = size;
    }

    @Override public Integer getSize() {
        return size;
    }

    @Override public int compareTo(Apple other) {
        return size.compareTo(other.size);
    }
}

Orange.java

class Orange implements Fruit, Comparable<Orange> {
    private final Integer size;

    public Orange(Integer size) {
        this.size = size;
    }

    @Override public Integer getSize() {
        return size;
    }

    @Override public int compareTo(Orange other) {
        return size.compareTo(other.size);
    }
}

Main.java

class Main {
    public static void main(String[] args) {
        Apple apple1 = new Apple(3);
        Apple apple2 = new Apple(4);
        apple1.compareTo(apple2);

        Orange orange1 = new Orange(3);
        Orange orange2 = new Orange(4);
        orange1.compareTo(orange2);

        apple1.compareTo(orange1);  // Error: different types
    }
}

解决方案

在这段代码中,我们能够实现我们的目标,即比较相同类型的水果,如苹果和苹果,橙子和橙子。当我们将一个苹果与一个橙子进行比较时,我们得到了一个错误,这正是我们想要的。

问题

问题在于实现compareTo()方法的代码在AppleOrange类中重复了。而且,在我们未来创建新水果时,所有继承自Fruit的类中都会重复此代码。在我们的示例中,重复的代码量较少,但在真实世界中,重复的代码可能有数百行。


将重复代码移到公共类中

Fruit.java

class Fruit implements Comparable<Fruit> {
    private final Integer size;

    public Fruit(Integer size) {
        this.size = size;
    }

    public Integer getSize() {
        return size;
    }

    @Override public int compareTo(Fruit other) {
        return size.compareTo(other.getSize());
    }
}

Apple.java

class Apple extends Fruit {
    public Apple(Integer size) {
        super(size);
    }
}

Orange.java

class Orange extends Fruit {
    public Orange(Integer size) {
        super(size);
    }
}

解决方案

在这一步中,我们通过将compareTo()方法的重复代码移动到超类中来消除它。我们的扩展类AppleOrange不再受到常见代码的污染。

问题

问题在于,我们现在可以比较不同类型的对象,比较苹果和橙子不再导致错误:

apple1.compareTo(orange1);    // No error

介绍类型参数

Fruit.java

class Fruit<T> implements Comparable<T> {
    private final Integer size;

    public Fruit(Integer size) {
        this.size = size;
    }

    public Integer getSize() {
        return size;
    }

    @Override public int compareTo(T other) {
        return size.compareTo(other.getSize());     // Error: getSize() not available.
    }
}

Apple.java

class Apple extends Fruit<Apple> {
    public Apple(Integer size) {
        super(size);
    }
}

Orange.java

class Orange extends Fruit<Orange> {
    public Orange(Integer size) {
        super(size);
    }
}

解决方案

为了限制不同类型之间的比较,我们引入了一个类型参数 T。这样可比较的 Fruit<Apple> 就不能与可比较的 Fruit<Orange> 进行比较。请注意我们的 AppleOrange 类;它们现在分别继承自类型 Fruit<Apple>Fruit<Orange>。现在如果我们尝试比较不同类型,IDE 将显示错误,这是我们期望的行为:

apple1.compareTo(orange1);  // Error: different types

问题

但在这一步中,我们的Fruit类无法编译。编译器不认识TgetSize()方法。这是因为我们Fruit类的类型参数T没有任何限制。所以,T可以是任何类,而不是每个类都有getSize()方法。因此,编译器在不识别TgetSize()方法时是正确的。


引入递归类型绑定

Fruit.java

class Fruit<T extends Fruit<T>> implements Comparable<T> {
    private final Integer size;

    public Fruit(Integer size) {
        this.size = size;
    }

    public Integer getSize() {
        return size;
    }

    @Override public int compareTo(T other) {
        return size.compareTo(other.getSize());     // Now getSize() is available.
    }
}

Apple.java

class Apple extends Fruit<Apple> {
    public Apple(Integer size) {
        super(size);
    }
}

Orange.java

class Orange extends Fruit<Orange> {
    public Orange(Integer size) {
        super(size);
    }
}

最终解决方案

因此,我们告诉编译器,我们的 TFruit 的子类型。换句话说,我们指定上限为 T extends Fruit<T>。这确保只允许作为类型参数的 Fruit 的子类型。现在编译器知道 getSize() 方法可以在 Fruit 类的子类型(AppleOrange 等)中找到,因为 Comparable<T> 也接收了包含 getSize() 方法的我们的类型 (Fruit<T>)。

这使我们能够摆脱 compareTo() 方法的重复代码,并且还允许我们比较同一类型的水果,例如苹果和苹果,橘子和橘子。

现在 compareTo() 方法可以在题目中给出的 max() 函数中使用。


递归类型边界的定义

在泛型中,当引用类型具有一个由其自身作为边界的类型参数时,那么该类型参数被称为具有递归类型边界。

在我们的示例中,泛型类型 Fruit<T extends Fruit<T>>Fruit 是我们的引用类型,其类型参数 T 受到 Fruit 本身的限制,因此,类型参数 T 具有递归类型边界 Fruit<T>

递归类型是指包括一个使用该类型本身作为某个参数或其返回值类型的函数的类型。在我们的示例中,compareTo(T other) 是具有递归类型的函数,它将相同的递归类型作为参数。


注意事项

这种模式存在一个注意事项。编译器不会阻止我们创建具有另一个子类型的类型参数的类:

class Orange extends Fruit<Orange> {...}
class Apple extends Fruit<Orange> {...}    // No error

注意上面的Apple类,在错误的情况下,我们将Orange而不是Apple本身作为类型参数传递。这导致compareTo(T other)方法使用Orange而不是Apple。现在,当比较不同类型时,我们不再出现错误,无法将苹果与苹果进行比较:

apple1.compareTo(apple2);     // Error
apple1.compareTo(orange1);    // No error

因此,开发人员在扩展类时需要小心。


就是这样!希望能对您有所帮助。


2
这是一个很棒的答案!感谢您花时间以这种方式解释。 - hfontanez
1
谢谢您提供这么详细的答案。我有一个问题。如果递归类型的整个动机是为了为T建立一个上限,那么在“引入类型参数”部分,为什么我们不能只写public class Fruit <T> implements Comparable<T extends Fruit>?当我尝试时,我的IDE显示“意外的边界”。这不应该足以告诉编译器T应该是Fruit的子类吗? - rgbk21
1
@rgbk21,我们无法像Comparable这样已经存在的类或接口更改类型,因为它在Java库中声明为Comparable<T>而不是Comparable<T extends SomeClass>。这是我们必须履行的契约。您在Fruit上指定的任何限制都会自动传递给Comparable<T>。例如,如果您编写Fruit<T extends Fruit<T>>,则Comparable将在幕后自动接收Fruit<T>。还要注意,递归类型绑定的目的是建立该类型本身的绑定。希望这能解决您的疑虑。 - Yogesh Umesh Vaity
1
太棒了,太棒了,太棒了! - undefined

36
什么是递归类型限定?
这样写:<T extends Comparable<T>> 请注意,类型参数T也是超级接口Comparable<T>的签名的一部分。
上述代码如何实现互相比较?
它确保您只能比较类型为T的对象。如果没有类型限定,Comparable将比较任意两个Objects。有了类型限定,编译器可以确保只比较两个类型为T的对象。

10

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接