如何使方法返回类型通用?

时间:2009-01-16 15:43:09

标签: java generics return-value

考虑这个例子(典型的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的额外参数?或者至少通过传递类型的类而不是虚拟实例 我理解泛型用于编译时类型检查,但有没有解决方法呢?

20 个答案:

答案 0 :(得分:806)

您可以这样定义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();

此代码的好处是不会生成任何编译器警告。当然,这实际上只是从通用前日期开始的铸造的更新版本,并没有增加任何额外的安全性。

答案 1 :(得分:111)

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

答案 2 :(得分:100)

你可以像这样实现它:

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

(是的,这是合法代码;请参阅Java Generics: Generic type defined as return type only。)

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

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

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

虽然这可能比投射更好,但你可能最好给Animal类一个抽象的talk()方法,正如David Schmitt所说。

答案 3 :(得分:26)

这个问题非常类似于有效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);

在这两种方法中,您应该检查参数是否合理。有关详细信息,请参阅Effective Java和Class javadoc。

答案 4 :(得分:14)

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

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

Oracle Java文档中的更多示例here

答案 5 :(得分:12)

以下是更简单的版本:

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

        }
    }

答案 6 :(得分:10)

正如你所说的,通过课程会没问题,你可以这样写:

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泛型相当。有一种方法可以实现Typesafe Heterogenous Containers (THC) using Super Type Tokens,但这又有其自身的问题。

答案 7 :(得分:7)

基于与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];
  }
}

但我认为这可能会破坏目的,因为您现在需要为每个字符串创建新的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中的type参数,但它确实意味着你可以在以后根据需要从标识符中检索类型。

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

答案 8 :(得分:6)

"Is there a way to figure out the return type at runtime without the extra parameter using instanceof?"

As an alternative solution you could utilise the Visitor pattern like this. Make Animal abstract and make it implement Visitable:

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

Visitable just means that an Animal implementation is willing to accept a visitor:

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

And a visitor implementation is able to visit all the subclasses of an animal:

public interface Visitor {
    void visit(Dog d);
    void visit(Duck d);
    void visit(Mouse m);
}

So for example a Dog implementation would then look like this:

public class Dog extends Animal {
    public void bark() {}

    @Override
    public void accept(Visitor v) { v.visit(this); }
}

The trick here is that as the Dog knows what type it is it can trigger the relevant overloaded visit method of the visitor v by passing "this" as a parameter. Other subclasses would implement accept() exactly the same way.

The class that wants to call subclass specific methods must then implement the Visitor interface like this:

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

I know it's a lot more interfaces and methods than you bargained for, but it's a standard way to get a handle on every specific subtype with precisely zero instanceof checks and zero type casts. And it's all done in a standard language agnostic fashion so it's not just for Java but any OO language should work the same.

答案 9 :(得分:5)

不可能。如果只给出一个String键,Map应该如何知道它将获得哪个Animal类?

唯一可行的方法是,如果每个Animal只接受一种类型的朋友(那么它可能是Animal类的参数),或者callFriend()方法获得了一个类型参数。但是看起来你似乎错过了继承点:只有在使用超类方法时才能统一处理子类。

答案 10 :(得分:5)

我写了一篇文章,其中包含概念证明,支持类和测试类,演示了如何在运行时由类检索超类型标记。 简而言之,它允许您根据调用者传递的实际通用参数委派替代实现。例如:

  • TimeSeries<Double>委托给使用double[]
  • 的私人内部课程
  • TimeSeries<OHLC>委托给使用ArrayList<OHLC>
  • 的私人内部课程

请参阅: Using TypeTokens to retrieve generic parameters

由于

Richard Gomes - Blog

答案 11 :(得分:2)

你在这里寻找的是抽象。更多的接口代码,你应该做更少的铸造。

以下示例在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!");
    }
}
}

答案 12 :(得分:2)

因为问题是基于假设数据的,所以这里的一个很好的例子是返回扩展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"));
    }
}

答案 13 :(得分:2)

这里有很多很棒的答案,但这是我为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是类型扩展的超类,意味着您可以使用它的任何子项(duh)
  • type.getConstructor(Param.class等)允许您与之交互 类型的构造函数。所有期望的类之间的构造函数应该相同。
  • newInstance接受您要传递给新对象构造函数的声明变量

如果您不想抛出错误,可以像下面这样抓住它们:

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

答案 14 :(得分:2)

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

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

答案 15 :(得分:1)

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

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

子类:

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

至少这在当前类中有效并且具有强类型引用时。多重继承有效,但后来真的很棘手:)

答案 16 :(得分:1)

怎么样

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

}

答案 17 :(得分:1)

我知道这是一个完全不同的问题。解决这个问题的另一种方法是反思。我的意思是,这并没有从Generics中获益,但是它可以让你以某种方式模仿你想要执行的行为(做狗吠,做鸭子嘎嘎等)而不需要处理类型转换:< / p>

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

    }

}

答案 18 :(得分:0)

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

答案 19 :(得分:-2)

var BASE_URL: String {
  return "http://api.colnect.net/" + LANG + "/api/" + API_KEY + "/"
}

您可以返回任何类型并直接接收。不需要进行类型转换。

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

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