我最近遇到两个重载问题,我找不到答案,也没有java环境来运行一些测试代码。我希望有人可以通过汇编java编译器遵循的所有规则列表来帮助我重载或者交替指向我已经存在的列表。
首先,当两个方法的区别仅在于最终的varargs参数时,在什么情况下每个方法都被调用,你可以在没有任何args的情况下调用varargs方法吗?
private void f(int a) { /* ... */ }
private void f(int a, int... b) { /* ... */ }
f(12); // calls the former? I would expect it to
f(12, (int[])null); // calls latter, but passes null for b?
// Can I force the compiler to call the second method in the same fashion
// as would happen if the first method didn't exist?
第二个问题,当两个方法因为彼此继承而被调用的类型不同时?我希望能够调用派生最多的版本,并且允许调用另一个版本。
interface A {}
class B implements A {}
class C implements A {}
private void f(A a) {}
private void f(B b) {}
f(new C()); // calls the first method
f(new B()); // calls the second method?
f((A)(new B()); // calls the first method using a B object?
这是两个例子,但作为代码阅读器,我更喜欢用于解决此问题的精确有序规则的规范列表,因为我经常没有时间设置构建环境来检查编译器是什么做。
答案 0 :(得分:19)
重载与覆盖
方法的正确实现的选择是在运行时完成的,正如您所指出的那样,现在要调用的方法的签名是在编译时决定的。
在编译时重载方法选择
第15.12节Java Language Specification中的Method Invocation Expressions(JLS)详细解释了编译器选择正确调用方法所遵循的过程。
在那里,您会注意到这是编译时任务。 JLS在第15.12.2小节中说:
此步骤使用方法名称和参数表达式的类型 找到既可访问又适用的方法 可能存在多个这样的方法,在这种情况下,选择最具体的方法。
通常,如果varargs方法与其他候选方法竞争,则它们是最后选择的方法,因为它们被认为不如接收相同参数类型的方法具体。
要验证此编译时的性质,您可以进行以下测试。
声明一个这样的类并编译它。
public class ChooseMethod {
public void doSomething(Number n){
System.out.println("Number");
}
}
声明第二个类,它调用第一个方法并编译它。
public class MethodChooser {
public static void main(String[] args) {
ChooseMethod m = new ChooseMethod();
m.doSomething(10);
}
}
如果您调用main,则输出显示为Number
。
现在,向ChooseMethod
类添加第二个更具体的重载方法,并重新编译它(但不要重新编译其他类)。
public void doSomething(Integer i) {
System.out.println("Integer");
}
如果再次运行main,则输出仍为Number
。
基本上,因为它是在编译时决定的。如果重新编译MethodChooser
类(具有main的类)并再次运行程序,则输出将为Integer
。
因此,如果要强制选择其中一个重载方法,则参数类型必须与编译时参数的类型相对应,而不仅仅是在运行时。
在运行时覆盖方法选择
同样,方法的签名在编译时决定,但实际的实现是在运行时决定的。
声明一个这样的类并编译它。
public class ChooseMethodA {
public void doSomething(Number n){
System.out.println("Number A");
}
}
然后声明第二个扩展类并编译:
public class ChooseMethodB extends ChooseMethodA { }
在MethodChooser类中,您可以:
public class MethodChooser {
public static void main(String[] args) {
ChooseMethodA m = new ChooseMethodB();
m.doSomething(10);
}
}
如果你运行它,你得到输出Number A
,这是好的,因为该方法尚未在ChooseMethodB
中被覆盖,因此被调用的实现是ChooseMethodA
的实现
现在,在MethodChooserB
中添加覆盖方法:
public void doSomething(Number n){
System.out.println("Number B");
}
重新编译这个,然后再次运行main方法。
现在,您获得输出Number B
因此,在运行时选择了实现,而不需要重新编译MethodChooser
类。
答案 1 :(得分:1)
第一个问题:
你的假设是正确的。第二次调用f()
将调用varargs方法。您可以让编译器使用以下命令调用第二个方法:
private void f(int a) {
f(a, null);
}
第二个问题:
是。但是,您无法扩展接口。如果将A
更改为抽象类,则会编译。