|
教学提示:封装、继承与多态是面向对象程序设计的三个核心特性。封装是面向对象技术中的一个重要概念,是一种信息隐藏技术,它体现了面向对象程序设计的思想。继承是面向对象程序设计方法的一个重要手段,通过继承可以更有效地组织程序结构,明确类间的关系,充分利用已有的类来完成更复杂、更深入的程序开发。而多态允许以一种统一的风格处理已存在的变量和相关的类,多态性使得向系统增加功能变得容易。
教学目标:理解封装、继承和多态的概念。了解如何在应用程序的开发中利用这些思想简化程序的开发。
7.1 封装
7.1.1 封装的概念
封装的含义是:类的设计者把类设计成一个黑匣子,使用者只能看见类中定义的公共变量和公共方法,而看不见方法的具体实现细节,也不能对类中非公共的数据进行操作。这样可以防止外部的干扰和误用。
封装的目的在于将对象的使用者和对象的设计者分开,使用者不必知道行为实现的细节,只须用设计者提供的接口来访问对象。同时减少了程序各部分之间的依赖性,降低了程序的复杂性,由于隐藏了其内部信息的细节,使内部信息不易受到破坏,安全性有了保证,同时也为外界访问提供了简单方便的界面。
7.1.2 封装的实现
Java定义了4种访问权限:公有的(public)、保护的(protected)、私有的(private)和默认的。
1.公有的(public)
用public修饰的类成员(包括变量和方法)称为公有的,公有成员允许Java系统中所有的类访问,即不仅允许类内部的成员访问,也允许同一个包或不同包中的类直接访问。外界可以直接存取公有数据,调用公有方法。
【例7-1】 成员变量说明为公有
//file name:Demo1.java
class p1
{
public intx; //定义公有变量
public voidset_x(int i)
{ x=i; }
publicvoid show_x() //定义公有方法
{ System.out.println("x="+x); }
}
class Demo1
{
publicstatic void main(String args[])
{
p1obj=new p1();
obj.set_x(3); //通过调用类的公有方法给公有变量赋值
obj.show_x(); //通过调用类的公有方法显示公有变量的值
obj.x=18; //直接给obj对象的公有变量赋值
System.out.println("x="+obj.x); //直接输出公有变量的值
}
}
程序输出结果如图7-1所示:
x=3;
x=18;
图7-1 程序运行结果
2.私有的(private)
用private修饰的类成员称为私有的,类的私有成员只能被这个类的方法直接访问。如果在例7-1中将x声明为私有的,则会出现编译错误,即obj.x=18这个语句出错,因为私有变量不能在类外直接访问,只能通过obj对象的公有方法访问x。
一般把不需要外界知道的数据说明为私有,这样有利于数据的安全性,也符合程序设计中隐藏内部信息处理细节的原则。
3.被保护的(protected)
用protected修饰的类成员成为被保护的。类的被保护成员允许其归属的类、由此类派生的子类以及同一个包中的其他类访问。
如果一个类有派生子类,为了使子类能够直接访问父类的成员,则把这些成员(大部分是数据)说明为被保护的。
【例7-2】访问类的被保护成员
//file name:MyDemoClass.java
class ParentClass
{ protectedint n=3;
protectedvoid show_n()
{ System.out.println("n="+n); }
}
class MyDemoClass extends ParentClass
{
publicstatic void main(String args[])
{
MyClassobj=new MyClass();
obj.show_n(); //在MyClass类中直接访问父类中被保护的方法
obj.n=18; //直接访问父类中被保护的数据
obj.show_n();
}
}
程序的运行结果如图7-2所示:
n=3;
n=18
图7-2 程序运行结果
4.默认的
如果类成员前面没有任何关键字修饰,则称为默认的。默认的类成员除了允许被其归属的类访问外,还允许同一个包中的其他类访问。若两个类不在同一个包中,即使是这个类的子类,也不允许访问这个类的默认成员。
如果将例7-1中的变量x前面的public关键字去掉,则成为默认类型的变量,程序运行结果不变。如果将例7-2中的变量x前面的protected关键字去掉,则运行结果同样不变。
对类成员的4种访问权限总结在表7-1中。
表7-1 类成员的访问权限
|
同一个类 |
同一个包 |
不同包中的子类 |
其他包 |
public |
√ |
√ |
√ |
√ |
protected |
√ |
√ |
√ |
|
默认的 |
√ |
√ |
|
|
private |
√ |
|
|
|
7.2 继承
7.2.1 继承的概念
继承是一种由已有类创建新类的机制。利用继承,我们可以先创建一个共有属性的一般类,根据该一般类再创建具有特殊属性的新类。新类继承一般类的状态和行为,并根据需要增加它自己的状态和行为。
从现有类出发定义一个新类,称为新类继承了现有的类,其中被继承的现有类叫做超类(superclass)或父类,由继承而得到的类称为子类(subclass)。
例如,当类sub1继承类super时,就表明sub1是super的子类,即super是sub1的超类(父类)。子类从超类继承变量和方法,从而可以共享数据和方法。sub1类由两部分组成:继承部分和增加部分。继承部分是从super继承过来的,把super的成员映射成sub1的继承成员;增加部分是专为sub1编写的代码,如图7-3所示。
图7-3 继承性
在Java中规定,一个父类可以同时拥有多个子类,但一个子类只能有一个父类,即单重继承,而且允许多层继承,即子类还可以有它自己的子类,在下一层的继承关系中原先的子类就变成了父类。这样的继承关系就形成了继承树。
7.2.2 类继承的实现
类继承用关键字extends实现,格式为:
class 子类名extends 父类名
{子类的类体}
如果没有extends子句,则这个类直接继承Object。
【例7-3】 类继承和传递性
设计思路:设计三个类A、B、C,A类中定义多个成员变量和方法,B类继承A类,C类继承B类,并且在B类中增加新的成员变量,在C类中增加新的成员变量。分别创建三个类的对象,在B、C类对象中访问父类的成员变量,观察继承的传递性。
代码://file name: MyClass.java
class A
{
int a1=1;
public int a2=2;
protected int a3=3;
private int a4=4;
int geta4()
{ return a4;}
}
class B extends A
{
intb=5; //添加新的数据成员b,同时B类还有从A类继承过来的数据成员
//a1,a2,a3和成员方法geta4()
}
class C extends B
{
int c=6; //添加新的数据成员c
voidchange() //添加新的方法change()
{
a1+=10;a2+=10; a3+=10; b+=10; c+=10;
}
}
class MyClass
{
publicstatic void main(String args[])
{
A aa=new A();
System.out.println("A:"+aa.a1+""+aa.a2+" "+aa.a3+" "+aa.geta4());
B bb=new B();
System.out.println("B:"+bb.a1+""+bb.a2+" "+bb.a3+" "+bb.geta4()
+""+bb.b);
C cc=new C();
System.out.println("C:"+cc.a1+""+cc.a2+" "+cc.a3+" "+cc.geta4()+
""+cc.b+" "+cc.c);
cc.change();
System.out.println("C:"+cc.a1+""+cc.a2+" "+cc.a3+" "+cc.geta4()
+""+cc.b+" "+cc.c);
}
}
请读者自己写出输出结果。
此例中A、B、C 3个类的层次关系如图7-4所示。
图7-4 类继承的层次关系
7.2.3 成员变量的继承
子类继承父类中所有可被子类访问的成员变量。继承原则如下:
1. 能够继承那些声明为public和protected的成员变量。
2. 能够继承同一包中的那些默认修饰符的成员变量。
3. 不能继承那些声明为private的成员变量。
4. 如果子类声明一个与父类成员变量同名的成员变量,则不能继承父类的成员变量。此时子类的成员变量称做隐藏了父类的成员变量。
总之,子类可继承父类的public、protected和默认修饰变量,不能继承private变量。反之,如果父类不允许其子类访问它的某些成员,那么它必须以private方式声明该成员。这一点充分体现了类封装的信息隐蔽原则。
【例7-4】 子类继承父类的成员变量。
设计思路:设Person类定义抽象“人”具有的特征和行为包括姓名、年龄。Student类定义学生这群人的特征和行为,包括姓名、年龄、系别等。分别设计Person类与student类。
代码:
public class Person
{
Stringname; //姓名
intage; //年龄
}
class Student
{
Stringname; //姓名
intage; //年龄
Stringdept; //系别
}
这样的定义其中有大量的重复。利用继承的原则,将Student类定义为Person类的子类。程序如下:
public class Person
{
Stringname; //姓名
intage; //年龄
}
class Student extends Person //Student是Person类的子类
{
String dept; //系别
}
此时Student共有3个成员变量,从Person类中继承了两个成员变量name和age,自己增加了成员dept。
7.2.4 成员方法的继承
子类继承成员方法的规则类似于继承成员变量的规则:子类继承父类中所有可被子类访问的成员方法。继承规则如下:
1. 能够继承那些声明为public和protected的成员方法。
2. 能够继承同一包中的默认修饰符的成员方法。
3. 不能继承那些声明为private的成员方法。
4. 不能继承父类的构造方法。
如果子类方法与父类方法同名,则不能继承。子类方法称为覆盖了父类中的那个方 法。
总之,子类可继承其父类的public、protected和默认修饰方法,不能继承private方法。子类除了可以继承父类中的变量及方法,还可以增加自己的成员。当一个父类成员不适合该子类时,子类会重新定义它,如果是重新定义的是成员变量就是隐藏父类的变量,如果是成员方法就是覆盖父类的方法。
【例7-5】 子类继承超类的成员方法。
设计思路:重新设计Person类及其子类Student1。Person类中声明了两个保护变量name、age及两个方法:setdata用于赋初值,print用于打印成员变量的值。Student1类继承Person类,增加了自己的成员变量dept。
代码://file name:Student1.java
public class Person
{
protectedString name; //保护成员
protectedint age;
void setdata(Stringn1,int a1)
{
name=n1;
age=a1;
}
public voidprint()
{
System.out.println(name+","+age);
}
}
class Student1 extends Person
{
protectedString dept;
publicstatic void main(String args[])
{
Person p1=new Person();
p1.setdata("张军",21);
p1.print();
Student1s1=new Student1();
s1.setdata("陈丽华",20); //调用父类的成员方法
s1.dept="计算机系"; //访问本类的成员变量
s1.print();
}
}
程序的运行结果如图7-5所示:
张军:21
陈丽华:20
图7-5 程序运行结果
在该程序中,语句:
p1.setdata(“张军”,21);
表示父类Person对象p1通过调用本类方法为p1的成员变量name、age赋值。而Student1子类继承了其父类Person的成员变量name、age及方法setdata、print,语句:
s1.setdata(“陈丽华”,19);
表示子类对象s1通过调用继承来的setdata方法,为s1的成员变量name、age赋值。
在Student1子类中虽然增加了成员变量dept,但在使用中仍然存在问题,不够圆满:
1.调用s1.setdate()只能为超类中成员赋值,却无法为子类成员dept赋值。这是因为s1所调用的setdate和print方法是由超类定义的,其中没有对dept的操作。改进的方法是使用构造方法。
2.调用s1.print()只能显示超类成员值,却无法显示子类成员dept的值。为了能使s1.print()显示dept的值,就需要在Student1子类中对超类的方法print进行重新定义,称为方法的覆盖。
7.2.5 重写
重写是指在继承过程中,子类中的成员(包括数据和方法)与其父类中的成员同名,但功能不同,此时,子类的成员“覆盖”从父类继承过来的成员。包括两种情况:一是数据覆盖,称为数据隐藏,即父、子类中数据成员的名称相同,类型不同,它们实际上是完全不同的两个数据;二是方法覆盖,称为方法重写,即父、子类中方法的名称相同,参数表也完全相同,但功能不同。
在数据隐藏和方法覆盖后,子类成员覆盖了父类的同名成员,要访问父类的这些成员,
需用super关键字来引用当前类的父类。super的用法有3种情况。
1.super.变量名:访问父类中被隐藏的成员变量。
2.super.方法名([参数表]):调用父类中被重写的方法。
3.super([参数表]):调用父类的构造方法,此时,可用super来表示父类的构造方法。
【例7-6】 变量隐藏和方法重写。
设计思路:设计类A和类B,类B继承类A,在类A中定义一个整型变量A和一个方法show(),在类B中将A变量类型改变为double型,并将show()方法的方法体重新写,改变在类A中的功能。在main()中创建一个类B的对象调用show()方法。
代码://file name:TestClass.java
class A
{
intx=1;
voidshow()
{
System.out.println("class A:");
}
}
class B extends A
{
double x=7.8; //子类B的成员变量double型的x隐藏了父类A的int型的x
voidshow() //子类B的成员方法覆盖了父类A的成员方法show()
{
System.out.println("class B:");
}
voidshow1()
{
super.show(); //访问父类A的成员方法show()
System.out.println(super.x); //访问父类A的成员变量x
show(); //访问本类B的成员方法show()
System.out.println(x); //访问本类B的成员变量x
}
}
class TestClass
{
publicstatic void main(String srgs[])
{
Bbb=new B();
bb.show1();
}
}
输出结果如图7-6所示:
class A:
1
class B:
7.8
图7-6 程序运行结果
7.3 多态
多态(polymorphism),是指一个名字可具有多种语义。在面向对象语言中,多态是在一棵继承树中的类中可以有多个同名但不同方法体及不同形参的方法。通常有两种途径实现多态:方法的重载和覆盖。
多态性允许以统一的风格处理已存在的变量及相关的类。多态性使得向系统增加新功能变得容易。继承性和多态性是降低软件复杂性的有效技术。
7.3.1 方法的重载
方法重载时:
l 参数必须不同即参数个数不同,类型也可以不同。
l 返回值可以相同,也可以不同。
重载的价值在于它允许通过使用一个普通的方法名来访问一系列相关的方法。当调用一个方法时具体执行哪一个方法根据调用方法的参数决定,Java运行系统仅执行与调用的参数相匹配的重载方法。尽管Java并没有规定重载方法之间必须有联系,但习惯上,为了程序的可读性,最好重载相同含义的方法。
例如,输出语句print的参数可以是Java中的任何基本类型,其实就是对print()方法的重载:
public void print(boolean b)
public void print(char c)
public void print(int i)
public void print(long l)
public void print(float f)
public void print(double d)
【例7-7】 构造方法的重载。
设计思路:本例还是以Person构造方法为例说明方法的重载。子类继承超类中所有的成员变量和成员方法,但子类不能继承超类的构造方法。解决办法有两种:一是使用默认的构造方法,二是编写多个直接的构造方法。本例使用第二种方法。
代码: //file name:Student.java
class Person
{
static intcount=0;
protectedString name;
protectedint age;
publicPerson(String n1,int a1)
{
name=n1;
age=a1;
count++;
}
publicPerson(String n1) { //构造方法重载
this(n1,0); //调用本类的构造方法
}
publicPerson(int a1) { //构造方法重载
this("未知名", a1);
}
publicPerson(){ //构造方法重载
this("未知名");
}
public voidprint(){
System.out.print(this.getClass().getName()+" ");
System.out.print("count="+this.count+" ");
System.out.println(" "+name+","+age);
}
}
public class Student extends Person{
protectedString dept;
Student(Stringn1,int a1,String d1) {
super(n1,a1);
dept=d1;
}
Student(){
this("未知名",0,"未知系");
}
publicstatic void main(String args[]) {
Person p1 = new Person("王小红",21);
p1.print();
Person p2 = new Person("张小云");
p2.print();
Person p3 = new Person(19);
p3.print();
Person p4 = new Person();
p4.print();
Student s1 = new Student("赵小丽",19,"计算机系");
s1.print();
Student s2 = new Student();
s2.print();
}
}
程序运行结果如图7-7所示:
图7-7 程序运行结果
7.3.2 方法的覆盖
在前述继承规则中有一条:子类继承超类中所有可被子类访问的成员方法,如果子类方法与超类方法同名,则不能继承,此时子类的方法称为覆盖(override)了超类中的那个方法。在进行覆盖时,应注意以下三点:
l 子类不能覆盖超类中声明为final或static的方法。
l 子类必须覆盖超类中声明为abstract的方法,或者子类也声明为abstract。
l 子类覆盖超类中同名方法时,子类方法声明必须与超类被覆盖方法的声明一样。
方法的覆盖(method overriding)与类的继承有密切的关系。覆盖体现了子类补充或改变超类方法的能力。通过覆盖使一个方法在不同的子类间表现出不同的行为。
在面向对象系统中,对象封装了方法。恰恰利用诸如重名、重定义让各对象自己去解释执行,而这种多义性决不会带来混乱。这对于需求分析、模型设计是极为有利的,因为这些
工作不需要涉及具体的数据结构和类型,只是着重于揭示系统的逻辑合理性。
【例7-8】 方法的覆盖。
设计思路:设计Person类定义toString和print方法,print方法显示本类名和父类名,SuperStudent子类中的toString和print方法覆盖了超类Person的同名方法。
代码://file name:SuperStudent
class Person
{
static intcount=0;
protectedString name;
protectedint age;
publicPerson(String n1,int a1)
{
name=n1;
age=a1;
this.count++; //超类对象计数
}
publicString toString()
{
return this.name+"," +this.age;
}
public voidprint()
{
System.out.println("本类名="+this.getClass().getName()+""+
"超类名="+this.getClass().getSuperclass().getName()+"");
System.out.print("Person.count="+this.count+" ");
System.out.print("SuperStudent.count="+SuperStudent.count+" ");
Object s1=this;
if(s1 instanceof Person) //判断对象属于哪个类
{
System.out.println(s1.toString()+"是Person类对象。");
}
if(s1instanceof SuperStudent)
{
System.out.println(s1.toString()+"是SuperStudent类对象。");
}
}
}
public class SuperStudent extends Person
{
static intcount=0; //隐藏了超类的count
protected String dept;
protected SuperStudent(String n1,inta1,String d1)
{
super(n1,a1); //调用超类的构造方法
dept=d1;
this.count++; //子类对象计数
}
public String toString() //覆盖超类的同名方法
{
return super.toString()+","+dept; //调用超类的同名方法
}
public void print() //覆盖超类的同名方法
{
super.print(); //调用超类的方法
System.out.println("super.count="+super.count); //引用超类变量
System.out.println("this·count="+this.count);
}
public static void main(String args[])
{
Person p1= new Person("王小明",21);
p1.print();
SuperStudent s1=new SuperStudent("张小云",19,"计算机系");
s1.print();
}
}
程序运行结果如图7-8所示:
图7-8 程序运行结果
从本例可以看出:
(1)在子类中声明了与超类成员count同名的变量时,超类和子类中都有静态变量count,子类中的count隐藏了超类中的count,此时两者不是继承关系,可以用this.count引用本类的变量,而用super.count引用被子类隐藏了的超类的同名变量。
(2)子类方法与超类方法同名时,子类方法覆盖超类的同名方法。例如: s1.toString()方法覆盖了p1.toString()方法,s1.print()方法覆盖p1.print()方法。
不同的对象执行属于自己类的方法。例如,pl.print执行的是Person类中方法,输出前两行结果;sl.print 执行的是子类SuperStudent中的方法,首先通过“ super.print ( ); ”调用超类的同名方法,再执行自己的语句,输出后五行的结果。
(3)子类对象“即是”超类对象,所以Person.count也统计子类对象。可用instanceof
运算符判断一个对象是否为某个类的对象,如“if(sl instanceof Student)”。
(4)this和super引用的用法区别。
this有两种用法:既可以指代对象自身,如s1=this;也可以通过this引用本类的成员变量和方法,如this.count和this.toString()。
super只有一种用法,即通过super引用超类中的成员变量和方法,如super.count和super.print(),而不能单独使用super,如sl=super是非法的。因为子类与超类存在继承关系,子类从超类继承变量和方法,但子类对象与超类对象却不存在类似继承的对应关系,所以super无所指。