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


Considérons cet exemple (typique dans les livres de POO):

J'ai une classe Animal, où chaque Animal peut avoir beaucoup d'amis.
Et les sous-classes comme 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 typecasting:

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

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

Est-il possible d'utiliser des génériques pour le type de retour pour se débarrasser du typecasting, 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 en tant que paramètre qui n'est jamais utilisé.

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

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

Author: Lii, 2009-01-16

19 answers

, Vous pouvez définir callFriend de cette façon:

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 a l'avantage de ne générer aucun avertissement du compilateur. Bien sûr, ce n'est vraiment qu'une version mise à jour de casting des jours pré-génériques et n'ajoute aucune sécurité supplémentaire.

 748
Author: laz, 2012-08-17 17:03:52

Non. Le compilateur ne peut pas savoir quel type jerry.callFriend("spike") retournerait. En outre, votre implémentation cache simplement le cast dans la méthode sans aucune sécurité de type supplémentaire. Considérez 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 la remplacer de manière appropriée 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();
 104
Author: David Schmitt, 2014-05-24 01:41:26

Vous pouvez l'implémenter comme ceci:

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

(Oui, c'est du code légal; voir Java Generics: Generic type défini comme type de retour uniquement.)

Le type de retour sera déduit de l'appelant. Cependant, notez l'annotation @SuppressWarnings: qui vous indique que ce code n'est pas typesafe. Vous devez le vérifier vous-même, sinon 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 la seule façon de rendre le compilateur heureux est de l'appeler comme ceci:

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

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

 93
Author: Michael Myers, 2017-05-23 12:02:47

Cette question est très similaire à Item 29 dans Effective Java - "Considérez 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 la 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 sains. Voir Java efficace et la classe javadoc pour plus d'informations.

 26
Author: Craig P. Motlin, 2016-05-17 16:22:31

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 à la documentation Oracle Java

 11
Author: isuru chathuranga, 2016-06-24 05:12:36

Comme vous avez dit que passer une classe serait OK, vous pouvez écrire ceci:

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

Et ensuite l'utiliser comme ceci:

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

Pas parfait, mais c'est à peu près aussi loin que vous obtenez avec les génériques Java. Il existe un moyen d'implémenter Typesafe Heterogenous Containers (THC) en utilisant des jetons de type Super, mais cela a à nouveau ses propres problèmes.

 9
Author: Fabian Steeg, 2009-01-16 15:57:40

Basé sur la même idée que les jetons Super Type, vous pouvez créer un id 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 peut aller à l'encontre du but, 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 comme vous le vouliez à l'origine, sans les moulages.

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

Cela cache simplement le paramètre type à l'intérieur de l'id, bien que cela signifie vous pouvez récupérer le type de l'identificateur plus tard si vous le souhaitez.

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

 7
Author: Mike Houston, 2009-01-23 02:00:28

Pas possible. Comment la Carte est-elle censée savoir quelle sous-classe d'animal elle va obtenir, avec seulement une clé de chaîne?

La seule façon que cela soit possible est si chaque Animal n'acceptait qu'un seul type d'ami (alors il pourrait s'agir d'un paramètre de la classe Animal), ou si la méthode callFriend() obtenait un paramètre de type. Mais il semble vraiment que vous manquiez le point d'héritage: c'est que vous ne pouvez traiter les sous-classes uniformément que lorsque vous utilisez exclusivement les méthodes de superclasse.

 5
Author: Michael Borgwardt, 2009-01-16 15:52:14

" Existe-t-il un moyen de comprendre le type de retour à l'exécution sans le paramètre supplémentaire en utilisant instanceof?"

Comme solution alternative, vous pouvez utiliser le modèle de visiteur comme ceci. Rendre l'animal abstrait et le rendre 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 Animale est prête à accepter 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);
}

Donc par exemple un chien l'implémentation ressemblerait alors à ceci:

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

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

L'astuce ici est que, comme le Chien sait de quel type il s'agit, il peut déclencher la méthode de visite surchargée pertinente du visiteur v en passant "this" comme paramètre. D'autres sous-classes implémenteraient accept() exactement de la même manière.

La classe qui veut appeler des méthodes spécifiques à la sous-classe doit alors implémenter l'interface Visiteur comme ceci:

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 c'est beaucoup plus d'interfaces et de méthodes que ce que vous aviez prévu, mais c'est un moyen standard d'obtenir un handle sur chaque sous-type spécifique avec précisément zéro instanceof checks et zéro type casts. Et tout cela est fait de manière agnostique au langage standard, donc ce n'est pas seulement pour Java, mais tout langage OO devrait fonctionner de la même manière.

 5
Author: Antti Siiskonen, 2015-05-24 21:09:23

J'ai écrit un article qui contient une preuve de concept, des classes de support et une classe de test qui montre comment les jetons de type Super peuvent être récupérés 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 passés par l'appelant. Exemple:

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

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

Merci

Richard Gomes - Blog

 5
Author: Richard Gomes, 2015-12-08 16:06:19

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 entièrement 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();

        }
    }
 3
Author: webjockey, 2018-01-11 12:12:53

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 comme une écorce ou un charlatan par ses sous-classes?

 2
Author: sk., 2009-01-16 15:51:47

Ce que vous cherchez ici, c'est 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
Author: Nestor Ledon, 2014-05-21 17:02:15

Il y a beaucoup de bonnes réponses ici, mais c'est l'approche que j'ai adoptée pour un test Appium où agir sur un seul élément peut entraîner des états d'application différents en fonction des paramètres de l'utilisateur. Bien qu'il 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 l'un de ses enfants (duh)
  • type.getConstructor(Param.de classe, etc) vous permet d'interagir avec le constructeur du type. Ce constructeur doit être le même entre toutes les classes.
  • newInstance prend une variable déclarée que vous souhaitez transmettre au constructeur new objects

Si vous ne voulez pas lancer 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;
}
 2
Author: MTen, 2017-06-30 17:17:50

Je sais que c'est une chose complètement différente que celui qui a demandé. 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 cela vous permet d'émuler, d'une certaine manière, le comportement que vous souhaitez effectuer (faire aboyer un chien, faire un charlatan de canard, etc.) sans prendre soin de type coulée:

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
Author: , 2013-10-24 20:39:46

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
Author: gafadr, 2015-02-06 01:19:02

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

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

Sous-classe:

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

Au moins, cela fonctionne à l'intérieur de la classe actuelle et lorsque vous avez une référence typée forte. L'héritage multiple fonctionne, mais devient vraiment délicat alors:)

 1
Author: R.Moeller, 2015-06-28 19:07:21

Il existe une autre approche, vous pouvez restreindre le type de retour lorsque vous remplacez une méthode. Dans chaque sous-classe, vous devrez remplacer callFriend pour renvoyer cette sous-classe. Le coût serait les multiples déclarations de callFriend, mais vous pouvez isoler les parties communes à une méthode appelée en interne. Cela me semble beaucoup plus simple que les solutions mentionnées ci-dessus, et n'a pas besoin d'un argument supplémentaire pour déterminer le type de retour.

 0
Author: FeralWhippet, 2017-05-08 18:23:43
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;
}

Vous pouvez retourner n'importe quel type et recevoir directement comme. Pas besoin de typecast.

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

C'est mieux si vous voulez personnaliser tout autre type d'enregistrements au lieu de vrais curseurs.

 -1
Author: user6083049, 2016-03-18 14:38:01