为啥编译器更喜欢 char 的 int 重载而不是 varargs char 重载?

Posted

技术标签:

【中文标题】为啥编译器更喜欢 char 的 int 重载而不是 varargs char 重载?【英文标题】:Why does the compiler prefer an int overload to a varargs char overload for a char?为什么编译器更喜欢 char 的 int 重载而不是 varargs char 重载? 【发布时间】:2015-12-04 22:44:13 【问题描述】:

代码

public class TestOverload 

    public TestOverload(int i)System.out.println("Int");
    public TestOverload(char... c)System.out.println("char");

    public static void main(String[] args) 
        new TestOverload('a');
        new TestOverload(65);
    

输出

Int
Int

这是预期的行为吗?如果是这样,那为什么?我期待:char,Int

注意:我使用的是 Java 8

【问题讨论】:

解决方案 - 添加一个(char) 方法 The First Rule of Programming: It's Always Your Fault 他们在那里做出了有趣的设计选择。在 C# 中,我相信 '...' 的等价物是参数。 C# 会选择 char,我认为更明智的行为,但它当然是另一种语言。 @NathanCooper:可变参数是在相当晚的时候添加的,因此它们极低的优先级对于向后兼容,或者更确切地说是库的可进化性是必要的。您不希望现有客户端突然选择不同的重载,只是因为您向现有公共 API 添加了可变参数重载。 哪种方法可以采用 int 或 char 并根据参数类型做一些与众不同的事情。不是理论上,而是实际上。如果你能回答这个问题,你能告诉我你会给这样一个函数起什么名字,为什么重载它比使用不同的名字更好?仅发生在 TestOverload() 的问题并不是真正的问题。 【参考方案1】:

当编译器确定选择哪个重载方法时,具有可变参数 (...) 的方法的优先级最低。因此,当您使用单个char 参数'a' 调用TestOverload 时,会选择TestOverload(int i) 而不是TestOverload(char... c),因为char 可以自动提升为int

JLS 15.12.2:

    第一阶段(第 15.12.2.2 节)执行重载解析不允许装箱或拆箱转换,或使用可变参数 方法调用。如果在此阶段没有找到适用的方法 然后处理继续到第二阶段。 这保证了在 Java 编程中有效的任何调用 Java SE 5.0 之前的语言不被认为是模棱两可的结果 引入可变数量方法、隐式装箱和/或 拆箱。但是,变量arity 方法的声明(§8.4.1) 可以更改为给定方法方法调用选择的方法 表达式,因为可变的arity方法被视为固定的 第一阶段的arity方法。例如,声明 m(Object...) 在已经声明 m(Object) 的类中导致 m(Object) 没有 不再为某些调用表达式(例如 m(null))选择,如 m(Object[]) 更具体。

    第二阶段(第 15.12.2.3 节)执行重载解决方案,同时允许装箱和拆箱,但仍然禁止使用可变参数方法调用。如果在此期间没有找到适用的方法 阶段然后处理继续到第三阶段。这确保了永远不会通过可变数量来选择方法 方法调用,如果它通过固定数量方法适用 调用。

    第三阶段(第 15.12.2.4 节)允许将重载与可变参数方法、装箱和拆箱结合使用。

编辑:

如果您希望强制编译器调用TestOverload(char... c) 构造函数,您可以将char[] 调用传递给构造函数:

new TestOverload (new char[] 'a');

【讨论】:

哦,我虽然它会调用正确的方法或者会给出一个模棱两可的错误。有没有办法在不改变方法签名的情况下调用正确的方法? @AmitGupta 您可以传递具有单个字符的字符数组:new TestOverload (new char[] 'a'); new TestOverload(new char[] 'a') @Eran 为了完整起见,您可能应该将如何显式调用 varargs 方法的说明添加到您的答案中。 :-) 如果添加public TestOverload(char c) this(new char[] c ); ,调用构造函数会变得更好:new TestOverload('a'),就像OP想要的那样。【参考方案2】:

是的,这是预期的行为。方法调用的优先级如下:

    加宽 拳击 可变参数

以下是Java docs 的摘录:-

确定适用性的过程始于确定可能适用的方法 (§15.12.2.1)。

流程的其余部分分为三个阶段,以确保与 Java SE 5.0 之前的 Java 编程语言版本兼容。阶段是:

第一阶段(第 15.12.2.2 节)执行重载决议,不允许装箱或拆箱转换,或使用可变的 arity 方法调用。如果在此阶段没有找到适用的方法,则处理继续到第二阶段。

