程序员面试必考题(三十)--Java中多态的基本概念
示例。定义一个Student类,从它再间接派生UndergradStudent类。仅保留类中必需的几个方法。方法display可以显示对象中的数据。但它显示的数据及显示多少,要依用来调用该方法的对象的类型而定。UndergradStudent重写了Student中定义的方法display。
//Student.java
publicclass Student
{
private Name fullName;
private String id; // Identification number
public Student()
{
fullName = new Name();
id = "";
} // end default constructor
public Student(String studentId)
{
fullName = new Name();
id = studentId;
} // end constructor
public void display()
{
System.out.println("Student:" + this.getId());
}
public void setId(String studentId)
{
id = studentId;
} // end setId
public String getId()
{
return id;
} // end getId
public String toString()
{
return id + " " +fullName.toString();
} // end toString
public void displayAt(int numberOfLines)
{
for (int count = 0; count <numberOfLines; count++)
System.out.println();
display();
} // end displayAt
} // end Student
//Name.java
publicclass Name
{ private String first; // First name
private String last; // Last name
public Name()
{
} // end default constructor
public String toString()
{
return first + " " +last;
} // end toString
} // end Name
classCollegeStudent extends Student
{
public void display()
{
System.out.println("output:" + this.getId());
}
}
classUndergradStudent extends CollegeStudent
{
public UndergradStudent(StringUndergradStudentId)
{
super();
this.setId (UndergradStudentId);
} // end default constructor
public void display()
{
System.out.println("Undergrad:" + this.getId());
}
}
测试的代码在main()方法中。
publicclass Client
{
public static void main(String[] args)
{
UndergradStudent ug = newUndergradStudent("ug1");
Student s = new Student("stu1");
//第一轮调用
ug.displayAt(2);
s.displayAt(2);
//第二轮调用
s = ug;
ug.displayAt(2);
s.displayAt(2);
//第三轮调用
ug.displayAt(2);
s = (Student)ug;
s.displayAt(2);
}
}
在main()中,分别定义两个类的变量ug和s,并赋了初值。然后调用displayAt()来显示对象的值。
第一轮调用时,因为对象的类型与实际的引用类型是一样的,所以显示的内容也是我们预期的。ug.displayAt(2)显示的是“Undergrad:ug1”,s.displayAt(2)显示的是“Student:stu1”。
第二轮调用前,将s指向ug。实际上,它指向的不再是本类型的对象,而是UndergradStudent类型的实例。使用displayAt()来调用display()时,调用的将是UndergradStudent中的方法。所以显示的内容是一样的,都是“Undergrad:ug1”
方法displayAt定义在类Student中,但它调用定义在类UndergradStudent中的display方法。甚至在类UndergradStudent定义之前,就能为类Student编译displayAt的代码。换句话说,编译的这段代码可以使用displayAt被编译时甚至都还没有写的方法display的定义。
当编译displayAt的代码时,对display的调用产生一条注解,说“使用display的相应定义”。然后,当调用ug.displayAt(2)时,为displayAt编译的代码执行到这条注解,并用与ug对应的display的版本来替换这条注解。因为这种情况下ug是UndergradStudent类型的,所以display的版本是类UndergradStudent中定义的。
决定使用哪个版本的定义,依赖于继承链中接收对象的位置,而不是对象变量名的类型。
给Student类型的变量赋值类Undergradstudent的一个对象,是完全合法的。这里,变量s只是ug指向的对象的另一个名字。即,s和ug都是别名。但对象仍记着它创建为一个UndergradStudent。这种情形下,s.displayAt(2)最终会使用UndergradStudent中给出的display的定义,而不是Student中给出的display的定义。
变量的静态类型(static type)是出现在声明中的类型。例如,变量s的静态类型是Student。静态类型是在代码编译时固定且确定下来的。运行时某一时刻变量指向的对象的类型称为动态类型(dynamic type)。变量的动态类型随运行进程会改变。当执行前一段代码中的赋值语句s = ug时,s的动态类型是UndergradStudent。引用类型的变量称为多态变量(polymorphic variable),因为执行过程中,它的动态类型可以不同于静态类型,且可改变。
具体到我们的例子,Java查看是哪个构造方法创建了对象,从而确定要使用display的哪个定义。即,Java使用变量s的动态类型,来做出判断。
调用稍后可能被重写的方法的这种处理方式,称为动态绑定(dynamic binding)或后绑定(late binding),因为在程序执行之前,方法调用的含义没有与方法调用的位置进行绑定。执行前面这段代码时,如果Java不使用动态绑定,就不会看到Undergraduatestudent的数据。相反,只能看到Student类提供的方法display所显示的内容。
再看第三轮调用。
Java能分清要使用方法的哪个定义,即使类型转型也骗不过它。我们知道,可以使用类型转型将一个值的类型转为其他的类型。尽管有类型转型,s.displayAt(2)还是使用UndergradStudent中给出的display的定义,而不是Student中给出的display的定义。对象的动态类型,而不是它的名字,是选择要调用的正确方法的决定因素。
类型检查和动态绑定。必须要知道动态绑定如何与Java的类型检查互动。例如,如果UndergradStudent是Student类的一个子类,可以将UndergradStudent类型的对象赋给Student类型的变量,如
Student s = new UndergradStudent();
但这还没有完。
虽然,可以将UndergradStudent类型的对象赋给Student类型的变量s,但不能使用s来调用仅定义在UndergradStudent类内的方法。不过,如果在类UndergradStudent的定义中重写了一个方法,则会使用UndergradStudent内定义的这个版本。换句话说,变量决定使用哪个方法名,但对象决定使用方法名的哪个定义。如果想藉由Student类型的变量s命名的对象,来使用首次定义在类UndergradStudent中的方法名,则必须使用类型转型。
示例。Student实现了方法display。且UndergradStudent是Student的子类。下列语句是合法的:
Student s = new UndergradStudent(. ..);
s.setName(new Name("Jamie","Jones"));
s.display();
使用的是UndergradStudent类内给出的display的定义。记住,对象,而不是变量,决定将使用方法的哪个定义。若仅在UndergradStudent中定义了setDegree()方法,则下列语句是不合法的:
s.setDegree("B.A.");// ILLEGAL
因为setDegree不是Student类内的方法名。记住,变量决定使用哪个方法名。
变量s是Student类型的,但它指向UndergradStudent类型的一个对象。那个对象仍可以调用方法setDegree,但编译程序不知道这一点。为了让调用合法,必须进行类型转型,如下这样:
UndergradStudent ug =(UndergradStudent)s;