ChatGPT解决这个技术问题 Extra ChatGPT

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

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


l
laz

您可以这样定义 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() 调用的参数之间仍然没有编译时类型检查。
这是迄今为止最好的答案 - 但您应该以同样的方式更改 addFriend。由于您在两个地方都需要该类文字,因此编写错误变得更加困难。
@Jaider,不完全相同,但这会起作用: // Animal Class public T CallFriend(string name) where T : Animal { return friends[name] as T; } // 调用类 jerry.CallFriend("spike").Bark(); jerry.CallFriend("quacker").Quack();
C
Community

你可以像这样实现它:

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

(是的,这是法律法规;参见 Java Generics: Generic type defined as return type only。)

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

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

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

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


方法链并不是真正的意图。我不介意将值分配给子类型变量并使用它。感谢您的解决方案。
这在进行方法调用链接时非常有效!
我真的很喜欢这种语法。我认为在 C# 中它是 jerry.CallFriend<Dog>(...,我认为它看起来更好。
有趣的是,JRE 自己的 java.util.Collections.emptyList() 函数就是这样实现的,而且它的 javadoc 宣称自己是类型安全的。
@TiStrga:很有趣! Collections.emptyList() 可以逃脱的原因是每个空列表的定义,没有类型 T 的元素对象。因此没有将对象转换为错误类型的风险。只要没有元素,列表对象本身就可以使用任何类型。
A
Andrei Volgin

不,编译器不知道 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 编程,将来会为您省去一些麻烦。
这是获得相同结果的正确方法。请注意,目标是在运行时获得派生类特定的行为,而无需自己显式地编写代码来进行丑陋的类型检查和强制转换。 @laz 提出的方法有效,但将类型安全抛到了窗外。此方法需要更少的代码行(因为方法实现是后期绑定的,并且无论如何都会在运行时查找),但仍然可以让您轻松地为 Animal 的每个子类定义独特的行为。
但最初的问题不是询问类型安全。我阅读它的方式只是想知道是否有一种方法可以利用泛型来避免强制转换。
@laz:是的,最初的问题——正如提出的那样——与类型安全无关。这并没有改变这样一个事实,即有一种安全的方法来实现它,消除类转换失败。另请参阅weblogs.asp.net/alex_papadimoulis/archive/2005/05/25/…
我不反对这一点,但我们正在处理 Java 及其所有设计决策/弱点。我认为这个问题只是试图了解 Java 泛型中的可能性,而不是需要重新设计的 xyproblem (meta.stackexchange.com/questions/66377/what-is-the-xy-problem)。像任何模式或方法一样,有时我提供的代码是合适的,有时需要完全不同的东西(就像你在这个答案中建议的那样)。
C
Craig P. Motlin

这个问题与 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。


m
mppfiles

这是更简单的版本:

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

        }
    }

Casting to T not needed in this case but it's a good practice to do 是什么。我的意思是,如果在运行时得到妥善处理,“良好做法”是什么意思?
我的意思是说显式转换 (T) 不是必需的,因为方法声明上的返回类型声明 应该足够了
i
isuru chathuranga

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

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

Oracle Java 文档中的更多示例 here


F
Fabian Steeg

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

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 泛型所能获得的。有一种实现 Typesafe Heterogenous Containers (THC) using Super Type Tokens 的方法,但它又存在自己的问题。


我很抱歉,但这与 laz 的答案完全相同,所以你要么抄袭他,要么他抄袭你。
这是传递类型的一种巧妙方法。但它仍然不像施密特所说的那样安全。我仍然可以通过不同的课程并且类型转换会爆炸。 mmyers 2nd reply to set the Type in Return type 似乎更好
Nemo,如果您查看发布时间,您会发现我们几乎在同一时间发布了它们。而且,它们并不完全相同,只有两行。
@Fabian 我发布了一个类似的答案,但是 Bloch 的幻灯片与 Effective Java 中发布的幻灯片之间存在重要区别。他使用 Class 而不是 TypeRef。但这仍然是一个很好的答案。
K
Kothar

基于与 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 的比较和散列方法。


A
Antti Siiskonen

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

作为替代解决方案,您可以像这样使用 the Visitor pattern。使 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 只是意味着 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,而且任何 OO 语言都应该以同样的方式工作。


M
Michael Borgwardt

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

唯一可行的方法是,如果每个 Animal 只接受一种类型的朋友(那么它可能是 Animal 类的参数),或者 callFriend() 方法的类型参数。但看起来你真的错过了继承的重点:只有在只使用超类方法时,你才能统一对待子类。


R
Richard Gomes

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

TimeSeries 委托给使用 double[] 的私有内部类

TimeSeries 委托给使用 ArrayList 的私有内部类

看:

使用 TypeTokens 检索泛型参数(网络存档)

使用 TypeTokens 检索泛型参数(博客)

谢谢

理查德·戈麦斯 - Blog


确实,感谢您分享您的见解,您的文章确实说明了一切!
原始博客文章(太棒了!)可通过 Internet Archive here 获得
@ScottBabcock:感谢您让我知道这个断开的链接。我也发布了指向我的新博客的链接。
M
MTen

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

据我了解,这是使用泛型的最佳和最优雅的方式。
s
sk.

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

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


如果动物有多种方法,甚至不属于可以抽象的共同行动,该怎么办?我需要这个用于具有不同操作的子类之间的通信,其中我可以传递类型,而不是实例。
你真的只是回答了你自己的问题——如果一个动物有一个独特的动作,那么你必须对那个特定的动物施法。如果动物具有可以与其他动物分组的动作,那么您可以在基类中定义抽象或虚拟方法并使用它。
N
Nestor Ledon

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

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

这个问题与编码而不是概念有关。
R
R.Moeller

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

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

子类化:

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

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


佚名

我知道这是一个完全不同的人问的事情。解决这个问题的另一种方法是反射。我的意思是,这并没有从泛型中受益,但它可以让你以某种方式模拟你想要执行的行为(让狗吠,让鸭子嘎嘎等等),而不需要照顾类型转换:

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

    }

}

C
Cassio Seffrin

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

F
FeralWhippet

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


我不确定您所说的“缩小返回类型”是什么意思。 Afaik、Java 和大多数类型化语言不会基于返回类型重载方法或函数。例如,从编译器的角度来看,public int getValue(String name){}public boolean getValue(String name){} 无法区分。您需要更改参数类型或添加/删除参数才能识别重载。也许我只是误解了你...
在java中,您可以覆盖子类中的方法并指定更“窄”(即更具体)的返回类型。请参阅stackoverflow.com/questions/14694852/…
D
Dharman

关于什么

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