有没有一种方法可以使用类型变量引用当前类型?

61
假设我正在编写一个函数,以返回当前类型的实例。是否有一种方法可以使 T 引用精确的子类型(因此在类 BT 应该引用 B)?
class A {
    <T extends A> foo();
}

class B extends A {
    @Override
    T foo();
}

2
我认为@PaulBellora的答案非常好。总结一下:虽然有解决方案,但它很脆弱;最好在编写和控制整个类/子类树时使用此解决方案。一个粗心的程序员可能会扩展抽象类并产生一个没有意义或完全错误的子类(请参见下面的EvilLeafClass)。 - The Impaler
最好完全避免递归泛型类型所引起的混乱,而是通过Manifold使用Self类型 - Scott
6个回答

84

为了扩展StriplingWarrior的答案,我认为以下模式是必要的(这是一个分层流畅构建器API的配方)。

解决方案

首先,需要一个基础抽象类(或接口),它规定了返回扩展该类实例的运行时类型的契约:

/**
 * @param <SELF> The runtime type of the implementor.
 */
abstract class SelfTyped<SELF extends SelfTyped<SELF>> {

   /**
    * @return This instance.
    */
   abstract SELF self();
}

所有中间扩展类都必须是抽象的并保持递归类型参数SELF

public abstract class MyBaseClass<SELF extends MyBaseClass<SELF>>
extends SelfTyped<SELF> {

    MyBaseClass() { }

    public SELF baseMethod() {

        //logic

        return self();
    }
}

进一步的派生类可以按照同样的方式进行。但是,这些类中没有一个可以直接用作变量类型,除非使用原始类型或通配符(这违背了该模式的目的)。例如(如果MyClass不是abstract):
//wrong: raw type warning
MyBaseClass mbc = new MyBaseClass().baseMethod();

//wrong: type argument is not within the bounds of SELF
MyBaseClass<MyBaseClass> mbc2 = new MyBaseClass<MyBaseClass>().baseMethod();

//wrong: no way to correctly declare the type, as its parameter is recursive!
MyBaseClass<MyBaseClass<MyBaseClass>> mbc3 =
        new MyBaseClass<MyBaseClass<MyBaseClass>>().baseMethod();

这就是我称之为“中间类”的原因,也是为什么它们都应该被标记为abstract。 为了闭合循环并利用模式,“叶子”类是必要的,它们使用自己的类型解析继承的类型参数SELF并实现self()。 它们还应该被标记为final以避免违反契约:
public final class MyLeafClass extends MyBaseClass<MyLeafClass> {

    @Override
    MyLeafClass self() {
        return this;
    }

    public MyLeafClass leafMethod() {

        //logic

        return self(); //could also just return this
    }
}

这些类使模式更易用:
MyLeafClass mlc = new MyLeafClass().baseMethod().leafMethod();
AnotherLeafClass alc = new AnotherLeafClass().baseMethod().anotherLeafMethod();

这里的价值在于可以在类层次结构中上下链式调用方法,同时保持相同的特定返回类型。

免责声明

以上是Java中奇异递归模板模式的实现。该模式并不绝对安全,应仅保留在内部API的内部工作中使用。原因是在上述示例中,无法保证类型参数SELF实际上将解析为正确的运行时类型。例如:

public final class EvilLeafClass extends MyBaseClass<AnotherLeafClass> {

    @Override
    AnotherLeafClass self() {
        return getSomeOtherInstanceFromWhoKnowsWhere();
    }
}

这个例子揭示了模式中的两个漏洞:

  1. EvilLeafClass 可以“说谎”,并将任何扩展 MyBaseClass 的类型替换为 SELF
  2. 与此无关,self() 是否实际返回 this 是不确定的,这可能是一个问题或不是问题,这取决于基本逻辑中状态的使用。

由于这些原因,此模式很容易被误用或滥用。为了防止这种情况发生,不允许公开扩展涉及的任何类 - 注意我在 MyBaseClass 中使用包私有构造函数来替换隐式公共构造函数:

MyBaseClass() { }

如果可能的话,也请将self()保持为包私有,这样它就不会对公共API造成干扰和混淆。不幸的是,只有在SelfTyped是抽象类时才能实现这一点,因为接口方法隐式地为public。
正如zhong.j.yu在评论中指出的那样,绑定SELF的限制可能会被移除,因为它最终无法确保"self类型":
abstract class SelfTyped<SELF> {

   abstract SELF self();
}

Yu建议只依赖合同,避免因不直观的递归绑定而产生混淆或虚假安全感。就我个人而言,我更喜欢保留这个绑定,因为SELF extends SelfTyped<SELF>代表了Java中自类型的最接近的可能表达方式。但是,Yu的意见确实与Comparable所设定的先例相一致。


结论

这是一种有价值的模式,可以流畅而表达性地调用您的构建器 API。我在一些重要工作中使用过它,特别是编写自定义查询构建器框架,使得调用站点像这样:

List<Foo> foos = QueryBuilder.make(context, Foo.class)
    .where()
        .equals(DBPaths.from_Foo().to_FooParent().endAt_FooParentId(), parentId)
        .or()
            .lessThanOrEqual(DBPaths.from_Foo().endAt_StartDate(), now)
            .isNull(DBPaths.from_Foo().endAt_PublishedDate())
            .or()
                .greaterThan(DBPaths.from_Foo().endAt_EndDate(), now)
            .endOr()
            .or()
                .isNull(DBPaths.from_Foo().endAt_EndDate())
            .endOr()
        .endOr()
        .or()
            .lessThanOrEqual(DBPaths.from_Foo().endAt_EndDate(), now)
            .isNull(DBPaths.from_Foo().endAt_ExpiredDate())
        .endOr()
    .endWhere()
    .havingEvery()
        .equals(DBPaths.from_Foo().to_FooChild().endAt_FooChildId(), childId)
    .endHaving()
    .orderBy(DBPaths.from_Foo().endAt_ExpiredDate(), true)
    .limit(50)
    .offset(5)
    .getResults();

