简体   繁体   English

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

[英]How do I make the method return type generic?

Consider this example (typical in OOP books):考虑这个例子(OOP 书中的典型例子):

I have an Animal class, where each Animal can have many friends.我有一个Animal类,每个Animal可以有很多朋友。
And subclasses like Dog , Duck , Mouse etc which add specific behavior like bark() , quack() etc.以及像DogDuckMouse等添加特定行为的子类,例如bark()quack()等。

Here's the Animal class:这是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);
    }
}

And here's some code snippet with lots of typecasting:这是一些带有大量类型转换的代码片段:

Mouse jerry = new Mouse();
jerry.addFriend("spike", new Dog());
jerry.addFriend("quacker", new Duck());

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

Is there any way I can use generics for the return type to get rid of the typecasting, so that I can say有什么方法可以使用泛型作为返回类型来摆脱类型转换,以便我可以说

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

Here's some initial code with return type conveyed to the method as a parameter that's never used.这是一些初始代码,返回类型作为从未使用过的参数传递给方法。

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

Is there a way to figure out the return type at runtime without the extra parameter using instanceof ?有没有办法在运行时确定返回类型而无需使用instanceof的额外参数? Or at least by passing a class of the type instead of a dummy instance.或者至少通过传递一个类型的类而不是一个虚拟实例。
I understand generics are for compile time type-checking, but is there a workaround for this?我知道泛型用于编译时类型检查,但是有解决方法吗?

You could define callFriend this way:你可以这样定义callFriend

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

Then call it as such:然后这样称呼它:

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

This code has the benefit of not generating any compiler warnings.此代码的好处是不生成任何编译器警告。 Of course this is really just an updated version of casting from the pre-generic days and doesn't add any additional safety.当然,这实际上只是从前通用时代开始的更新版本,并没有增加任何额外的安全性。

No. The compiler can't know what type jerry.callFriend("spike") would return.不。编译器不知道jerry.callFriend("spike")会返回什么类型。 Also, your implementation just hides the cast in the method without any additional type safety.此外,您的实现只是隐藏了方法中的强制转换,而没有任何额外的类型安全。 Consider this:考虑一下:

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

In this specific case, creating an abstract talk() method and overriding it appropriately in the subclasses would serve you much better:在这种特定情况下,创建一个抽象的talk()方法并在子类中适当地覆盖它会更好地为您服务:

Mouse jerry = new Mouse();
jerry.addFriend("spike", new Dog());
jerry.addFriend("quacker", new Duck());

jerry.callFriend("spike").talk();
jerry.callFriend("quacker").talk();

You could implement it like this:你可以像这样实现它:

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

(Yes, this is legal code; see Java Generics: Generic type defined as return type only .) (是的,这是合法的代码;请参阅Java 泛型:仅定义为返回类型的泛型类型。)

The return type will be inferred from the caller.将从调用者推断返回类型。 However, note the @SuppressWarnings annotation: that tells you that this code isn't typesafe .但是,请注意@SuppressWarnings注释:它告诉您此代码不是 typesafe You have to verify it yourself, or you could get ClassCastExceptions at runtime.您必须自己验证,否则您可能会在运行时获得ClassCastExceptions

Unfortunately, the way you're using it (without assigning the return value to a temporary variable), the only way to make the compiler happy is to call it like this:不幸的是,您使用它的方式(没有将返回值分配给临时变量),使编译器满意的唯一方法是这样调用它:

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

While this may be a little nicer than casting, you are probably better off giving the Animal class an abstract talk() method, as David Schmitt said.尽管这可能比强制转换要好一些,但正如 David Schmitt 所说,您最好为Animal类提供一个抽象的talk()方法。

This question is very similar to Item 29 in Effective Java - "Consider typesafe heterogeneous containers."这个问题与Effective Java 中的第 29 条非常相似——“考虑类型安全的异构容器”。 Laz's answer is the closest to Bloch's solution. Laz 的答案最接近 Bloch 的解决方案。 However, both put and get should use the Class literal for safety.但是,为了安全起见, put 和 get 都应该使用 Class 字面量。 The signatures would become:签名将变成:

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

