【问题标题】:Java Polymorphic Generic CallsJava 多态泛型调用
【发布时间】:2014-03-01 15:04:39
【问题描述】:

我最近刚刚开始在 Java 中使用泛型,并且遇到了一些奇怪的行为。这是简化版本,但我有一个基类,它由多个类扩展,这些类被传递到一个通用函数中。在这个函数内部,我调用了一个方法,该方法具有多个版本,这些版本采用不同的基类或派生类作为参数。

如果我有以下情况:

public class A{};
public class B extends A{};
public class C extends A{};
public class Runner
{
    public static void Run( ClassA a ){Do Something};
    public static void Run( ClassB b ){Do Something};
    public static void Run( ClassC c ){Do Something};
}
void SomeRandomCall<B extends ClassA>( B b )
{
     Runner.Run( b );
}
SomeRandomCall<ClassB>( new ClassB() );

我在调试中发现 Runner.Run 正在调用 Run( ClassA a ) 而不是 Run( ClassB b ) 函数。鉴于这两个函数,由于提供了特定于类型的函数,不应该调用Run( ClassB b ) 吗?

如果不是这种情况,我将如何拥有一个可以调用的通用函数能够调用具有采用基类和派生类的签名的函数?

【问题讨论】:

  • “泛型”?你的意思是“继承”对吧?
  • void SomeRandomCall&lt;B extends ClassA&gt;( B b ) 应该是什么?为什么&lt;&gt;(什么表示泛型)中的 B(我是一个类)?
  • “公共类 B 扩展 A{};”中的“B”与“void SomeRandomCall(B b)”中的“B”完全无关。也可能是“void SomeRandomCall( Z z )”
  • 实际上,“void SomeRandomCall( B b )”是一个无效的方法声明。我假设您的意思是“ void SomeRandomCall(B b)”(在返回类型之前,没有“Class”!)
  • 你能用你用过的实际代码替换你的摘要伪代码吗?您发布的内容有几个语法错误,意图不明确。

标签: java generics inheritance polymorphism


【解决方案1】:

由于您的缩写让我有点困惑,我做了一个可运行的小示例。我假设SomeRandomCall 中的B 是通用类型,而不是B 类。

这里是:

public class Main {

    public static class A{};
    public static class B extends A{};
    public static class C extends A{};
    public static class Runner{
        public static void Run( A a ){System.out.println("A");};
        public static void Run( B b ){System.out.println("B");};
        public static void Run( C c ){System.out.println("C");};
    }

    static <T extends A> void SomeRandomCall( T x ){
         Runner.Run( x );
    }

    public static void main(String[] args) {
        B b = new B();
        new Runner().Run( b );
    }
}

输出是:B。

这是您所期望的,这很好。在编译时,Java 编译器为方法选择候选方法,它们是Run( A a )Run( B b )。运行时,参数的类型是B,所以打印B。

但是,请注意以下示例:

    public static void main(String[] args) {
        A ab = new B();
        new Runner().Run( ab );
    }

现在会发生什么?现在输出是 A。原因是,在编译时,ab 的类型是 A,所以我们只有一个候选方法可以执行:Run( A a )ab 在运行时的实际类型不再重要,因为我们只有一个可以执行的候选者。无论abAB 还是C,输出都是A

【讨论】:

    【解决方案2】:

    好吧,既然A 扩展了B,那么B 类型的对象可以作为参数传递到一个接受A 类型参数的方法中。同样的情况也发生在这里。由于run(A a) 出现在run(B b) 之前,所以前者被执行。因此,您的错误(如果您正在考虑)。

    如需进一步阐述,请考虑以下示例:

    Programmer 是一个超类(如A)。 Java ProgrammerProgrammer 的子类,C++ programmer 也是如此(如 BC)。

    现在,有三扇门,按照距离递增的顺序:

    1. 面向程序员。
    2. 适用于 Java 程序员。
    3. 适用于 C++ 程序员。

    由于第一道门离你最近,并且条件满足,你马上进入。你不想花更多的精力去第二扇门。同样的情况也发生在这里。

    【讨论】:

      【解决方案3】:
      void SomeRandomCall<T extends SomeClass>( T t )
      

      被编译为就像它是

      void SomeRandomCall( Someclass t )
      

      这就是为什么

      EnclosingClass.<ClassB>SomeRandomCall( new ClassB() );
      

      将导致A,因为ClassB 作为编译时类型在方法中不再可见。

      如果您通过javap -c -s(即反编译)查看类(以下示例中的Main),这也是可见的

        static <T extends Main$A> void SomeRandomCall(T);
          Signature: (LMain$A;)V
          Code:
             0: aload_0
             1: invokestatic  #18                 // Method Main$Runner.Run:(LMain$A;)V
             4: return
      

      它的签名是void SomeRandomCall( A ),它会一直调用Run( A )

      另请参阅 Overloading in Java and multiple dispatch 为什么编译时类型对重载方法很重要。


      public static class Runner{
          public static void Run( A a ){System.out.println("A");};
          public static void Run( B b ){System.out.println("B");};
          public static void Run( C c ){System.out.println("C");};
      }
      
      public static void main(String[] args) {
          B b = new B();
          Runner.Run(b);
          A a = b;
          Runner.Run(a);
      }
      

      编译器只会在可能的情况下选择正确的重载方法。例如。以上将反编译为

        public static void main(java.lang.String[]);
          Signature: ([Ljava/lang/String;)V
          Code:
             0: new           #29                 // class Main$B
             3: dup
             4: invokespecial #31                 // Method Main$B."<init>":()V
             7: astore_1
             8: aload_1
             9: invokestatic  #32                 // Method Main$Runner.Run:(LMain$B;)V
            12: aload_1
            13: astore_2
            14: aload_2
            15: invokestatic  #18                 // Method Main$Runner.Run:(LMain$A;)V
            18: return
      

      Run 的每次调用都将使用不同的重载版本(由invokestatic 后面的数字标识)

      【讨论】:

      • 所以期望是正确的,但我必须在到达 SomeRandomCall 之前将 B 作为 A 传递/转换为某处。我会在星期一重新检查我的代码,如果找不到演员表,我会再次发布。
      • @Theopile 它以B 到达SomeRandomCall,但SomeRandomCall 中的参数将始终为A,因为这是签名中方法参数的类型和对Run(A) 的调用在该方法中硬编码。它不能动态调用Run(B)。这将需要一种仅接受B 的不同方法。如果您希望能够调用所需的方法,请查看 double dispatch patterns。你不可能使用一种方法来像这样对 3 个不同的方法进行调配。
      • 我试图避免调度,但它确实有效,谢谢。
      猜你喜欢
      • 2020-01-06
      • 2012-12-10
      • 1970-01-01
      • 2010-12-18
      • 1970-01-01
      • 2013-10-11
      • 1970-01-01
      • 2017-08-20
      • 1970-01-01
      相关资源
      最近更新 更多