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


588

考虑以下示例(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?或者至少通过传递类型的类而不是虚拟实例。
我了解泛型用于编译时类型检查,但是是否有解决方法?

Answers:


902

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

此代码的好处是不会生成任何编译器警告。当然,这实际上只是前代产品的更新版本,不会增加任何额外的安全性。


29
...但是在callFriend()调用的参数之间仍然没有编译时类型检查。
David Schmitt

2
到目前为止,这是最好的答案-但您应该以相同的方式更改addFriend。由于在两个地方都需要该类文字,因此编写bug变得更加困难。
Craig P. Motlin,2009年

@Jaider,不完全相同,但是可以使用:// Animal Class public T CallFriend <T>(字符串名称)其中T:Animal {return friends [name] as T; } //调用类jerry.CallFriend <Dog>(“ spike”)。Bark(); jerry.CallFriend <Duck>(“ quacker”)。Quack();
Nestor Ledon 2014年

124

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

10
虽然mmyers方法可能有效,但我认为此方法是更好的OO编程,将来会为您节省一些麻烦。
James McMahon

1
这是获得相同结果的正确方法。注意,目标是在运行时获取派生类特定的行为,而无需自己编写代码来进行丑陋的类型检查和转换。@laz提出的方法有效,但是将类型安全性扔到了窗外。该方法需要较少的代码行(因为方法实现是后期绑定的,并且无论如何都在运行时查找),但是仍然可以让您轻松地为Animal的每个子类定义唯一的行为。
dcow

但是最初的问题不是关于类型安全性的问题。按照我的阅读方式,发问者只是想知道是否有一种方法可以利用泛型来避免强制转换。
laz 2013年

2
@laz:是的,最初提出的问题与类型安全无关。这并没有改变这样一个事实,那就是存在一种安全的方法来实现,从而消除了类转换失败。另请参见weblogs.asp.net/alex_papadimoulis/archive/2005/05/25/…–
David Schmitt

3
我不同意这一点,但是我们正在处理Java及其所有设计决策/可行性。我认为这个问题只是试图学习Java泛型中的可能,而不是需要重新设计的xyproblem(meta.stackexchange.com/questions/66377/what-is-the-xy-problem)。像任何模式或方法一样,有时我提供的代码是适当的,有时则需要完全不同的内容(例如您在此答案中建议的内容)。
laz 2013年

114

您可以这样实现:

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

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

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

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

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

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


方法链接并不是真正的意图。我不介意将值分配给Subtyped变量并使用它。感谢您的解决方案。
Sathish

这在进行方法调用链接时非常有效!
Hartmut P.

我真的很喜欢这种语法。我认为在C#中jerry.CallFriend<Dog>(...看起来更好。
andho

有趣的是,JRE自己的java.util.Collections.emptyList()功能正是这样实现的,并且其javadoc宣传自己是类型安全的。
Ti Strga

31

这个问题与有效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);

在这两种方法中,您应该检查参数是否合理。有关更多信息,请参见有效的Java和 javadoc。



17

这是更简单的版本:

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

        }
    }

1
有什么作用Casting to T not needed in this case but it's a good practice to do。我的意思是,如果在运行时妥善处理了“良好实践”是什么意思?
Farid

我的意思是说不需要显式强制转换(T),因为方法声明上的返回类型声明<T>应该足够了
webjockey

9

正如您所说的通过类可以,您可以这样编写:

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泛型所获得的效果差不多。有一种使用超级类型令牌实现类型安全异构容器(THC)的方法,但这又有其自身的问题。


抱歉,这与laz的答案完全相同,因此您正在复制他或他正在复制您。
James McMahon

那是传递Type的一种简洁方法。但还是像施密特所说的那样,它不是安全的类型。我仍然可以通过一个不同的班级,打字将炸弹。mmyers第2次回复来设置“返回类型”类型似乎更好
Sathish

4
Nemo,如果您查看发布时间,您会发现我们几乎完全在同一时间发布了它们。此外,它们也不完全相同,只有两行。
Fabian Steeg

@Fabian我发布了类似的答案,但是Bloch的幻灯片和有效Java中发布的幻灯片之间存在重要区别。他使用Class <T>而不是TypeRef <T>。但这仍然是一个很好的答案。
Craig P. Motlin,2009年

8

基于与超级类型令牌相同的想法,您可以创建要使用的类型化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();

这只是将type参数隐藏在id中,尽管这确实意味着您可以稍后根据需要从标识符中检索类型。

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


8

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

我知道它的接口和方法比您讨价还价的要多得多,但这是一种使用精确的instanceof检验和类型转换为零的方法来处理每个特定子类型的标准方法。而且所有操作均以与标准语言无关的方式完成,因此不仅适用于Java,而且任何OO语言都应具有相同的功能。


6

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

唯一可行的方法是,如果每个动物只接受一种类型的朋友(那么它可能是动物类的参数),或者callFriend()方法的类型参数得到了。但是,实际上看起来好像您缺少继承的要点:当您仅使用超类方法时,只能统一对待子类。


5

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

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

请参阅: 使用TypeTokens检索通用参数

谢谢

理查德·戈麦斯(Richard Gomes)- 博客


确实,感谢您分享您的见解,您的文章确实说明了一切!
晏-盖尔Guéhéneuc

3

这里有很多很好的答案,但这是我用于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;
}

据我了解,这是使用泛型的最好,最优雅的方式。
cbaldan '17

2

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

您是否不能向Animal添加抽象的makeNoise()方法,该方法将被其子类实现为吠叫或嘎嘎声?


1
如果动物有多种方法甚至不属于可以抽象的共同作用,该怎么办?我需要这样做,以便具有不同操作的子类之间的通信可以通过Type而不是实例进行传递。
Sathish

2
您确实已经回答了自己的问题-如果一种动物具有独特的作用,那么您就必须投与该特定动物。如果一种动物的动作可以与其他动物组合在一起,则可以在基类中定义一个抽象或虚拟方法并使用它。
马特·乔丹

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

这个问题与编码无关,而与概念有关。

2

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

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

子类化:

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

至少这在当前类内部以及具有强类型引用的情况下有效。多重继承是可行的,但随后变得非常棘手:)


1

我知道这是一个完全不同的问题。解决此问题的另一种方法是反思。我的意思是,这没有从“泛型”中受益,而是让您以某种方式模仿了您想要执行的行为(使狗吠,使鸭嘎嘎等等),而无需进行类型转换:

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

    }

}

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

}


0

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


我不确定“缩小返回类型”的含义。Afaik,Java和大多数类型的语言不会根据返回类型重载方法或函数。例如public int getValue(String name){}public boolean getValue(String name){}从编译器的角度来看是无法区分的。您需要更改参数类型或添加/删除参数才能识别过载。也许我只是误会了您……
一位真正的Colter

在Java中,您可以覆盖子类中的方法,并指定更“窄”(即更具体)的返回类型。参见stackoverflow.com/questions/14694852/…
FeralWhippet

-3
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.

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


2
您说“不需要强制转换”,但是显然涉及强制转换:(Cursor) cursor例如。
萨米·莱恩

这是对泛型的完全不适当的使用。此代码必须cursorCursor,并且始终返回Person(或为null)。使用泛型可消除对这些约束的检查,从而使代码不安全。
安迪·特纳
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.