考虑下面的例子(OOP书籍中的典型例子):

我有一个动物班,每个动物都有很多朋友。 和子类,如狗,鸭,老鼠等,其中添加特定的行为,如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的额外参数的情况下在运行时找出返回类型?或者至少通过传递该类型的类而不是虚拟实例。 我知道泛型是用于编译时类型检查的,但是对此有解决方法吗?


当前回答

基于与超级类型令牌相同的思想,你可以创建一个类型化的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的比较和散列方法。

其他回答

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

这段代码的好处是不会生成任何编译器警告。当然,这实际上只是前泛型时代的强制转换的更新版本,并没有增加任何额外的安全性。

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

“有没有一种方法可以在不使用instanceof的额外参数的情况下在运行时找出返回类型?”

作为另一种解决方案,您可以像这样利用访问者模式。使动物抽象,使其实现可访问:

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

而一个visitor实现能够访问动物的所有子类:

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,而且适用于任何面向对象语言。

你在这里寻找的是抽象。多针对接口编写代码,就应该少做类型转换。

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

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

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