Inside both methods you should check that the parameters are sane.在这两种方法中,您应该检查参数是否正常。 See Effective Java and the Class javadoc for more info.有关详细信息,请参阅 Effective Java 和Class javadoc。

Here is the simpler version:这是更简单的版本:

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
}

Fully working code:完全工作代码:

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

        }
    }

Additionally you can ask the method to return the value in a given type this way此外,您可以要求该方法以这种方式返回给定类型的值

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

More examples here at Oracle Java documentation更多的例子在这里在甲骨文的Java文档

As you said passing a class would be OK, you could write this:正如你所说的通过一个类就可以了,你可以这样写:

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

And then use it like this:然后像这样使用它:

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

Not perfect, but this is pretty much as far as you get with Java generics.不完美,但这与 Java 泛型差不多。 There is a way to implement Typesafe Heterogenous Containers (THC) using Super Type Tokens , but that has its own problems again.有一种方法可以使用 Super Type Tokens实现Typesafe Heterogenous Containers (THC) ,但这也有其自身的问题。

Based on the same idea as Super Type Tokens, you could create a typed id to use instead of a string:基于与 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];
  }
}

But I think this may defeat the purpose, since you now need to create new id objects for each string and hold on to them (or reconstruct them with the correct type information).但我认为这可能会违背目的,因为您现在需要为每个字符串创建新的 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());

But you can now use the class in the way you originally wanted, without the casts.但是您现在可以按照您最初想要的方式使用该类,而无需进行强制转换。

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

This is just hiding the type parameter inside the id, although it does mean you can retrieve the type from the identifier later if you wish.这只是将类型参数隐藏在 id 中,尽管这确实意味着您可以稍后从标识符中检索类型,如果您愿意的话。

You'd need to implement the comparison and hashing methods of TypedID too if you want to be able to compare two identical instances of an id.如果您希望能够比较一个 id 的两个相同实例,您还需要实现 TypedID 的比较和散列方法。

"Is there a way to figure out the return type at runtime without the extra parameter using instanceof?" “有没有办法在运行时找出返回类型,而无需使用 instanceof 的额外参数?”

As an alternative solution you could utilise the Visitor pattern like this.作为替代解决方案,您可以像这样使用访问者模式 Make Animal abstract and make it implement Visitable:使 Animal 抽象并使其实现 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:可访问仅意味着 Animal 实现愿意接受访问者:

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:因此,例如 Dog 实现将如下所示:

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.这里的技巧是,当 Dog 知道它是什么类型时,它可以通过将“this”作为参数传递来触发访问者 v 的相关重载访问方法。 Other subclasses would implement accept() exactly the same way.其他子类将以完全相同的方式实现 accept()。

The class that wants to call subclass specific methods must then implement the Visitor interface like this:想要调用子类特定方法的类必须像这样实现 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(); }
}

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.而且这一切都是以标准语言不可知的方式完成的,因此它不仅适用于 Java,而且任何面向对象的语言都应该以相同的方式工作。

Not possible.不可能。 How is the Map supposed to know which subclass of Animal it's going to get, given only a String key?仅给定一个 String 键,Map 如何知道它将获得 Animal 的哪个子类?

The only way this would be possible is if each Animal accepted only one type of friend (then it could be a parameter of the Animal class), or of the callFriend() method got a type parameter.唯一可能的方法是,如果每个 Animal 只接受一种类型的朋友(那么它可以是 Animal 类的参数),或者 callFriend() 方法得到一个类型参数。 But it really looks like you're missing the point of inheritance: it's that you can only treat subclasses uniformly when using exclusively the superclass methods.但看起来您确实错过了继承的重点:当仅使用超类方法时,您只能统一对待子类。

I've written an article which contains a proof of concept, support classes and a test class which demonstrates how Super Type Tokens can be retrieved by your classes at runtime.我写了一篇文章,其中包含一个概念证明、支持类和一个测试类,它演示了您的类如何在运行时检索超级类型令牌。 In a nutshell, it allows you to delegate to alternative implementations depending on actual generic parameters passed by the caller.简而言之,它允许您根据调用者传递的实际通用参数委托替代实现。 Example:示例:

  • TimeSeries<Double> delegates to a private inner class which uses double[] TimeSeries<Double>委托给使用double[]的私有内部类
  • TimeSeries<OHLC> delegates to a private inner class which uses ArrayList<OHLC> TimeSeries<OHLC>委托给使用ArrayList<OHLC>的私有内部类

