¿Cómo hago que el tipo de retorno del método sea genérico?

Resuelto Sathish asked hace 15 años • 19 respuestas

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

Tengo una Animalclase, donde cada uno Animalpuede tener muchos amigos.
Y subclases como Dog, etc. que agregan comportamientos específicos Duckcomo ,Mousebark()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 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 usar el parámetro adicional 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?

Sathish avatar Jan 16 '09 22:01 Sathish
Aceptado

Podrías definir callFriendde esta manera:

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

Entonces llámalo así:

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

Este código tiene la ventaja de no generar ninguna advertencia del compilador. Por supuesto, esta es en realidad solo una versión actualizada del casting de los días anteriores a los genéricos y no agrega ninguna seguridad adicional.

laz avatar Jan 16 '2009 15:01 laz

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 Genéricos de Java: tipo genérico definido solo como tipo de retorno ).

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

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

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

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

Michael Myers avatar Jan 16 '2009 15:01 Michael Myers

No. El compilador no puede saber qué tipo jerry.callFriend("spike")devolverá. 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 talk()método abstracto y anularlo adecuadamente en las subclases sería mucho mejor para usted:

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

jerry.callFriend("spike").talk();
jerry.callFriend("quacker").talk();
David Schmitt avatar Jan 16 '2009 15:01 David Schmitt

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

        }
    }
webjockey avatar Jul 15 '2017 15:07 webjockey

Esta pregunta es muy similar al elemento 29 en Java efectivo : "Considere contenedores heterogéneos con seguridad de tipos". 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 Class por seguridad. Las firmas pasarían a ser:

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 debes verificar que los parámetros sean correctos. Consulte Java efectivo y el javadoc de clase para obtener más información.

Craig P. Motlin avatar Jan 16 '2009 17:01 Craig P. Motlin