关于Java:如何使方法返回类型泛型?

How do I make the method return type generic?

考虑这个例子(在OOP书籍中很典型):

我有一个Animal班,每个Animal都可以有很多朋友。以及DogDuckMouse等亚类,增加了bark()quack()等具体行为。

这是Animal类:

1
2
3
4
5
6
7
8
9
10
11
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);
    }
}

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

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
public<T extends Animal> T callFriend(String name, T unusedTypeObj){
    return (T)friends.get(name);        
}

有没有一种方法可以在运行时使用instanceof在不使用额外参数的情况下确定返回类型?或者至少传递一个类型的类,而不是一个虚拟实例。我知道泛型用于编译时类型检查,但是否有解决方法?


您可以这样定义callFriend

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

此代码的好处是不生成任何编译器警告。当然,这实际上只是一个新版本的铸造前一般的日子,并没有增加任何额外的安全性。


不,编译器不知道会返回什么类型的jerry.callFriend("spike")。另外,您的实现只是隐藏方法中的强制转换,而不需要任何额外的类型安全性。考虑一下:

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

在这种特定的情况下,创建一个抽象的talk()方法,并在子类中适当地覆盖它,将为您提供更好的服务:

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泛型:仅定义为返回类型的泛型类型)。

将从调用方推断返回类型。但是,请注意@SuppressWarnings注释:这说明此代码不是类型安全的。你必须自己验证,否则你可以在运行时得到ClassCastExceptions

不幸的是,您使用它的方式(不将返回值赋给临时变量),使编译器满意的唯一方法是这样调用它:

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

虽然这可能比施法好一点,但正如大卫施密特所说,你最好给Animal类一个抽象的talk()方法。


这个问题非常类似于有效Java中的第29项——"考虑类型化异构容器"。Laz的答案是最接近布洛赫的解决方案。但是,为了安全起见,put和get都应该使用类文本。签名将变成:

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

在这两种方法中,您应该检查参数是否正常。有关更多信息,请参见有效的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
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);
  }
}

可访问性只是指动物执行机构愿意接受访问者:

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

}


我在我的自由康特拉克特里做了如下的事情:

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.

如果您想自定义任何其他类型的记录而不是真正的光标,这是最好的方法。