重载与重写
导航 类和对象 主题: ) |
在一个类中,可以有几个同名方法。但是它们必须有不同的签名。方法的签名由其名称、参数类型及其参数的顺序组成。方法的签名不包含其返回类型、可见性或它可能抛出的异常。在同一个类中定义两个或多个共享相同名称但具有不同参数的方法的做法称为方法重载。
在一个类中具有相同名称的方法被称为重载方法。重载方法对 JVM 没有特殊的好处,但对程序员来说,让几个方法执行相同的事情但使用不同的参数非常有用。例如,我们可能有操作runAroundThe
表示为两个同名但输入参数类型不同的方法
代码部分 4.22:方法重载。
public void runAroundThe(Building block) {
...
}
public void runAroundThe(Park park) {
...
}
|
一种类型可以是另一种类型的子类
|
|
虽然两种方法都适合调用带有String
参数的方法,但将调用参数类型最接近的方法。更准确地说,它将调用参数类型是另一个方法的参数类型的子类的方法。因此,aObject
将输出Object
。注意!参数类型由对象的声明类型定义,不是其实例化类型!
以下两个方法定义是有效的
代码部分 4.23:带类型顺序的方法重载。
public void logIt(String param, Error err) {
...
}
public void logIt(Error err, String param) {
...
}
|
因为类型顺序不同。如果两个输入参数都是 String 类型,那就会有问题,因为编译器将无法区分两者
代码部分 4.24:错误的方法重载。
public void logIt(String param, String err) {
...
}
public void logIt(String err, String param) {
...
}
|
编译器也会对以下方法定义给出错误
代码部分 4.25:另一种错误的方法重载。
public void logIt(String param) {
...
}
public String logIt(String param) {
String retValue;
...
return retValue;
}
|
请注意,返回类型不是唯一签名的部分。为什么不呢?原因是可以在不将方法的返回值赋值给变量的情况下调用方法。此功能来自 C 和 C++。因此,对于调用
代码部分 4.26:不明确的方法调用。
logIt(msg);
|
编译器将不知道调用哪个方法。对于抛出的异常也是如此。
问题 4.6:Question6
类的哪些方法会导致编译错误?
Question6.java
public class Question6 {
public void example1() {
}
public int example1() {
}
public void example2(int x) {
}
public void example2(int y) {
}
private void example3() {
}
public void example3() {
}
public String example4(int x) {
return null;
}
public String example4() {
return null;
}
}
|
Question6.java
public class Question6 {
public void example1() {
}
public int example1() {
}
public void example2(int x) {
}
public void example2(int y) {
}
private void example3() {
}
public void example3() {
}
public String example4(int x) {
return null;
}
public String example4() {
return null;
}
}
|
example1
、example2
和 example3
方法会导致编译错误。example1
方法不能共存,因为它们具有相同的签名(记住,返回类型不是签名的一部分)。example2
方法不能共存,因为参数的名称不是签名的一部分。example3
方法不能共存,因为方法的可见性不是签名的一部分。example4
方法可以共存,因为它们具有不同的方法签名。
您不必重载,也可以使用动态数量的参数。在最后一个参数之后,您可以传递相同类型的可选无限参数。这些参数通过添加最后一个参数并在其类型后添加...
来定义。动态参数将作为数组接收
代码部分 4.27:可变参数。
public void registerPersonInAgenda(String firstName, String lastName, Date... meeting) {
String[] person = {firstName, lastName};
lastPosition = lastPosition + 1;
contactArray[lastPosition] = person;
if (meeting.length > 0) {
Date[] temporaryMeetings = new Date[registeredMeetings.length + meeting.length];
for (i = 0; i < registeredMeetings.length; i++) {
temporaryMeetings[i] = registeredMeetings[i];
}
for (i = 0; i < meeting.length; i++) {
temporaryMeetings[registeredMeetings.length + i] = meeting[i];
}
registeredMeetings = temporaryMeetings;
}
}
|
上面的方法可以用动态数量的参数调用,例如
代码部分 4.27:构造函数调用。
registerPersonInAgenda("John", "Doe");
registerPersonInAgenda("Mark", "Lee", new Date(), new Date());
|
此功能在 Java 1.5 之前不可用。
构造函数可以重载。您可以定义多个具有不同参数的构造函数。例如
代码清单 4.12:构造函数。
public class MyClass {
private String memberField;
/**
* MyClass Constructor, there is no input parameter
*/
public MyClass() {
...
}
/**
* MyClass Constructor, there is one input parameter
*/
public MyClass(String param1) {
memberField = param1;
...
}
}
|
在代码清单 4.12中,我们定义了两个构造函数,一个没有输入参数,另一个有一个输入参数。您可能会问将调用哪个构造函数。这取决于使用new
关键字创建对象的方式。见下文
代码部分 4.29:构造函数调用。
// The constructor with no input parameter will be called
MyClass obj1 = new MyClass();
// The constructor with one input param. will be called
MyClass obj2 = new MyClass("Init Value");
|
在代码部分 4.29中,我们从同一个类创建了两个对象,或者也可以说obj1
和obj2
都具有相同的类型。这两者的区别在于,在第一个中,memberField
字段未初始化,在第二个中,它被初始化为"Init Value"
。构造函数也可以从另一个构造函数调用,见下文
代码清单 4.13:构造函数池。
public class MyClass {
private String memberField;
/**
* MyClass Constructor, there is no input parameter
*/
public MyClass() {
MyClass("Default Value");
}
/**
* MyClass Constructor, there is one input parameter
*/
public MyClass(String param1) {
memberField = param1;
...
}
}
|
在代码清单 4.13中,没有输入参数的构造函数调用了具有默认初始值的另一个构造函数。此调用必须是构造函数的第一条指令,否则会发生编译错误。该代码为用户提供了选择,可以选择使用默认值创建对象,或者使用指定的值创建对象。第一个构造函数也可以使用this
关键字编写
代码部分 4.30:另一种构造函数池。
public MyClass() {
this("Default Value");
}
|
这样的调用减少了代码重复。
为了便于记住在方法重写中可以做什么,请记住,在类对象中可以做的所有操作,在子类对象中也可以做,只是行为可能会改变。子类应该是协变的。
虽然方法签名在类内部必须是唯一的,但可以在不同的类中定义相同的签名。如果我们定义了一个在超类中存在的方法,那么我们就重写了超类方法。这被称为方法重写。这与方法重载不同。方法重载是指名称相同但签名不同的方法。方法重写是指在继承类之间名称和签名相同的方法。
返回值类型会导致我们上面看到的问题。当我们重写超类方法时,返回值类型也必须相同。如果不相同,编译器会报错。
注意!如果一个类声明了两个具有相同名称的公共方法,而一个子类重写了其中一个方法,则子类仍然会继承另一个方法。在这方面,Java编程语言与C++不同。
方法重写与动态链接或运行时绑定有关。为了使方法重写起作用,要调用的方法调用不能在编译时确定。它将在运行时决定,并在表中查找。
代码部分 4.31:运行时绑定。
MyClass obj;
if (new java.util.Calendar().get(java.util.Calendar.AM_PM) == java.util.Calendar.AM) {
// Executed during a morning
obj = new SubOfMyClass();
} else {
// Executed during an afternoon
obj = new MyClass();
}
obj.myMethod();
|
在代码部分 4.31中,如果在早上执行,第 3 行的表达式为真,如果在下午执行,则为假。因此,obj
的实例将是MyClass
或SubOfMyClass
,具体取决于执行时间。因此,在编译时无法确定方法地址。因为obj
引用可以指向一个对象及其所有子对象,并且只有在运行时才会知道,所以会维护一个包含所有可能方法地址的表以供调用。不要混淆
代码部分 4.32:声明类型和实例化类型。
obj.myMethod(myParameter);
|
使用被调用对象的实例化类型(obj
)和参数对象的声明类型(myParameter
)来搜索此方法的实现。
还有另一个规则是,当你进行重写时,重写超类方法的新方法的可见性不能降低。但是,可见性可以提高。因此,如果超类方法的可见性是public
,则重写方法不能是package
或private
。重写方法必须抛出与超类相同的异常,或其子异常。
super
引用父类(即 super.someMethod()
)。它可以在子类中使用来访问子类已重写的继承方法或子类已隐藏的继承字段。
一个常见的错误是认为如果我们可以重写方法,我们也可以重写成员变量。事实并非如此,因为这样做毫无意义。你不能重新定义超类中私有的变量,因为这样的变量是不可见的。 |