我最近刚开始乱用Java中的Generics,并且遇到了一些奇怪的行为。这是简化版本,但我有一个基类,它由多个类扩展,并传递给泛型函数。在这个函数中,我调用了一个方法,它有几个版本,它们将不同的基类或派生类作为参数。
如果我有以下内容:
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 )
而不是运行( ClassB b )
函数。鉴于这两个函数,不应该调用Run( ClassB b )
,因为提供了特定于类型的函数?
如果不是这种情况,我怎么能拥有一个通用函数来调用能够调用具有基类和派生类的签名的函数?
答案 0 :(得分:2)
由于你的缩写对我来说有点混乱,我做了一个小的,可运行的例子。我假设B
中的SomeRandomCall
是通用类型,而不是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
的实际类型不再重要,因为我们只有一个候选者可以执行。无论ab
是A
,B
还是C
,输出都是 A 。
答案 1 :(得分:1)
好吧,由于A
扩展B
,因此类型B
的对象可以作为参数传递给接受类型为A
的参数的方法。这里也是如此。由于run(A a)
之前存在run(B b)
,因此执行前者。因此,您的错误(如果您正在考虑它)。
有关进一步详细说明,请考虑以下示例:
Programmer
是一个超类(如A
)。
Java Programmer
是Programmer
的子类,C++ programmer
也是B
和C
。
现在,有三个门,按距离增加的顺序:
由于第一扇门离您最近且条件满足,您可以立即输入。你不想花更多的精力去第二门。这里也是如此。
答案 2 :(得分:1)
void SomeRandomCall<T extends SomeClass>( T t )
被编译为好像是
void SomeRandomCall( Someclass t )
这就是为什么
EnclosingClass.<ClassB>SomeRandomCall( new ClassB() );
将导致A
,因为ClassB
作为编译时类型在方法中不再可见。
如果您通过Main
(即已反编译)查看课程(以下示例中的javap -c -s
),也可以看到此内容
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
之后的数字标识)