Come faccio a rendere generico il tipo restituito dal metodo?


589

Considera questo esempio (tipico nei libri OOP):

Ho una Animallezione, dove ognuno Animalpuò avere molti amici.
E sottoclassi piace Dog, Duck, Mouseecc, che aggiunge un comportamento specifico come bark(), quack()etc.

Ecco la Animallezione:

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

Ed ecco alcuni frammenti di codice con molti tipi di 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();

Esiste un modo in cui posso usare generics per il tipo restituito per sbarazzarmi del typecasting, in modo da poterlo dire

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

Ecco del codice iniziale con il tipo restituito trasmesso al metodo come parametro che non è mai stato utilizzato.

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

C'è un modo per capire il tipo di ritorno in fase di esecuzione senza l'utilizzo del parametro aggiuntivo instanceof? O almeno passando una classe del tipo anziché un'istanza fittizia.
Comprendo che i generici servono per la verifica del tipo in fase di compilazione, ma esiste una soluzione alternativa per questo?

Risposte:


903

Puoi definire in callFriendquesto modo:

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

Quindi chiamalo come tale:

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

Questo codice ha il vantaggio di non generare alcun avviso del compilatore. Naturalmente questa è davvero solo una versione aggiornata del casting dei giorni pre-generici e non aggiunge alcuna sicurezza aggiuntiva.


29
... ma non ha ancora il controllo del tipo di tempo di compilazione tra i parametri della chiamata callFriend ().
David Schmitt,

2
Questa è la risposta migliore finora - ma dovresti cambiare addFriend allo stesso modo. Rende più difficile scrivere bug poiché hai bisogno di quella classe letterale in entrambi i posti.
Craig P. Motlin,

@Jaider, non esattamente lo stesso ma questo funzionerà: // Animal Class pubblico T CallFriend <T> (nome stringa) dove T: Animal {restituisce amici [nome] come T; } // Calling Class jerry.CallFriend <Dog> ("spike"). Bark (); jerry.CallFriend <Anatra> ( "quacker") Quack ().;
Nestor Ledon,

124

No. Il compilatore non può sapere quale tipo jerry.callFriend("spike")restituirebbe. Inoltre, l'implementazione nasconde il cast nel metodo senza ulteriore sicurezza del tipo. Considera questo:

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

In questo caso specifico, creare un talk()metodo astratto e sovrascriverlo in modo appropriato nelle sottoclassi ti servirebbe molto meglio:

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

jerry.callFriend("spike").talk();
jerry.callFriend("quacker").talk();

10
Mentre il metodo mmyers può funzionare, penso che questo metodo sia una migliore programmazione OO e ti farà risparmiare qualche problema in futuro.
James McMahon,

1
Questo è il modo corretto per ottenere lo stesso risultato. Si noti che l'obiettivo è ottenere un comportamento specifico della classe derivata in fase di esecuzione senza scrivere esplicitamente il codice per eseguire il brutto controllo e il cast del tipo. Il metodo proposto da @laz funziona, ma lancia la sicurezza del tipo fuori dalla finestra. Questo metodo richiede meno righe di codice (poiché le implementazioni del metodo sono associate in ritardo e comunque esaminate in fase di esecuzione) ma consentono comunque di definire facilmente un comportamento unico per ogni sottoclasse di Animali.
dc

Ma la domanda originale non si pone sulla sicurezza del tipo. Nel modo in cui l'ho letto, il richiedente vuole solo sapere se esiste un modo per sfruttare i generici per evitare di dover lanciare.
Laz

2
@laz: sì, la domanda originale - come posta - non riguarda la sicurezza del tipo. Ciò non cambia il fatto che esiste un modo sicuro per implementarlo, eliminando gli errori di cast di classe. Vedi anche weblogs.asp.net/alex_papadimoulis/archive/2005/05/25/…
David Schmitt

