程序员面试必考题(三十)--Java中多态的基本概念


术语“多态”来自于希腊语,意思是“多种形式”。多态作为一个概念,实际上在英文中常见。例如,英语教学“做你最喜爱的运动”对不同的人意味着不同的事情。对某个人它意味着是打蓝球。对另一个人它意味着是踢足球。在Java中,多态(polymorphism)允许同一条程序指令在不同的上下文中意味着不同的事情。具体来说,一个方法名当作一条指令时,依据执行这个动作的对象类型,可能导致不同的动作。


示例。定义一个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;

ug.setDegree("B.A.");// LEGAL
(文章来自微信公众账号:开点工作室(kaidiancs))

《横扫offer---程序员招聘真题详解700题》,开点工作室著,清华大学出版社出版,天猫、京东等各大网上书店及实体书店均已开始发售。

全部评论

相关推荐

JamesGosling1:同一个公司的实习为什么写三次,就算是不同的小组的话,直接写一段要好点吧
点赞 评论 收藏
分享
挣K存W养DOG:我记得好多人说这个公司就是白嫖方案的,现在有大体方案要让你给他展示实现细节了,也是无敌了
点赞 评论 收藏
分享
评论
点赞
收藏
分享

创作者周榜

更多
牛客网
牛客企业服务