現在の型のインスタンスを返す関数を作成しようとしているとします。 T
が正確なサブタイプを参照するようにする方法はありますか(したがって、T
はクラスB
のB
を参照する必要があります)?
class A {
<T extends A> foo();
}
class B extends A {
@Override
T foo();
}
StriplingWarriorの答え に基づいて構築するには、次のパターンが必要だと思います(これは階層的な流暢なビルダー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();
}
}
_
さらに派生クラスも同じ方法で追跡できます。ただし、これらのクラスはいずれも、rawtypeまたはワイルドカード(パターンの目的を損なう)に頼らずに、変数の型として直接使用することはできません。例(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()
を実装する「leaf」クラスが必要です。また、契約の違反を避けるために、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();
}
}
_
この例では、パターンに2つの穴があります。
EvilLeafClass
は「嘘をつき」、MyBaseClass
を拡張する他のタイプをSELF
に置き換えることができます。self()
が実際にthis
を返すという保証はありません。これは、基本ロジックでの状態の使用に応じて、問題になる場合と問題にならない場合があります。これらの理由により、このパターンは誤用または乱用される可能性が非常に高くなります。これを防ぐには、関係するクラスのnoneをパブリックに拡張できるようにします-暗黙のパブリックコンストラクターを置き換えるMyBaseClass
でのpackage-privateコンストラクターの使用に注意してください:
_MyBaseClass() { }
_
可能であれば、self()
package-privateも保持して、パブリックAPIにノイズや混乱を加えないようにします。残念ながら、これはSelfTyped
が抽象クラスである場合にのみ可能です。これは、インターフェイスメソッドが暗黙的にパブリックであるためです。
コメントのzhong.j.yu 指摘 のように、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
は単なるフラットな実装ではなく、ビルダークラスの複雑な階層から拡張された「リーフ」であったということです。 Where
、Having
、Or
などのヘルパーにも同じパターンが使用されました。これらはすべて、重要なコードを共有する必要がありました。
ただし、これらすべてが最終的には糖衣構文にすぎないという事実を見失うことはありません。一部の経験豊富なプログラマー CRTパターンに対して厳しい姿勢をとる 、または少なくとも 追加された複雑さと比較してその利点に懐疑的です 。彼らの懸念は正当です。
結論として、実装する前に本当に必要かどうかをよく調べてください。必要な場合は、公に拡張可能にしないでください。
Scalaに似たものが必要な場合
trait T {
def foo() : this.type
}
いいえ、これはJavaでは不可能です。また、this
を除いて、Scalaで同様に型指定された関数から返すことができるものは多くないことにも注意してください。
書くだけ:
class A {
A foo() { ... }
}
class B extends A {
@Override
B foo() { ... }
}
Java 1.5 +( 共変リターンタイプ )を使用していると仮定します。
私は質問を完全には理解していないかもしれませんが、これを行うだけでは十分ではありません(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);
}
}
Manifold 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");
私は 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");