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


589

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

Tengo una Animalclase, donde cada uno Animalpuede tener muchos amigos.
Y subclases, por ejemplo Dog, Duck, Mouseetc, que añadir un comportamiento específico, como bark(), quack()etc.

Aquí está la Animalclase:

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 letra:

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 usar genéricos para el tipo de retorno para deshacerme de la conversión de texto, de modo que pueda decir

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

Aquí hay un código inicial con el tipo de retorno 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 manera de averiguar el tipo de retorno en tiempo de ejecución sin el uso del parámetro adicional 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?

Respuestas:


903

Podría definir de callFriendesta manera:

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

Entonces llámalo como tal:

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

Este código tiene el beneficio de no generar ninguna advertencia del compilador. Por supuesto, esta es solo una versión actualizada del casting de los días pregenéricos y no agrega ninguna seguridad adicional.


29
... pero aún no tiene comprobación de tipo de tiempo de compilación entre los parámetros de la llamada callFriend ().
David Schmitt el

2
Esta es la mejor respuesta hasta ahora, pero debe cambiar addFriend de la misma manera. Hace que sea más difícil escribir errores, ya que necesita esa clase literal en ambos lugares.
Craig P. Motlin el

@Jaider, no es exactamente lo mismo, pero esto funcionará: // Animal Class public T CallFriend <T> (nombre de cadena) donde T: Animal {return friends [name] como T; } // Clase de llamada jerry.CallFriend <Dog> ("spike"). Bark (); jerry.CallFriend <Duck> ("quacker"). Quack ();
Nestor Ledon

124

No. El compilador no puede saber qué tipo jerry.callFriend("spike")devolvería. Además, su implementación solo oculta el reparto 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 talk()método abstracto y anularlo de manera apropiada en las subclases te 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();

10
Si bien el método mmyers puede funcionar, creo que este método es una mejor programación OO y le ahorrará algunos problemas en el futuro.
James McMahon el

1
Esta es la forma correcta de lograr el mismo resultado. Tenga en cuenta que el objetivo es obtener un comportamiento derivado específico de la clase en tiempo de ejecución sin escribir explícitamente el código usted mismo para hacer la comprobación y conversión de tipos feos. El método propuesto por @laz funciona, pero arroja la seguridad de tipos por la ventana. Este método requiere menos líneas de código (debido a que las implementaciones de métodos están vinculadas tarde y se buscan en el tiempo de ejecución de todos modos) pero aún así le permite definir fácilmente un comportamiento único para cada subclase de Animal.
dcow

Pero la pregunta original no es sobre la seguridad de los tipos. De la forma en que lo leí, el autor de la pregunta solo quiere saber si hay una manera de aprovechar los genéricos para evitar tener que lanzar.
laz

2
@laz: sí, la pregunta original, como se plantea, no se trata de la seguridad de tipos. Eso no cambia el hecho de que hay una forma segura de implementar eso, eliminando las fallas de lanzamiento de clase. Ver también weblogs.asp.net/alex_papadimoulis/archive/2005/05/25/…
David Schmitt

3
No estoy en desacuerdo con eso, pero estamos tratando con Java y todas sus decisiones / debilidades de diseño. Veo esta pregunta simplemente tratando de aprender qué es posible en los genéricos de Java, no como un xyproblem ( meta.stackexchange.com/questions/66377/what-is-the-xy-problem ) que necesita ser rediseñado. Al igual que cualquier patrón o enfoque, hay momentos en que el código que proporcioné es apropiado y momentos en los que se requiere algo totalmente diferente (como lo que ha sugerido en esta respuesta).
laz

114

Podrías implementarlo así:

@SuppressWarnings("unchecked")
public <T extends Animal> T callFriend(String name) {
    return (T)friends.get(name);
}

(Sí, este es un código legal; consulte Java Generics: Tipo genérico definido como tipo de retorno solamente ).

El tipo de retorno se deducirá de la persona que llama. Sin embargo, tenga en cuenta la @SuppressWarningsanotación: eso le dice que este código no es seguro . Debe verificarlo usted mismo, o podría obtenerlo ClassCastExceptionsen tiempo de ejecución.

Desafortunadamente, la forma en que lo está utilizando (sin asignar el valor de retorno a una variable temporal), la única forma de hacer feliz al compilador es llamarlo así:

