为了理解递归类型边界的概念,让我们解决一个简单的问题。通过解决实际问题来更容易地理解这个概念。在理解概念后,我会在最后提供递归类型边界的定义,因为这样更有意义。
问题:
假设我们必须按大小对水果进行排序。我们被告知只能比较相同类型的水果。例如,我们不能将苹果与橙子进行比较(双关语)。所以,我们创建一个简单的类型层次结构,如下所示:
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);
}
}
解决方案
在这段代码中,我们能够实现我们的目标,即比较相同类型的水果,如苹果和苹果,橙子和橙子。当我们将一个苹果与一个橙子进行比较时,我们得到了一个错误,这正是我们想要的。
问题
问题在于实现compareTo()
方法的代码在Apple
和Orange
类中重复了。而且,在我们未来创建新水果时,所有继承自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()
方法的重复代码移动到超类中来消除它。我们的扩展类Apple
和Orange
不再受到常见代码的污染。
问题
问题在于,我们现在可以比较不同类型的对象,比较苹果和橙子不再导致错误:
apple1.compareTo(orange1)
介绍类型参数
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());
}
}
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>
进行比较。请注意我们的 Apple
和 Orange
类;它们现在分别继承自类型 Fruit<Apple>
和 Fruit<Orange>
。现在如果我们尝试比较不同类型,IDE 将显示错误,这是我们期望的行为:
apple1.compareTo(orange1)
问题
但在这一步中,我们的Fruit
类无法编译。编译器不认识T
的getSize()
方法。这是因为我们Fruit
类的类型参数T
没有任何限制。所以,T
可以是任何类,而不是每个类都有getSize()
方法。因此,编译器在不识别T
的getSize()
方法时是正确的。
引入递归类型绑定
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());
}
}
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
的子类型。换句话说,我们指定上限为 T extends Fruit<T>
。这确保只允许作为类型参数的 Fruit
的子类型。现在编译器知道 getSize()
方法可以在 Fruit
类的子类型(Apple
、Orange
等)中找到,因为 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)
apple1.compareTo(orange1)
因此,开发人员在扩展类时需要小心。
就是这样!希望能对您有所帮助。
T
建立一个上限,那么在“引入类型参数”部分,为什么我们不能只写public class Fruit <T> implements Comparable<T extends Fruit>
?当我尝试时,我的IDE显示“意外的边界”。这不应该足以告诉编译器T
应该是Fruit的子类吗? - rgbk21Comparable
这样已经存在的类或接口更改类型,因为它在Java库中声明为Comparable<T>
而不是Comparable<T extends SomeClass>
。这是我们必须履行的契约。您在Fruit
上指定的任何限制都会自动传递给Comparable<T>
。例如,如果您编写Fruit<T extends Fruit<T>>
,则Comparable
将在幕后自动接收Fruit<T>
。还要注意,递归类型绑定的目的是建立该类型本身的绑定。希望这能解决您的疑虑。 - Yogesh Umesh Vaity