web-dev-qa-db-fra.com

Comment rendre le type de retour de méthode générique?

Considérez cet exemple (typique dans OOP books):

J'ai une classe Animal, où chaque Animal peut avoir de nombreux amis.
Et des sous-classes telles que Dog, Duck, Mouse etc qui ajoutent un comportement spécifique comme bark(), quack() etc.

Voici la classe 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);
    }
}

Et voici un extrait de code avec beaucoup de transtypage:

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

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

Existe-t-il un moyen d'utiliser des génériques pour le type de retour afin de supprimer le transtypage, afin que je puisse dire

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

Voici un code initial avec le type de retour transmis à la méthode sous forme de paramètre jamais utilisé.

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

Existe-t-il un moyen de déterminer le type de retour au moment de l'exécution sans le paramètre supplémentaire utilisant instanceof? Ou du moins en passant une classe du type à la place d'une instance factice.
Je comprends que les génériques sont destinés à la vérification de type à la compilation, mais existe-t-il une solution de contournement pour cela?

544
Sathish

Vous pouvez définir callFriend comme suit:

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

Puis appelez-le comme tel:

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

Ce code présente l'avantage de ne générer aucun avertissement du compilateur. Bien sûr, il ne s’agit en réalité que d’une version mise à jour du casting des jours pré-génériques et n’ajoute aucune sécurité supplémentaire.

831
laz

Non, le compilateur ne peut pas savoir quel type jerry.callFriend("spike") renverrait. En outre, votre implémentation masque simplement le transtypage dans la méthode sans sécurité de type supplémentaire. Considère ceci:

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

Dans ce cas spécifique, créer une méthode abstraite talk() et le remplacer correctement dans les sous-classes vous servirait beaucoup mieux:

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

jerry.callFriend("spike").talk();
jerry.callFriend("quacker").talk();
114
David Schmitt

Vous pouvez l'implémenter comme ceci:

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

(Oui, il s'agit d'un code légal; voir Génériques Java: type générique défini comme type de retour uniquement .)

Le type de retour sera déduit de l'appelant. Cependant, notez l'annotation @SuppressWarnings: cela vous indique que ce code n'est pas typé . Vous devez le vérifier vous-même, ou vous pourriez obtenir ClassCastExceptions au moment de l'exécution.

Malheureusement, la façon dont vous l'utilisez (sans affecter la valeur de retour à une variable temporaire), le seul moyen de rendre le compilateur heureux est de l'appeler comme suit:

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

Bien que cela puisse être un peu plus agréable que le casting, il vaut probablement mieux donner à la classe Animal une méthode abstraite talk(), comme l'a dit David Schmitt.

102
Michael Myers

Cette question est très similaire à élément 29 dans Effective Java - "Considérons les conteneurs hétérogènes typesafe." La réponse de Laz est la plus proche de la solution de Bloch. Cependant, put et get doivent utiliser le littéral de classe pour plus de sécurité. Les signatures deviendraient:

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

Dans les deux méthodes, vous devez vérifier que les paramètres sont corrects. Voir Effective Java et le Class javadoc pour plus d'informations.

28
Craig P. Motlin

De plus, vous pouvez demander à la méthode de renvoyer la valeur dans un type donné de cette façon

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

Plus d'exemples ici chez Oracle Java documentation

15
isuru chathuranga

Voici la version la plus 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
}

Code pleinement fonctionnel:

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

        }
    }
12
webjockey

Comme vous l'avez dit, passer un cours serait OK, vous pouvez écrire ceci:

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

Et puis utilisez-le comme ceci:

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

Pas parfait, mais c'est à peu près tout ce que vous obtenez avec Java génériques. Il existe un moyen d'implémenter conteneurs sécurisés Typesafe (THC) à l'aide de jetons Super Type , mais cela pose à nouveau ses propres problèmes.

9
Fabian Steeg

Sur la même idée que les jetons de type super, vous pouvez créer un identifiant typé à utiliser à la place d'une chaîne:

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

Mais je pense que cela pourrait aller à l'encontre du but recherché, car vous devez maintenant créer de nouveaux objets id pour chaque chaîne et les conserver (ou les reconstruire avec les informations de type correctes).

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

Mais vous pouvez maintenant utiliser la classe de la façon dont vous le vouliez au départ, sans le lancer.

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

Ceci cache simplement le paramètre type à l'intérieur de l'id, bien que cela signifie que vous pouvez récupérer le type à partir de l'identifiant ultérieurement si vous le souhaitez.

Vous devez également implémenter les méthodes de comparaison et de hachage de TypedID si vous souhaitez pouvoir comparer deux instances identiques d'un identifiant.

8
Mike Houston

"Y a-t-il un moyen de déterminer le type de retour au moment de l'exécution sans le paramètre supplémentaire utilisant instanceof?"

Comme solution alternative, vous pourriez utiliser le modèle de visiteur comme ceci. Rendre animal abstrait et le rendre implémenter 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 signifie simplement qu'une implémentation Animal accepte un visiteur:

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