这保证了在 Java SE 5.0 之前在 Java 编程语言中有效的任何调用都不会因为引入可变数量方法、隐式装箱和/或拆箱而被视为模棱两可。但是,变量 arity 方法的声明(第 8.4.1 节)可以更改为给定方法方法调用表达式选择的方法,因为变量 arity 方法在第一阶段被视为固定 arity 方法。例如,在已经声明 m(Object) 的类中声明 m(Object...) 会导致不再为某些调用表达式(例如 m(null))选择 m(Object),如 m(Object[] ) 更具体。

第二阶段(第 15.12.2.3 节)执行重载决议,同时允许装箱和拆箱,但仍排除使用可变参数方法调用。如果在此阶段没有找到适用的方法,则处理继续到第三阶段。

这确保了一个方法永远不会通过可变的 arity 方法调用来选择,如果它适用于通过固定的 arity 方法调用。

第三阶段(第 15.12.2.4 节)允许将重载与可变参数方法、装箱和拆箱相结合。

【讨论】:

对不起,我开始编辑那个问题,但后来意识到它是我的能力背后的:-)。如此低估并保存了我修复过的东西。最初我虽然只有问题主体需要修复。但我无法格式化任何问题。【参考方案3】:

来自 Joshua Bloch 的可靠建议(Effective Java,第 2 版):

“只选择那些具有完全不同类型的重载方法作为参数。”

具有完全不同类型的对象是不能合理地转换为另一种参数类型的对象。遵循此规则可能会为您节省数小时调试神秘错误的时间,当编译器在编译时选择您未预料到的方法重载时,可能会发生这种错误。

您的代码行违反了此规则并为错误打开了大门:

public TestOverload(int i)System.out.println("Int");
public TestOverload(char... c)System.out.println("char");

char 可以与 int 相互转换,因此您可以预测调用会发生什么的唯一方法是转到 Java 语言规范并阅读一些关于如何解决重载的晦涩难懂的规则。

幸运的是,这种情况不需要 JLS 研究。如果您的论点彼此之间没有根本不同,那么最好的选择可能是不要重载。为这些方法命名不同的名称,这样可能需要维护代码的任何人都不会出错或混淆。

时间就是金钱。

【讨论】:

如果重载的行为非常相似,我认为使用类似类型的重载是可以接受的。例如int max(int x, int y)long max(long x, long y) 非常好。 唯一的问题是当你重载了构造函数而不是方法。您不能选择不同的名称。就像我的情况一样。但是我修改了我的代码,以便它使用这个运行时多态性来确定应该调用哪个方法。 :) 为了放大一个关键点,什么样的方法可以有用获取一个char或一个int并在每种情况下做一些独特的事情?我实在想不出一个。我也同意“最好的选择是不要超载”。语言错误的好答案。 我创建了自己的原始列表,用户可以在其中传递列表的大小或仅传递字符...来创建它的对象。【参考方案4】:

我从this link 获取代码并修改了其中的一些部分:

    public static void main(String[] args) 
    Byte i = 5;
    byte k = 5;
    aMethod(i, k);


//method 1
static void aMethod(byte i, Byte k) 
    System.out.println("Inside 1");


//method 2
static void aMethod(byte i, int k) 
    System.out.println("Inside 2");


//method 3
static void aMethod(Byte i, Byte k) 
    System.out.println("Inside 3 ");


//method 4
static void aMethod(Byte  i, Byte ... k) 
    System.out.println("Inside 4 ");

编译器对方法 1、2 和 3 而不是 4(为什么?)给出错误(该方法对于类型重载不明确)(为什么?)

答案在于 java 用于将方法调用与方法签名匹配的机制。该机制分三个阶段完成,每个阶段如果找到匹配的方法就会停止:

+阶段一:使用加宽查找匹配方法(未找到匹配方法)

+第二阶段:(也)使用装箱/拆箱寻找匹配方法(方法1,2和3匹配)

+第三阶段:(也)使用var args(方法4匹配!)

【讨论】:

以上是关于为啥编译器更喜欢 char 的 int 重载而不是 varargs char 重载?的主要内容,如果未能解决你的问题,请参考以下文章

为啥编译器将“char”匹配到“int”而不是“short”?

为啥有些编译器更喜欢手工制作的解析器而不是解析器生成器?

为啥编译器更喜欢 f(const void*) 而不是 f(const std::string &)?

为啥 putchar、toupper、tolow 等采用 int 而不是 char?

ANSI C:为啥字符函数接受 int 参数而不是 char 参数?

为啥游戏逻辑更喜欢 update() 而不是 didFinishUpdate?