继承
单继承(single inheritance)
在面向对象一章中我们学习了OO的特征之一:继承,是任何面向对象的语言必然实现的特性,java也不例外,但我们应该注意的是,java和某些面向对象语言(如c++)在实现继承的不同之处在于java只支持单继承,不支持多重继承。
即,java中一个类只能继承于另一个类。我们将被继承的类称之为父类(基类),继承类称之为子类(派生类)。在java中用关键字extends来实现单继承,语法如下:
class subclass extends superclass{...}
在前面所讲已知,实现继承关系的类之间有着必然的联系,不能将不相关的类实现继承,就象人类不能继承于鸟类!
那怎么去判断类和类之间是否有着必然联系呢?实际上,在第一章里面,我们已知当某类A和类B之间有着共同的属性和行为时,那么类A和类B之间就可能是继承关系或者有着共同的父类。
下面,假设我们开发某公司的员工管理系统,已知类Manager和类Employee,代码如下:
class Employee
{
public String f_name;
public String l_name;
public float salary = 0.0f;
public String getEmpDetails()
{....}
}
class Manager
{
public String f_name;
public String l_name;
public float salary;
public String dept;
public String getEmpDetails()
{....}
}
通过分析得知,在类Employee和类Manager中存在许多共同的属性和行为,在现实生活中,Manager是公司Employee之一,因此,我们可以将Manager类定义成Employee类的子类,修改类Manager如下:
class Manager extends Employee
{
public String dept;
public String getEmpDetails()
{
return "This is Manager!";
}
}
UML中类图表示为:
图片附件: 游客没有浏览图片的权限,请
登录 或
注册
大家可能会质疑:Manager类重新定义后,原有的属性减少了(f_name,f_name,salary),岂不是违背了需求?!
当类A继承于类B时,子类A拥有父类B的所有成员变量和方法,换句话说,子类继承了父类的所有成员属性和方法,在父类中已定义的属性和方法,在子类中可以无需定义(除非方法覆盖)。
所以,在子类Manager中已继承了父类Employee中的属性和方法,无需再定义在父类中已有的属性(f_name,f_name,salary)。
好,我们已经学会了基本的继承语法,下面就来探讨一下继承带来的一些好处:
a.减少代码冗余
从上面的例子就可以看出,类Manager通过继承而无需再定义属性(f_name,f_name,salary),从而减少了代码量,试想一下,当公司员工分为许多不同级别员工时(如定义秘书、工程师、CEO等员工类),如果没有继承,那将是怎样的结果?
b.维护变得简单
假设公司要求给所有员工添加生日这一属性,那么,在没有继承时,我们的维护将变得困难(需修改每一个级别的员工类)。
c.扩展变得容易
当一个新的级别员工类需创建时,我们只需将该类继承所有员工父类Employee,接着再定义属于该员工的特有属性即可。
当然,以上所举例子只是继承带来的好处的部分体现,在下面的学习中我们将逐渐深入体会继承带来的优势。
小测试:以下代码是否正确?
class B{...}
class C{...}
class D{...}
class A extends B,C,D
{.....}
java中一个类只能继承一个类,但一个类可以被多个类所继承。如:
class Engineer extends Employee{...}
class Secretary extends Employee
{...}
class Manager extends Employee
{
public String dept;
...
}
以上三个不同类分别继承了Employee类,即三个类拥有从父类继承过来的共同属性和方法。但是,请注意:这仍旧是单继承!以UML类图表示为:
图片附件: 游客没有浏览图片的权限,请
登录 或
注册
提醒:构造方法不能被继承!一个类得到构造构造方法只有两种途径:自定义构造方法;使用JVM分配的缺省构造方法。但是,可以在子类中访问父类的构造方法,后面我们会深入。
访问控制
在java中是通过各种访问区分符来实现数据封装的,共分为四种访问级别(由高到低):private(私有)、friendly(缺省)、protected(受保护)、public(公共)。
注意:以上四种访问修饰符可以作用于任何变量和方法,类只可以定义为默认或公共级别(嵌套类除外)。
¨
public(公共)
当变量或方法被public修饰时,该变量和方法可以在任何地方(指的是任何包中)的任何类中被访问。
//类PublicSample中的构造方法、成员变量及方法均被定义为公共的访问级别
package com.itjob;
class PublicSample {
public PublicSample(){....}
public int num1;
public byte bt1;
public char ch1;
public void method1(){....}
}
通过以上定义,类PublicSample中的成员方法和变量可以在任何包中的任何类中访问,即访问是公共的,不受限制的。以下访问都是允许的:
访问1
class PublicSample {
......
public static void main(String[] args) {
new PublicSample().num1 = 100;
new PublicSample().ch1 = '\u0000';
new PublicSample().bt1 = 10;
new PublicSample().method1();
}
}
访问2
package com.java;
import com.itjob.*;
class A {
public void method1() {
new PublicSample().ch1 = 'a';
....
}
}
¨
protected(受保护的)
当类的变量或方法被protected修饰时,该变量和方法只可以在同包中的任何类、不同包中的任何当前类的子类中所访问。即不同包中的任何不是该类的子类不可访问级别为protected的变量和方法。
//受保护的变量
protected Stirng str = "";
//受保护的方法
protected String get(){return "";}
¨
friendly(缺省的)
当类的变量和方法没有显式地被任何访问区分符修饰时,该变量和方法的访问级别是缺省的。缺省的变量和方法只能在同包的类中访问。
//缺省访问级别的变量和方法、类
Float f1 = null;
void method(){...}
class C1{...}
¨
private(私有的)
被private所修饰的所有变量和方法只能在所属类中被访问。即类的私有成员和变量只能在当前类中被访问。
//私有的构造方法和成员变量、方法
private ClassName(){....}
private int num = 0;
private void method(){....}
通过以上学习,我们可以通过以下表格来描述四种访问区分符的访问限制:
方法重载(method overloading)
¨
成员方法重载
学习重载之前,我们来了解一下在java中方法的特征。
在java中,每一个方法都有自己的特征,其特征主要是指方法名以及方法的参数。
void method1(){}
void method2(){}
method1()和method2()可以被理解为是两个方法名不同的方法,即方法的特征不一致。
void method1(int x){}
void method1(){}
第一个method1()与第二个method1()虽然名字一样,但是却有不同的参数,因此,这两个同名方法仍有着不同的特征。
对于java编译器来说,它只依据方法的名称、参数列表 的不同来判断两个方法是否相同,如果出现两个名称相同、参数也完全一致的方法,那么编译器就认为这两个方法是完全一样的,也就是说方法被重复定义!
以下定义是错误的:
class ClassName {
void m1(){}
void m1(){}
}
对于以上两个方法定义语句,java解释器认为这两个方法完全相同,当执行到第二条语句时,它会告诉你方法m1()已在类ClassName中被定义!
可以这样理解,当我们在一个类中不能定义相同名称的多个方法,除非这些方法具有不同的方法特征(参数的不一致)。将上面语句修改为:
class ClassName {
void m1(int x){}
void m1(){}
}
这样,虽然方法名相同,但由于两个方法的参数不一致,因此,编译器就认为这是两个不同的方法,从而不会产生歧义。
好,在这里,大家可能就会质疑,把其中的某个方法换成不同的名称不也可以正常运行吗?
对,这样确实可以解决问题,但我们知道,在现实中,往往一个类会实现复杂的功能,其中定义的多种方法可能实现的功能意义都是一样,比如我们已经熟悉的System类中的静态对象中方法println(),在该类中println()被定义了多个,每一个方法都有不同的参数,现在我们已知道每一个println()都具有相同的功能:在控制台上输出内容!我们来假想一下,如果按照每个方法定义一个不同名称,那么我们将在System类中定义十多种不同名称的打印方法,虽然功能实现了,首先,我们是否需要编写代码前给这十几种方法取不同名称,并且还得保证名称唯一,这就会增加我们的工作量;其次我们还得记住每一个方法名对应的功能,如果稍有记错,那就会得到错误的结果!
因此,我们有更好的解决办法,通过重载,可以在一个类中定义相同名称、不同参数的实现相同功能的多个方法,这样就避免了给每个方法取不同名称、熟记每个不同名的方法对应的功能的额外工作量,提高了我们的开发效率。
当一个类中的多个同名方法满足以下条件时之一时,即实现了方法重载:
a.不同的参数个数
b.不同的参数类型
c.不同的参数顺序
小测试:以下哪几组方法实现了重载,满足了重载的那一个条件?
组一:
void m1(int x){}
void m1(int x, int y){}
组二:
void m1(int x, String str){}
void m1(String str, int x){}
组三:
void m1(int x, int y){}
void m1(int y, int x){}
组四:
void m1(int x){}
int m1(int x, int y){}
组五:
void m1(int x){}
void m2(int x){}
¨
构造方法重载
如果有一个类带有几个构造函数,那么也许会想复制其中一个构造函数的某些功能到另一个构造函数中。可以通过使用关键字this作为一个方法调用来达到这个目的。
public class Employee {
private String name;
private int salary;
public Employee(String n, int s) {
name = n;
salary = s;
}
public Employee(String n) {
this(n, 0);
}
public Employee() {
this(" Unknown ");
}
}
在第二个构造函数中,有一个字符串参数,调用this(n,0)将控制权传递到构造函数的另一个版本,即采用了一个String参数和一个int参数的构造函数中。
在第三个构造函数中,它没有参数,调用this("Unknownn")将控制权传递到构造函数的另一个版本,即采用了一个String参数的构造函数中。
注:对于this的任何调用,如果出现,在任何构造函数中必须是第一个语句。
构造函数中调用另一构造函数,其调用(this()、super())有且只能有一次,并不能同时出现调用。
分析例题3(Example3.java)的执行结果。
提醒:方法的重载都是基于同一个类!
方法覆盖(method overriding)
覆盖是基于继承的,没有继承就没有覆盖。在java中,覆盖的实现是在子类中对从父类中继承过来的非私有方法的内容进行修改或扩展 的一个动作(注意:不能违反访问级别的限制,即子类方法的访问级别不能低于父类方法的访问级别 )。
下面我们来分析一个例子:
class A {
public void method() {
System.out.println("SuperClass method()");
}
}
class B extends A {
public static void main(String[] args) {
new B().method();
}
}
执行以上代码得到的打印结果:"SuperClass method()"。
结果分析:
子类B继承了父类A中的公共方法method(),调用子类B的method()方法实际就是调用从父类中继承过来的method()的方法。
修改子类B:
class B extends A
{
public void method() {
System.out.println("SubClass method()");
}
// main()
..............
}
再次执行以上代码得到的结果是:"SubClass method()"。
结果分析:
在类B中实现方法覆盖,父类 方法method()被子类B 覆盖了。在子类B定义了一个返回类型、方法名、方法参数列表 都和从父类中继承过来的方法method()一样的方法,那么,新定义的方法就会覆盖原有的方法,实际上就是对从父类继承过来的方法重写(覆盖) !
实现方法的覆盖必须满足以下所有条件:
a. 覆盖方法的返回类型必须与父类中被覆盖方法的返回类型相同
b. 覆盖方法的参数列表 类型、次序和方法名称必须与被覆盖方法的参数列表 类型、次序和方法名称相同
c. 覆盖方法的访问级别不能比被覆盖方法访问级别低
d. 覆盖方法不能比它所覆盖的方法抛出更多的异常。(异常将在下一个模块中讨论)
父类中定义的方法:public void m1(int x, String str){...}
以下是在各个子类中定义的方法:
子类1:public String m1(int x, String str){...}
分析:没有实现覆盖,方法的返回类型不同
子类2:public void m1(String str, int x){...}
分析:没有实现覆盖,方法的参数类型不同
子类3:void m1(int x, String str){...}
分析:没有实现覆盖,方法的访问级别不能被降低
子类4:public void m1(int x, String str){...}
分析:已实现覆盖,满足覆盖的所有条件