Java中CRTP的替代方案

8

CRTP模式允许在Java中模拟所谓的self types,例如:

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实例:
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

此外,类型检查纯粹是编译时的检查,也就是说,可以轻松地在单个TreeSet中混合使用C1和C2实例,并将它们彼此比较。
在Java中,有没有替代CRTP的方法来模拟self types而不会像上面所示那样被滥用?
附言:我注意到这种模式在标准库中并不常用——只有EnumSet及其后代实现了它。

2
什么度量决定了一种替代方案是否比CRTP更好?这个问题听起来很主观。 - jaco0646
@jaco0646 嗯,我知道这个问题可能听起来是基于意见的,但我会感激_任何_回答,描述可能的替代方案。已相应地编辑了问题。 - Bass
1
如果“任何”替代方案都可以接受,那么问题就变得太宽泛了:没有比较潜在答案的基础。顺便说一下,我认为这个问题很有趣,所以我正在试图将其推向更少开放式的方向。也许,“在Java中,有哪些CRTP的替代方案可以模拟自类型而不会像上面展示的那样被滥用?” - jaco0646
@jaco0646 谢谢,已按照您的建议进行了编辑。 - Bass
1
@Bass 这可能是什么? - Eugene
显示剩余3条评论
1个回答

5

我认为你所展示的并不是"滥用"。只要不进行任何不安全的转换,所有使用 AbstractFooC3 的代码仍然是完全类型安全的。在AbstractFooSELF 的边界意味着代码可以依赖于 SELFAbstractFoo<SELF> 的子类型,但代码不能依赖于 AbstractFoo<SELF>SELF 的子类型。因此,例如,如果 AbstractFoo 有一个返回 SELF 的方法,并且它通过返回 this 来实现(如果它确实是一个“自我类型”应该是可能的),那么它将无法编译:

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

编译器不允许你编译这个代码,因为它是不安全的。例如,在C3上运行此方法将会返回类型C1的this(实际上是一个C3实例),这将在调用代码中引起类转换异常。如果你试图通过使用强制转换来绕过编译器,例如:return (SELF)this;,那么你会得到一个未经检查的强制转换警告,这意味着你需要对它的不安全性负责。
如果你的AbstractFoo只是按照SELF extends AbstractFoo<SELF>的要求使用,而不依赖于AbstractFoo<SELF> extends SELF的事实,那么你为什么还要关心对C3的“滥用”呢?你仍然可以很好地编写你的类C1 extends AbstractFoo<C1>C2 extends AbstractFoo<C2>。如果其他人决定编写一个类C3 extends AbstractFoo<C1>,只要他们没有使用不安全的强制转换方式,那么编译器保证它仍然是类型安全的。也许这样的类可能做不了什么有用的事情;我不知道。但它还是安全的,那么这是个问题吗?
递归边界<SELF extends AbstractFoo<SELF>>之所以没有被广泛使用,是因为在大多数情况下,它并不比<SELF>更有用。例如,Comparable接口的类型参数就没有边界。如果有人决定编写一个类Foo extends Comparable<Bar>,他们可以这样做,而且是类型安全的,尽管并不是很有用,因为在大多数使用Comparable的类和方法中,它们都有一个类型变量<T extends Comparable<? super T>>,这需要T与自身可比较,因此Foo类不能作为任何这些位置的类型参数使用。但是如果有人想这样做,编写Foo extends Comparable<Bar>仍然是可以的。
递归边界<SELF extends AbstractFoo<SELF>>只有在真正利用了SELF扩展AbstractFoo<SELF>这个事实的地方才会用到,这种情况非常少见。其中一个地方是在构建器模式之类的东西中,它具有返回对象本身的方法,可以链接使用。因此,如果你有这样的方法:
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泛型中,没有一种方法可以使类型参数必须与当前实现类的类型相同。假设有这样一种机制,那么这可能会导致继承方面的棘手问题:

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 必须是实现类的类型的约定。如果 SubC1.compareTo() 必须带一个 C1 参数,那么收到的东西的类型不再与当前对象本身的类型相同。如果 SubC1.compareTo() 可以带一个 SubC1 参数,那么它不再覆盖 C1.compareTo(),因为它不再像超类中的方法那样具有广泛的参数集。


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