考虑这个例子(典型的OOP书籍):
我有一个Animal
班级,每个人都Animal
可以有很多朋友.
和子类一样Dog
,Duck
,Mouse
等它添加特定的行为一样bark()
,quack()
等等.
这是Animal
班级:
public class Animal { private Mapfriends = new HashMap<>(); public void addFriend(String name, Animal animal){ friends.put(name,animal); } public Animal callFriend(String name){ return friends.get(name); } }
这里有一些代码片段有很多类型转换:
Mouse jerry = new Mouse(); jerry.addFriend("spike", new Dog()); jerry.addFriend("quacker", new Duck()); ((Dog) jerry.callFriend("spike")).bark(); ((Duck) jerry.callFriend("quacker")).quack();
有什么办法可以使用泛型来返回类型来摆脱类型转换,这样我就可以说了
jerry.callFriend("spike").bark(); jerry.callFriend("quacker").quack();
这里有一些返回类型的初始代码作为一个从未使用过的参数传递给方法.
publicT callFriend(String name, T unusedTypeObj){ return (T)friends.get(name); }
有没有办法在运行时找出返回类型而不使用额外的参数instanceof
?或者至少通过传递类型的类而不是虚拟实例.
我理解泛型用于编译时类型检查,但是有一个解决方法吗?
你可以这样定义callFriend
:
publicT callFriend(String name, Class type) { return type.cast(friends.get(name)); }
然后这样称呼它:
jerry.callFriend("spike", Dog.class).bark(); jerry.callFriend("quacker", Duck.class).quack();
此代码的好处是不会生成任何编译器警告.当然,这实际上只是从通用前日期开始的更新版本,并没有增加任何额外的安全性.
不.编译器无法知道jerry.callFriend("spike")
返回的类型.此外,您的实现只是隐藏了方法中的强制转换,而没有任何其他类型的安全性.考虑一下:
jerry.addFriend("quaker", new Duck()); jerry.callFriend("quaker", /* unused */ new Dog()); // dies with illegal cast
在这种特定情况下,创建一个抽象talk()
方法并在子类中适当地覆盖它将为您提供更好的服务:
Mouse jerry = new Mouse(); jerry.addFriend("spike", new Dog()); jerry.addFriend("quacker", new Duck()); jerry.callFriend("spike").talk(); jerry.callFriend("quacker").talk();
你可以像这样实现它:
@SuppressWarnings("unchecked") publicT callFriend(String name) { return (T)friends.get(name); }
(是的,这是合法代码;请参阅Java Generics:仅定义为返回类型的泛型类型.)
返回类型将从调用者推断.但是,请注意@SuppressWarnings
注释:它告诉您此代码不是类型安全的.您必须自己验证它,或者您可以ClassCastExceptions
在运行时获得它.
不幸的是,你使用它的方式(没有将返回值赋给临时变量),让编译器满意的唯一方法就是这样调用它:
jerry.callFriend("spike").bark();
虽然这可能比投射更好,但你可能最好给这个Animal
班级一个抽象的talk()
方法,正如大卫施密特所说.
这个问题非常类似于Effective Java中的第29项 - "考虑类型安全的异构容器".Laz的答案是最接近Bloch的解决方案.但是,put和get都应该使用Class literal来保证安全.签名将成为:
publicvoid addFriend(String name, Class type, T animal); public T callFriend(String name, Class type);
在这两种方法中,你应该检查参数是否合理.有关详细信息,请参阅Effective Java和Class javadoc.
此外,您可以要求方法以这种方式返回给定类型的值
T methodName(Class var);
更多的例子在这里在甲骨文的Java文档
这是更简单的版本:
publicT callFriend(String name) { return (T) friends.get(name); //Casting to T not needed in this case but its a good practice to do }
完全工作的代码:
public class Test { public static class Animal { private Mapfriends = 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(); } }
正如你所说的,传递一个类就没问题,你可以这样写:
publicT callFriend(String name, Class clazz) { return (T) friends.get(name); }
然后像这样使用它:
jerry.callFriend("spike", Dog.class).bark(); jerry.callFriend("quacker", Duck.class).quack();
并不完美,但这与Java泛型相当.有一种方法可以使用超类型标记来实现Typesafe Heterogenous Containers(THC),但这又有其自身的问题.
基于与Super Type Tokens相同的想法,您可以创建一个使用的类型ID而不是字符串:
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]; } }
但我认为这可能会破坏目的,因为您现在需要为每个字符串创建新的id对象并保持它们(或使用正确的类型信息重建它们).
Mouse jerry = new Mouse(); TypedIDspike = new TypedID ("spike") {}; TypedID quacker = new TypedID ("quacker") {}; jerry.addFriend(spike, new Dog()); jerry.addFriend(quacker, new Duck());
但是你现在可以按照你原来想要的方式使用这个类,没有强制转换.
jerry.callFriend(spike).bark(); jerry.callFriend(quacker).quack();
这只是隐藏了id中的type参数,但它确实意味着如果你愿意,你可以稍后从标识符中检索类型.
如果您希望能够比较两个相同的id实例,则还需要实现TypedID的比较和散列方法.
"有没有办法在没有使用instanceof的额外参数的情况下在运行时找出返回类型?"
作为替代解决方案,您可以像这样使用访问者模式.使动物抽象并使其实现可访问:
abstract public class Animal implements Visitable { private Mapfriends = new HashMap (); public void addFriend(String name, Animal animal){ friends.put(name,animal); } public Animal callFriend(String name){ return friends.get(name); } }
可访问只是意味着Animal实现愿意接受访问者:
public interface Visitable { void accept(Visitor v); }
访问者实现能够访问动物的所有子类:
public interface Visitor { void visit(Dog d); void visit(Duck d); void visit(Mouse m); }
因此,例如Dog实现将如下所示:
public class Dog extends Animal { public void bark() {} @Override public void accept(Visitor v) { v.visit(this); } }
这里的技巧是,当Dog知道它是什么类型时,它可以通过传递"this"作为参数来触发访问者v的相关重载访问方法.其他子类将以完全相同的方式实现accept().
然后,要调用子类特定方法的类必须实现Visitor接口,如下所示:
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,而且任何OO语言都应该是相同的.
不可能.如果只给出一个String键,Map应该如何知道它将获得哪个Animal类?
唯一可行的方法是,如果每个Animal只接受一种类型的朋友(那么它可能是Animal类的参数),或者callFriend()方法获得一个类型参数.但是看起来你似乎错过了继承点:只有在使用超类方法时才能统一处理子类.
我写了一篇文章,其中包含概念证明,支持类和测试类,它演示了类在运行时如何检索超类型标记.简而言之,它允许您根据调用者传递的实际通用参数委派替代实现.例:
TimeSeries
委托给使用的私人内部类 double[]
TimeSeries
委托给使用的私人内部类 ArrayList
请参阅: 使用TypeTokens检索通用参数
谢谢
理查德戈麦斯 - 博客