3
Non sono d'accordo, ma abbiamo a che fare con Java e tutte le sue decisioni di progettazione / problemi. Vedo questa domanda solo come cercare di imparare ciò che è possibile nei generici Java, non come un xyproblem ( meta.stackexchange.com/questions/66377/what-is-the-xy-problem ) che deve essere riprogettato. Come ogni modello o approccio, ci sono momenti in cui il codice che ho fornito è appropriato e momenti in cui è richiesto qualcosa di totalmente diverso (come quello che hai suggerito in questa risposta).
Laz

114

Potresti implementarlo in questo modo:

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

(Sì, questo è un codice legale; consultare Java Generics: tipo generico definito solo come tipo restituito .)

Il tipo restituito verrà dedotto dal chiamante. Tuttavia, nota l' @SuppressWarningsannotazione: che ti dice che questo codice non è typesafe . Devi verificarlo tu stesso o potresti arrivare ClassCastExceptionsin fase di esecuzione.

Sfortunatamente, il modo in cui lo stai utilizzando (senza assegnare il valore restituito a una variabile temporanea), l'unico modo per rendere felice il compilatore è chiamarlo in questo modo:

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

Anche se questo potrebbe essere un po 'più bello del casting, probabilmente stai meglio dando alla Animalclasse un talk()metodo astratto , come ha detto David Schmitt.


Il concatenamento dei metodi non era proprio un'intenzione. non mi dispiace assegnare il valore a una variabile Sottotipo e usarlo. Grazie per la soluzione
Sathish,

funziona perfettamente quando si esegue il concatenamento delle chiamate di metodo!
Hartmut P.

Mi piace molto questa sintassi. Penso che in C # sia quello jerry.CallFriend<Dog>(...che penso abbia un aspetto migliore.
andho,

Interessante che la stessa java.util.Collections.emptyList()funzione di JRE sia implementata esattamente in questo modo, e il suo javadoc si pubblicizza come una macchina da scrivere.
Ti Strga,

31

Questa domanda è molto simile all'elemento 29 in Java efficace - "Considera contenitori eterogenei typesafe". La risposta di Laz è la più vicina alla soluzione di Bloch. Tuttavia, sia put che get dovrebbero usare la classe letterale per sicurezza. Le firme diventerebbero:

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

All'interno di entrambi i metodi è necessario verificare che i parametri siano sani. Vedi Java efficace e il javadoc di classe per maggiori informazioni.


17

Inoltre, puoi chiedere al metodo di restituire il valore in un determinato tipo in questo modo

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

Altri esempi qui nella documentazione di Oracle Java


17

Ecco la versione più semplice:

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
}

Codice completamente funzionante:

    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
Cosa fa Casting to T not needed in this case but it's a good practice to do. Voglio dire se è gestito correttamente durante il runtime che cosa significa "buona pratica"?
Farid,

Volevo dire che il casting esplicito (T) non è necessario poiché la dichiarazione del tipo di ritorno <T> sulla dichiarazione del metodo dovrebbe essere sufficiente
webjockey il

9

Come hai detto, passare una lezione sarebbe OK, puoi scrivere questo:

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

E poi usalo in questo modo:

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

Non perfetto, ma questo è praticamente per quanto riguarda i generici Java. Esiste un modo per implementare i contenitori eterogenei Typesafe (THC) usando i token Super Type , ma questo ha di nuovo i suoi problemi.


Mi dispiace ma questa è la stessa identica risposta che Laz ha, quindi o lo stai copiando o lui ti sta copiando.
James McMahon,

Questo è un modo pulito per passare il Tipo. Ma non è ancora sicuro come ha detto Schmitt. Potrei ancora passare una classe diversa e il typecasting bombarderà. mmyers 2a risposta per impostare il tipo in Return tipo sembra migliore
Sathish

4
Nemo, se controlli l'orario di pubblicazione vedrai che li abbiamo pubblicati praticamente nello stesso momento. Inoltre, non sono esattamente gli stessi, solo due righe.
Fabian Steeg,

@Fabian Ho pubblicato una risposta simile, ma c'è una differenza importante tra le diapositive di Bloch e ciò che è stato pubblicato in Effective Java. Usa la classe <T> invece di TypeRef <T>. Ma questa è ancora un'ottima risposta.
Craig P. Motlin,

