我如何使方法返回类型泛型?

考虑下面的例子(OOP书籍中的典型例子):

我有一个Animal类,每个Animal可以有很多朋友 和子类,如DogDuckMouse等添加特定的行为,如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);
}
}

下面是一些带有大量类型转换的代码片段:

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

下面是一些初始代码,其中返回类型作为从未使用过的参数传递给方法。

public<T extends Animal> T callFriend(String name, T unusedTypeObj){
return (T)friends.get(name);
}
是否有一种方法可以在运行时计算出返回类型,而不使用instanceof额外的参数?或者至少通过传递该类型的类而不是虚拟实例 我知道泛型是用于编译时类型检查的,但是对此有解决方法吗?< / p >
860966 次浏览

不。编译器无法知道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();

不完全是,因为如你所说,编译器只知道callFriend()返回的是Animal,而不是Dog或Duck。

你不能给Animal添加一个抽象的makeNoise()方法吗?这个方法将被它的子类实现为树皮或嘎嘎声。

你可以这样实现它:

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

(是的,这是法律代码;见# EYZ0。)

返回类型将从调用者推断出来。但是,请注意@SuppressWarnings注释:它告诉您这段代码不是类型安全的。您必须自己验证它,或者您可以在运行时获得ClassCastExceptions

不幸的是,你使用它的方式(没有将返回值分配给临时变量),唯一让编译器高兴的方法是像这样调用它:

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

虽然这可能比强制转换要好一点,但正如David Schmitt所说,最好给Animal类一个抽象的talk()方法。

不可能的。如果只给一个String键,Map怎么知道它将得到Animal的哪个子类呢?

唯一可能的方法是每个Animal只接受一种类型的friend(那么它可以是Animal类的一个参数),或者callFriend()方法获得了一个类型参数。但是,看起来您确实忽略了继承的要点:在独占地使用超类方法时,只能统一地对待子类。

你可以这样定义callFriend:

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

那么就这样称呼它吧:

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

这段代码的好处是不会生成任何编译器警告。当然,这实际上只是前泛型时代的强制转换的更新版本,并没有增加任何额外的安全性。

正如你所说,通过一门课程就可以了,你可以这样写:

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

然后像这样使用它:

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

这并不完美,但这几乎是您使用Java泛型所能达到的极限。有一种实现使用超级类型令牌的类型安全异构容器的方法,但它又有自己的问题。

这个问题与Effective Java中的第29项——“考虑类型安全的异构容器”非常相似。拉兹的答案最接近布洛赫的答案。但是,为了安全起见,put和get都应该使用Class字面值。签名将变成:

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

在这两个方法中,您应该检查参数是否正常。有关更多信息,请参阅Effective Java和 javadoc。

基于与超级类型令牌相同的思想,你可以创建一个类型化的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];
}
}

但我认为这可能会破坏目的,因为现在需要为每个字符串创建新的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());

但是您现在可以按照您最初想要的方式使用该类,而不需要强制类型转换。

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

这只是将类型参数隐藏在id中,尽管这意味着如果您愿意,稍后可以从标识符中检索类型。

如果希望能够比较一个id的两个相同实例,还需要实现TypedID的比较和散列方法。

我已经写了一篇文章,其中包含了一个概念的证明,支持类和一个测试类,演示了超级类型令牌如何在运行时由类检索。 简而言之,它允许您根据调用者传递的实际泛型参数委托其他实现。例子:< / p >
  • TimeSeries<Double>委托给一个使用double[]的私有内部类
  • TimeSeries<OHLC>委托给一个使用ArrayList<OHLC>的私有内部类

看到的:

谢谢

理查德·戈麦斯——博客

我知道这和刚才那个人问的完全不同。解决这个问题的另一种方法是反思。我的意思是,这并没有从泛型中获益,但它让你在某种程度上模拟你想要执行的行为(让狗叫,让鸭子嘎嘎叫,等等),而不考虑类型转换:

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


}


}

你在这里寻找的是抽象。多针对接口编写代码,就应该少做类型转换。

下面的例子是用c#编写的,但是概念是一样的。

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

是什么

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

“有没有一种方法可以在不使用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);
}
}

可访问的意思是Animal的实现愿意接受一个访问者:

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

而一个visitor实现能够访问动物的所有子类:

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

我知道它的接口和方法比您所期望的要多得多,但是它是一种标准的方法,可以用精确的零instanceof检查和零类型强制转换来处理每个特定的子类型。而且这一切都是以一种标准语言不可知的方式完成的,因此它不仅适用于Java,而且适用于任何面向对象语言。

我在我的lib kontraktor中做了以下事情:

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

子类化:

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

至少这在当前类内部和具有强类型引用时是有效的。多重继承工作,但真的很棘手:)

此外,您还可以通过这种方式要求该方法以给定类型返回值

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

Oracle Java文档中在这里的更多示例

还有另一种方法,当你重写一个方法时,你可以缩小返回类型。在每个子类中,您必须重写callFriend以返回该子类。代价是callFriend的多次声明,但是可以将公共部分隔离到内部调用的方法中。对我来说,这似乎比上面提到的解决方案简单得多,并且不需要额外的参数来确定返回类型。

这里有很多很好的答案,但这是我在Appium测试中采用的方法,在该测试中,对单个元素的操作可能导致基于用户设置的不同应用程序状态。虽然它没有遵循OP的例子的惯例,但我希望它能帮助到一些人。

public <T extends MobilePage> T tapSignInButton(Class<T> type) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
//signInButton.click();
return type.getConstructor(AppiumDriver.class).newInstance(appiumDriver);
}
  • MobilePage是该类型扩展的超类,这意味着您可以使用它的任何子类(胡说)
  • type.getConstructor(Param.class等)允许您与 类型的构造函数。这个构造函数在所有期望的类之间应该是相同的
  • newInstance接受一个声明的变量,您希望将该变量传递给new objects构造函数

如果你不想抛出错误,你可以像这样捕获它们:

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

下面是一个简单的版本:

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
}

完全工作的代码:

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


}
}

由于这个问题是基于假设的数据,这里有一个很好的例子,返回一个扩展Comparable接口的泛型。

public class MaximumTest {
// find the max value using Comparable interface
public static <T extends Comparable<T>> T maximum(T x, T y, T z) {
T max = x; // assume that x is initially the largest


if (y.compareTo(max) > 0){
max = y; // y is the large now
}
if (z.compareTo(max) > 0){
max = z; // z is the large now
}
return max; // returns the maximum value
}




//testing with an ordinary main method
public static void main(String args[]) {
System.out.printf("Maximum of %d, %d and %d is %d\n\n", 3, 4, 5, maximum(3, 4, 5));
System.out.printf("Maximum of %.1f, %.1f and %.1f is %.1f\n\n", 6.6, 8.8, 7.7, maximum(6.6, 8.8, 7.7));
System.out.printf("Maximum of %s, %s and %s is %s\n", "strawberry", "apple", "orange",
maximum("strawberry", "apple", "orange"));
}
}