[java] ¿Cómo hago que el método de devolución sea genérico?


Answers

No. El compilador no puede saber qué tipo jerry.callFriend("spike") . Además, su implementación solo oculta el modelo en el método sin ningún tipo de seguridad adicional. Considera esto:

jerry.addFriend("quaker", new Duck());
jerry.callFriend("quaker", /* unused */ new Dog()); // dies with illegal cast

En este caso específico, crear un método abstracto de talk() y anularlo adecuadamente en las subclases le serviría mucho mejor:

Mouse jerry = new Mouse();
jerry.addFriend("spike", new Dog());
jerry.addFriend("quacker", new Duck());

jerry.callFriend("spike").talk();
jerry.callFriend("quacker").talk();
Question

Considere este ejemplo (típico en los libros OOP):

Tengo una clase de Animal , donde cada Animal puede tener muchos amigos.
Y subclases como Dog , Duck , Mouse , etc. que agregan un comportamiento específico como bark() , quack() etc.

Aquí está la clase de Animal :

public class Animal {
    private Map<String,Animal> friends = new HashMap<>();

    public void addFriend(String name, Animal animal){
        friends.put(name,animal);
    }

    public Animal callFriend(String name){
        return friends.get(name);
    }
}

Y aquí hay un fragmento de código con muchos tipos de conversión:

Mouse jerry = new Mouse();
jerry.addFriend("spike", new Dog());
jerry.addFriend("quacker", new Duck());

((Dog) jerry.callFriend("spike")).bark();
((Duck) jerry.callFriend("quacker")).quack();

¿Hay alguna forma de que pueda usar genéricos para el tipo de devolución para deshacerme de la conversión de tipos, para que pueda decir

jerry.callFriend("spike").bark();
jerry.callFriend("quacker").quack();

Aquí hay un código inicial con el tipo de devolución transmitido al método como un parámetro que nunca se usa.

public<T extends Animal> T callFriend(String name, T unusedTypeObj){
    return (T)friends.get(name);        
}

¿Hay alguna forma de averiguar el tipo de devolución en tiempo de ejecución sin el parámetro adicional utilizando instanceof ? O al menos pasando una clase del tipo en lugar de una instancia ficticia.
Entiendo que los genéricos son para la verificación de tipos en tiempo de compilación, pero ¿hay alguna solución para esto?




Imposible. ¿Cómo se supone que el Mapa sabrá qué subclase de Animal va a obtener, solo con una clave de Cadena?

La única forma en que esto sería posible es si cada Animal acepta solo un tipo de amigo (entonces podría ser un parámetro de la clase Animal), o si el método callFriend () tiene un parámetro de tipo. Pero realmente parece que te falta el punto de la herencia: es que solo puedes tratar las subclases de manera uniforme cuando se usan exclusivamente los métodos de superclase.




Además, puede pedirle al método que devuelva el valor en un tipo determinado de esta manera

<T> T methodName(Class<T> var);

Más ejemplos here en la documentación de Oracle Java




Hice lo siguiente en mi lib kontraktor:

public class Actor<SELF extends Actor> {
    public SELF self() { return (SELF)_self; }
}

subclassing:

public class MyHttpAppSession extends Actor<MyHttpAppSession> {
   ...
}

al menos esto funciona dentro de la clase actual y al tener una fuerte referencia tipada. La herencia múltiple funciona, pero se vuelve realmente complicada, entonces :)




Hay otro enfoque, puede restringir el tipo de devolución cuando anula un método. En cada subclase, debe anular callFriend para devolver esa subclase. El costo sería las declaraciones múltiples de callFriend, pero podría aislar las partes comunes de un método llamado internamente. Esto me parece mucho más simple que las soluciones mencionadas anteriormente, y no necesita un argumento adicional para determinar el tipo de devolución.




Esta pregunta es muy similar al Punto 29 en Java efectivo : "Considere contenedores heterogéneos de tipo seguro". La respuesta de Laz es la más cercana a la solución de Bloch. Sin embargo, tanto put como get deberían usar el literal Class para seguridad. Las firmas se convertirían en:

public <T extends Animal> void addFriend(String name, Class<T> type, T animal);
public <T extends Animal> T callFriend(String name, Class<T> type);

Dentro de ambos métodos, debe verificar que los parámetros sean correctos. Consulte Java efectivo y el javadoc de Class para obtener más información.




