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


589

Considérez cet exemple (typique dans les livres OOP):

J'ai une Animalclasse, où chacun Animalpeut avoir beaucoup d'amis.
Et les sous - classes aiment Dog, Duck, Mouseetc qui ajoutent des comportements spécifiques comme bark(), quack()etc.

Voici la Animalclasse:

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 pour me débarrasser de la conversion de type, afin que je puisse dire

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

Voici un code initial avec un 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 déterminer le type de retour à l'exécution sans utiliser le paramètre supplémentaire instanceof? Ou au moins en passant une classe du type au lieu d'une instance factice.
Je comprends que les génériques sont destinés à la vérification du type de compilation, mais existe-t-il une solution à cela?

Réponses:


903

Vous pouvez définir de callFriendcette façon:

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

Appelez-le ensuite 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 du casting des jours pré-génériques et n'ajoute aucune sécurité supplémentaire.


29
... mais n'a toujours pas de vérification de type de temps de compilation entre les paramètres de l'appel callFriend ().
David Schmitt

2
C'est la meilleure réponse jusqu'à présent - mais vous devez changer addFriend de la même manière. Il est plus difficile d'écrire des bogues car vous avez besoin de ce littéral de classe aux deux endroits.
Craig P. Motlin,

@Jaider, pas exactement la même chose, mais cela fonctionnera: // Animal Class public T CallFriend <T> (nom de chaîne) où T: Animal {return friends [name] as T; } // Appel de la classe jerry.CallFriend <Dog> ("spike"). Bark (); jerry.CallFriend <Duck> ("quacker"). Quack ();
Nestor Ledon

124

Non. Le compilateur ne peut pas savoir quel type jerry.callFriend("spike")retournerait. En outre, votre implémentation masque simplement le casting dans la méthode sans aucune 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 talk()méthode abstraite 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();

10
Bien que la méthode mmyers puisse fonctionner, je pense que cette méthode est une meilleure programmation OO et vous évitera des ennuis à l'avenir.
James McMahon

1
C'est la bonne façon d'obtenir le même résultat. Notez que le but est d'obtenir un comportement spécifique à la classe dérivée lors de l'exécution sans écrire explicitement vous-même le code pour effectuer la vérification et la conversion de type laid. La méthode proposée par @laz fonctionne, mais jette la sécurité de type par la fenêtre. Cette méthode nécessite moins de lignes de code (car les implémentations de méthode sont liées tardivement et ont quand même cherché à l'exécution), mais vous permet tout de même de définir facilement un comportement unique pour chaque sous-classe d'Animal.
dcow

Mais la question d'origine ne concerne pas la sécurité des types. La façon dont je le lis, le demandeur veut juste savoir s'il existe un moyen de tirer parti des génériques pour éviter d'avoir à lancer.
laz

2
@laz: oui, la question d'origine - telle qu'elle a été posée - ne concerne pas la sécurité des types. Cela ne change pas le fait qu'il existe un moyen sûr de l'implémenter, éliminant les échecs de transtypage de classe. Voir aussi weblogs.asp.net/alex_papadimoulis/archive/2005/05/25/…
David Schmitt

3
Je ne suis pas en désaccord à ce sujet, mais nous traitons de Java et de toutes ses décisions / faiblesses en matière de conception. Je vois cette question comme simplement essayer d'apprendre ce qui est possible dans les génériques Java, pas comme un xyproblem ( meta.stackexchange.com/questions/66377/what-is-the-xy-problem ) qui doit être repensé . Comme tout modèle ou approche, il y a des moments où le code que j'ai fourni est approprié et des moments où quelque chose de totalement différent (comme ce que vous avez suggéré dans cette réponse) est requis.
laz

114

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 Java Generics: type générique défini comme type de retour uniquement .)

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

Malheureusement, la façon dont vous l'utilisez (sans affecter la valeur de retour à une variable temporaire), 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 Animalclasse une talk()méthode abstraite , comme l'a dit David Schmitt.


Le chaînage des méthodes n'était pas vraiment une intention. cela ne me dérange pas d'attribuer la valeur à une variable sous-typée et de l'utiliser. Merci pour la solution.
Sathish

cela fonctionne parfaitement lors du chaînage d'appels de méthode!
Hartmut P.

J'aime vraiment cette syntaxe. Je pense qu'en C #, jerry.CallFriend<Dog>(...je pense que c'est mieux.
andho

Il est intéressant de noter que la propre java.util.Collections.emptyList()fonction du JRE est implémentée exactement comme ceci, et son javadoc se présente comme étant de type sécurisé.
Ti Strga

31

Cette question est très similaire à l' article 29 de Java efficace - «Considérez les conteneurs hétérogènes sécuritaires». La réponse de Laz est la plus proche de la solution de Bloch. Cependant, put et get doivent utiliser le littéral Class 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 raisonnables. Voir Java efficace et la classe javadoc pour plus d'informations.


17

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


17

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

        }
    }

1
Que fait-il Casting to T not needed in this case but it's a good practice to do. Je veux dire, si elle est correctement gérée pendant l'exécution, que signifie «bonne pratique»?
Farid

Je voulais dire que le casting explicite (T) n'était pas nécessaire car la déclaration de type de retour <T> sur la déclaration de méthode devrait être suffisante
webjockey

9

Comme vous avez dit que 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 aussi loin que vous obtenez avec les génériques Java. Il existe un moyen d'implémenter des conteneurs hétérogènes Typesafe (THC) à l'aide de jetons Super Type , mais cela a à nouveau ses propres problèmes.