Et une implémentation visiteur est capable de visiter toutes les sous-classes d'un animal:

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

Ainsi, par exemple, une implémentation de Dog ressemblerait à ceci:

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

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

Le truc ici est que, comme le chien sait de quel type il est, il peut déclencher la méthode de visite surchargée appropriée du visiteur v en passant "this" en tant que paramètre. Les autres sous-classes implémenteraient accept () exactement de la même manière.

La classe qui souhaite appeler des méthodes spécifiques à une sous-classe doit ensuite implémenter l'interface de visiteur de la manière suivante:

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

Je sais que le nombre d'interfaces et de méthodes que vous avez prévu est beaucoup plus grand que ce que vous aviez prévu, mais c'est un moyen standard d'obtenir un descripteur sur chaque sous-type spécifique avec exactement zéro instance de vérifications et zéro type. Et tout cela est fait dans un langage standard agnostique, donc ce n'est pas juste pour Java mais n'importe quel langage OO devrait fonctionner de la même manière.

6
Antti Siiskonen

J'ai écrit un article contenant une preuve de concept, des classes de support et une classe de test qui montre comment les classes peuvent être extraites par vos classes lors de l'exécution. En un mot, il vous permet de déléguer des implémentations alternatives en fonction des paramètres génériques réels transmis par l'appelant. Exemple:

  • TimeSeries<Double> délégués à une classe interne privée qui utilise double[]
  • TimeSeries<OHLC> délégués à une classe interne privée qui utilise ArrayList<OHLC>

Voir: tilisation de TypeTokens pour récupérer des paramètres génériques

Merci

Richard Gomes - Blog

5
Richard Gomes

Pas possible. Comment la carte est-elle censée savoir quelle sous-classe d’Animal elle va obtenir, avec seulement une clé String?

Cela ne serait possible que si chaque animal acceptait un seul type d'amis (ce pourrait être un paramètre de la classe Animal) ou si la méthode callFriend () obtenait un paramètre de type. Mais on dirait vraiment que vous manquez du point de vue de l'héritage: c'est que vous ne pouvez traiter les sous-classes que de manière uniforme en utilisant exclusivement les méthodes de la superclasse.

5
Michael Borgwardt

Il y a beaucoup de bonnes réponses ici, mais c'est l'approche que j'ai adoptée pour un test Appium selon laquelle le fait d'agir sur un seul élément peut entraîner le passage à des états d'application différents en fonction des paramètres de l'utilisateur. Bien que cela ne suive pas les conventions de l'exemple d'OP, j'espère que cela aidera quelqu'un.

public <T extends MobilePage> T tapSignInButton(Class<T> type) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
    //signInButton.click();
    return type.getConstructor(AppiumDriver.class).newInstance(appiumDriver);
}
  • MobilePage est la super classe que le type étend, ce qui signifie que vous pouvez utiliser n'importe lequel de ses enfants (duh)
  • type.getConstructor (Param.class, etc.) vous permet d'interagir avec le constructeur du type. Ce constructeur doit être identique pour toutes les classes attendues.
  • newInstance prend une variable déclarée que vous voulez transmettre au constructeur du nouvel objet

Si vous ne voulez pas jeter les erreurs, vous pouvez les attraper comme ceci:

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;
}
3
MTen

Ce que vous cherchez ici, c'est de l'abstraction. Code contre les interfaces plus et vous devriez avoir à faire moins de casting.

L'exemple ci-dessous est en C # mais le concept reste le même.

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

Pas vraiment, car comme vous le dites, le compilateur sait seulement que callFriend () renvoie un animal, pas un chien ou un canard.

Ne pouvez-vous pas ajouter une méthode abstraite makeNoise () à Animal qui serait implémentée sous forme d'écorce ou de charlatan par ses sous-classes?

2
sk.

Je sais que c'est une chose complètement différente que celle demandée. Une autre façon de résoudre ce problème serait la réflexion. Je veux dire, cela ne profite pas des génériques, mais il vous permet d'imiter, d'une certaine manière, le comportement que vous voulez avoir (aboyer un chien, faire un canard, etc.) sans prendre en charge le transtypage:

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
user743489

qu'en est-il de

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

}

1
gafadr

J'ai fait ce qui suit dans ma lib kontraktor:

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

sous-classement:

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

au moins, cela fonctionne dans la classe actuelle et lorsqu’on a une forte référence typée. L'héritage multiple fonctionne, mais devient vraiment délicat alors :)

1
R.Moeller

Il existe une autre approche: vous pouvez affiner le type de retour lorsque vous substituez une méthode. Dans chaque sous-classe, vous devez remplacer callFriend pour renvoyer cette sous-classe. Le coût correspondrait aux déclarations multiples de callFriend, mais vous pourriez isoler les parties communes d'une méthode appelée en interne. Cela me semble beaucoup plus simple que les solutions mentionnées ci-dessus et ne nécessite aucun argument supplémentaire pour déterminer le type de retour.

0
FeralWhippet