Jak sprawić, by metoda zwracała Typ generyczny?

Rozważ ten przykład (typowy w książkach OOP):

Mam Animal klasę, gdzie każdy Animal może mieć wielu przyjaciół.
I podklasy jak Dog, Duck, Mouse itp, które dodają określone zachowanie jak bark(), quack() itd.

Oto Animal klasa:

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

A oto fragment kodu z mnóstwem typecastingu:

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

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

Czy Jest jakiś sposób na użycie generyków dla typu return, aby pozbyć się typecastingu, tak że mogę powiedzieć

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

Oto jakiś początkowy kod z typem powrotu przekazywanym do metody jako parametr, który nigdy nie jest używany.

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

Czy istnieje sposób na określenie typu zwracanego w czasie wykonywania bez dodatkowego parametru za pomocą instanceof? Albo przynajmniej przez podanie klasy tego typu zamiast atrapy instancji.
Rozumiem, że generyki służą do sprawdzania typu w czasie kompilacji, ale czy istnieje obejście tego problemu?

Author: Lii, 2009-01-16

19 answers

Można zdefiniować callFriend w ten sposób:

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

Więc nazwij to tak:

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

Ten kod ma tę zaletę, że nie generuje żadnych ostrzeżeń kompilatora. Oczywiście jest to tak naprawdę tylko zaktualizowana wersja castingu z przed-ogólnych dni i nie dodaje żadnego dodatkowego bezpieczeństwa.

 748
Author: laz,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2012-08-17 17:03:52

Nie. Kompilator nie może wiedzieć, jaki typ jerry.callFriend("spike") zwróci. Ponadto, Twoja implementacja po prostu ukrywa odlew w metodzie bez dodatkowego bezpieczeństwa typu. Rozważ to:

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

W tym konkretnym przypadku stworzenie abstrakcyjnej metody talk() i odpowiednie nadpisanie jej w podklasach służyłoby Ci znacznie lepiej:

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,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2014-05-24 01:41:26

Możesz zaimplementować to tak:

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

(tak, jest to kod prawny; zobacz Java Generics: Typ generyczny zdefiniowany jako typ zwracany tylko.)

Typ powrotu zostanie wywnioskowany z wywołującego. Zwróć jednak uwagę na adnotację @SuppressWarnings, która mówi, że ten kod nie jest typowany . Musisz to sam zweryfikować, albo możesz uzyskać ClassCastExceptions w czasie wykonywania.

Niestety, sposób w jaki go używasz (Bez przypisywania wartości zwracanej do zmiennej tymczasowej), jedynym sposobem na uszczęśliwienie kompilatora jest nazwanie go tak:

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

Chociaż może to być trochę ładniejsze niż casting, prawdopodobnie lepiej będzie dać klasie Animal abstrakcyjną metodę talk(), Jak powiedział David Schmitt.

 93
Author: Michael Myers,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-05-23 12:02:47

To pytanie jest bardzo podobne do pozycji 29 w efektywnej Javie - " rozważmy typy kontenerów heterogenicznych."Odpowiedź Laza jest najbliższa rozwiązaniu Blocha. Jednak zarówno put, jak i get powinny używać klasy dosłownej dla bezpieczeństwa. Podpisy będą:

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

Wewnątrz obu metod należy sprawdzić, czy parametry są prawidłowe. Zobacz Effective Java i klasę javadoc, aby uzyskać więcej informacji.

 26
Author: Craig P. Motlin,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2016-05-17 16:22:31

Dodatkowo możesz poprosić metodę o zwrócenie wartości w danym typie w ten sposób

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

Więcej przykładów tutaj W Oracle Java documentation

 11
Author: isuru chathuranga,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2016-06-24 05:12:36

Jak powiedziałeś zdanie klasy będzie OK, możesz napisać to:

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

A następnie użyj go w ten sposób:

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

Nie jest to idealne rozwiązanie, ale w Java generics jest to prawie tyle, ile się da. Istnieje sposób na zaimplementowanie Typesafe heterogenicznych kontenerów (THC) za pomocą tokenów Super Type , ale to znowu ma swoje własne problemy.

 9
Author: Fabian Steeg,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2009-01-16 15:57:40

Na podstawie tego samego pomysłu, co Super Type Tokens, można utworzyć wpisane id do użycia zamiast ciągu znaków:

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

Ale myślę, że może to nie być cel, ponieważ teraz trzeba tworzyć nowe obiekty id dla każdego łańcucha i trzymać się ich (lub zrekonstruować je z poprawną informacją o typie).

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());
Ale teraz możesz korzystać z klasy w sposób, w jaki pierwotnie chciałeś, bez odlewów.
jerry.callFriend(spike).bark();
jerry.callFriend(quacker).quack();

To tylko ukrywanie parametru type wewnątrz identyfikatora, chociaż oznacza to możesz pobrać typ z identyfikatora później, jeśli chcesz.

Musisz zaimplementować metody porównywania i haszowania TypedID, jeśli chcesz mieć możliwość porównania dwóch identycznych instancji id.

 7
Author: Mike Houston,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2009-01-23 02:00:28

Niemożliwe. Skąd Mapa ma wiedzieć, którą podklasę zwierząt dostanie, biorąc pod uwagę tylko klucz Łańcuchowy?

Jest to możliwe tylko wtedy, gdy każde zwierzę zaakceptuje tylko jeden typ przyjaciela (wtedy może to być parametr klasy zwierzęcia), lub gdy metoda callFriend() otrzyma parametr typu. Ale naprawdę wygląda na to, że brakuje ci punktu dziedziczenia: chodzi o to, że możesz traktować podklasy równomiernie tylko przy użyciu wyłącznie metod klasy nadrzędnej.

 5
