【问题标题】:How do I make the method return type generic?如何使方法返回类型通用?
【发布时间】:2010-10-01 20:05:24
【问题描述】:

考虑这个例子(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);
    }
}

这里有一些带有大量类型转换的代码 sn-p:

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 来计算返回类型而不需要额外的参数?或者至少通过传递该类型的类而不是虚拟实例。
我知道泛型用于编译时类型检查,但有解决方法吗?

【问题讨论】:

    标签: java generics return-value


    【解决方案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() 调用的参数之间仍然没有编译时类型检查。
    • 这是迄今为止最好的答案 - 但您应该以同样的方式更改 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();
    【解决方案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 编程,将来会为您省去一些麻烦。
    • 这是获得相同结果的正确方法。请注意,目标是在运行时获得派生类特定的行为,而无需自己显式编写代码来进行丑陋的类型检查和强制转换。 @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)。像任何模式或方法一样,有时我提供的代码是合适的,有时需要完全不同的东西(比如你在这个答案中建议的)。
    【解决方案3】:

    你可以这样实现它:

    @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&lt;Dog&gt;(...,我认为它看起来更好。
    • 有趣的是,JRE 自己的 java.util.Collections.emptyList() 函数就是这样实现的,而且它的 javadoc 宣称自己是类型安全的。
    • @TiStrga:很有趣! Collections.emptyList() 可以逃脱的原因是每个空列表的定义,没有 T 类型的元素对象。因此没有将对象转换为错误类型的风险。只要没有元素,列表对象本身就可以使用任何类型。
    【解决方案4】:

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

    【讨论】:

      【解决方案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();
      
              }
          }
      

      【讨论】:

      • Casting to T not needed in this case but it's a good practice to do 是什么意思。我的意思是,如果在运行时处理得当,“良好做法”是什么意思?
      • 我的意思是说不需要显式转换 (T),因为方法声明上的返回类型声明 应该足够了
      【解决方案6】:

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

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

      Oracle Java 文档中的更多示例here

      【讨论】:

        【解决方案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 泛型差不多。有一种方法可以实现Typesafe Heterogenous Containers (THC) using Super Type Tokens,但这又存在问题。

        【讨论】:

        • 很抱歉,这与 laz 的回答完全相同,所以你要么抄袭他,要么他抄袭你。
        • 这是传递类型的好方法。但它仍然不像施密特所说的那样安全。我仍然可以通过不同的课程并且类型转换会爆炸。 mmyers 第二次回复设置返回类型中的类型似乎更好
        • Nemo,如果您查看发布时间,您会发现我们几乎在同一时间发布了它们。而且,它们并不完全相同,只有两行。
        • @Fabian 我发布了一个类似的答案,但 Bloch 的幻灯片与 Effective Java 中发布的幻灯片之间存在重要区别。他使用 Class 而不是 TypeRef。但这仍然是一个很好的答案。
        【解决方案8】:

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

        【讨论】:

          【解决方案9】:

          “有没有办法在运行时不使用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()。

          想要调用子类特定方法的类必须像这样实现访问者接口:

          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 语言都应该同样工作。

          【讨论】:

            【解决方案10】:

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

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

            【讨论】:

              【解决方案11】:

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

              • TimeSeries&lt;Double&gt; 委托给使用 double[] 的私有内部类
              • TimeSeries&lt;OHLC&gt; 委托给使用 ArrayList&lt;OHLC&gt; 的私有内部类

              见:

              谢谢

              理查德·戈麦斯 - Blog

              【讨论】:

              • 确实,感谢您分享您的见解,您的文章真的说明了一切!
              • 原始博客文章(太棒了!)可通过 Internet Archive here获得
              • @ScottBabcock:感谢您让我知道这个断开的链接。我也发布了一个指向我的新博客的链接。
              【解决方案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 是该类型扩展的超类,这意味着您可以使用它的任何子类(呵呵)
              • 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;
              }
              

              【讨论】:

              • 据我了解,这是使用泛型的最佳和最优雅的方式。
              【解决方案13】:

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

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

              【讨论】:

              • 如果动物有多种方法,甚至不属于可以抽象的共同动作,该怎么办?我需要这个用于具有不同操作的子类之间的通信,其中我可以传递类型,而不是实例。
              • 你真的只是回答了你自己的问题——如果一个动物有一个独特的动作,那么你必须对那个特定的动物施法。如果动物有一个可以与其他动物分组的动作,那么您可以在基类中定义一个抽象或虚拟方法并使用它。
              【解决方案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!");
                  }
              }
              }
              

              【讨论】:

              • 这个问题与编码而不是概念有关。
              【解决方案15】:

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

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

              子类化:

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

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

              【讨论】:

                【解决方案16】:

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

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

                【讨论】:

                  【解决方案17】:

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

                  【讨论】:

                    【解决方案18】:

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

                    【讨论】:

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

                    怎么样

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

                    【讨论】:

                      猜你喜欢
                      • 2012-04-06
                      • 2023-03-12
                      • 1970-01-01
                      • 1970-01-01
                      • 1970-01-01
                      • 2018-02-23
                      • 2020-10-03
                      • 1970-01-01
                      • 1970-01-01
                      相关资源
                      最近更新 更多