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