webentwicklung-frage-antwort-db.com.de

Wie mache ich die Methode Rückgabetyp generisch?

Betrachten Sie dieses Beispiel (typisch in OOP Büchern):

Ich habe eine Animal Klasse, in der jeder Animal viele Freunde haben kann.
Und Unterklassen wie Dog, Duck, Mouse usw., die spezifisches Verhalten wie bark(), quack() usw. hinzufügen.

Hier ist die Klasse 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);
    }
}

Und hier ist ein Code-Snippet mit viel Typumwandlung:

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

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

Gibt es eine Möglichkeit, Generika für den Rückgabetyp zu verwenden, um die Typumwandlung loszuwerden, so dass ich sagen kann

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

Hier ist ein erster Code mit dem Rückgabetyp, der der Methode als Parameter übergeben wird, der nie verwendet wird.

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

Gibt es eine Möglichkeit, den Rückgabetyp zur Laufzeit ohne den zusätzlichen Parameter mithilfe von instanceof zu ermitteln? Oder zumindest durch Übergeben einer Klasse des Typs anstelle einer Dummy-Instanz.
Ich verstehe, dass Generika für die Typüberprüfung zur Kompilierungszeit vorgesehen sind, aber gibt es eine Problemumgehung dafür?

544
Sathish

Sie können callFriend folgendermaßen definieren:

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

Dann nenne es so:

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

Dieser Code hat den Vorteil, dass keine Compiler-Warnungen generiert werden. Natürlich ist dies nur eine aktualisierte Version des Castings aus der Zeit vor der Generika und bietet keine zusätzliche Sicherheit.

831
laz

Nein. Der Compiler kann nicht wissen, welcher Typ jerry.callFriend("spike") zurückgeben würde. Darüber hinaus verbirgt Ihre Implementierung nur die Umwandlung in der Methode ohne zusätzliche Typensicherheit. Bedenken Sie:

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

In diesem speziellen Fall bietet es sich an, eine abstrakte talk() -Methode zu erstellen und diese in den Unterklassen entsprechend zu überschreiben:

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

Sie könnten es so implementieren:

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

(Ja, dies ist gesetzlicher Code; siehe Java Generics: Generischer Typ, der nur als Rückgabetyp definiert ist .)

Der Rückgabetyp wird vom Aufrufer abgeleitet. Beachten Sie jedoch die Anmerkung @SuppressWarnings: Diese besagt, dass dieser Code nicht typsicher ist . Sie müssen es selbst überprüfen, oder Sie könnten ClassCastExceptions zur Laufzeit erhalten.

Leider ist die Art und Weise, wie Sie es verwenden (ohne den Rückgabewert einer temporären Variablen zuzuweisen), die einzige Möglichkeit, den Compiler glücklich zu machen, es so aufzurufen:

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

Dies ist vielleicht ein bisschen netter als das Casting, aber Sie sind wahrscheinlich besser dran, der Klasse Animal eine abstrakte Methode talk() zu geben, wie David Schmitt sagte.

102
Michael Myers

Diese Frage ähnelt Punkt 29 in Effective Java - "Betrachten Sie typsichere heterogene Container." Die Antwort von Laz kommt der Lösung von Bloch am nächsten. Put und get sollten jedoch aus Sicherheitsgründen das Klassenliteral verwenden. Die Unterschriften würden:

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

In beiden Methoden sollten Sie überprüfen, ob die Parameter korrekt sind. Weitere Informationen finden Sie unter Effektives Java und Klasse javadoc.

28
Craig P. Motlin

Außerdem können Sie die Methode auf diese Weise auffordern, den Wert in einem bestimmten Typ zurückzugeben

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

Weitere Beispiele hier in der Dokumentation zu Oracle Java

15

Hier ist die einfachere Version:

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
}

Voll funktionsfähiger Code:

    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

Wie Sie sagten, wäre es in Ordnung, eine Klasse zu bestehen, können Sie Folgendes schreiben:

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

Und dann benutze es so:

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

Nicht perfekt, aber das ist so ziemlich das, was Sie mit Java Generika erreichen. Es gibt eine Möglichkeit, Typesafe Heterogenous Containers (THC) unter Verwendung von Super Type Tokens zu implementieren, aber das hat wieder seine eigenen Probleme.

9
Fabian Steeg

Basierend auf der gleichen Idee wie bei Super Type Tokens können Sie eine eingegebene ID anstelle einer Zeichenfolge erstellen:

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

Aber ich denke, dies könnte den Zweck zunichte machen, da Sie jetzt für jede Zeichenfolge neue ID-Objekte erstellen und diese beibehalten müssen (oder sie mit den richtigen Typinformationen rekonstruieren müssen).

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

Aber Sie können die Klasse jetzt wie gewünscht verwenden, ohne die Casts.

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

Dies versteckt nur den Typparameter in der ID, obwohl Sie den Typ später aus dem Bezeichner abrufen können, wenn Sie dies wünschen.

Sie müssen auch die Vergleichs- und Hashing-Methoden von TypedID implementieren, wenn Sie zwei identische Instanzen einer ID vergleichen möchten.

8
Mike Houston

"Gibt es eine Möglichkeit, den Rückgabetyp zur Laufzeit ohne den zusätzlichen Parameter mithilfe von instanceof zu ermitteln?"

Als alternative Lösung können Sie das Besuchermuster wie folgt verwenden. Machen Sie Animal abstrakt und implementieren Sie es 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 bedeutet nur, dass eine Animal-Implementierung bereit ist, einen Besucher aufzunehmen:

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