Sé que esto es algo completamente diferente que el que preguntó. Otra forma de resolver esto sería la reflexión. Es decir, esto no aprovecha los beneficios de Generics, pero le permite emular, de alguna forma, el comportamiento que desea realizar (hacer que un perro ladre, hacer un pato, etc.) sin ocuparse de la conversión de tipos:

import java.lang.reflect.InvocationTargetException;
import java.util.HashMap;
import java.util.Map;

abstract class AnimalExample {
    private Map<String,Class<?>> friends = new HashMap<String,Class<?>>();
    private Map<String,Object> theFriends = new HashMap<String,Object>();

    public void addFriend(String name, Object friend){
        friends.put(name,friend.getClass());
        theFriends.put(name, friend);
    }

    public void makeMyFriendSpeak(String name){
        try {
            friends.get(name).getMethod("speak").invoke(theFriends.get(name));
        } catch (IllegalArgumentException e) {
            e.printStackTrace();
        } catch (SecurityException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (InvocationTargetException e) {
            e.printStackTrace();
        } catch (NoSuchMethodException e) {
            e.printStackTrace();
        }
    } 

    public abstract void speak ();
};

class Dog extends Animal {
    public void speak () {
        System.out.println("woof!");
    }
}

class Duck extends Animal {
    public void speak () {
        System.out.println("quack!");
    }
}

class Cat extends Animal {
    public void speak () {
        System.out.println("miauu!");
    }
}

public class AnimalExample {

    public static void main (String [] args) {

        Cat felix = new Cat ();
        felix.addFriend("Spike", new Dog());
        felix.addFriend("Donald", new Duck());
        felix.makeMyFriendSpeak("Spike");
        felix.makeMyFriendSpeak("Donald");

    }

}



"¿Hay alguna forma de averiguar el tipo de devolución en tiempo de ejecución sin el parámetro adicional utilizando instanceof?"

Como solución alternativa, puede utilizar el patrón Visitor de esta manera. Haz que el animal sea abstracto y hazlo implementar Visitable:

abstract public class Animal implements Visitable {
  private Map<String,Animal> friends = new HashMap<String,Animal>();

  public void addFriend(String name, Animal animal){
      friends.put(name,animal);
  }

  public Animal callFriend(String name){
      return friends.get(name);
  }
}

Visitable solo significa que una implementación de Animal está dispuesta a aceptar un visitante:

public interface Visitable {
    void accept(Visitor v);
}

Y la implementación de un visitante puede visitar todas las subclases de un animal:

public interface Visitor {
    void visit(Dog d);
    void visit(Duck d);
    void visit(Mouse m);
}

Entonces, por ejemplo, una implementación de Dog se vería así:

public class Dog extends Animal {
    public void bark() {}

    @Override
    public void accept(Visitor v) { v.visit(this); }
}

El truco aquí es que, como el perro sabe qué tipo es, puede activar el método de visita sobrecargado relevante del visitante v pasando "this" como parámetro. Otras subclases implementarían accept () exactamente de la misma manera.

La clase que quiere llamar a métodos específicos de subclase debe implementar la interfaz Visitor de esta manera:

public class Example implements Visitor {

    public void main() {
        Mouse jerry = new Mouse();
        jerry.addFriend("spike", new Dog());
        jerry.addFriend("quacker", new Duck());

        // Used to be: ((Dog) jerry.callFriend("spike")).bark();
        jerry.callFriend("spike").accept(this);

        // Used to be: ((Duck) jerry.callFriend("quacker")).quack();
        jerry.callFriend("quacker").accept(this);
    }

    // This would fire on callFriend("spike").accept(this)
    @Override
    public void visit(Dog d) { d.bark(); }

    // This would fire on callFriend("quacker").accept(this)
    @Override
    public void visit(Duck d) { d.quack(); }

    @Override
    public void visit(Mouse m) { m.squeak(); }
}

Sé que hay muchas más interfaces y métodos de los que esperaba, pero es una forma estándar de manejar cada subtipo específico con cero pruebas de instancia y modelos de cero. Y todo se hace de una manera independiente del lenguaje estándar, por lo que no es solo para Java, sino que cualquier lenguaje OO debería funcionar de la misma manera.




En realidad no, porque como dices, el compilador solo sabe que callFriend () está devolviendo un Animal, no un Perro o un Pato.

¿No puede agregar un método abstracto makeNoise () a Animal que se implementaría como un ladrón o curandero por sus subclases?




Related