【问题标题】:Is there a way to refer to the current type with a type variable?有没有办法用类型变量引用当前类型?
【发布时间】:2011-11-13 08:39:42
【问题描述】:

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

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

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

【问题讨论】:

  • 我认为@PaulBellora 的回答非常好。总结一下:有一个解决方案,但它很脆弱;当您编写和控制整个类/子类树时,最好使用此解决方案。扩展抽象类的粗心程序员可能会生成一个没有意义或完全错误的子类(参见下面的 EvilLeafClass)。
  • 最好完全避免递归泛型类型引起的混淆,并通过Manifold使用the Self type

标签: java generics types


【解决方案1】:

Manifold 通过 @Self 注释为 Java 提供 self 类型。

简单案例:

public class Foo {
  public @Self Foo getMe() {
    return this;
  }
}

public class Bar extends Foo {
}

Bar bar = new Bar().getMe(); // Voila!

与泛型一起使用:

public class Tree {
  private List<Tree> children;

  public List<@Self Tree> getChildren() {
    return children;
  }
}

public class MyTree extends Tree {
}

MyTree tree = new MyTree();
...
List<MyTree> children = tree.getChildren(); // :)  

与扩展方法一起使用

package extensions.java.util.Map;
import java.util.Map;
public class MyMapExtensions {
  public static <K,V> @Self Map<K,V> add(@This Map<K,V> thiz, K key, V value) {
    thiz.put(key, value);
    return thiz;
  }
}

// `map` is a HashMap<String, String>
var map = new HashMap<String, String>()
  .add("bob", "fishspread")
  .add("alec", "taco")
  .add("miles", "mustard");

