Gibt es eine Möglichkeit, mit einer Typvariablen auf den aktuellen Typ zu verweisen?

Angenommen, ich versuche eine function zu schreiben, die eine Instanz des aktuellen Typs zurückgibt. Gibt es eine Möglichkeit, T sich auf den genauen Subtyp zu beziehen (also sollte sich T in class B auf B beziehen)?

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

Um auf die Antwort von StriplingWarrior zu bauen, denke ich, dass das folgende Muster notwendig wäre (dies ist ein Rezept für eine hierarchisch fließende Builder-API).

LÖSUNG

Zunächst eine abstrakte Basisklasse (oder Schnittstelle), die den Vertrag zum Zurückgeben des Laufzeittyps einer Instanz, die die class erweitert, ausgibt:

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

Alle intermediate extending-classn müssen abstract und den rekursiven Typparameter SELF :

 public abstract class MyBaseClass> extends SelfTyped { MyBaseClass() { } public SELF baseMethod() { //logic return self(); } } 

Weitere abgeleitete classn können auf die gleiche Weise folgen. Aber keine dieser classn kann direkt als Variablentypen verwendet werden, ohne auf Rawtypes oder Wildcards zurückzugreifen (was den Zweck des Musters vereitelt). Zum Beispiel (wenn MyClass nicht abstract ):

 //wrong: raw type warning MyBaseClass mbc = new MyBaseClass().baseMethod(); //wrong: type argument is not within the bounds of SELF MyBaseClass mbc2 = new MyBaseClass().baseMethod(); //wrong: no way to correctly declare the type, as its parameter is recursive! MyBaseClass> mbc3 = new MyBaseClass>().baseMethod(); 

Dies ist der Grund, warum ich diese classn als “intermediär” bezeichne, und deshalb sollten sie alle als abstract markiert werden. Um die Schleife zu schließen und das Muster zu verwenden, sind “Leaf” -classn erforderlich, die den geerbten Typparameter SELF mit seinem eigenen Typ auflösen und self() implementieren. Sie sollten auch als final gekennzeichnet werden, um einen Vertragsbruch zu vermeiden:

 public final class MyLeafClass extends MyBaseClass { @Override MyLeafClass self() { return this; } public MyLeafClass leafMethod() { //logic return self(); //could also just return this } } 

Solche classn machen das Muster nutzbar:

 MyLeafClass mlc = new MyLeafClass().baseMethod().leafMethod(); AnotherLeafClass alc = new AnotherLeafClass().baseMethod().anotherLeafMethod(); 

Der Wert hierbei ist, dass Methodenaufrufe in der classnhierarchie auf- und abwärts verkettet werden können, während derselbe spezifische Rückgabetyp beibehalten wird.


HAFTUNGSAUSSCHLUSS

Das obige ist eine Implementierung des seltsam wiederkehrenden Vorlagenmusters in Java. Dieses Muster ist nicht von Natur aus sicher und sollte nur für die innere functionsweise der eigenen internen API reserviert sein. Der Grund dafür ist, dass es keine Garantie gibt, dass der Typparameter SELF in den obigen Beispielen tatsächlich in den richtigen Laufzeittyp aufgetriggers wird. Beispielsweise:

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

In diesem Beispiel werden zwei Löcher im Muster sichtbar gemacht:

  1. EvilLeafClass kann “lügen” und jeden anderen Typ MyBaseClass , der MyBaseClass für SELF .
  2. Unabhängig davon gibt es keine Garantie, dass self() tatsächlich zurückgibt, was ein Problem sein kann oder nicht, abhängig von der Verwendung des Zustands in der Basislogik.

Aus diesen Gründen hat dieses Muster ein großes Potenzial, missbraucht oder missbraucht zu werden. Um dies zu verhindern, erlaube keiner der beteiligten classn, öffentlich erweitert zu werden – beachte meine Verwendung des Paket-privaten Konstruktors in MyBaseClass , der den impliziten öffentlichen Konstruktor ersetzt:

 MyBaseClass() { } 

Wenn möglich, behalte auch self() package-private, also fügt es der öffentlichen API kein Rauschen und Verwirrung hinzu. Leider ist dies nur möglich, wenn SelfTyped eine abstrakte class ist, da Schnittstellenmethoden implizit öffentlich sind.

Wie zhong.j.yu in den Kommentaren darauf hinweist , könnte die Bindung an SELF einfach entfernt werden, da sie letztlich nicht den “Selbst-Typ” sicherstellt:

 abstract class SelfTyped { abstract SELF self(); } 

Yu rät, sich nur auf den Vertrag zu verlassen und jegliche Verwirrung oder falsches Sicherheitsgefühl zu vermeiden, die von der unintuitiven rekursiven Grenze herrühren. Persönlich bevorzuge ich es, die Grenze zu verlassen, da SELF extends SelfTyped den nächstmöglichen Ausdruck des SELF extends SelfTyped in Java darstellt. Aber Yus Meinung stimmt definitiv mit dem Präzedenzfall von Comparable .


FAZIT

Dies ist ein würdiges Muster, das fließende und aussagekräftige Aufrufe an Ihre Builder-API ermöglicht. Ich habe es einige Male in seriöser Arbeit verwendet, vor allem, um ein benutzerdefiniertes Abfrage-Generator-Framework zu schreiben, das Call-Sites wie folgt zulässt:

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

Der entscheidende Punkt dabei war, dass QueryBuilder nicht nur eine flache Implementierung war, sondern das “Blatt”, das sich aus einer komplexen Hierarchie von Builder-classn entwickelte. Das gleiche Muster wurde für die Helfer wie Where , Having , Or usw. verwendet, die alle wichtigen Code teilen mussten.

Sie sollten jedoch nicht die Tatsache aus den Augen verlieren, dass all dies am Ende nur zu syntaktischem Zucker führt. Einige erfahrene Programmierer nehmen eine harte Haltung gegen das CRT-Muster ein oder sind zumindest skeptisch gegenüber den Vorteilen, die gegen die zusätzliche Komplexität stehen . Ihre Bedenken sind legitim.

Unterschätzen Sie, ob es wirklich notwendig ist, bevor Sie es implementieren – und wenn Sie es tun, machen Sie es nicht öffentlich erweiterbar.

Sie sollten dazu in der Lage sein, den rekursiven generischen Definitionsstil zu verwenden, den Java für enums verwendet:

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

Einfach schreiben:

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

Angenommen, Sie verwenden Java 1.5+ ( kovariante Rückgabetypen ).

Ich verstehe die Frage vielleicht nicht vollständig, aber reicht es nicht, dies einfach zu tun (beachten Sie, dass ich an T gerichtet bin):

  private static class 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; } } 

dann müssen die Unterklassen keine Überschreibung oder Kovarianz von Typen verwenden, damit Mutterklassenmethoden eine Referenz auf sie zurückgeben …

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

Wenn Sie etwas haben wollen, das Scala ähnlich ist

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

dann nein, das ist in Java nicht möglich. Sie sollten auch beachten, dass Sie nicht viel von einer ähnlich typisierten function in Scala zurückgeben können, abgesehen this .

Ich habe einen Weg gefunden , dies zu tun, es ist irgendwie albern, aber es funktioniert:

In der obersten class (A):

 protected final  T a(T type) { return type } 

Angenommen, C erweitert B und B erstreckt sich A.

Aufrufen:

 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");