Java 中 CRTP 的替代品

Alternatives to CRTP in Java

Java中的CRTP pattern allows to emulate the so called self types,e。 g.:

abstract class AbstractFoo<SELF extends AbstractFoo<SELF>> implements Comparable<SELF> {
    @Override
    public final int compareTo(final SELF o) {
        // ...
    }
}

final class C1 extends AbstractFoo<C1> {
    // ...
}

final class C2 extends AbstractFoo<C2> {
    // ...
}

使用上面的代码(为清楚起见,选择了 Comparable 接口;当然还有其他用例),我可以轻松比较 C1:[=23 的两个实例=]

new C1().compareTo(new C1());

但不是 AbstractFoo 个具体类型不同的后代:

new C1().compareTo(new C2()); // compilation error

滥用该模式很容易,但是:

final class C3 extends AbstractFoo<C1> {
    // ...
}

// ...

new C3().compareTo(new C1()); // compiles cleanly

此外,类型检查纯粹是编译时的,i。 e.可以轻松地将 C1C2 实例混合在一个 TreeSet 中,并将它们相互比较。

JavaCRTP 的替代方案可以模拟 self 类型 而无需如上所示是否存在滥用的可能性?

P。 S. 我观察到该模式在标准库中并未广泛使用——只有 EnumSet 及其后代实现了它。

我不认为你显示的是"abuse"。所有使用 AbstractFooC3 的代码仍然是完全类型安全的,只要它们不进行任何不安全的转换。 AbstractFooSELF 的边界意味着代码可以依赖于 SELFAbstractFoo<SELF> 的子类型这一事实,但代码不能依赖于 AbstractFoo<SELF>SELF 的子类型。因此,例如,如果 AbstractFoo 有一个 returned SELF 的方法,并且它是通过 returning this 实现的(如果它是真的是 "self-type"),它不会编译:

abstract class AbstractFoo<SELF extends AbstractFoo<SELF>> {
    public SELF someMethod() {
        return this; // would not compile
    }
}

编译器不允许您编译它,因为它不安全。例如,运行 C3 上的此方法会将 return this(实际上是 C3 实例)作为类型 C1,这将导致class 调用代码中的强制转换异常。如果您试图通过使用强制转换(例如 return (SELF)this;)来绕过编译器,那么您会收到一个未经检查的强制转换警告,这意味着您要对其不安全负责。

而且如果你的 AbstractFoo 的使用方式真的只依赖于 SELF extends AbstractFoo<SELF> (如绑定所说)这一事实,而不依赖于 AbstractFoo<SELF> extends SELF,那你为什么要关心C3的"abuse"呢?你仍然可以写你的 classes C1 extends AbstractFoo<C1>C2 extends AbstractFoo<C2> 没问题。如果其他人决定编写 class C3 extends AbstractFoo<C1>,那么只要他们以不使用不安全转换的方式编写它,编译器就会保证它仍然是类型安全的。也许这样的 class 可能无法做任何有用的事情;我不知道。但它仍然是安全的;那为什么会出现问题呢?

之所以没有像<SELF extends AbstractFoo<SELF>>这样的递归绑定,是因为在大多数情况下,它并不比<SELF>更有用。例如,Comparable 接口的类型参数没有界限。如果有人决定写一个 class Foo extends Comparable<Bar>,他们可以这样做,而且它是类型安全的,虽然不是很有用,因为在大多数 classes 和方法中使用 Comparable,它们有一个类型变量 <T extends Comparable<? super T>>,它要求 T 与它自己是可比较的,因此 Foo class 不能用作任何类型参数那些地方。但是有人愿意写Foo extends Comparable<Bar>还是可以的。

唯一像 <SELF extends AbstractFoo<SELF>> 这样的递归边界是在实际利用 SELF 扩展 AbstractFoo<SELF> 这一事实的地方,这是非常罕见的。一个地方是类似于构建器模式的东西,它具有 return 对象本身的方法,可以链接。所以如果你有像

这样的方法
abstract class AbstractFoo<SELF extends AbstractFoo<SELF>> {
    public SELF foo() { }
    public SELF bar() { }
    public SELF baz() { }
}

并且您的一般值为 AbstractFoo<?> x,您可以执行 x.foo().bar().baz() 之类的操作,如果将其声明为 abstract class AbstractFoo<SELF>.

,则您无法执行此操作

在 Java 泛型中没有办法使类型参数必须与当前实现 class 的类型相同。如果假设有这样一种机制,那可能会导致棘手的继承问题:

abstract class AbstractFoo<SELF must be own type> {
    public abstract int compareTo(SELF o);
}

class C1 extends AbstractFoo<C1> {
    @Override
    public int compareTo(C1 o) {
        // ...
    }
}

class SubC1 extends C1 {
    @Override
    public int compareTo(/* should it take C1 or SubC1? */) {
        // ...
    }
}

这里,SubC1 隐式继承了 AbstractFoo<C1>,但这违反了 SELF 必须是实现 class 类型的约定。如果 SubC1.compareTo() 必须采用 C1 参数,则接收到的事物的类型与当前对象本身的类型不再相同。如果 SubC1.compareTo() 可以采用 SubC1 参数,那么它不再覆盖 C1.compareTo(),因为它不再采用与 super[=79= 中的方法一样广泛的参数集]需要。