Question

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

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

Voici la <=> classe:

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 <=>? 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?

Était-ce utile?

La solution

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

Appelez-le ainsi:

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.

Autres conseils

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

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 @SuppressWarnings l'annotation: cela vous indique que ce code n'est pas dactylographique . Vous devez le vérifier vous-même ou vous pourriez obtenir ClassCastExceptions au moment de l'exécution.

Malheureusement, de la manière 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 suit:

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

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

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 Classe javadoc pour plus d'informations. info.

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

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

Autres exemples ici dans la documentation Oracle Java

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

        }
    }

Comme vous l'avez dit, 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 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 les génériques Java. Il existe un moyen de mettre en œuvre les conteneurs sécurisés de typeafe (THC) à l'aide de Super Type Tokens , mais cela pose à nouveau des problèmes.

Sur la même idée que les super-types de jetons, vous pouvez créer un identifiant saisi à 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 comme 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 extraire le type 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.

& "Existe-t-il un moyen de déterminer le type de retour au moment de 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 animal abstrait et le mettre en œuvre 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 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 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 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 aviez prévu est beaucoup plus grand, 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 typographie. Et tout 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.

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 (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 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.

J'ai écrit un article contenant une preuve de concept, des classes de support et une classe de test montrant comment les classes de type peuvent être récupérées 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ègue à une classe interne privée qui utilise double[]
  • TimeSeries<OHLC> délègue à une classe interne privée qui utilise ArrayList<OHLC>

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

Merci

Richard Gomes - Blog

Il ya beaucoup de bonnes réponses ici, mais c’est l’approche que j’ai choisie pour un test Appium, selon lequel le fait d’agir sur un seul élément peut donner lieu à différents états d’application 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 de nouveaux objets

Si vous ne voulez pas jeter les erreurs, vous pouvez les récupérer comme suit:

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

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 à Animal une méthode abstraite makeNoise () qui serait implémentée sous forme d'aboiement ou de charlatan par ses sous-classes?

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

Je sais que c'est une chose complètement différente que celle 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 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");

    }

}

Qu'en est-il

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

}

J'ai fait ce qui suit dans ma librairie 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 avec une référence forte typée. L'héritage multiple fonctionne, mais devient vraiment délicat alors:)

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.

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. Vous n'avez pas besoin de transtyper.

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

C’est mieux si vous souhaitez personnaliser tout autre type d’enregistrement plutôt que de vrais curseurs.

Licencié sous: CC-BY-SA avec attribution
Non affilié à StackOverflow
scroll top