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

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 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 generics 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 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 generics son para la verificación de tipos en tiempo de comstackción, pero ¿hay alguna solución para esto?

Podría definir callFriend esta manera:

 public  T callFriend(String name, Class 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 comstackdor. Por supuesto, esto es solo una versión actualizada del casting de los días pregenerics y no agrega ninguna seguridad adicional.

No. El comstackdor 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(); 

Podrías implementarlo así:

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

(Sí, esto es código legal; consulte Java Generics: tipo genérico definido solo como tipo de devolución ).

El tipo de devolución se deducirá de la persona que llama. Sin embargo, tenga en cuenta la anotación @SuppressWarnings : eso le dice que este código no es seguro . Debe verificarlo usted mismo, o puede obtener ClassCastExceptions en tiempo de ejecución.

Lamentablemente, la forma en que lo está usando (sin asignar el valor de retorno a una variable temporal), la única forma de hacer que el comstackdor sea feliz es llamarlo así:

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

Si bien esto puede ser un poco mejor que el casting, es mejor que le Animal clase Animal un método abstracto de talk() , como dijo David Schmitt.

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  void addFriend(String name, Class type, T animal); public  T callFriend(String name, Class type); 

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

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

  T methodName(Class var); 

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

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

 public  T callFriend(String name, Class 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 generics de Java. Hay una forma de implementar Contenedores Heterogéneos Typesafe (THC) usando Super Type Tokens , pero eso tiene sus propios problemas nuevamente.

En función de la misma idea que los Super Type Tokens, puede crear un ID tipado para usar en lugar de una cadena:

 public abstract class TypedID { 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 frustrar 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 spike = new TypedID("spike") {}; TypedID quacker = new TypedID("quacker") {}; jerry.addFriend(spike, new Dog()); jerry.addFriend(quacker, new Duck()); 

Pero ahora puedes usar la clase de la manera que originalmente quisiste, 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 significa que puede recuperar el tipo del identificador más tarde si lo desea.

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

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.

“¿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 friends = new HashMap(); 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.

He escrito un artículo que contiene una prueba de concepto, clases de soporte y una clase de prueba que demuestra cómo las clases pueden recuperar los tokens de tipo superpuesto durante el tiempo de ejecución. En pocas palabras, le permite delegar en implementaciones alternativas dependiendo de los parámetros generics reales aprobados por la persona que llama. Ejemplo:

  • TimeSeries delega en una clase interna privada que usa double[]
  • TimeSeries delega en una clase interna privada que usa ArrayList

Ver: Usando TypeTokens para recuperar parámetros generics

Gracias

Richard Gomes – Blog

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

 public  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 friends = new HashMap<>(); public void addFriend(String name, Animal animal){ friends.put(name,animal); } public  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(); } } 

En realidad no, porque como dices, el comstackdor 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?

Lo que estás buscando aquí es abstracción. Código contra interfaces más y debería tener que hacer menos lanzamientos.

El ejemplo siguiente 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(string name) where T : _Animal; } interface _Animal { void Speak(); } abstract class AnimalBase : _Animal, _HasFriends { private Dictionary friends = new Dictionary(); public abstract void Speak(); public void AddFriend(string name, _Animal animal) { friends.Add(name, animal); } public T CallFriend(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!"); } } } 

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

 public  T tapSignInButton(Class type) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException { //signInButton.click(); return type.getConstructor(AppiumDriver.class).newInstance(appiumDriver); } 
  • MobilePage es la superclase que se extiende, lo que significa que puede usar cualquiera de sus hijos (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 quieres arrojar los errores, puedes atraparlos así:

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

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> friends = new HashMap>(); private Map theFriends = new HashMap(); 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"); } } 

qué pasa

 public class Animal { private Map> friends = new HashMap>(); public  void addFriend(String name, T animal){ friends.put(name,animal); } public  T callFriend(String name){ return friends.get(name); } 

}

Hice lo siguiente en mi lib kontraktor:

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

subclassing:

 public class MyHttpAppSession extends Actor { ... } 

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.

 public  X nextRow(Y cursor) { return (X) getRow(cursor); } private  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 necesita encasillar.

 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.