Pregunta

Considere este ejemplo (típico en los libros de programación orientada a objetos):

Yo tengo un Animal clase, donde cada Animal Puede tener muchos amigos.
Y subclases como Dog, Duck, Mouse etc. que añaden un comportamiento específico como bark(), quack() etc.

Aquí esta la Animal clase:

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 mucho encasillamiento:

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 manera de que pueda usar genéricos para el tipo de retorno para deshacerme del encasillamiento, 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 calcular el tipo de retorno en tiempo de ejecución sin el parámetro adicional usando instanceof?O al menos pasando una clase del tipo en lugar de una instancia ficticia.
Entiendo que los genéricos sirven para la verificación de tipos en tiempo de compilación, pero ¿hay alguna solución para esto?

¿Fue útil?

Solución

Podría definir callFriend de esta 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.

Otros consejos

No.El compilador no puede saber qué tipo jerry.callFriend("spike") volvería.Además, su implementación simplemente oculta la conversión 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 resumen talk() método y anularlo adecuadamente en las subclases le serí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ía 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 solo tipo de retorno .)

El tipo de retorno se deducirá de la persona que llama. Sin embargo, tenga en cuenta la anotación @SuppressWarnings: que le dice que este código no es seguro para escribir con letras . Debe verificarlo usted mismo, o podría obtener ClassCastExceptions en 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 clase Animal un método abstracto talk(), como dijo David Schmitt.

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 estén sanos. Consulte Java efectivo y la Class javadoc para obtener más información info.

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

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

        }
    }

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 Typesafe Heterogenous Containers (THC) usando Super Type Tokens , pero eso tiene sus propios problemas nuevamente.

Basado en la misma idea que Super Type Tokens, puede 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.

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.

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

Como solución alternativa, puede utilizar el patrón de visitante de esta manera. 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 Perro 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 " this " como un 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 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 cero instancias de comprobaciones y conversiones de tipo cero. 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.

No es posible. ¿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.

He escrito un artículo que contiene una prueba de concepto, clases de soporte y una clase de prueba que demuestra cómo sus clases pueden recuperar tokens Super Type 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 utiliza double[]
  • TimeSeries<OHLC> delega a una clase interna privada que utiliza ArrayList<OHLC>

Ver:Usando TypeTokens para recuperar parámetros genéricos

Gracias

Richard Gómez- Blog

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 como resultado 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;
}

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?

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

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

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

    }

}

¿qué pasa con

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

}

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 complicada entonces :)

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.

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.

Licenciado bajo: CC-BY-SA con atribución
No afiliado a StackOverflow
scroll top