Come faccio a rendere il metodo di tipo generico?

Considera questo esempio (tipico nei libri OOP):

Ho una class di Animal , in cui ogni Animal può avere molti amici.
E sottoclassi come Dog , Duck , Mouse ecc. Che aggiungono comportamenti specifici come bark() , quack() ecc.

Ecco la class degli Animal :

 public class Animal { private Map 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 un sacco 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(); 

C’è un modo in cui posso usare i generici per il tipo di ritorno per sbarazzarmi del typecasting, così posso dire

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

Ecco un codice iniziale con il tipo restituito trasmesso al metodo come parametro che non viene mai utilizzato.

 public 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 il parametro extra usando instanceof ? O almeno passando una class del tipo invece di un’istanza fittizia.
Capisco che i generici sono per il controllo del tipo di compilazione, ma c’è una soluzione per questo?

È ansible definire callFriend questo modo:

 public  T callFriend(String name, Class 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. Ovviamente questa è solo una versione aggiornata del casting dai giorni pre-generici e non aggiunge alcuna sicurezza aggiuntiva.

No. Il compilatore non può sapere che tipo jerry.callFriend("spike") restituirebbe. Inoltre, l’implementazione nasconde semplicemente il cast nel metodo senza alcun tipo di sicurezza aggiuntivo. Considera questo:

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

In questo caso specifico, creare un metodo abstract talk() e sovrascriverlo appropriatamente 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(); 

Potresti implementarlo in questo modo:

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

(Sì, questo è un codice legale, vedi Java Generics: tipo generico definito come solo tipo di reso ).

Il tipo di ritorno sarà dedotto dal chiamante. Tuttavia, nota l’annotazione @SuppressWarnings : che ti dice che questo codice non è tipografico . Devi verificarlo da solo o potresti ottenere ClassCastExceptions in fase di esecuzione.

Sfortunatamente, nel modo in cui lo stai usando (senza assegnare il valore di ritorno a una variabile temporanea), l’unico modo per rendere felice il compilatore è chiamarlo in questo modo:

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

Sebbene questo possa essere un po ‘più bello del casting, probabilmente è meglio dare alla class Animal un metodo talk() astratto, come diceva David Schmitt.

Questa domanda è molto simile all’articolo 29 in Java efficace : “Considerare contenitori eterogenei di tipo typesafe”. La risposta di Laz è la più vicina alla soluzione di Bloch. Comunque, sia put che get dovrebbero usare il Class literal per sicurezza. Le firme diventerebbero:

 public  void addFriend(String name, Class type, T animal); public  T callFriend(String name, Class type); 

All’interno di entrambi i metodi è necessario verificare che i parametri siano sani. Vedi Java efficace e la class javadoc per maggiori informazioni.

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

  T methodName(Class var); 

Altri esempi qui alla documentazione di Oracle Java

Come hai detto, passare una class sarebbe OK, potresti scrivere questo:

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

E quindi usarlo in questo modo:

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

Non perfetto, ma questo è praticamente il massimo che si ottiene con i generici Java. C’è un modo per implementare i contenitori eterogenei Typesafe (THC) usando i token di tipo Super , ma questo ha di nuovo i suoi problemi.

Basata sulla stessa idea dei token di tipo super, è ansible creare un id tipizzato da usare al posto di una stringa:

 public abstract class TypedID { 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 devi creare nuovi oggetti id per ciascuna stringa e tenerli su (o ricostruirli con le informazioni di tipo corrette).

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

Ma ora puoi usare la class nel modo che volevi, senza i cast.

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

Questo è solo hide il parametro di tipo all’interno dell’ID, anche se significa che è ansible recuperare il tipo dall’identificatore in seguito, se lo si desidera.

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

Non ansible. In che modo la Mappa dovrebbe sapere quale sottoclass di Animal arriverà, dato solo una chiave di stringa?

L’unico modo in cui ciò sarebbe ansible è che ogni animale accettasse solo un tipo di amico (quindi potrebbe essere un parametro della class Animale) o che il metodo callFriend () avesse un parametro di tipo. Ma sembra davvero che manchi il punto di ereditarietà: è che puoi trattare le sottoclassi in modo uniforms solo quando usi esclusivamente i metodi della superclass.

“C’è un modo per capire il tipo di ritorno in fase di esecuzione senza il parametro extra usando instanceof?”

Come soluzione alternativa potresti utilizzare il pattern Visitor come questo. Rendi astratto l’animale e attualo Visitabile:

 abstract public class Animal implements Visitable { private Map friends = new HashMap(); public void addFriend(String name, Animal animal){ friends.put(name,animal); } public Animal callFriend(String name){ return friends.get(name); } } 

Visitable significa semplicemente che un’attuazione di Animal è 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 Dog potrebbe apparire come questa:

 public class Dog extends Animal { public void bark() {} @Override public void accept(Visitor v) { v.visit(this); } } 

Il trucco qui è che, come il Cane sa di che tipo è, può triggersre il metodo di visita sovraccarico rilevante del visitatore v trasmettendo “questo” come parametro. Altre sottoclassi implementerebbero accept () esattamente allo stesso modo.

La class che vuole chiamare i metodi specifici di sottoclassi 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 per cui è stato contrattato, ma è un modo standard per ottenere un handle su ogni sottotipo specifico con controlli di istanza zero e cast di tipo zero. Ed è tutto fatto in un linguaggio agnostico standard, quindi non è solo per Java, ma qualsiasi linguaggio OO dovrebbe funzionare allo stesso modo.

Ho scritto un articolo che contiene un proof of concept, classi di supporto e una class di test che dimostra come i token Super Type possono essere recuperati dalle tue classi in fase di runtime. In poche parole, consente di debind a implementazioni alternative in base ai parametri generici effettivamente passati dal chiamante. Esempio:

  • TimeSeries delega a una class interna privata che usa il double[]
  • TimeSeries delega a una class interna privata che utilizza ArrayList

Vedi: Uso di TypeTokens per recuperare parametri generici

Grazie

Richard Gomes – Blog

Ecco la versione più semplice:

 public  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 friends = new HashMap<>(); public void addFriend(String name, Animal animal){ friends.put(name,animal); } public  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(); } } 

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

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

Quello che stai cercando qui è l’astrazione. Codice contro le interfacce più e dovresti fare meno casting.

L’esempio sotto è 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(string name) where T : _Animal; } interface _Animal { void Speak(); } abstract class AnimalBase : _Animal, _HasFriends { private Dictionary friends = new Dictionary(); public abstract void Speak(); public void AddFriend(string name, _Animal animal) { friends.Add(name, animal); } public T CallFriend(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!"); } } } 

Ci sono un sacco di ottime risposte qui, ma questo è l’approccio che ho preso per un test di Appium in cui agire su un singolo elemento può portare a diversi stati di applicazione in base alle impostazioni dell’utente. Anche se non segue le convenzioni dell’esempio di OP, spero che aiuti qualcuno.

 public  T tapSignInButton(Class type) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException { //signInButton.click(); return type.getConstructor(AppiumDriver.class).newInstance(appiumDriver); } 
  • MobilePage è la super class che il tipo si estende nel senso che puoi usare qualsiasi dei suoi figli (duh)
  • type.getConstructor (Param.class, ecc.) 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 nuovo costruttore di oggetti

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

 public  T tapSignInButton(Class type) { // signInButton.click(); T returnValue = null; try { returnValue = type.getConstructor(AppiumDriver.class).newInstance(appiumDriver); } catch (Exception e) { e.printStackTrace(); } return returnValue; } 

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

 import java.lang.reflect.InvocationTargetException; import java.util.HashMap; import java.util.Map; abstract class AnimalExample { private Map> friends = new HashMap>(); private Map theFriends = new HashMap(); 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"); } } 

che dire

 public class Animal { private Map> friends = new HashMap>(); public  void addFriend(String name, T animal){ friends.put(name,animal); } public  T callFriend(String name){ return friends.get(name); } 

}

Ho fatto quanto segue nel mio lib kontraktor:

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

subclassing:

 public class MyHttpAppSession extends Actor { ... } 

almeno questo funziona all’interno della class corrente e quando si ha un forte riferimento tipizzato. L’ereditarietà funziona, ma diventa davvero difficile 🙂

C’è un altro approccio, è ansible restringere il tipo restituito quando si esegue l’override di un metodo. In ciascuna sottoclass, si dovrebbe sovrascrivere callFriend per restituire tale sottoclass. Il costo sarebbero le dichiarazioni multiple di callFriend, ma è ansible isolare le parti comuni in un metodo chiamato internamente. Questo mi sembra molto più semplice delle soluzioni menzionate sopra e non richiede un argomento aggiuntivo per determinare il tipo di ritorno.

 public  X nextRow(Y cursor) { return (X) getRow(cursor); } private  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 c’è bisogno di digitare.

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

Questo è il migliore se si desidera personalizzare qualsiasi altro tipo di record anziché veri cursori.