jerry.<Dog>callFriend("spike").bark();

Si bien esto puede ser un poco más agradable que el casting, probablemente sea mejor darle a la Animalclase un talk()método abstracto , como dijo David Schmitt.


El encadenamiento de métodos no era realmente una intención. No me importa asignar el valor a una variable Subtipo y usarlo. Gracias por la solucion.
Sathish el

¡Esto funciona perfectamente cuando se hace el método de encadenamiento de llamadas!
Hartmut P.

Realmente me gusta esta sintaxis. Creo que en C # es jerry.CallFriend<Dog>(...lo que creo que se ve mejor.
andho

Es interesante que la propia java.util.Collections.emptyList()función del JRE se implemente exactamente de esta manera, y su javadoc se anuncia como seguro.
Ti Strga

31

Esta pregunta es muy similar al Artículo 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 deben usar el literal de clase por 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 sanos. Consulte Java eficaz y la clase javadoc para obtener más información.


17

Además, puede solicitar al método que devuelva el valor de un tipo dado de esta manera

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

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


17

Aquí está la versión más simple:

public <T> T callFriend(String name) {
    return (T) friends.get(name); //Casting to T not needed in this case but its a good practice to do
}

Código completamente funcional:

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

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

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

        public static class Dog extends Animal {

            public void bark() {
                System.out.println("i am dog");
            }
        }

        public static class Duck extends Animal {

            public void quack() {
                System.out.println("i am duck");
            }
        }

        public static void main(String [] args) {
            Animal animals = new Animal();
            animals.addFriend("dog", new Dog());
            animals.addFriend("duck", new Duck());

            Dog dog = animals.callFriend("dog");
            dog.bark();

            Duck duck = animals.callFriend("duck");
            duck.quack();

        }
    }

1
Lo que hace Casting to T not needed in this case but it's a good practice to do. Quiero decir, si se cuida adecuadamente durante el tiempo de ejecución, ¿qué significa "buena práctica"?
Farid

Quise decir que la conversión explícita (T) no es necesaria ya que la declaración de tipo de retorno <T> en la declaración del método debería ser suficiente
webjockey

9

Como dijiste que pasar una clase estaría bien, podrías escribir esto:

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

Y luego úsalo así:

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

No es perfecto, pero esto es más o menos lo que obtienes con los genéricos de Java. Hay una manera de implementar contenedores heterogéneos Typesafe (THC) utilizando Super Type Tokens , pero eso tiene sus propios problemas nuevamente.


Lo siento, pero esta es exactamente la misma respuesta que tiene Laz, así que lo estás copiando o él te está copiando a ti.
James McMahon el

Esa es una buena manera de pasar el tipo. Pero aún así no es seguro como dijo Schmitt. Todavía podría pasar una clase diferente y el encasillamiento será una bomba. La segunda respuesta de mmyers para configurar el tipo de tipo de devolución parece mejor
Sathish

44
Nemo, si revisas el tiempo de publicación, verás que los publicamos exactamente en el mismo momento. Además, no son exactamente lo mismo, solo dos líneas.
Fabian Steeg el

@Fabian Publiqué una respuesta similar, pero hay una diferencia importante entre las diapositivas de Bloch y lo que se publicó en Effective Java. Utiliza la Clase <T> en lugar de TypeRef <T>. Pero esta sigue siendo una gran respuesta.
Craig P. Motlin el

8

Basado en la misma idea que Super Type Tokens, podría crear una identificación escrita para usar en lugar de una cadena:

public abstract class TypedID<T extends Animal> {
  public final Type type;
  public final String id;

  protected TypedID(String id) {
    this.id = id;
    Type superclass = getClass().getGenericSuperclass();
    if (superclass instanceof Class) {
      throw new RuntimeException("Missing type parameter.");
    }
    this.type = ((ParameterizedType) superclass).getActualTypeArguments()[0];
  }
}

Pero creo que esto puede vencer el propósito, ya que ahora necesita crear nuevos objetos de identificación para cada cadena y conservarlos (o reconstruirlos con la información de tipo correcta).

Mouse jerry = new Mouse();
TypedID<Dog> spike = new TypedID<Dog>("spike") {};
TypedID<Duck> quacker = new TypedID<Duck>("quacker") {};

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

