Java方法重载 - 通用参数&同一继承树

时间:2016-03-10 18:21:42

标签: java generics inheritance overloading

我们假设我有以下代码:

// Method acception generic parameter
public static <T> T foo(T para) {
    return para;
}

// Method accepting Integer parameter
public static Integer foo(Integer para) {
    return para + 1;
}

// Method accepting Number parameter
public static Number foo(Number para) {
    return para.intValue() + 2;
}

public static void main(String[] args) {
    Float f = new Float(1.0f);
    Integer i = new Integer(1);
    Number n = new Integer(1);
    String s = "Test";

    Number fooedFloat = foo(f);     // Uses foo(Number para)
    Number fooedInteger = foo(i);   // Uses foo(Integer para)
    Number fooedNumber = foo(n);    // Uses foo(Number para)
    String fooedString = foo(s);    // Uses foo(T para)

    System.out.println("foo(f): " + fooedFloat);
    System.out.println("foo(i): " + fooedInteger);
    System.out.println("foo(n): " + fooedNumber);
    System.out.println("foo(s): " + fooedString);
}

输出如下所示:

foo(f): 3
foo(i): 2
foo(n): 3
foo(s): Test

现在问题:

  1. foo(n)调用foo(Number para),很可能是因为n被定义为Number,即使它已分配Integer。所以我认为决定哪个重载方法是在编译时发生的,没有动态绑定? (关于静态动态绑定)的问题
  2. foo(f)使用foo(Number para),而foo(i)使用foo(Integer para)。只有foo(s)使用通用版本。因此,编译器始终会查看是否存在给定类型的非泛型实现,并且只有在它不回退到通用版本时才会这样做? (关于 generics 的问题)
  3. 同样,foo(f)使用foo(Number para),而foo(i)使用foo(Integer para)。但是,Integer i也是Number。所以总是使用&#34;最外层&#34;是继承树中的类型吗? (关于继承的问题
  4. 我知道这些问题很多,而且这个例子不是来自高效的代码,但我只是想知道&#34;背后发生了什么?#34;事情发生的原因。

    也非常感谢任何指向Java文档或Java规范的链接,我自己找不到它们。

3 个答案:

答案 0 :(得分:4)

language specification中解释了确定在编译时调用哪个方法签名的规则。特别重要的是选择the most specific method的部分。以下是与您的问题相关的部分:

  

如果多个成员方法都可访问并适用于方法调用,则必须选择一个为运行时方法调度提供描述符。 Java编程语言使用选择最具体方法的规则。

     

...

     

一个适用的方法m1比另一个适用的方法m2更具体,用于参数表达式e1,...,ek的调用,如果有的话以下是真实的:

     
      
  • m2是通用的,m1推断为参数表达式m2,...,e1ek更具体按§18.5.4。

  •   
  • m2不是通用的,m1m2适用于严格或宽松的调用,m1具有形式参数类型S1,... 。,Sn和m2具有形式参数类型T 1 ,...,T n ,类型S i 更多所有 i 的参数ei的特定于T i (1≤ i n ,< em> n = k )。

  •   
     

...

     

对于任何表达式,如果S&lt;:T(§4.10),则类型S 更具体而不是类型T.

在这种情况下,IntegerNumber更具体,因为Integer扩展Number,因此每当编译器检测到对foo的调用时变量声明类型为Integer,它将为foo(Integer)添加一个调用。

this section中解释了有关第二种方法与通用性相关的第一个条件的更多信息。这有点冗长,但我认为重要的部分是:

  

当测试一个适用的方法更具体而不是另一个(第15.12.2.5节)时,第二种方法是通用的,有必要测试第二种方法的类型参数的某些实例化是否可以推断使第一种方法比第二种更具体。

     

...

     

m1成为第一种方法,m2成为第二种方法。其中m2的类型参数为P 1 ,...,P p ,设α 1 ,...,α< sub> p 是推理变量,让θ为替换[P 1 :=α 1 ,...,P p :=α<子> p

     

...

     

确定m1是否比m2更具体的过程如下:

     

...

     

如果T i 是正确类型,如果S i 比T i 更具体,则结果为 true sub> for e i (§15.12.2.5),否则 false 。 (注意,S i 始终是正确的类型。)

这基本上意味着foo(Number)foo(Integer)都比foo(T)更具体,因为编译器可以推断出通用方法的至少一种类型(例如Number本身)这使foo(Number)foo(Integer)更具体(这是因为Integer <: NumberNumber <: Number)。

这也意味着在您的代码中foo(T)仅适用于(通常是最具体的方法,因为它只是适用的方法),用于传递String的调用。

答案 1 :(得分:2)

  

我是否正确地假设决定哪个重载方法在编译时发生,没有动态绑定?

是的,Java在编译时根据声明的参数类型从目标对象的声明类型所呈现的替代方案中选择方法的可用重载。

动态绑定适用于根据调用目标的运行时类型选择具有相同签名的方法。它与实际参数的运行时类型没有直接关系。

  

因此,编译器始终会查看是否存在给定类型的非泛型实现,并且只有在没有它回退到通用版本的情况下?

由于类型擦除,通用方法的实际签名是

Object foo(Object);

在您测试的参数类型中,这是仅String的重载选项中的最佳匹配。

  

所以总是在继承树中使用“最外层”类型的方法?

或多或少。在重载中进行选择时,编译器会选择与声明的参数类型最匹配的替代方法。对于引用类型的单个参数,这是其参数类型是参数的声明类型或其最近的超类型的方法。

如果Java必须在多参数方法的重载中进行选择,并且它没有完全匹配,那么事情会变得很糟糕。当存在原始参数时,它还必须考虑允许的参数转换。完整的细节占据了JLS的一大部分。

答案 2 :(得分:1)

所以,这非常简单:

1)是的,决定是在编译时做出的。编译器选择具有最特定匹配类型的方法。因此,当您作为参数传递的变量声明为Number时,编译器将选择Number版本,即使它在运行时是Integer。 (如果编译器找到两个&#34;同等匹配的&#34;方法,模糊的方法错误将使编译失败)

2)在运行时,没有泛型,一切都只是Object。泛型仅是编译时和源代码功能。因此编译器必须尽其所能,因为VM肯定不能。