How do I make the method return type generic?
考虑这个例子(在OOP书籍中很典型):
我有一个
这是
1 2 3 4 5 6 7 8 9 10 11 |
下面是一些带有大量类型转换的代码片段:
1 2 3 4 5 6 | Mouse jerry = new Mouse(); jerry.addFriend("spike", new Dog()); jerry.addFriend("quacker", new Duck()); ((Dog) jerry.callFriend("spike")).bark(); ((Duck) jerry.callFriend("quacker")).quack(); |
我有没有办法使用返回类型的泛型来消除类型转换,这样我就可以说
1 2 | jerry.callFriend("spike").bark(); jerry.callFriend("quacker").quack(); |
这里有一些初始代码,返回类型作为从未使用过的参数传递给方法。
1 2 3 |
有没有一种方法可以在运行时使用
您可以这样定义
1 2 3 | public <T extends Animal> T callFriend(String name, Class<T> type) { return type.cast(friends.get(name)); } |
然后这样称呼它:
1 2 | jerry.callFriend("spike", Dog.class).bark(); jerry.callFriend("quacker", Duck.class).quack(); |
此代码的好处是不生成任何编译器警告。当然,这实际上只是一个新版本的铸造前一般的日子,并没有增加任何额外的安全性。
不,编译器不知道会返回什么类型的
1 2 | jerry.addFriend("quaker", new Duck()); jerry.callFriend("quaker", /* unused */ new Dog()); // dies with illegal cast |
在这种特定的情况下,创建一个抽象的
1 2 3 4 5 6 | Mouse jerry = new Mouse(); jerry.addFriend("spike", new Dog()); jerry.addFriend("quacker", new Duck()); jerry.callFriend("spike").talk(); jerry.callFriend("quacker").talk(); |
您可以这样实现它:
1 2 3 4 | @SuppressWarnings("unchecked") public <T extends Animal> T callFriend(String name) { return (T)friends.get(name); } |
(是的,这是合法代码;参见Java泛型:仅定义为返回类型的泛型类型)。
将从调用方推断返回类型。但是,请注意
不幸的是,您使用它的方式(不将返回值赋给临时变量),使编译器满意的唯一方法是这样调用它:
1 | jerry.<Dog>callFriend("spike").bark(); |
虽然这可能比施法好一点,但正如大卫施密特所说,你最好给
这个问题非常类似于有效Java中的第29项——"考虑类型化异构容器"。Laz的答案是最接近布洛赫的解决方案。但是,为了安全起见,put和get都应该使用类文本。签名将变成:
1 2 |
在这两种方法中,您应该检查参数是否正常。有关更多信息,请参见有效的Java和类JavaDoc。
另外,您可以要求该方法以这种方式返回给定类型的值
1 | <T> T methodName(Class<T> var); |
在Oracle Java文档中的更多示例
正如你所说,通过一门课是可以的,你可以写下:
1 2 3 | public <T extends Animal> T callFriend(String name, Class<T> clazz) { return (T) friends.get(name); } |
然后这样使用:
1 2 | jerry.callFriend("spike", Dog.class).bark(); jerry.callFriend("quacker", Duck.class).quack(); |
并不完美,但这和Java泛型差不多。有一种方法可以使用超级类型标记来实现类型安全的异构容器(THC),但这又有它自己的问题。
下面是更简单的版本:
1 2 3 | 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 } |
完全工作代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 | 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(); } } |
基于与超级类型标记相同的思想,您可以创建一个要使用的类型化ID,而不是字符串:
1 2 3 4 5 6 7 8 9 10 11 12 13 | 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]; } } |
但我认为这可能会破坏目标,因为您现在需要为每个字符串创建新的ID对象并保留它们(或使用正确的类型信息重建它们)。
1 2 3 4 5 6 | 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()); |
但是你现在可以按照你最初想要的方式使用这个类,而不需要演员表。
1 2 | jerry.callFriend(spike).bark(); jerry.callFriend(quacker).quack(); |
这只是将类型参数隐藏在ID中,尽管这意味着您可以稍后从标识符中检索类型(如果需要)。
如果您希望能够比较一个ID的两个相同实例,那么也需要实现typedid的比较和哈希方法。
我写了一篇文章,其中包含概念证明、支持类和测试类,演示了如何在运行时由类检索超类型标记。简而言之,它允许您根据调用方传递的实际通用参数委托到可选实现。例子:
TimeSeries 委托给使用double[] 的私有内部类TimeSeries 委托给使用ArrayList 的私有内部类。
见:使用typetokens检索通用参数
谢谢
Richard Gomes-博客
不可能。如果只给了一个字符串键,这个地图怎么知道它将得到哪一个子类的动物呢?
唯一可能的方法是,如果每只动物只接受一种类型的友元(那么它可以是动物类的参数),或者callFriend()方法获得一个类型参数。但看起来您确实缺少了继承点:只有在专门使用超类方法时,才能统一地处理子类。
"是否有一种方法可以在运行时使用instanceof计算返回类型,而不使用额外的参数?"
作为另一种解决方案,您可以使用这样的访客模式。使动物抽象化并使其实现可见:
1 2 3 4 5 6 7 8 9 10 11 |
可访问性只是指动物执行机构愿意接受访问者:
1 2 3 | public interface Visitable { void accept(Visitor v); } |
访问者实现可以访问动物的所有子类:
1 2 3 4 5 | public interface Visitor { void visit(Dog d); void visit(Duck d); void visit(Mouse m); } |
例如,一个Dog实现会如下所示:
1 2 3 4 5 6 | public class Dog extends Animal { public void bark() {} @Override public void accept(Visitor v) { v.visit(this); } } |
这里的技巧是,由于狗知道它是什么类型,它可以通过将"this"作为参数传递来触发访问者v的相关重载访问方法。其他子类将以完全相同的方式实现accept()。
然后,想要调用子类特定方法的类必须实现这样的访问者接口:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | 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(); } } |
我知道它的接口和方法比你所期望的要多得多,但是它是一种标准的方法,可以处理每一个特定的子类型,检查的实例精确为零,类型转换为零。所有这些都是以标准语言不可知的方式完成的,所以不仅仅是Java,而且任何一种面向对象的语言都应该是一样的。
不完全是这样,因为正如您所说,编译器只知道callFriend()返回的是动物,而不是狗或鸭。
你不能给动物添加一个抽象的makenoise()方法,通过它的子类作为树皮或嘎嘎叫来实现吗?
你在这里寻找的是抽象。更多的针对接口的代码,您应该少做一些转换。
下面的例子在C中,但概念仍然相同。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 | 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!"); } } } |
这里有很多很好的答案,但这是我在Appium测试中采用的方法,在这个测试中,对单个元素的操作会导致根据用户的设置进入不同的应用程序状态。虽然它不遵循OP示例的惯例,但我希望它能帮助某些人。
1 2 3 4 | public <T extends MobilePage> T tapSignInButton(Class<T> type) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException { //signInButton.click(); return type.getConstructor(AppiumDriver.class).newInstance(appiumDriver); } |
- mobilepage是该类型扩展的超级类,意味着您可以使用它的任何子类(duh)
- type.getconstructor(param.class等)允许您与类型的构造函数。在所有期望的类之间,此构造函数应该是相同的。
- NewInstance接受要传递给New Objects构造函数的已声明变量
如果您不想抛出错误,可以这样捕获它们:
1 2 3 4 5 6 7 8 9 10 | 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; } |
怎么样
1 2 3 4 5 6 7 8 9 10 |
}
我在我的自由康特拉克特里做了如下的事情:
1 2 3 | public class Actor<SELF extends Actor> { public SELF self() { return (SELF)_self; } } |
子类:
1 2 3 | public class MyHttpAppSession extends Actor<MyHttpAppSession> { ... } |
至少在当前类中以及具有强类型引用时可以这样做。多重继承可以工作,但很难做到:)
我知道这和那个人问的完全不同。解决这一问题的另一种方法是反思。我的意思是,这并没有从仿制药中获益,但是它可以让你在某种程度上模仿你想表现的行为(狗吠叫,鸭子嘎嘎叫等),而不需要考虑类型转换:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 | 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"); } } |
还有另一种方法,您可以在重写方法时缩小返回类型。在每个子类中,您必须重写CallFriend才能返回该子类。成本可能是callFriend的多个声明,但您可以将公共部分隔离到内部调用的方法中。这对我来说比上面提到的解决方案简单得多,并且不需要额外的参数来确定返回类型。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | 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; } |
您可以返回任何类型并直接接收。不需要打字。
1 | Person p = nextRow(cursor); // cursor is real database cursor. |
如果您想自定义任何其他类型的记录而不是真正的光标,这是最好的方法。