¿Hay alguna manera de hacer referencia al tipo actual con una variable de tipo?
Supongamos que estoy intentando escribir una función para devolver una instancia del tipo actual. ¿ Hay alguna manera de hacer T
referencia al subtipo exacto (por lo que T
debería consultarse B
en clase B
)?
class A {
<T extends A> foo();
}
class B extends A {
@Override
T foo();
}
Para aprovechar la respuesta de StriplingWarrior , creo que sería necesario el siguiente patrón (esta es una receta para una API de creación jerárquica fluida).
SOLUCIÓN
Primero, una clase abstracta base (o interfaz) que establece el contrato para devolver el tipo de tiempo de ejecución de una instancia que extiende la clase:
/**
* @param <SELF> The runtime type of the implementor.
*/
abstract class SelfTyped<SELF extends SelfTyped<SELF>> {
/**
* @return This instance.
*/
abstract SELF self();
}
Todas las clases de extensión intermedias deben tener abstract
y mantener el parámetro de tipo recursivo SELF
:
public abstract class MyBaseClass<SELF extends MyBaseClass<SELF>>
extends SelfTyped<SELF> {
MyBaseClass() { }
public SELF baseMethod() {
//logic
return self();
}
}
Pueden seguir más clases derivadas de la misma manera. Pero ninguna de estas clases se puede utilizar directamente como tipos de variables sin recurrir a tipos sin formato o comodines (lo que contradice el propósito del patrón). Por ejemplo (si MyClass
no lo fuera 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();
Esta es la razón por la que me refiero a estas clases como "intermedias" y es por eso que todas deberían estar marcadas abstract
. Para cerrar el ciclo y hacer uso del patrón, son necesarias clases "hoja", que resuelven el parámetro de tipo heredado SELF
con su propio tipo e implemento self()
. También deberán estar marcados final
para evitar romper el contrato:
public final class MyLeafClass extends MyBaseClass<MyLeafClass> {
@Override
MyLeafClass self() {
return this;
}
public MyLeafClass leafMethod() {
//logic
return self(); //could also just return this
}
}
Estas clases hacen que el patrón sea utilizable:
MyLeafClass mlc = new MyLeafClass().baseMethod().leafMethod();
AnotherLeafClass alc = new AnotherLeafClass().baseMethod().anotherLeafMethod();
El valor aquí es que las llamadas a métodos se pueden encadenar hacia arriba y hacia abajo en la jerarquía de clases manteniendo el mismo tipo de retorno específico.
DESCARGO DE RESPONSABILIDAD
Lo anterior es una implementación del patrón de plantilla curiosamente recurrente en Java. Este patrón no es intrínsecamente seguro y debe reservarse únicamente para el funcionamiento interno de la API interna. La razón es que no hay garantía de que el parámetro de tipo SELF
en los ejemplos anteriores realmente se resuelva en el tipo de tiempo de ejecución correcto. Por ejemplo:
public final class EvilLeafClass extends MyBaseClass<AnotherLeafClass> {
@Override
AnotherLeafClass self() {
return getSomeOtherInstanceFromWhoKnowsWhere();
}
}
Este ejemplo expone dos agujeros en el patrón:
EvilLeafClass
Puede "mentir" y sustituir cualquier otro tipo que se extiendaMyBaseClass
porSELF
.- Independientemente de eso, no hay garantía
self()
de que realmente regresethis
, lo que puede ser un problema o no, dependiendo del uso de state en la lógica base.
Por estas razones, este patrón tiene un gran potencial de ser mal utilizado o abusado. Para evitar eso, no permita que ninguna de las clases involucradas se extienda públicamente; observe mi uso del constructor privado de paquete en MyBaseClass
, que reemplaza al constructor público implícito:
MyBaseClass() { }
Si es posible, mantenga self()
el paquete privado también, para que no agregue ruido ni confusión a la API pública. Desafortunadamente esto sólo es posible si SelfTyped
es una clase abstracta, ya que los métodos de interfaz son implícitamente públicos.
Como señala zhong.j.yu en los comentarios, el límite SELF
podría simplemente eliminarse, ya que en última instancia no garantiza el "tipo propio":
abstract class SelfTyped<SELF> {
abstract SELF self();
}
Yu aconseja confiar únicamente en el contrato y evitar cualquier confusión o falsa sensación de seguridad que surja del límite recursivo no intuitivo. Personalmente, prefiero dejar el límite ya que SELF extends SelfTyped<SELF>
representa la expresión más cercana posible del tipo self en Java. Pero la opinión de Yu definitivamente se alinea con el precedente sentado por Comparable
.
CONCLUSIÓN
Este es un patrón valioso que permite llamadas fluidas y expresivas a la API de su constructor. Lo he usado varias veces en trabajos serios, sobre todo para escribir un marco de creación de consultas personalizado, que permitía llamar a sitios como este:
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();
El punto clave es que QueryBuilder
no se trataba simplemente de una implementación plana, sino de una "hoja" que se extendía desde una compleja jerarquía de clases de constructores. Se utilizó el mismo patrón para los ayudantes como Where
,,, etc., todos los Having
cuales Or
necesitaban compartir código importante.
Sin embargo, no debes perder de vista el hecho de que al final todo esto solo equivale a azúcar sintáctico. Algunos programadores experimentados adoptan una postura dura contra el patrón CRT , o al menos se muestran escépticos respecto de sus beneficios comparados con la complejidad añadida . Sus preocupaciones son legítimas.
En pocas palabras, analice detenidamente si es realmente necesario antes de implementarlo y, si lo hace, no lo haga públicamente extensible.
Debería poder hacer esto usando el estilo de definición genérica recursiva que Java usa para enumeraciones:
class A<T extends A<T>> {
T foo();
}
class B extends A<B> {
@Override
B foo();
}
Puede que no haya entendido completamente la pregunta, pero ¿no es suficiente con hacer esto (observe el envío a 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;
}
}
entonces las subclases no tendrán que usar anulación o covarianza de tipos para hacer que los métodos de la clase madre les devuelvan referencias...
public class PersonBodyBuilder extends BodyBuilder<PersonBodyBuilder> {
public PersonBodyBuilder(int height, String color) {
super(height, color);
}
}