Author: Michael Borgwardt,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2009-01-16 15:52:14

" czy istnieje sposób na określenie typu zwracanego w czasie wykonywania bez dodatkowego parametru przy użyciu instanceof?"

Jako alternatywne rozwiązanie możesz użyćwzorca Odwiedzin w ten sposób. Make Animal abstract and make it implementable 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 oznacza po prostu, że zwierzę jest skłonne zaakceptować odwiedzającego:

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

A odwiedzający może odwiedzić wszystkie podklasy zwierzęcia:

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

Więc na przykład pies implementacja wyglądałaby wtedy tak:

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

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

Sztuczka polega na tym, że pies wie, jaki to typ, może wywołać odpowiednią przeciążoną metodę odwiedzin odwiedzającego v, przekazując "this" jako parametr. Inne podklasy implementowałyby accept () dokładnie w ten sam sposób.

Klasa, która chce wywołać określone metody podklasy, musi zaimplementować interfejs użytkownika w następujący sposób:

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

Wiem, że to o wiele więcej interfejsów i metod niż się spodziewałeś, ale to standardowy sposób, aby uzyskać obsługę każdego konkretnego podtypu z dokładnie zerową instancją kontroli i zerowym typem odlewów. A wszystko odbywa się w standardowym języku, więc nie tylko dla Javy, ale każdy język OO powinien działać tak samo.

 5
Author: Antti Siiskonen,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2015-05-24 21:09:23

Napisałem artykuł, który zawiera proof of concept, klasy wsparcia i klasę testową, która demonstruje, w jaki sposób tokeny Super Type mogą być pobierane przez twoje klasy w czasie wykonywania. W skrócie, pozwala na delegowanie do alternatywnych implementacji w zależności od rzeczywistych parametrów generycznych przekazywanych przez wywołującego. Przykład:

  • TimeSeries<Double> deleguje do prywatnej klasy wewnętrznej, która używa double[]
  • TimeSeries<OHLC> delegatów do prywatnej klasy wewnętrznej, która używa ArrayList<OHLC>

Zobacz: używanie TypeTokens do pobierania ogólnych parametrów

Dzięki

Richard Gomes - Blog

 5
Author: Richard Gomes,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2015-12-08 16:06:19

Oto prostsza wersja:

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
}

W pełni działający kod:

    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,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2018-01-11 12:12:53

Nie do końca, ponieważ jak mówisz, kompilator wie tylko, że callFriend () zwraca zwierzę, a nie psa czy kaczkę.

Czy nie można dodać abstrakcyjnej metody makeNoise () do zwierzęcia, która byłaby zaimplementowana jako kora lub Kwak przez jego podklasy?

 2
Author: sk.,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2009-01-16 15:51:47

To, czego szukasz, to abstrakcja. Kod przeciwko interfejsom więcej i trzeba zrobić mniej casting.

Poniższy przykład znajduje się w C# , ale koncepcja pozostaje taka sama.

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,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2014-05-21 17:02:15

Jest tu wiele świetnych odpowiedzi, ale takie podejście zastosowałem w teście Appium, w którym działanie na jednym elemencie może skutkować przechodzeniem do różnych stanów aplikacji w zależności od ustawień użytkownika. Chociaż nie podąża za konwencjami przykładu OP, mam nadzieję, że komuś pomoże.

public <T extends MobilePage> T tapSignInButton(Class<T> type) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
    //signInButton.click();
    return type.getConstructor(AppiumDriver.class).newInstance(appiumDriver);
}
  • MobilePage jest super klasą, że typ rozszerza znaczenie można użyć dowolnego z jego dzieci (duh)
  • Typ.getConstructor (Param.klas, itp.) Pozwala na interakcję z na konstruktor typu. Konstruktor ten powinien być taki sam pomiędzy wszystkimi oczekiwanymi klasami.
  • newInstance pobiera zadeklarowaną zmienną, którą chcesz przekazać do konstruktora nowych obiektów

Jeśli nie chcesz wyrzucać błędów możesz je złapać w ten sposób:

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,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-06-30 17:17:50

Wiem, że to zupełnie inna rzecz, o którą pytał ten jeden. Innym sposobem rozwiązania tego problemu byłaby refleksja. Chodzi mi o to, że nie czerpie to korzyści z leków generycznych, ale pozwala naśladować w jakiś sposób zachowanie, które chcesz wykonać (zrobić psa szczekającego, zrobić Kaczora kwaka itp.) bez dbania o Typ odlewu:

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: ,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2013-10-24 20:39:46

A co z

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,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2015-02-06 01:19:02

Zrobiłem co następuje w moim lib kontraktor:

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

Podklasa:

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

Przynajmniej działa to wewnątrz bieżącej klasy i gdy ma silne odniesienie do typowania. Wielokrotne dziedziczenie działa, ale wtedy staje się naprawdę trudne:)

 1
Author: R.Moeller,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2015-06-28 19:07:21

Istnieje inne podejście, możesz zawęzić Typ zwracania po nadpisaniu metody. W każdej podklasie będziesz musiał nadpisać callFriend, aby zwrócić tę podklasę. Kosztowałby wiele deklaracji callFriend, ale można by wyodrębnić części wspólne do metody nazwanej wewnętrznie. Wydaje mi się to o wiele prostsze niż rozwiązania wymienione powyżej i nie wymaga dodatkowego argumentu do określenia typu zwrotu.

 0
Author: FeralWhippet,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
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;
}

Możesz zwrócić dowolny typ i odbierać bezpośrednio jak. Nie trzeba pisać.

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

Jest to najlepsze, jeśli chcesz dostosować dowolny inny rodzaj rekordów zamiast rzeczywistych kursorów.

 -1
Author: user6083049,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2016-03-18 14:38:01