¿Hay alguna manera de hacer referencia al tipo actual con una variable de tipo?

Resuelto jz87 asked hace 13 años • 0 respuestas

Supongamos que estoy intentando escribir una función para devolver una instancia del tipo actual. ¿ Hay alguna manera de hacer Treferencia al subtipo exacto (por lo que Tdebería consultarse Ben clase B)?

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

class B extends A {
    @Override
    T foo();
}
jz87 avatar Sep 09 '11 04:09 jz87
Aceptado

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 abstracty 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 MyClassno 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 SELFcon su propio tipo e implemento self(). También deberán estar marcados finalpara 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 SELFen 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:

  1. EvilLeafClassPuede "mentir" y sustituir cualquier otro tipo que se extienda MyBaseClasspor SELF.
  2. Independientemente de eso, no hay garantía self()de que realmente regrese this, 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 SelfTypedes 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 SELFpodrí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 QueryBuilderno 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 Havingcuales Ornecesitaban 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.

Paul Bellora avatar Sep 08 '2011 22:09 Paul Bellora

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();
}
StriplingWarrior avatar Sep 08 '2011 21:09 StriplingWarrior

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

    }
Tomasz Mularczyk avatar Jul 03 '2016 12:07 Tomasz Mularczyk