Und eine Besucherimplementierung kann alle Unterklassen eines Tieres besuchen:

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

So würde zum Beispiel eine Dog-Implementierung dann so aussehen:

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

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

Der Trick dabei ist, dass der Hund, wenn er weiß, um welchen Typ es sich handelt, die relevante überladene Besuchsmethode des Besuchers v auslösen kann, indem er "this" als Parameter übergibt. Andere Unterklassen würden accept () genauso implementieren.

Die Klasse, die unterklassenspezifische Methoden aufrufen möchte, muss die Visitor-Schnittstelle folgendermaßen implementieren:

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

Ich weiß, dass es viel mehr Schnittstellen und Methoden gibt, als Sie erwartet haben, aber es ist eine Standardmethode, um jeden bestimmten Subtyp mit genau null Instanzen von Prüfungen und Null-Typ-Casts in den Griff zu bekommen. Und es wird alles in einer sprachenunabhängigen Standardweise durchgeführt, so dass es nicht nur für Java, sondern für jede OO Sprache gleich funktionieren sollte.

6
Antti Siiskonen

Ich habe einen Artikel geschrieben, der einen Proof of Concept, Unterstützungsklassen und eine Testklasse enthält, die zeigt, wie Super Type Tokens von Ihren Klassen zur Laufzeit abgerufen werden können. Kurz gesagt, können Sie abhängig von den vom Aufrufer übergebenen generischen Parametern an alternative Implementierungen delegieren. Beispiel:

  • TimeSeries<Double> delegiert an eine private innere Klasse, die double[] verwendet
  • TimeSeries<OHLC> delegiert an eine private innere Klasse, die ArrayList<OHLC> verwendet

Siehe: Verwenden von TypeTokens zum Abrufen generischer Parameter

Vielen Dank

Richard Gomes - Blog

5
Richard Gomes

Nicht möglich. Wie soll die Map wissen, welche Unterklasse von Animal sie erhalten wird, wenn nur ein String-Schlüssel vergeben wird?

Dies ist nur dann möglich, wenn jedes Tier nur einen Typ von Freund akzeptiert (dann könnte es ein Parameter der Animal-Klasse sein), oder wenn die callFriend () -Methode einen Typparameter hat. Aber es sieht wirklich so aus, als ob Sie den Vererbungspunkt verfehlen: Sie können Unterklassen nur dann einheitlich behandeln, wenn Sie ausschließlich die Methoden der Oberklasse verwenden.

5

Hier gibt es viele gute Antworten, aber dies ist der Ansatz, den ich für einen Appium-Test gewählt habe, bei dem das Einwirken auf ein einzelnes Element je nach den Einstellungen des Benutzers zu unterschiedlichen Anwendungszuständen führen kann. Obwohl es nicht den Konventionen des OP-Beispiels entspricht, hoffe ich, dass es jemandem hilft.

public <T extends MobilePage> T tapSignInButton(Class<T> type) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
    //signInButton.click();
    return type.getConstructor(AppiumDriver.class).newInstance(appiumDriver);
}
  • MobilePage ist die Superklasse, die der Typ erweitert, dh Sie können eines seiner untergeordneten Elemente verwenden (duh).
  • mit type.getConstructor (Param.class usw.) können Sie mit dem Konstruktor des Typs interagieren. Dieser Konstruktor sollte für alle erwarteten Klassen gleich sein.
  • newInstance akzeptiert eine deklarierte Variable, die Sie an den Konstruktor für neue Objekte übergeben möchten

Wenn Sie die Fehler nicht werfen möchten, können Sie sie folgendermaßen abfangen:

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

Was Sie hier suchen, ist Abstraktion. Code gegen Schnittstellen mehr und Sie sollten weniger Casting tun müssen.

Das folgende Beispiel ist in C #, aber das Konzept bleibt dasselbe.

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

Nicht wirklich, denn wie Sie sagen, weiß der Compiler nur, dass callFriend () ein Tier zurückgibt, keinen Hund oder eine Ente.

Können Sie Animal keine abstrakte makeNoise () -Methode hinzufügen, die von ihren Unterklassen als Bark oder Quack implementiert wird?

2
sk.

Ich weiß, das ist eine ganz andere Sache, die der eine gefragt hat. Ein anderer Weg, dies zu lösen, wäre die Reflexion. Ich meine, dies nutzt nicht die Vorteile von Generics, aber es lässt Sie in gewisser Weise das Verhalten nachahmen, das Sie ausführen möchten (Hunderinde bellen lassen, Ente quaken lassen usw.), ohne sich um das Typ-Casting zu kümmern:

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

wie wäre es mit

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

Ich habe folgendes in meinem lib kontraktor gemacht:

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

unterklassen:

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

zumindest funktioniert dies innerhalb der aktuellen Klasse und wenn Sie eine starke typisierte Referenz haben. Mehrfachvererbung funktioniert, wird dann aber richtig knifflig :)

1
R.Moeller

Es gibt einen anderen Ansatz: Sie können den Rückgabetyp einschränken, wenn Sie eine Methode überschreiben. In jeder Unterklasse müssten Sie callFriend überschreiben, um diese Unterklasse zurückzugeben. Die Kosten wären die mehrfachen Deklarationen von callFriend, aber Sie könnten die gemeinsamen Teile einer intern aufgerufenen Methode zuordnen. Dies erscheint mir viel einfacher als die oben genannten Lösungen und erfordert kein zusätzliches Argument, um den Rückgabetyp zu bestimmen.

0
FeralWhippet