Pero ahora puede usar la clase de la manera que originalmente quería, sin los moldes.

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

Esto solo oculta el parámetro de tipo dentro de la identificación, aunque sí significa que puede recuperar el tipo del identificador más adelante si lo desea.

Debería implementar también los métodos de comparación y hash de TypedID si desea poder comparar dos instancias idénticas de una identificación.


8

"¿Hay alguna manera de averiguar el tipo de retorno en tiempo de ejecución sin el parámetro adicional usando instanceof?"

Como una solución alternativa, puede utilizar el patrón Visitor como este. Haga que Animal abstract y haga que se implemente 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 de qué tipo es, puede activar el método de visita sobrecargado relevante del visitante v al pasar "esto" como parámetro. Otras subclases implementarían accept () exactamente de la misma manera.

La clase que desea llamar a los métodos específicos de la subclase debe implementar la interfaz de visitante 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 son muchas más interfaces y métodos de los que esperaba, pero es una forma estándar de manejar cada subtipo específico con exactamente cero instancias de verificaciones y cero tipos de conversiones. Y todo se realiza de manera independiente del lenguaje estándar, por lo que no es solo para Java, sino que cualquier lenguaje OO debería funcionar igual.


6

Imposible. ¿Cómo se supone que el Mapa sabe qué subclase de Animal obtendrá, dada solo una clave de Cadena?

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


5

He escrito un artículo que contiene una prueba de concepto, clases de soporte y una clase de prueba que demuestra cómo tus clases pueden recuperar Super Type Tokens en tiempo de ejecución. En pocas palabras, le permite delegar a implementaciones alternativas dependiendo de los parámetros genéricos reales pasados ​​por la persona que llama. Ejemplo:

  • TimeSeries<Double> delega a una clase interna privada que usa double[]
  • TimeSeries<OHLC> delega a una clase interna privada que usa ArrayList<OHLC>

Ver: Uso de TypeTokens para recuperar parámetros genéricos

Gracias

Richard Gomes - Blog


De hecho, gracias por compartir su conocimiento, ¡su artículo realmente lo explica todo!
Yann-Gaël Guéhéneuc

3

Aquí hay muchas respuestas excelentes, pero este es el enfoque que tomé para una prueba de Appium donde actuar sobre un solo elemento puede dar lugar a ir a diferentes estados de aplicación según la configuración del usuario. Si bien no sigue las convenciones del ejemplo de OP, espero que ayude a alguien.

public <T extends MobilePage> T tapSignInButton(Class<T> type) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
    //signInButton.click();
    return type.getConstructor(AppiumDriver.class).newInstance(appiumDriver);
}
  • MobilePage es la superclase que el tipo extiende, lo que significa que puede usar cualquiera de sus elementos secundarios (duh)
  • type.getConstructor (Param.class, etc.) le permite interactuar con el constructor del tipo. Este constructor debe ser el mismo entre todas las clases esperadas.
  • newInstance toma una variable declarada que desea pasar al nuevo constructor de objetos

Si no desea arrojar los errores, puede atraparlos así:

public <T extends MobilePage> T tapSignInButton(Class<T> type) {
    // signInButton.click();
    T returnValue = null;
    try {
       returnValue = type.getConstructor(AppiumDriver.class).newInstance(appiumDriver);
    } catch (Exception e) {
        e.printStackTrace();
    }
    return returnValue;
}

A mi entender, esta es la mejor y más elegante forma de usar los genéricos.
cbaldan

2

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 sus subclases implementarían como un ladrido o graznido?


1
¿Qué pasa si los animales tienen múltiples métodos que ni siquiera caen bajo una acción común que pueda abstraerse? Necesito esto para la comunicación entre subclases con diferentes acciones donde estoy de acuerdo con pasar el tipo, no una instancia.
Sathish el

2
Realmente acabas de responder a tu propia pregunta: si un animal tiene una acción única, entonces tienes que lanzar a ese animal específico. Si un animal tiene una acción que se puede agrupar con otros animales, puede definir un método abstracto o virtual en una clase base y usarlo.
Matt Jordan el

2

Lo que estás buscando aquí es abstracción. Codifique contra interfaces más y debería hacer menos casting.

El siguiente ejemplo está en C # pero el concepto sigue siendo el mismo.

using System;
using System.Collections.Generic;
using System.Reflection;