8

Sulla base della stessa idea dei token Super Type, è possibile creare un ID digitato da utilizzare anziché una stringa:

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

Ma penso che questo possa vanificare lo scopo, dal momento che ora è necessario creare nuovi oggetti ID per ogni stringa e tenerli su (o ricostruirli con le informazioni corrette sul tipo).

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

Ma ora puoi usare la classe come volevi in ​​origine, senza i cast.

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

Questo sta semplicemente nascondendo il parametro type all'interno dell'id, anche se ciò significa che è possibile recuperare il tipo dall'identificatore in un secondo momento, se lo si desidera.

Dovresti implementare anche i metodi di confronto e hashing di TypedID se vuoi essere in grado di confrontare due istanze identiche di un ID.


8

"Esiste un modo per capire il tipo di ritorno in fase di esecuzione senza il parametro aggiuntivo utilizzando instanceof?"

Come soluzione alternativa è possibile utilizzare il modello Visitatore in questo modo. Rendi astratto l'animale e rendilo implementabile Visitabile:

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

Visitabile significa solo che un'implementazione di Animali è disposta ad accettare un visitatore:

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

E l'implementazione di un visitatore è in grado di visitare tutte le sottoclassi di un animale:

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

Quindi ad esempio un'implementazione di Dog sarebbe quindi simile a questa:

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

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

Il trucco qui è che siccome il Cane sa che tipo è, può innescare il relativo metodo di visita sovraccarico del visitatore v passando "questo" come parametro. Altre sottoclassi implementerebbero accettano () esattamente allo stesso modo.

La classe che desidera chiamare metodi specifici della sottoclasse deve quindi implementare l'interfaccia Visitor in questo modo:

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

So che ci sono molte più interfacce e metodi di quelli che ti aspettavi, ma è un modo standard per ottenere un controllo su ogni sottotipo specifico con zero istanze di controlli e zero tipi di cast. Ed è tutto fatto in modo agnostico in linguaggio standard, quindi non è solo per Java ma qualsiasi linguaggio OO dovrebbe funzionare allo stesso modo.


6

Non possibile. In che modo la mappa dovrebbe sapere quale sottoclasse di Animal otterrà, data solo una chiave String?

L'unico modo in cui ciò sarebbe possibile è se ogni Animale accettasse solo un tipo di amico (quindi potrebbe essere un parametro della classe Animale), o del metodo callFriend () ottenuto un parametro di tipo. Ma sembra davvero che ti manchi il punto di ereditarietà: è che puoi usare le sottoclassi solo in modo uniforme quando usi esclusivamente i metodi superclasse.


5

Ho scritto un articolo che contiene una prova di concetto, classi di supporto e una classe di test che dimostra come i token Super Type possono essere recuperati dalle tue classi in fase di esecuzione. In breve, consente di delegare implementazioni alternative a seconda dei parametri generici effettivi passati dal chiamante. Esempio:

  • TimeSeries<Double> delega a una classe interna privata che utilizza double[]
  • TimeSeries<OHLC> delega a una classe interna privata che utilizza ArrayList<OHLC>

Vedere: Utilizzo di TypeTokens per recuperare parametri generici

Grazie

Richard Gomes - Blog


In effetti, grazie per aver condiviso la tua visione, il tuo articolo spiega davvero tutto!
Yann-Gaël Guéhéneuc,

3

Ci sono molte ottime risposte qui, ma questo è l'approccio che ho adottato per un test Appium in cui agire su un singolo elemento può comportare il passaggio a stati di applicazione diversi in base alle impostazioni dell'utente. Sebbene non segua le convenzioni dell'esempio di OP, spero che aiuti qualcuno.