【讨论】:

    【解决方案2】:

    我可能没有完全理解这个问题,但仅仅这样做还不够(注意投射到 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);
            }
    
        }
    

    【讨论】:

    • 是的,就是这样。
    【解决方案3】:

    我找到了一个way 这样做,这有点傻但它有效:

    在顶级类(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");
    

    【讨论】:

      【解决方案4】:

      要在 StriplingWarrior's answer 上构建,我认为以下模式是必要的(这是分层流式构建器 API 的秘诀)。

      解决方案

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

      /**
       * @param <SELF> The runtime type of the implementor.
       */
      abstract class SelfTyped<SELF extends SelfTyped<SELF>> {
      
         /**
          * @return This instance.
          */
         abstract SELF self();
      }
      

      所有中间扩展类必须是abstract,并保持递归类型参数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();
      

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


      免责声明

      上面是curiously recurring template pattern在Java中的一个实现。这种模式本质上并不安全,应该只保留用于内部 API 的内部工作。原因是无法保证上述示例中的类型参数SELF 实际上会被解析为正确的运行时类型。例如:

      public final class EvilLeafClass extends MyBaseClass<AnotherLeafClass> {
      
          @Override
          AnotherLeafClass self() {
              return getSomeOtherInstanceFromWhoKnowsWhere();
          }
      }
      

      这个例子暴露了图案中的两个洞:

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

      由于这些原因,这种模式极有可能被误用或滥用。为防止这种情况发生,请允许 none 所涉及的类被公开扩展 - 请注意我在 MyBaseClass 中使用了包私有构造函数,它替换了隐式公共构造函数:

      MyBaseClass() { }
      

      如果可能,也请保留self() package-private,这样它就不会给公共 API 添加噪音和混乱。不幸的是,这只有在 SelfTyped 是抽象类时才有可能,因为接口方法是隐式公共的。

      作为 cmets 中的 zhong.j.yu points outSELF 上的绑定可能会被简单地删除,因为它最终无法确保“自我类型”:

      abstract class SelfTyped<SELF> {
      
         abstract SELF self();
      }
      

      Yu 建议只依赖合约,并避免因不直观的递归绑定而产生的任何混淆或错误的安全感。就个人而言,我更喜欢离开这个界限,因为SELF extends SelfTyped&lt;SELF&gt; 代表 Java 中 self 类型的 最接近 可能的表达式。但余的观点绝对符合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 等助手,所有这些都需要共享重要代码。

      但是,您不应忽视这样一个事实,即所有这些最终都只是语法糖。一些有经验的程序员take a hard stance against the CRT pattern,或者至少are skeptical of the its benefits weighed against the added complexity。他们的担忧是合理的。

      归根结底,在实施之前仔细看看它是否真的有必要——如果有,不要让它公开可扩展。

      【讨论】:

      • +1。如果您知道扩展 MyClass 的人将始终遵循您建立的规则,这可能是一个很好的解决方案。但是,没有办法使用编译规则来强制执行此操作。
      • @StriplingWarrior 同意 - 它更适合 API 的内部工作,不能公开扩展。我最近实际上将这个构造用于流畅的构建器模式。例如,可以调用new MyFinalClass().foo().bar(),其中foo() 在超类中声明,bar()MyFinalClass 中定义。只有final 的具体类暴露在包之外,所以我的团队成员不能乱用self() 等。直到现在我才意识到维护类型参数的中间类不能直接使用。
      • 你是什么意思?没有 SELF 的界限,一切仍然正常。
      • SelfTyped 类中已经为self() 提供了一个(最终)实现,因此不需要在每个子类中都有它的琐碎实现。 @SuppressWarnings("unchecked") protected final SELF self() { return (SELF) this; } 这样EvilLeafClass 不能返回任意对象。相反,如果它不遵守合同,则会抛出ClassCastException
      • @PaulBellora 我刚刚注意到的一件事是MyBaseClass&lt;?&gt; mbc2 = new MyBaseClass&lt;&gt;().baseMethod(); 确实可以工作(编译并成功运行)而不需要 MyBaseClass 是抽象的。至少在我的情况下,这已经足够了,因为该类型的唯一优点是能够正确链接所有可用的方法。
      【解决方案5】:

      如果你想要类似于 Scala 的东西

      trait T {
        def foo() : this.type
      }
      

      那么不,这在 Java 中是不可能的。您还应该注意,除了 this 之外,您可以从 Scala 中类似类型的函数返回的内容不多。

      【讨论】:

        【解决方案6】:

        您应该能够使用 Java 用于枚举的递归泛型定义样式来做到这一点:

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

        【讨论】:

        • A 在这种情况下是抽象类吗?否则我看不到它将如何实现foo()
        • class C extends B 时会发生什么?
        • @Kublai:我希望会是这样,但是如果A 的构造函数将Class&lt;T&gt; 作为参数,它可能会使用反射来实现foo
        • @jpalecek:这是一个很好的观点:C.foo 会返回一个B。 Java 的枚举通过阻止人们扩展枚举类型来解决这个问题。在这种情况下,我们显然没有那么奢侈。
        • @Stripling - 请看我的回答。我试图建立在你所拥有的基础上,但正在寻找确认。
        【解决方案7】:

        只写:

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

        假设您使用的是 Java 1.5+ (covariant return types)。

        【讨论】:

        • 我不确定 OP 是否希望 A 的 foo 返回类型 A...只是扩展它的东西。看起来只有在B 的特定情况下,他才希望foo 返回B
        • @Michael:这可能很适合 OP 的需求。如果他调用new A().foo();,他应该有一个强类型A。如果他调用new B().foo();,他应该有一个强类型B。唯一缺少的是A 的定义并没有强制任何扩展它的B必须foo 返回一个B
        • 好吧,如果 OP 想要返回当前类型的实例 - 这就是他应该做的。否则他应该使用递归泛型类型定义(必须......忘记......痛苦......)。
        • 但这需要覆盖子类中的方法来更改返回类型。
        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2019-12-20
        • 1970-01-01
        • 1970-01-01
        • 2021-08-12
        • 1970-01-01
        相关资源
        最近更新 更多