有没有办法用类型变量来引用当前类型?

2022-08-31 17:15:23

假设我正在尝试编写一个函数来返回当前类型的实例。有没有办法让引用确切的子类型(所以应该在课堂上引用)?TTBB

class A {
    <T extends A> foo();
}

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

答案 1

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

溶液

首先,一个基本抽象类(或接口),它列出了用于返回扩展该类的实例的运行时类型的协定:

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

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

所有中间扩展类都必须是和保持递归类型参数:abstractSELF

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

    MyBaseClass() { }

    public SELF baseMethod() {

        //logic

        return self();
    }
}

进一步的派生类可以以相同的方式跟随。但是,这些类都不能直接用作变量类型而不诉诸原始类型或通配符(这违背了模式的目的)。例如(如果不是):MyClassabstract

//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();

这就是我将这些类称为“中间”的原因,这就是为什么它们都应该被标记为 .为了闭环并利用模式,“leaf”类是必要的,它们使用自己的类型解析继承的类型参数并实现。它们也应该被标记,以避免违反合同:abstractSELFself()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 的内部工作。原因是无法保证上述示例中的 type 参数实际上将解析为正确的运行时类型。例如:SELF

public final class EvilLeafClass extends MyBaseClass<AnotherLeafClass> {

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

此示例公开了模式中的两个孔:

  1. EvilLeafClass可以“撒谎”并替换任何其他类型扩展为 。MyBaseClassSELF
  2. 除此之外,不能保证实际返回,这可能是也可能不是问题,这取决于基本逻辑中状态的使用。self()this

由于这些原因,这种模式有很大的可能被误用或滥用。为了防止这种情况,不允许任何涉及的类公开扩展 - 请注意我在 中使用包私有构造函数,它取代了隐式公共构造函数:MyBaseClass

MyBaseClass() { }

如果可能的话,也要保持包的私有性,这样它就不会给公共 API 增加噪音和混乱。不幸的是,这只有在抽象类时才有可能,因为接口方法是隐式的公共的。self()SelfTyped

正如 zhong.j.yu 在评论中指出的那样,绑定可能只是被删除,因为它最终无法确保“自我类型”:SELF

abstract class SelfTyped<SELF> {

   abstract SELF self();
}

Yu建议只依赖合同,避免任何来自不直观递归绑定的混淆或虚假的安全感。就个人而言,我更喜欢离开边界,因为它代表了Java中最接近的self类型的表达式。但余文生的观点绝对符合Compare所开创的先例。SELF extends SelfTyped<SELF>


结论

这是一个有价值的模式,允许对构建器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();

关键的一点是,这不仅仅是一个扁平的实现,而是从构建器类的复杂层次结构中延伸出来的“叶子”。相同的模式用于帮助程序,如、 、 等,所有这些都需要共享重要的代码。QueryBuilderWhereHavingOr

然而,你不应该忽视这样一个事实,即所有这些最终只相当于句法糖。一些有经验的程序员对CRT模式采取强硬立场,或者至少对其增加的复杂性所带来的好处持怀疑态度。他们的关切是合理的。

底线,在实现它之前,仔细看看它是否真的有必要 - 如果你这样做,不要让它公开扩展。


答案 2

您应该能够使用 Java 用于枚举的递归泛型定义样式来执行此操作:

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