namespace GenericsTest
{
class MainClass
{
    public static void Main (string[] args)
    {
        _HasFriends jerry = new Mouse();
        jerry.AddFriend("spike", new Dog());
        jerry.AddFriend("quacker", new Duck());

        jerry.CallFriend<_Animal>("spike").Speak();
        jerry.CallFriend<_Animal>("quacker").Speak();
    }
}

interface _HasFriends
{
    void AddFriend(string name, _Animal animal);

    T CallFriend<T>(string name) where T : _Animal;
}

interface _Animal
{
    void Speak();
}

abstract class AnimalBase : _Animal, _HasFriends
{
    private Dictionary<string, _Animal> friends = new Dictionary<string, _Animal>();


    public abstract void Speak();

    public void AddFriend(string name, _Animal animal)
    {
        friends.Add(name, animal);
    }   

    public T CallFriend<T>(string name) where T : _Animal
    {
        return (T) friends[name];
    }
}

class Mouse : AnimalBase
{
    public override void Speak() { Squeek(); }

    private void Squeek()
    {
        Console.WriteLine ("Squeek! Squeek!");
    }
}

class Dog : AnimalBase
{
    public override void Speak() { Bark(); }

    private void Bark()
    {
        Console.WriteLine ("Woof!");
    }
}

class Duck : AnimalBase
{
    public override void Speak() { Quack(); }

    private void Quack()
    {
        Console.WriteLine ("Quack! Quack!");
    }
}
}

Esta pregunta tiene que ver con la codificación, no con el concepto.

2

Hice lo siguiente en mi lib kontraktor:

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

subclases:

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

al menos esto funciona dentro de la clase actual y cuando se tiene una referencia tipada fuerte. La herencia múltiple funciona, pero se vuelve realmente difícil :)


1

Sé que esto es algo completamente diferente a lo que preguntó. Otra forma de resolver esto sería la reflexión. Quiero decir, esto no toma el beneficio de los genéricos, pero le permite emular, de alguna manera, el comportamiento que desea realizar (hacer ladrar a un perro, hacer un graznido de pato, etc.) sin tener en cuenta el tipo de conversión:

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

    }

}

1

qué pasa

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

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

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

}


0

Hay otro enfoque, puede reducir el tipo de retorno cuando anula un método. En cada subclase, tendría que anular callFriend para devolver esa subclase. El costo serían las múltiples declaraciones de callFriend, pero podría aislar las partes comunes a 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 retorno.


No estoy seguro de lo que quieres decir con "reducir el tipo de retorno". Afaik, Java y la mayoría de los lenguajes mecanografiados no sobrecargan los métodos o funciones basados ​​en el tipo de retorno. Por ejemplo, public int getValue(String name){}es indistinguible desde public boolean getValue(String name){}el punto de vista de los compiladores. Debería cambiar el tipo de parámetro o agregar / eliminar parámetros para que se reconozca la sobrecarga. Tal vez solo te estoy malinterpretando ...
The One True Colter

en java puede anular un método en una subclase y especificar un tipo de retorno más "estrecho" (es decir, más específico). Ver stackoverflow.com/questions/14694852/… .
FeralWhippet

-3
public <X,Y> X nextRow(Y cursor) {
    return (X) getRow(cursor);
}

private <T> Person getRow(T cursor) {
    Cursor c = (Cursor) cursor;
    Person s = null;
    if (!c.moveToNext()) {
        c.close();
    } else {
        String id = c.getString(c.getColumnIndex("id"));
        String name = c.getString(c.getColumnIndex("name"));
        s = new Person();
        s.setId(id);
        s.setName(name);
    }
    return s;
}

Puede devolver cualquier tipo y recibir directamente como. No es necesario escribir a máquina.

Person p = nextRow(cursor); // cursor is real database cursor.

Esto es mejor si desea personalizar cualquier otro tipo de registros en lugar de cursores reales.


2
Usted dice que "no es necesario escribir en formato", pero claramente hay un tipo de conversión involucrado: (Cursor) cursorpor ejemplo.
Sami Laine

Este es un uso totalmente inapropiado de genéricos. Este código requiere cursorser un Cursor, y siempre devolverá un Person(o nulo). El uso de genéricos elimina la comprobación de estas restricciones, lo que hace que el código sea inseguro.
Andy Turner
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.