繁体   English   中英

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

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

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

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

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

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

你可以像这样实现它:

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

(是的,这是合法的代码;请参阅Java 泛型:仅定义为返回类型的泛型类型。)

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

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

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

尽管这可能比强制转换要好一些,但正如 David Schmitt 所说,您最好为Animal类提供一个抽象的talk()方法。

这个问题与Effective Java 中的第 29 条非常相似——“考虑类型安全的异构容器”。 Laz 的答案最接近 Bloch 的解决方案。 但是,为了安全起见, 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 和Class javadoc。

这是更简单的版本:

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

        }
    }

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

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

更多的例子在这里在甲骨文的Java文档

正如你所说的通过一个类就可以了,你可以这样写:

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 泛型差不多。 有一种方法可以使用 Super Type Tokens实现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];
  }
}

但我认为这可能会违背目的,因为您现在需要为每个字符串创建新的 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 的比较和散列方法。

“有没有办法在运行时找出返回类型,而无需使用 instanceof 的额外参数?”

作为替代解决方案,您可以像这样使用访问者模式 使 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);
  }
}

可访问仅意味着 Animal 实现愿意接受访问者:

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

并且访问者实现能够访问动物的所有子类:

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

我知道它的接口和方法比您预想的要多得多,但这是一种标准方法,可以通过精确地零实例检查和零类型转换来处理每个特定子类型。 而且这一切都是以标准语言不可知的方式完成的,因此它不仅适用于 Java,而且任何面向对象的语言都应该以相同的方式工作。

不可能。 仅给定一个 String 键,Map 如何知道它将获得 Animal 的哪个子类?

唯一可能的方法是,如果每个 Animal 只接受一种类型的朋友(那么它可以是 Animal 类的参数),或者 callFriend() 方法得到一个类型参数。 但看起来您确实错过了继承的重点:当仅使用超类方法时,您只能统一对待子类。

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

  • TimeSeries<Double>委托给使用double[]的私有内部类
  • TimeSeries<OHLC>委托给使用ArrayList<OHLC>的私有内部类

见:

谢谢

理查德戈麦斯 -博客

这里有很多很好的答案,但这是我在 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, etc) 允许您与类型的构造函数进行交互。 这个构造函数在所有预期的类之间应该是相同的。
  • 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;
}

你在这里寻找的是抽象。 更多地针对接口编写代码,您应该减少强制转换。

下面的示例使用 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!");
    }
}
}

我在 lib kontraktor 中执行了以下操作:

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

子类化:

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

至少这在当前类中有效并且在具有强类型引用时有效。 多重继承工作,但变得非常棘手:)

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

你不能向 Animal 添加一个抽象的 makeNoise() 方法,它会被它的子类实现为 bark 或 quack 吗?

我知道这是一个完全不同的问题。 解决这个问题的另一种方法是反射。 我的意思是,这不会从泛型中受益,但它可以让您以某种方式模拟您想要执行的行为(发出狗吠声、发出鸭子嘎嘎声等),而无需考虑类型转换:

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

    }

}

怎么样

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

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

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

您可以返回任何类型并直接收到像。 不需要打字。

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

如果您想自定义任何其他类型的记录而不是实际的游标,则最好。

暂无
暂无

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

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