重点是,QueryBuilder 不仅仅是一个平面实现,而是来自于一系列复杂的构建器类中的“叶子”。同样的模式也被用于像 WhereHavingOr 等帮助类中,它们都需要共享大量代码。
然而,你不应该忽视最终这只是语法糖。一些有经验的程序员坚决反对 CRT 模式,或者至少怀疑其优点是否超过了增加的复杂性。他们的担忧是合理的。
总之,在实现之前,请仔细考虑是否真的有必要,如果确实需要,请不要公开扩展它。

3
@StriplingWarrior同意了——这更适合用于API的内部工作,而不是公开可扩展的。我最近实际上使用了这种结构来构建流畅的生成器模式。例如,一个人可以调用new MyFinalClass().foo().bar(),其中foo()在超类中声明,而bar()MyFinalClass中定义。只有final具体类在包外部公开,所以我的团队成员无法干扰self()等内容。我直到现在才意识到维护类型参数的中间类无法直接使用。 - Paul Bellora
1
你的意思是什么?没有SELF限制,一切仍然正常工作。 - ZhongYu
这个方案是可行的,但有一些限制,包括没有具体的中间类、需要泛型、防止顶层类扩展现有(具体)类、可能会暴露出 EvilLeafClass 等。不过,我实际上更喜欢我的解决方案,尽管它需要在每个方法调用之前进行一次调用。 - ggb667
2
通过在SelfTyped类中已经提供了self()的(最终)���现,因此不需要在每个子类中都有它的平凡实现。这样,EvilLeafClass就不能返回任意对象。相反,如果它不遵循合同,则会抛出ClassCastException@SuppressWarnings("unchecked") protected final SELF self() { return (SELF) this; } - neXus
1
@PaulBellora 我刚刚注意到的一件事是,MyBaseClass<?> mbc2 = new MyBaseClass<>().baseMethod();可以工作(编译并成功运行),而不需要MyBaseClass是抽象的。至少在我的情况下,这已经足够了,因为该类型的唯一优势是能够正确地链接所有可用的方法。 - Octavian Theodor
显示剩余7条评论

10
你可以使用Java用于枚举的递归泛型定义样式来实现此操作:
class A<T extends A<T>> {
    T foo();
}
class B extends A<B> {
    @Override
    B foo();
}

在这种情况下,A是一个抽象类吗?否则我看不出它如何实现foo() - Paul Bellora
class C extends B 时会发生什么? - jpalecek
@Kublai:我认为这是可能的,但如果A的构造函数需要一个Class<T>作为参数,那么它也有可能使用反射来实现foo - StriplingWarrior
@jpalecek:这是一个很好的观点:C.foo将返回一个B。Java的枚举类型通过禁止人们扩展枚举类型来解决了这个问题。显然,在这种情况下我们没有这样的便利。 - StriplingWarrior
@Stripling - 请看我的回答。我试图在你的基础上进行完善,但需要确认。 - Paul Bellora

4
我可能没有完全理解这个问题,但是只做这个(注意要将其转换为T)不就足够了吗?
   private static class BodyBuilder<T extends BodyBuilder> {

        private final int height;
        private final String skinColor;
        //default fields
        private float bodyFat = 15;
        private int weight = 60;

        public BodyBuilder(int height, String color) {
            this.height = height;
            this.skinColor = color;
        }

        public T setBodyFat(float bodyFat) {
            this.bodyFat = bodyFat;
            return (T) this;
        }

        public T setWeight(int weight) {
            this.weight = weight;
            return (T) this;
        }

        public Body build() {
            Body body = new Body();
            body.height = height;
            body.skinColor = skinColor;
            body.bodyFat = bodyFat;
            body.weight = weight;
            return body;
        }
    }

那么子类就不必使用重写或类型协变来使母类方法返回对它们的引用了...
    public class PersonBodyBuilder extends BodyBuilder<PersonBodyBuilder> {

        public PersonBodyBuilder(int height, String color) {
            super(height, color);
        }

    }

没错,就是这个。 - Yaza

3

只需编写:

class A {
    A foo() { ... }
}

class B extends A {
    @Override
    B foo() { ... }
}

假设您正在使用Java 1.5+(协变返回类型)。

我不确定 OP 是否希望 A 的 foo 返回类型为 A,只是希望返回一个扩展它的东西。看起来只有在特定情况下的 B,他才希望 foo 返回一个 B - Michael McGowan
1
@Michael:这可能非常适合OP的需求。如果他调用new A().foo();,他应该有一个强类型的A。如果他调用new B().foo();,他应该有一个强类型的B。唯一缺少的是,A的定义没有强制要求任何扩展它的B必须从foo返回一个B - StriplingWarrior
如果 OP 想要返回当前类型的实例,那么他应该这样做。否则,他应该使用递归泛型类型定义(必须忘记痛苦)。 - dm3
但是这需要在子类中重写方法来改变返回类型。 - Tomasz Mularczyk

3
如果你想要类似于Scala的东西
trait T {
  def foo() : this.type
}

那么,在Java中是不可能的。你还应该注意到,除了this之外,在Scala中同样类型的函数中你不能返回太多东西。


1
我找到了一种方法来做到这一点,有点傻但有效:
在顶级类(A)中:
protected final <T> T a(T type) {
        return type
}

假设C继承自B,B继承自A。
调用:
C c = new C();
//Any order is fine and you have compile time safety and IDE assistance.
c.setA("a").a(c).setB("b").a(c).setC("c");

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