Java 方法重载 - 同一继承树中的通用参数和参数

Java method overloading - Generic parameter & parameters within same inheritance tree

假设我有以下代码:

// 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。那么我是否正确地假设决定采用哪些重载方法是在编译时发生的,没有动态绑定? (关于staticdynamic绑定的问题)
  2. foo(f) 使用 foo(Number para),而 foo(i) 使用 foo(Integer para)。只有 foo(s) 使用通用版本。因此,编译器总是会查看给定类型是否存在非泛型实现,只有在没有的情况下才会回退到泛型版本吗? (关于泛型的问题)
  3. 同样,foo(f) 使用 foo(Number para),而 foo(i) 使用 foo(Integer para)。然而,Integer i 也将是 Number。所以总是采用继承树中 "outer-most" 类型的方法? (关于继承的问题)

我知道这些问题很多,示例并非来自生产代码,但我只是想知道 "happens behind" 是什么以及为什么会发生这种情况。

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

所以,这很简单:

1) 是的,这个决定是在编译时做出的。编译器选择具有最具体匹配类型的方法。因此,当您作为参数传递的变量声明为 Number 时,编译器将选择 Number 版本,即使它在 运行 时是 Integer。 (如果编译器找到两个"equally matching"方法,一个方法不明确的错误会导致编译失败)

2) 在 运行 时代,没有泛型,一切都只是一个 Object。泛型只是一个编译时和源代码特性。因此编译器必须尽力而为,因为虚拟机肯定做不到。

language specification. Specifically important is the section on choosing the most specific method 中解释了确定在编译时调用哪个方法签名的规则。以下是与您的问题相关的部分:

If more than one member method is both accessible and applicable to a method invocation, it is necessary to choose one to provide the descriptor for the run-time method dispatch. The Java programming language uses the rule that the most specific method is chosen.

...

One applicable method m<sub>1</sub> is more specific than another applicable method m<sub>2</sub>, for an invocation with argument expressions e<sub>1</sub>, ..., e<sub>k</sub>, if any of the following are true:

  • m<sub>2</sub> is generic, and m<sub>1</sub> is inferred to be more specific than m<sub>2</sub> for argument expressions e<sub>1</sub>, ..., e<sub>k</sub> by §18.5.4.

  • m<sub>2</sub> is not generic, and m<sub>1</sub> and m<sub>2</sub> are applicable by strict or loose invocation, and where m<sub>1</sub> has formal parameter types S1, ..., Sn and m<sub>2</sub> has formal parameter types T1, ..., Tn, the type Si is more specific than Ti for argument e<sub>i</sub> for all i (1 ≤ in, n = k).

...

A type S is more specific than a type T for any expression if S <: T (§4.10).

在这种情况下,IntegerNumber 更具体,因为 Integer 扩展了 Number,所以每当编译器检测到对 foo 的调用时获取类型为 Integer 的变量 declared,它将添加对 foo(Integer).

的调用

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

When testing that one applicable method is more specific than another (§15.12.2.5), where the second method is generic, it is necessary to test whether some instantiation of the second method's type parameters can be inferred to make the first method more specific than the second.

...

Let m<sub>1</sub> be the first method and m<sub>2</sub> be the second method. Where m<sub>2</sub> has type parameters P1, ..., Pp, let α1, ..., αp be inference variables, and let θ be the substitution [P1:=α1, ..., Pp:=αp].

...

The process to determine if m<sub>1</sub> is more specific than m<sub>2</sub> is as follows:

...

If Ti is a proper type, the result is true if Si is more specific than Ti for ei (§15.12.2.5), and false otherwise. (Note that Si is always a proper type.)

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

这也意味着在您的代码中 foo(T) 仅适用于传递 String.[=31= 的调用(并且本质上是最具体的方法,因为它是唯一适用的方法) ]

Am I right in the assumption that the decision, which of the overloaded methods is taken happens at compile-time, without dynamic binding?

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

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

So the compiler always looks if there is a non-generic implementation for the given types, and only if not it falls back to the generic version?

由于类型擦除,您的泛型方法的实际签名是

Object foo(Object);

在您测试的参数类型中,这是仅 String.

的重载选项中的最佳匹配

So always the method with the "outer-most" type within the inheritance tree is taken?

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

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