3.2 类的继承性
继承性是面向对象语言的又一个基本特性,它是一种由已有的类创建新类的机制,正是因为有这种机制,才使得面向对象语言编写的程序代码具有较高的复用性。
3.2.1 类的继承
继承是指相关的类之间的层次关系。利用继承,可以先创建一个共有属性的一般类,根据该一般类再创建具有特殊属性的新类,新类继承一般类的状态和行为,并根据需要增加它自己的新的状态和行为。
由继承而得到的类称为子类,被继承的类称为父类。类继承的具体定义格式如下。
class subclassname extends superclassname{ … }
在上述类的声明中,通过使用关键字extends来创建一个类的子类,其中,subclassname是声明的子类的类名,superclassname是继承的父类的类名。
在3.1.1小节中介绍过Java类的声明,在当时定义的类并没有使用extends关键字,那么是不是表示当时定义的类没有继承其他的类呢?答案是否定的。在Java语言中,Object类是所有类的祖先类,也就是说所有的类都是直接或者间接继承Object类的。所以当没有使用extends关键字时,就表示这个类只是Object类的子类。
说明
与C++语言中的继承不同,Java语言不支持多重继承,子类只能有一个父类。
【实例3-7】实现类的继承。
首先定义父类Student。
01 class Student { 02 int stu_id; //父类的成员变量 03 void set_id(int id) { //父类的成员方法 04 stu_id=id; 05 } 06 void show_id() { //父类的成员方法 07 System.out.println(“The student_ID is:”+stu_id); 08 } 09 }
【代码说明】该类中第2行定义一个int型的成员变量,第3行和第6行分别定义名称为set_id和show_id()的两个成员方法。
然后定义继承Student类的子类Granduate类。
01 class Granduate extends Student { 02 int dep_number; //子类的成员变量 03 void set_dep(int dep_num) { //子类的成员方法 04 dep_number=dep_num; 05 } 06 void show_dep() { //子类的成员方法 07 System.out.println(“The department number is:”+dep_number); 08 } 09 }
【代码说明】该类是Student类的子类,因此,它可以直接使用Student类中定义的成员变量和成员方法,而无须重复定义。除此之外,在第2行还定义了自己的成员变量dep_number,在第3行和第6行分别定义了成员方法set_dep()和show_dep()。
最后定义调用Granduate类的应用程序类。
01 public class Student_Show { 02 public static void main(String args[]) { 03 Granduate sun=new Granduate(); //创建子类对象 04 sun.set_id(102); //调用父类中定义的方法 05 sun.set_dep(6); //调用子类中定义的方法 06 sun.show_id(); //调用父类中定义的方法 07 sun.show_dep(); //调用子类中定义的方法 08 } 09 }
【代码说明】在应用程序类中的第3行创建了Granduate类,第4行和第6行分别调用其父类中定义的成员方法,第5行和第7行则分别调用子类中定义的成员方法。
【运行结果】该程序的输出结果如图3.3所示。
图3.3 程序运行结果
说明
根据类的继承关系,创建的子类对象一定也是父类的对象。例如上例中创建的Granduate类的对象sun也是其父类Student类的对象,但是反过来,父类的对象则不一定是子类的对象。
3.2.2 方法的重载和覆盖
当类之间出现继承关系之后,在子类中就将可能出现成员方法的重载和覆盖。这是面向对象编程中多态性的体现。
1.方法的覆盖
如果在子类中定义了与父类中的成员方法同名的成员方法,那么当子类的对象在程序中调用该成员方法时,调用的将是子类中新定义的成员方法,而子类中继承下来的父类中的成员方法就将被覆盖掉了,如果要访问被覆盖的成员方法,则只有通过父类的对象来调用它了。
例如,在实例3-7中子类Granduate中定义的成员方法与其继承的父类中的成员方法没有同名,所以不存在方法的覆盖。下面修改Granduate类的定义,具体代码如下:
01 class Granduate extends Student { 02 int dep_number; 03 void set_dep(int dep_num) { 04 dep_number=dep_num; 05 } 06 //定义与父类中成员方法同名的方法,实现方法的覆盖 07 void show_id() { 08 System.out.println(“The department number is:”+dep_number); 09 } 10 }
【代码说明】 子类Granduate中的第7行,定义了一个与父类Student中成员方法同名的方法,这就是方法的覆盖。
然后将调用Granduate类的应用程序类的代码修改如下:
01 public class Student_Show { 02 public static void main(String args[]) { 03 Granduate sun=new Granduate(); 04 sun.set_id(102); 05 sun.set_dep(6); 06 //由于方法覆盖,这里调用的将是子类中定义的show_id()方法 07 sun.show_id(); 08 } 09 }
图3.4 调用子类中覆盖后的方法
【代码说明】在应用程序的第7 行通过子类实例调用了show_id()方法,由于在子类和父类中都定义了该方法,那么通过子类调用时将调用子类中定义的show_id()方法。
【运行结果】执行应用程序时,将调用子类中定义的show_id()方法,最终程序输出结果如图3.4所示。
2.方法的重载
如果在一个类中,定义了两个或者两个以上的具有不同参数列表的同名方法,这种情况就被称为方法的重载,方法的重载体现了Java作为面向对象语言的多态性。
那么什么是不同的参数列表呢?仅仅是形参名称不同的两个参数列表实际上仍然是相同的,但如果是参数的个数或者参数的数据类型不同,则这样的参数列表才是不同的。因此,同一个对象在调用具有相同名称的方法时,会根据传递进来的参数的个数或数据类型的不同,而自动选择对应的方法。
在类的继承关系中,如果当子类中定义了与父类中同名的方法时,但是方法的参数列表不同,这时在子类中将对该方法进行重载,即子类中既继承下来父类的方法,又定义了自己的新的成员方法,不会对父类的方法进行覆盖。
下面修改实例3-7中的Granduate类的定义,具体代码如下:
01 class Granduate extends Student { 02 int dep_number; 03 void set_dep(int dep_num) { 04 dep_number=dep_num; 05 } 06 //定义与父类中成员方法同名的方法,但参数列表不同 07 void show_id(String name) { 08 System.out.println(“The department number of”+name+” is:”+dep_number); 09 } 10 }
【代码说明】在该类中的第7行定义了与父类中成员方法同名的方法,但是与父类中的show_id()方法的参数列表不同,因此实际上在Granduate类中将存在两个名称为show_id的成员方法,但是这两个方法一个是参数为空,一个是具有一个字符串类型参数。
然后将调用Granduate类的应用程序类的代码修改如下:
01 public class Student_Show { 02 public static void main(String args[]) { 03 Granduate sun=new Granduate(); 04 sun.set_id(102); 05 sun.set_dep(6); 06 //由于方法重载,这里调用子类中的两个不同的show_id()方法 07 sun.show_id(); 08 sun.show_id(“sun”); 09 } 10 }
【代码说明】在应用程序的第7 行和第8 行分别两次调用了show_id()方法,但是这两次调用的方法的参数不同,第7行将调用父类Student中定义的show_id()方法,第8行将调用子类Granduate中定义的show_id()方法。
【运行结果】执行应用程序时,将调用子类中重载的两个不同的show_id()方法,最终程序输出结果如图3.5所示。
图3.5 调用重载的方法
3.2.3 抽象类和最终类
在一般情况下,Java中的所有的类都可以被其他类继承,也可以直接被实例化使用。但是有两种特殊的类,一种是专门用来给其他类做父类的,自己不能够被实例化,另一种是不能够再被其他类所继承的。这就是本小节要详细介绍的抽象类和最终类。
1.抽象类
面向对象的编程思想使得开发者可以编写模块化的程序,然后用类似搭积木的方式来组织这些模块以实现特定的功能。甚至可以对一个未定的功能预留一个模块的位置,留待以后去实现。
将这种思想应用于类的定义中,对于某种未定的操作,可以在类中先只定义方法声明,不定义方法实现,以后再在这个类的子类中去具体实现这个方法。这样的方法被称为抽象方法,而包含抽象方法的类就被称为抽象类。
抽象方法和抽象类的定义方式都是在方法名和类名之前加上abstract关键字。其中,抽象方法的声明与普通方法基本一致,也需要有方法名、参数列表和返回值类型,只是没有方法体。类的成员方法的声明格式如下:
[<修饰符>] abstract <返回类型> <方法名> ([<参数列表>]);
说明
在参数列表的括号后面一定要加上分号。
抽象类由于包含抽象方法,因此它不能使用new关键字进行实例化,也不能在类中定义构造函数和静态方法,但是抽象类中可以包含具体实现了的非静态方法。所以读者不要误解成抽象类中的方法全部都是抽象方法,而是只有一个类中包含一个抽象方法,那么这个类就是抽象类。抽象类的子类中应该对抽象方法进行具体的实现,否则子类本身也就成为抽象类了。
【实例3-8】抽象类的定义和使用。
首先定义抽象类Student,该类中包含一个抽象方法show_id()。
01 abstract class Student { 02 int stu_id; 03 void set_id(int id) { 04 stu_id=id; 05 } 06 //定义抽象方法 07 abstract void show_id(); 08 }
【代码说明】在该抽象类的第7行定义了一个抽象方法show_id()。
然后定义继承Student类的子类Granduate类。
01 class Granduate extends Student { 02 int dep_number; 03 void set_dep(int dep_num) { 04 dep_number=dep_num; 05 } 06 //具体定义父类中的抽象方法 07 void show_id() { 08 System.out.println(“The department number is:”+dep_number); 09 } 10 }
【代码说明】在该类的第7行定义了父类中的抽象方法show_id()的具体实现。
说明
必须具体定义父类中的抽象方法,否则该类也将成为抽象类。
最后定义调用Granduate类的应用程序类。
01 public class Student_Show { 02 public static void main(String args[]) { 03 Granduate sun=new Granduate();//创建子类的实例 04 sun.set_id(102); 05 sun.set_dep(6); 06 sun.show_id();//调用子类中已经实现了的父类中的抽象方法 07 } 08 }
【代码说明】在应用程序类的第6行调用show_id()方法,由于该方法已经在Granduate类中被实现,所以可以被正确调用。
【运行结果】执行应用程序,将调用子类中具体定义后的show_id()方法,最终程序输出结果与如图3.4所示的结果一样。
2.最终类
如果在一个类定义前加上final关键字,那么这个类就不能再被其他的类所继承,这样的类被称做最终类。最终类可以避免开发者编写的类被别人继承后加以修改。
例如,下面的类定义和继承:
final class A { ….. } class B extends A { …. }
这样的继承将是错误的,在程序编译时将提示类A是不能被继承的。
final关键字除了可以用在声明最终类时,还可以应用在成员变量和成员方法的声明上,如果在成员变量的定义前加上final关键字,则这个变量的值在以后的程序中只能被引用,而不能被改变,final在这里的作用相当于C++语言中的const。
说明
使用final的成员变量必须在声明时同时给定初始值。
例如,声明不能被修改的成员变量的示例代码如下:
class A { final float PI=3.14159f; final float E=2.71828f; …. }
如果在成员方法定义前加上final关键字,那么表示这个方法在子类中不能被覆盖。