public <T extends MobilePage> T tapSignInButton(Class<T> type) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
    //signInButton.click();
    return type.getConstructor(AppiumDriver.class).newInstance(appiumDriver);
}
  • MobilePage è la super classe che il tipo estende, il che significa che puoi usare uno qualsiasi dei suoi figli (duh)
  • type.getConstructor (Param.class, ecc.) ti consente di interagire con il costruttore del tipo. Questo costruttore dovrebbe essere lo stesso tra tutte le classi previste.
  • newInstance accetta una variabile dichiarata che si desidera passare al costruttore di nuovi oggetti

Se non vuoi lanciare gli errori puoi prenderli in questo modo:

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

Per quanto ne so, questo è il modo migliore ed elegante per usare i generici.
cbaldan,

2

Non proprio, perché come dici tu, il compilatore sa solo che callFriend () sta restituendo un animale, non un cane o un'anatra.

Non puoi aggiungere un metodo astratto makeNoise () ad Animal che verrebbe implementato come una corteccia o un ciarlatano dalle sue sottoclassi?


1
cosa succede se gli animali hanno metodi multipli che non rientrano nemmeno in un'azione comune che può essere astratta? Ho bisogno di questo per la comunicazione tra sottoclassi con azioni diverse in cui vado bene passare il Tipo, non un'istanza.
Sathish,

2
Hai appena risposto alla tua domanda: se un animale ha un'azione unica, devi lanciare quell'animale specifico. Se un animale ha un'azione che può essere raggruppata con altri animali, allora puoi definire un metodo astratto o virtuale in una classe base e usarlo.
Matt Jordan,

2

Quello che stai cercando qui è l'astrazione. Più codice contro le interfacce e dovresti fare meno casting.

L'esempio che segue è in C # ma il concetto rimane lo stesso.

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

Questa domanda ha a che fare con la codifica non con il concetto.

2

Nel mio lib kontraktor ho fatto quanto segue:

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

sottoclasse:

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

almeno questo funziona all'interno della classe corrente e quando si ha un forte riferimento tipizzato. L'ereditarietà multipla funziona, ma diventa davvero complicata allora :)


1

So che questa è una cosa completamente diversa da quella richiesta. Un altro modo di risolvere questo sarebbe la riflessione. Voglio dire, questo non beneficia di Generics, ma ti consente di emulare, in qualche modo, il comportamento che vuoi eseguire (abbaiare un cane, fare un ciarlatano di papera, ecc.) Senza occuparti del casting del tipo:

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

che dire

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

Esiste un altro approccio, è possibile restringere il tipo restituito quando si ignora un metodo. In ogni sottoclasse dovresti sovrascrivere callFriend per restituire quella sottoclasse. Il costo sarebbe costituito dalle molteplici dichiarazioni di callFriend, ma è possibile isolare le parti comuni in un metodo chiamato internamente. Questo mi sembra molto più semplice delle soluzioni sopra menzionate e non ha bisogno di un argomento in più per determinare il tipo di ritorno.


Non sono sicuro di cosa intendi per "restringere il tipo restituito". Afaik, Java e la maggior parte dei linguaggi digitati non sovraccaricano i metodi o le funzioni in base al tipo restituito. Ad esempio public int getValue(String name){}è indistinguibile dal public boolean getValue(String name){}punto di vista dei compilatori. È necessario modificare il tipo di parametro o aggiungere / rimuovere i parametri per riconoscere il sovraccarico. Forse ti sto solo fraintendendo ...
The One True Colter,

in Java puoi sovrascrivere un metodo in una sottoclasse e specificare un tipo di ritorno più "stretto" (cioè più specifico). Vedi 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;
}

Puoi restituire qualsiasi tipo e ricevere direttamente come. Non è necessario il typecast.

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

È preferibile se si desidera personalizzare qualsiasi altro tipo di record anziché cursori reali.


2
Dici che "non è necessario il typecast", ma chiaramente è coinvolto il typecasting: (Cursor) cursorad esempio.
Sami Laine,

Questo è un uso totalmente inappropriato dei generici. Questo codice richiede cursordi essere a Cursore restituirà sempre a Person(o null). L'uso di generics rimuove il controllo di questi vincoli, rendendo il codice non sicuro.
Andy Turner,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.