Je suis désolé mais c'est exactement la même réponse que laz a, donc vous le copiez ou il vous copie.
James McMahon

C'est une bonne façon de passer le type. Mais il n'est toujours pas sûr comme Schmitt l'a dit. Je pourrais toujours passer une classe différente et le transtypage va exploser. mmyers 2e réponse pour définir le type de type de retour semble mieux
Sathish

4
Nemo, si vous vérifiez l'heure de publication, vous verrez que nous les avons publiées à peu près exactement au même moment. De plus, ce ne sont pas exactement les mêmes, seulement deux lignes.
Fabian Steeg

@Fabian J'ai posté une réponse similaire, mais il y a une différence importante entre les diapositives de Bloch et ce qui a été publié dans Effective Java. Il utilise la classe <T> au lieu de TypeRef <T>. Mais c'est toujours une excellente réponse.
Craig P. Motlin,

8

Basé sur la même idée que les Super Type Tokens, 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 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 transtypages.

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 que vous pouvez récupérer le type de l'identifiant plus tard 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

"Existe-t-il un moyen de déterminer 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 celui-ci. Rendre Animal abstrait et le rendre implémentable Visible:

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

Visible signifie simplement qu'une implémentation Animal 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);
}

Ainsi, par exemple, une implémentation Dog ressemblerait à 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 souhaite appeler des méthodes spécifiques à la sous-classe doit ensuite implémenter l'interface Visitor 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 négocié, mais c'est un moyen standard d'obtenir une poignée sur chaque sous-type spécifique avec précisément zéro instanceof vérifications et zéro type cast. Et tout est fait d'une manière indépendante du langage standard, donc ce n'est pas seulement pour Java, mais tout langage OO devrait fonctionner de la même manière.


6

Pas possible. Comment la carte est-elle censée savoir quelle sous-classe d'animal elle obtiendra, étant donné uniquement une clé de chaîne?

La seule manière possible serait que chaque animal n'accepte qu'un seul type d'ami (alors ce pourrait être un paramètre de la classe Animal), ou que la méthode callFriend () obtienne un paramètre de type. Mais il semble vraiment que vous manquez le point d'héritage: c'est que vous ne pouvez traiter les sous-classes de manière uniforme qu'en utilisant exclusivement les méthodes de superclasse.


5

J'ai écrit un article qui contient une preuve de concept, des classes de support et une classe de test qui montre comment les Super Type Tokens 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


En effet, merci de partager votre point de vue, votre article explique vraiment tout!
Yann-Gaël Guéhéneuc

3

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 le passage à différents états d'application en fonction des paramètres de l'utilisateur. Bien qu'il ne respecte pas les conventions de l'exemple de OP, j'espère que cela aide 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 le même entre toutes les classes attendues.
  • newInstance prend une variable déclarée que vous souhaitez transmettre au nouveau constructeur d'objets

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

À ma connaissance, c'est la meilleure et la plus élégante façon d'utiliser les génériques.
cbaldan

2

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?


1
que se passe-t-il si les animaux ont plusieurs méthodes qui ne relèvent même pas d'une action commune qui peut être abstraite? J'ai besoin de cela pour la communication entre les sous-classes avec différentes actions où je suis d'accord pour passer le type, pas une instance.
Sathish

2
Vous venez vraiment de répondre à votre propre question - si un animal a une action unique, alors vous devez lancer sur cet animal spécifique. Si un animal a une action qui peut être groupée avec d'autres animaux, vous pouvez définir une méthode abstraite ou virtuelle dans une classe de base et l'utiliser.
Matt Jordan

2

Ce que vous cherchez ici, c'est l'abstraction. Codez plus contre les interfaces et vous devriez faire moins de cast.

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

Cette question concerne le codage et non le concept.

2

J'ai fait ce qui suit dans mon 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 à l'intérieur de la classe actuelle et avec une référence typée forte. L'héritage multiple fonctionne, mais devient alors très délicat :)


1

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 cela vous permet d'émuler, en quelque sorte, le comportement que vous souhaitez effectuer (faire un aboiement de chien, faire un charlatan de canard, etc.) sans prendre soin du casting de type:

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

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

}


0

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 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 n'a pas besoin d'un argument supplémentaire pour déterminer le type de retour.


Je ne sais pas ce que vous entendez par "restreindre le type de retour". Afaik, Java et la plupart des langages typés ne surchargent pas les méthodes ou les fonctions basées sur le type de retour. Par exemple, il public int getValue(String name){}est impossible de distinguer du public boolean getValue(String name){}point de vue des compilateurs. Vous devrez soit changer le type de paramètre, soit ajouter / supprimer des paramètres pour que la surcharge soit reconnue. Peut-être que je vous comprends mal ...
The One True Colter

en java, vous pouvez remplacer une méthode dans une sous-classe et spécifier un type de retour plus "étroit" (c'est-à-dire plus spécifique). Voir stackoverflow.com/questions/14694852/… .
FeralWhippet

-3
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 transtyper.

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

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


2
Vous dites "pas besoin de transtyper", mais il y a clairement une transtypage impliquée: (Cursor) cursorpar exemple.
Sami Laine

Il s'agit d'une utilisation totalement inappropriée des génériques. Ce code doit cursorêtre un Cursor, et retournera toujours un Person(ou null). L'utilisation de génériques supprime la vérification de ces contraintes, rendant le code dangereux.
Andy Turner
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.