See:见:

Thanks谢谢

Richard Gomes - Blog理查德戈麦斯 -博客

There are a lot of great answers here, but this is the approach I took for an Appium test where acting on a single element can result in going to different application states based on the user's settings.这里有很多很好的答案,但这是我在 Appium 测试中采用的方法,在该测试中,根据用户的设置对单个元素进行操作可能会导致进入不同的应用程序状态。 While it doesn't follow the conventions of OP's example, I hope it helps someone.虽然它不遵循 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 is the super class that the type extends meaning you can use any of its children (duh) MobilePage 是该类型扩展的超类,这意味着您可以使用其任何子类(废话)
  • type.getConstructor(Param.class, etc) allows you to interact with the constructor of the type. type.getConstructor(Param.class, etc) 允许您与类型的构造函数进行交互。 This constructor should be the same between all expected classes.这个构造函数在所有预期的类之间应该是相同的。
  • newInstance takes a declared variable that you want to pass to the new objects constructor newInstance 接受一个你想传递给新对象构造函数的声明变量

If you don't want to throw the errors you can catch them like so:如果你不想抛出错误,你可以像这样捕捉它们:

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

What you're looking for here is abstraction.你在这里寻找的是抽象。 Code against interfaces more and you should have to do less casting.更多地针对接口编写代码,您应该减少强制转换。

The example below is in C# but the concept remains the same.下面的示例使用 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!");
    }
}
}

I did the following in my lib kontraktor:我在 lib kontraktor 中执行了以下操作:

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

subclassing:子类化:

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

at least this works inside the current class and when having a strong typed reference.至少这在当前类中有效并且在具有强类型引用时有效。 Multiple inheritance works, but gets really tricky then :)多重继承工作,但变得非常棘手:)

Not really, because as you say, the compiler only knows that callFriend() is returning an Animal, not a Dog or Duck.不是真的,因为正如您所说,编译器只知道 callFriend() 返回的是 Animal,而不是 Dog 或 Duck。

Can you not add an abstract makeNoise() method to Animal that would be implemented as a bark or quack by its subclasses?你不能向 Animal 添加一个抽象的 makeNoise() 方法,它会被它的子类实现为 bark 或 quack 吗?

I know this is a completely different thing that the one asked.我知道这是一个完全不同的问题。 Another way of resolving this would be reflection.解决这个问题的另一种方法是反射。 I mean, this does not take the benefit from Generics, but it lets you emulate, in some way, the behavior you want to perform (make a dog bark, make a duck quack, etc.) without taking care of type casting:我的意思是,这不会从泛型中受益,但它可以让您以某种方式模拟您想要执行的行为(发出狗吠声、发出鸭子嘎嘎声等),而无需考虑类型转换:

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

    }

}

what about怎么样

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

There is another approach, you can narrow the return type when you override a method.还有另一种方法,您可以在覆盖方法时缩小返回类型。 In each subclass you would have to override callFriend to return that subclass.在每个子类中,您必须重写 callFriend 以返回该子类。 The cost would be the multiple declarations of callFriend, but you could isolate the common parts to a method called internally.成本将是 callFriend 的多个声明,但您可以将公共部分隔离到内部调用的方法。 This seems a lot simpler to me than the solutions mentioned above, and does not need an extra argument to determine the return type.这对我来说似乎比上面提到的解决方案简单得多,并且不需要额外的参数来确定返回类型。

As the question is based in hypothetical data here is a good exemple returning a generic that extends Comparable interface.由于问题基于假设数据,这里是一个很好的例子,返回扩展 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"));
    }
}
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;
}

You can return any type and receive directly like. 您可以返回任何类型并直接收到像。 Don't need to typecast. 不需要打字。

Person p = nextRow(cursor); // cursor is real database cursor.

This is best if you want to customize any other kind of records instead of real cursors. 如果您想自定义任何其他类型的记录而不是实际的游标,则最好。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM