为什么在Java 7中使用方法重载时,自动装箱不能覆盖可变参数?

19
我们的Java项目中有一个LogManager类,长这样:
public class LogManager {

    public void log(Level logLevel, Object... args) {
        // do something
    }

    public void log(Level logLevel, int value, Object... args) {
        // do something else
    }
}

在Debian下使用OpenJDK 6编译项目时一切正常。但是,当使用OpenJDK 7进行构建(使用ant完成)时,则会出现以下错误并导致构建失败:

[javac] /…/LogManager.java:123: error: reference to log is ambiguous,
                      both method log(Level,Object...) in LogManager
                      and method log(Level,int,Object...) in LogManager match
[javac]       log(logLevel, 1, logMessage);
[javac]       ^
[javac] /…/SomeOtherClass.java:123: error: reference to log is ambiguous,
                      both method log(Level,Object...) in LogManager
                      and method log(Level,int,Object...) in LogManager match
[javac]       logger.log(logLevel, 1, logMessage);
[javac]             ^
只要 1 没有自动装箱,方法调用就应该是清晰的,因为 1 是 int 类型,无法向上转型为 Object。那么为什么自动装箱没有在此处覆盖 varargs?
使用从eclipse.org下载的 tar.gz 安装的 Eclipse 不管是否安装了 OpenJDK 6 都可以编译它。
非常感谢您的帮助!
编辑:编译器在两种情况下都会得到选项 source="1.6"target="1.6"。Eclipse 的编译说明只是作为注释。
3个回答

17
我猜这与bug #6886431有关,这个问题在OpenJDK 7中似乎也已经修复了。
问题在于JLS 15.12.2.5 选择最具体的方法中提到,当前一个方法的形式参数类型是后一个方法的形式参数类型的子类型时,前一个方法比后一个方法更具体。
由于int不是Object的子类型,因此你的两个方法都不是最具体的,因此你的调用是模棱两可的。
然而,以下的解决方法是可行的,因为IntegerObject的子类型:
public void log(Level logLevel, Object... args) { ... }
public void log(Level logLevel, Integer value, Object... args) { ... } 

我尝试使用Integer代替int,它可以运行(在OpenJDK 6和7中都可以)。但是对于int和调用log(logLevel,1,logMessage1)的情况下,我的方法签名应该更具体,因为只需要向上转型,而不需要自动装箱。所以我能看到这种方式可以运行,但我真的不理解为什么。我本来期望错误会出现在你的签名中而不是我的。 - Michael
+1,因为在评论中得出相同的结论之前,我没有重新阅读其他答案。 - Edwin Buck

2
Eclipse使用自己的编译器,所以最终遵循SUN/Oracle提供的编译器所做的事情; 但是,有时候(就像在这种情况下)会有一些差异。这可能会“两者都行”,在Java 6中,该问题没有得到详细解决。由于Java有强烈的要求在其环境中减少“模棱两可”的含义的数量(以强制在许多平台上实现相同的行为),我想他们在7版中收紧了(或直接指定了)决定性的行为。您刚刚被新规范澄清的“错误”方面抓住了。抱歉,但我认为您将需要写一些内容。
public void log(Level logLevel, Object... args) {
    if (args != null && args[0] instanceof Integer) {
      // do something else
    } else {
      // do something
    }
}

将其集成到您的新解决方案中。

1
使用您的解决方案可以编译项目,但会破坏方法重写的美感 :( - Michael
@Michael:你可以尝试声明一个方法log(Level level, int i)来调用log(level, i, new Object[] {}),帮助编译器 - 我现在手头没有Java 7环境来尝试。(此外,方法重载并不美观,因为它使方法解析变得不必要地复杂,导致这种情况的出现;至少与命名和可选参数相比,后者可以很好地覆盖您的用例。) - millimoose
这只会导致一个未使用的方法,因为我们永远不需要没有进一步参数的日志方法,即日志消息。显然我是指方法重载而不是覆盖。6年前我不喜欢它,但现在我能看到它的美。我认为它并不是不必要地复杂,因为它可以非常清晰地定义。从我的角度来看,这是OpenJDK 7编译器的一个错误;) - Michael
@Michael,编译器没有错误,但是语言可能存在问题。当将内置类型提升为对象类型时,会出现概念上的冲突。你尝试过 log(Level, Integer, Object ...) 吗?这可能会利用自动装箱来帮助你,而不会与 Integer 是 Object 快速冲突,从而导致与 log(Level, Object ...) 绑定。就我个人而言,我理解内置类型的妥协,但我更喜欢连常量都是对象的语言。为什么不使用 int x = 4; System.out.println(x.toString()) 呢? - Edwin Buck
int x 不是一个常量。在Java中没有真正的常量(只有“final”类型)。将int作为非对象具有其优点,因为它们更有效率。允许像x.toString()这样的东西将被限制在很少的函数中,并且对程序员的好处不会太大。顺便说一下,我更喜欢来自函数式编程世界的语言 :) - Michael

0

这种做法比谨慎的方式更接近边缘。除非您能在规范中找到明确的行为语言,否则我会避免任何模棱两可的事情。

即使它在规范中,您代码的读者也不会进行语言解释,因此您需要添加注释来解释,并且他们可能不会阅读该注释。他们甚至可能不考虑其中的替代方案 - 只看到一个适合的重载,然后运行。这是一场等待发生的事故。


目前还没有人遇到理解代码这部分的问题。也许是因为这是计算机科学系的一个项目,只供硕士和博士生使用 :) 但毕竟,我认为Java认为自动装箱+向上转型比仅向上转型更昂贵。 - Michael

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接