考虑这个示例(在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在运行时确定返回类型而无需额外的参数?或者至少通过传递类型的类而不是虚拟实例。
我理解泛型用于编译时类型检查,但是有解决方法吗?

#1 楼

您可以这样定义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()调用的参数之间仍然没有编译时类型检查。

–David Schmitt
09年1月16日在16:32

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

– Craig P. Motlin
09年1月16日在17:01

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

– Nestor Ledon
14年5月21日在16:27



#2 楼

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


评论


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

–詹姆斯·麦克马洪
09年1月16日在15:54

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

–dcow
13年7月8日在23:58

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

– laz
2013年7月9日在3:46



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

–David Schmitt
2013年7月9日在13:19



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

– laz
2013年7月9日14:22



#3 楼

您可以这样实现:

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


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

返回类型将由调用方推断出来。但是,请注意@SuppressWarnings注释:它告诉您该代码不是类型安全的。您必须自己进行验证,否则可能会在运行时得到ClassCastExceptions。不幸的是,您使用它的方式(没有将返回值分配给临时变量)是使编译器很高兴这样称呼它:

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


虽然这可能比强制转换更好,但您最好给Animal类一个抽象的talk()方法,如David Schmitt所说。

评论


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

– Sathish
09年1月16日16:25

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

– Hartmut P.
15年12月16日在21:03

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

– andho
17年9月7日在7:42

有趣的是,JRE自己的java.util.Collections.emptyList()函数是完全像这样实现的,并且它的javadoc宣传自己是类型安全的。

– Ti Strga
18年6月7日在16:50

@TiStrga:这很有趣! Collections.emptyList()之所以可以避免使用它,是因为每个空列表的定义都没有类型T的元素对象。因此,没有将对象转换为错误类型的风险。列表对象本身可以使用任何类型,只要没有元素即可。

– Lii
11月17日10:05



#4 楼

这个问题与有效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 and Class javadoc。

#5 楼

这是更简单的版本:

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,但这是一个好习惯。我的意思是,如果在运行时妥善处理了“良好实践”是什么意思?

–轮渡
19年8月31日在17:05

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

– webjockey
3月6日18:28



#6 楼

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

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


Oracle Java文档中的更多示例在这里

#7 楼

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

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的答案完全相同,因此您正在复制他或他正在复制您。

–詹姆斯·麦克马洪
09年1月16日16:06

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

– Sathish
09年1月16日16:10

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

–法比安·斯蒂格(Fabian Steeg)
09年1月16日在16:17

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

– Craig P. Motlin
09年1月16日在17:10

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


这只是将类型参数隐藏在id内,尽管这确实意味着您可以从中检索类型如果需要,可以稍后再使用该标识符。

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

#9 楼

“是否有一种方法可以在运行时使用instanceof来计算返回类型,而无需使用额外的参数?”

作为一种替代解决方案,您可以使用Visitor模式。使Animal抽象并使其实现可访问性:

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”作为a传递来触发访客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语言都应能正常工作。

#10 楼

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

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

#11 楼

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


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


TimeSeries<OHLC>委托使用ArrayList<OHLC>的内部私有类


请参见:


使用TypeTokens检索通用参数(Web存档)

使用TypeTokens检索通用参数(博客)

感谢
Richard Gomes-博客

评论


确实,感谢您分享您的见解,您的文章确实说明了一切!

–扬·盖尔·盖埃纳克(Yann-GaëlGuéhéneuc)
13年4月12日在9:20

原始博客文章(太棒了!)可通过此处的Internet Archive获得。

–斯科特·巴布科克(Scott Babcock)
8月31日4:34

@ScottBabcock:感谢您让我知道此断开的链接。我也发布了指向新博客的链接。

–理查德·戈麦斯(Richard Gomes)
9月4日9:26

#12 楼

这里有很多很好的答案,但这是我在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年11月30日在13:19

#13 楼

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

不能向Animal添加抽象的makeNoise()方法吗?被其子类实现为树皮或庸俗?

评论


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

– Sathish
09年1月16日在16:01

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

–马特·乔丹
09年1月16日在16:15

#14 楼

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

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


评论


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

–user1043000
2015年2月6日下午4:55

#15 楼

我在lib kontraktor中做了以下操作:

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


子类化:

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


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

#16 楼

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


#17 楼

我知道这是一个完全不同的问题。解决此问题的另一种方法是反思。我的意思是,这不能从“泛型”中受益,但是可以让您以某种方式模仿您想要执行的行为(使狗吠,使鸭嘎嘎等等),而无需进行类型转换: br />
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 楼

关于

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


}
呢?

#19 楼

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

评论


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

–真正的柯特
17年8月6日在2:09



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

–FeralWhippet
17年8月9日在2:01