版权说明:本文档由用户提供并上传,收益归属内容提供方,若内容存在侵权,请进行举报或认领
文档简介
4.1继承
4.2多态
4.3最终类与抽象类
4.4接口4.5程序包
4.6内部类
4.7小结
4.8习题
4.1.1引例——“学生与学位”
假定根据学生的三门学位课程的分数决定其是否可以拿到学位,对于本科生,如果三门课程的平均分超过60分即表示通过,而对于研究生,则需要平均分超过80分才能通过。编一程序判定学生能否拿到学位。4.1继承
1.问题剖析
按第3章所学的内容单独考虑本科生和研究生就必须将各自的所有特征都列出来,这样就有相当多的内容是互相重复的。如果考虑本科生和研究生都是学生,只要说明本科生和研究生的不同特征就可以避免相互重复。一方面,可以认为本科是学生的一个特例,研究生也是学生的一个特例,他们除了学生的特征(如学号、姓名等)之外,各自还有相互区别的特征。另一方面,学生是本科生、研究生共同特征的一种抽象。因此,我们可以设计一个学生类,将本科生与研究生的共同特征描述出来,再定义本科生类,应用一种机制将学生类中描述的特征全部包含起来,只声明本科生与研究生的不同特征(60分通过而不是80分通过);同理,再定义一个研究生类,将上述学生类中的数据成员与方法成员全部包含过来,只声明研究生与本科生的不同特征(80分通过而不是60分通过)。
2.继承机制
1)继承
面向对象程序设计方法中利用现有的类来定义新的类的过程,称为继承(inheritance)。继承是面向对象程序设计的一大特色。
面向对象程序设计的一个原则是,不必每次都从头开始定义一个新的类,而是将这个新的类作为一个或若干个现有类的扩充或特殊化。如果不使用继承,每个类都必须显式地定义它所有的特征。然而使用继承后,定义一个新的类时只需定义其与其他类不同的特征,那些通用特征则可以从其他类继承下来,而不必逐一显式地定义。类B继承类A,即在类B中除了自己定义的成员之外,还自动包括了类A中定义的数据成员与成员方法,这些自动继承下来的成员称为类B的继承成员,类A称为类B的父类(parent)、超类(super-class)或基类(base),而类B则称为类A的子类(child)或派生类(derived-class)。一个类的祖先类(ancestor)包括了其父类以及父类的祖先类,一个类的后代类(descendant)包括了其子类以及子类的后代类。类B继承类A也可以说成是类A派生出类B。
2)继承的语法
在Java语言中,一个名为Class1的类继承另一个已有类Class2的语法形式为
publicclassClass1extendsClass2{
成员列表;
}
其中,类Class2称为类Class1的基类,新定义的类Class1称为类Class2的派生类,extends是Java语言的保留字,指明类之间的继承关系。花括号中定义的是Class1中新增加的数据成员与方法成员。第3章已经讨论过由保留字public与private定义的类成员访问控制方式,在Java语言中还有一种类成员访问控制方式,即用保留字protected定义,在protected后定义的数据成员或方法成员称为受保护成员。受保护成员具有公有成员与私有成员的双重角色:它可以被后代类的方法成员访问,但不可以被其他不相关的类的方法成员所访问。
3.问题的解决
下面用Java语言编写程序,解决“学生与学位”问题,我们可以先定义一个基类——学生类。
【例4-1】Student.java程序。
//学生类
publicclassStudent
{
publicfinalstaticintNUM_OF_SCORE=3;
//每个学生的分数个数
privateStringname; //姓名,私有成员
protectedint[]scores;
//分数,受保护成员,能被后代类的方法成员访问publicStudent()
{
name="";
scores=newint[NUM_OF_SCORE];
}
publicvoidsetName(StringnewName)
{
name=newName;
}publicStringgetName()
{
returnname;
}
publicvoidsetScore(intscoreNum,intscore)
{
//检查参数的正确性
if(scoreNum<0||scoreNum>=NUM_OF_SCORE)return;
if(score<0||score>100)return;
scores[scoreNum]=score;
}publicintgetScore(intscoreNum)
{
if(scoreNum<0||scoreNum>=NUM_OF_SCORE)return-1;
returnscores[scoreNum];
}
//计算学生平均成绩publicintgetAverageScore()
{
inttotal=0;
for(inti=0;i<NUM_OF_SCORE;i++)
total=total+scores[i];
return(int)(total/NUM_OF_SCORE);
}
}例4-2和例4-3根据本科生与研究生的不同分别继承并扩展上述基类,以计算是否通过学位考试。
【例4-2】Undergraduate.java程序。
//本科生类Undergraduate,继承学生类
publicclassUndergraduateextendsStudent
{
publicUndergraduate()
{}publicStringcomputeGrade()
{
inttotal=0;
for(inti=0;i<NUM_OF_SCORE;i++)
total=total+scores[i];
if(total/NUM_OF_SCORE>60)return"PASS";
elsereturn"NO_PASS";
}
}【例4-3】Graduate.java程序。
//研究生类,继承学生类
publicclassGraduateextendsStudent
{
publicStringcomputeGrade()
{
inttotal=0;
for(inti=0;i<NUM_OF_SCORE;i++)
total=total+scores[i];if(total/NUM_OF_SCORE>80)
return“PASS”;
else
return“NO_PASS”;
}
}
上述两个程序的成员方法computeGrade()都访问了超类的受保护成员scores,这是合法的访问,因为子类的成员方法可以访问超类的受保护成员。【例4-4】StudentDemo.java程序。
publicclassStudentDemo
{
publicstaticvoidmain(String[]args)
{
Graduatestudent1=newGraduate();
student1.setName("郭敬");
student1.setScore(0,78);
student1.setScore(1,92);
student1.setScore(2,72);System.out.println("Thegradeofgraduatestudent"+student1.getName()+
"is"+puteGrade());
Undergraduatestudent2=newUndergraduate();
student2.setName("黄蓉");
student2.setScore(0,80);
student2.setScore(1,78);
student2.setScore(2,75);
System.out.println("Thegradeofgraduatestudent"+student2.getName()+
"is"+puteGrade());
}
}例4-4程序分别通过对象引用student1和student2访问了超类的公有成员方法setName()和setScore(),这是合法的访问,因为子类继承了超类的成员,而且超类的公有成员被子类继承后仍然是公有成员。
从上面程序可以看出,Java语言继承机制的语法十分简单,只要在声明一个类时使用保留字exdents指名其超类即可,上面三个类之间的继承关系如图4-1所示。图4-1三个类之间的继承关系4.1.2继承与成员访问控制
继承机制引入了受保护成员,提供了一种新的成员访问控制级别,因此需要进一步了解Java语言的类成员访问控制规则。通过第3章的学习我们知道,一个类自身的方法成员可以访问它所有成员,而其他类的方法成员则只能访问这个类的公有成员,不能访问这个类的私有和受保护成员,这是Java语言最基本的成员访问控制规则。在引入继承机制后,子类继承了超类除构造方法以外的所有成员,这些成员称为子类的继承成员。注意,继承成员不仅包括在超类中定义的公有、受保护及私有成员,还包括超类的继承成员,即超类继承它的祖先类得到的成员。在子类的内部,不仅能够访问子类自己定义的所有成员,也能够访问超类中公有和受保护的成员,但不能访问超类中私有的成员。例如,在例4-2中子类Undergraduate继承了超类Student,它从超类Student得到的继承成员包括超类的私有成员name,受保护成员scores,公有成员NUM_OF_SCORE、setName()、getName()、setScore()和getScore(),而方法computeGrade()则是它自己定义的成员。在子类Undergraduate的内部既能访问它自己定义的成员computeGrade(),也能访问继承成员scores、NUM_OF_SCORE、setName()、getName()、setScore()和getScore(),但不能直接访问继承成员name。
Java的类继承层次是一种单根层次结构,Java程序中的每个类都是类Object的后代类。如果一个类没有指名其超类,则它的超类暗指类Object。因此例4-1的类Student是类Object的子类,它继承了类Object的成员,这样类Undergraduate是类Student的继承成员,也是类Object的继承成员。子类的某些继承成员也能被使用子类的程序访问,而哪些成员能被访问则由继承成员在子类中的访问控制决定。继承成员在子类的访问控制与它们在超类中的访问控制相同,即原先在超类中是公有的成员,被子类继承后仍然是子类的公有(继承)成员,原先在超类中是受保护的成员,被子类继承后仍然是子类的受保护(继承)成员,原先在超类中是私有成员,被子类继承后仍然是子类的私有(继承)成员(注意私有继承成员即使是子类内部也不能被直接访问)。因此,使用子类的程序能够访问子类的公有(继承)成员,但不能访问子类的受保护(继承)成员,更不能访问子类的私有(继承)成员。例4-4的类StudentDemo就是使用子类Undergraduate的程序,它能通过子类Undergraduate的对象引用student2访问它的公有继承成员NUM_OF_SCORE、setName()、getName()、setScore()和getScore(),但不能直接访问受保护的继承成员scores,更不能直接访问私有继承成员name。
总之,子类可继承超类的所有成员,这些成员是子类的继承成员,在子类内部能访问公有和受保护的继承成员,但不能直接访问私有的继承成员。使用子类的程序能访问子类的公有继承成员,但不能直接访问子类的受保护和私有的继承成员。子类自己定义的成员的访问控制没有改变,即子类内部能访问它自己定义的所有成员,使用子类的程序只能访问子类自己定义的公有成员,而不能访问子类自己定义的受保护的成员和私有成员。4.2.1程序的多态性
早在介绍Java语言的基本数据类型与运算符时,我们已经知道二元运算符“/”对于不同类型的数据具有不同含义的运算:如果两个操作数都是整数类型,那么“/”进行整除运算;如果其中一个操作数是浮点或双精度类型,那么“/”进行除法运算。有些程序设计语言(如Pascal)使用不同的运算符表示整除与除法,如用div表示整除、用“/”表示除法,从而将这两者明显区别开来。但Java语言采用同一符号“/”表示两种不同含义的运算。在程序中同一符号或名字在不同情况下具有不同解释的现象称为多态性(polymorphism)。4.2多态在面向对象程序设计语言中,由程序员设计的多态性有两种最基本的形式:编译时多态性和运行时多态性。许多程序设计语言都或多或少地支持编译时多态性,但运行时多态性是面向对象程序设计语言的一大特点。
编译时多态性是指在程序编译阶段即可确定下来的多态性,主要通过使用重载机制获得。Java语言允许重载方法,即允许程序员用相同的名字表示两个或更多的方法,使得语义非常相似的方法可以用同样的标识符命名。运行时多态性是指必须等到程序运行时才可确定的多态性,主要通过继承结合动态绑定获得。要产生运行时多态性必须先设计一个类层次,子类重定义超类的某些方法,然后使用超类类型的对象引用调用这些方法,则JVM会根据这些对象引用的动态类型选择所调用方法的版本,即如果该对象引用的是超类的对象实例,则调用超类定义的方法;如果引用的是子类的对象实例,则调用子类定义的方法,这就是所谓的动态绑定。简单地说,绑定(binding)就是将一个名字与它的特性相关联的过程。静态绑定就是绑定过程在编译时进行,而动态绑定就是绑定过程在运行时进行。程序中最重要的名字是变量名和方法名,变量最重要的特性是它的类型,方法名最重要的特性是它的实现体,因此程序中最常见的绑定是确定变量的类型和确定方法调用所调用的实现体。在Java语言中,任何数据都在编译时确定其静态类型,基本数据类型的变量只有静态类型,对象引用有静态类型和动态类型,对象引用的动态类型在运行时确定,而且肯定是其静态类型的子类型。在Java语言中,确定一个方法调用表达式到底调用哪个实现体比较复杂。简单地说,互为重载的方法中哪个被调用是编译时确定的,而被重定义的方法到底调用哪个方法则是运行时确定的。4.2.2方法重载
在Java语言中,方法重载是指在一个类的方法成员(包括其继承成员)中有多个名字相同但参数列表不同的方法。方法重载的好处是程序员可以为语义相似的方法起相同的名字,从而一方面凸现这些方法之间的关联关系,另一方面减少使用者的记忆量。方法重载的代价是要明确一个方法调用表达式到底调用哪个方法的实现体时比较困难,也就是说,如果设计不当的话,可能使得方法调用产生二义性。因此,初学者要谨慎使用方法重载,真正语义相似的方法才能进行重载,而且应尽量避免由此带来的方法调用方面的困扰。
1.方法重载的途径
一个类的方法成员中如果有多个名字相同但参数列表不同的方法,就称这些方法为互为重载的方法。下面通过例子来说明方法重载,例4-5使用方法重载机制提供了几个互为重载的find()方法从一个学生数组中查找满足某种条件的学生。
【例4-5】Finder.java程序。//演示方法重载,使用几种不同的方式从一组学生中查找满足条件的学生
publicclassFinder
{
privatestaticfinalintNUM_OF_STUDENT=10;
privatestaticStudent[]data=newStudent[NUM_OF_STUDENT];
//使用随机的形式初始化学生数组data,这样可省略输入测试数据的麻烦
publicstaticvoidinitialize()
{for(inti=0;i<NUM_OF_STUDENT;i++)
{
data[i]=newStudent();
//为方便起见,学生的名字设置为“Student1”等形式
data[i].setName("Student"+i);
for(intj=0;j<Student.NUM_OF_SCORE;j++)
{//使用1~100的随机数来初始化该学生的每一个分数
intscore=1+(int)(Math.random()*100);
//为了更逼真一点,如果得到的随机数小于60,我们再给他加40
if(score<60)score=score+40;
data[i].setScore(j,score);
}
}
}
//根据学生的名字查找学生,返回学生在数组中的下标,如果没有该学生则返回-1
//在学生数组中查找的起点为start(包含start),设置起点使得使用者可查找所有满足条件的学生
publicstaticintfind(intstart,Stringname)
{
//检查参数的正确性
if(start<0||start>=NUM_OF_STUDENT)return-1;
for(inti=start;i<NUM_OF_STUDENT;i++)
{//注意要使用equals来比较两个字符串是否相等
if(name.equals(data[i].getName()))returni;
}
return-1;
}
//根据学生的第scoreIndex门课程的分数是否处在scoreMin和scoreMax(包括这两个数)之间查找学生,
//从学生数组的start下标开始查找,返回第一个满足条件的学生的下标,如果没有满足条件的学生,
//返回-1publicstaticintfind(intstart,intscoreIndex,intscoreMin,intscoreMax)
{
//检查参数的正确性
if(start<0||start>=NUM_OF_STUDENT)return-1;
if(scoreIndex<0||scoreIndex>=Student.NUM_OF_SCORE)return-1;
for(inti=start;i<NUM_OF_STUDENT;i++)
{intscore=data[i].getScore(scoreIndex);
if(score>=scoreMin&&score<=scoreMax)returni;
}
return-1;
}
//根据学生三门课程的平均分数是否处在scoreMin和scoreMax(包括这两个数)之间查找学生,
//从学生数组的start下标开始查找,返回第一个满足条件的学生的下标,如果没有满足条件的学生,
//返回-1
publicstaticintfind(intstart,intscoreMin,intscoreMax){
//检查参数的正确性
if(start<0||start>=NUM_OF_STUDENT)return-1;
for(inti=start;i<NUM_OF_STUDENT;i++)
{
intscore=data[i].getAverageScore();
if(score>=scoreMin&&score<=scoreMax)returni;
}
return-1;
}//演示的主程序
publicstaticvoidmain(String[]args)
{
intindex=-1;
initialize(); //初始化用于测试的学生数组
//查找名字为“Student5”的学生
index=find(0,"Student3");
if(index>=0)System.out.println("Student3是数组的第"+index+"个元素!");
else
System.out.println("Student3不在学生数组中!");
//查找第一门课程的成绩分数在60~80之间的所有学生
intcount=1;
index=-1;
while(index<NUM_OF_STUDENT)
{
index=find(index+1,1,60,80);
if(index>=0)
{System.out.println("第1门课程的成绩分数在60和80之间的第"+count+"个学生是:
"+data[index].getName()+",分数是"+data[index].getScore(1));
}elsebreak; //终止查找过程
count=count+1;
}
//查找平均分数在60~80之间的第一个学生index=find(0,60,80);
if(index>=0)
{
System.out.println("平均分数在60和80之间的第一个学生是:"+data[index].getName());
}
else
System.out.println("没有学生的平均分数在60和80之间!");
}
}注意例4-5中查找所有第一门课程的分数在60~80之间的学生的方法,读者可模仿这段程序,将后面查找平均分数在60~80之间的第一个学生改为查找所有满足该条件的学生。另外也请读者注意程序中对于学生数组的初始化,为了减轻输入的负担,上述程序使用了随机数进行初始化,这是使用大批量数据进行测试的一种常用手段。有兴趣的读者可以修改方法initialize(),让用户输入有关数据来初始化学生数组。从例4-5可以看出,Java语言的方法重载十分简单,只要给出几个同名但参数列表不同的方法就是方法重载。这里要强调的是,只有参数列表,即参数个数及参数类型是区分重载方法的因素,方法的返回类型及访问控制以及其他修饰(如是否是静态成员,是否有final修饰等)等都不能区分重载方法,例如:
publicintfunction(intx){…}
publicintfunction(Stringx){…}是互为重载的两个方法,但:
publicintfunction(intx){…}
publicStringfunction(intx){…}
就不是互为重载的两个方法,因为它们的参数列表是完全一样的,如果这两个方法出现在同一个类中,则编译器会报告编译错误,同样:
publicstaticfinalintfunction(intx){…}
privatefunction(intx){…}
也不是互为重载的两个方法。
2.构造方法的重载法
从某种意义上说,构造方法可能是引入方法重载机制的最主要因素,因为程序员不能为构造方法选择名字,它必须具有与类相同的名字,所以当程序员想为一个类的对象提供几种不同的初始化方式时,必须重载构造方法。
重载构造方法的方式与重载一般方法相同,只是要遵循编写构造方法的规定,即必须以类名作为访问名,不能有返回值等。下面为例4-5中的Student类编写几个互为重载的构造方法。publicclassStudent
{
publicStudent(Stringname)
{
=name;//使用this区分成员数据与方法的参数,this的一种用法
scores=newint[NUM_OF_SCORE];
}
publicStudent()
{this(“NoName”);
//使用this调用上一个构造方法,this的另一种用法
}
publicStudent(Stringname,int[]initScore)
{
=name;
scores=newint[NUM_OF_SCORE];
for(inti=0;i<NUM_OF_SCORE&&i<initScore.length;i++)
score[i]=initScore[i];
}
…… //其他省略
}在第三个构造方法中,一般不直接使用语句“scores=initScore;”来初始化scores,这样只是使得scores与initScore引用相同的数组元素,那么当在其他地方对引用的数组元素进行修改时,也会影响类Student内部的scores,从而破坏了封装性。上述第二个构造方法中使用语句“this(”NoName“);”传入缺省的字符串“NoName”调用第一个构造方法,这是this除用来指示当前类的成员(如)之外的另一种典型用法。
细心的读者可以发现,除了在使用new创建对象实例时隐含调用构造方法之外,还有两种方式对构造方法进行显式的调用:一是在子类的构造方法中使用super调用直接基类的构造方法;二是在某个类的构造方法中使用this调用这个类的其他构造方法(准确地说也可递归地调用这个构造方法本身)。4.2.3数据成员的隐藏
数据成员的隐藏是指在子类中重新定义一个与父类中已定义的数据成员名完全相同的数据成员,即子类拥有了两个相同名字的数据成员,一个是继承父类的,另一个是自己定义的。当子类引用这个同名的数据成员时,默认操作是它自己定义的数据成员,而把从父类那里继承来的数据成员“隐藏”起来。当子类要引用继承自父类的同名数据成员时,可以使用关键字super.数据成员名来引用。4.2.4成员方法的覆盖
子类可以重新定义与父类同名的成员方法,实现对父类方法的覆盖。方法覆盖与数据成员隐藏的不同之处在于:子类隐藏父类的数据成员只是使之不可见,父类同名的数据成员在子类对象中仍然占有自己的独立的内存空间;子类方法对父类同名方法的覆盖将清除父类方法占用的内存,从而使父类方法在子类对象中不复存在。
【例4-6】SubPrintme.java程序。方法的覆盖中需要注意的是,子类在重新定义父类已有的方法时,应保持与父类完全相同的方法名、返回值类型和参数列表,否则就不是方法的覆盖,而是子类定义自己特有的方法,与父类的方法无关。4.2.5this与super
1.this的使用场合
在一些容易混淆的场合,例如,当成员方法的形参名与数据成员名相同,或者成员方法的局部变量名与数据成员名相同时,可以在方法内借助this来明确表示引用的是类的数据成员,而不是形参或局部变量,从而提高程序的可读性。简单地说,this代表了当前对象的一个引用,可将其理解为对象的另一个名字,通过这个名字可以顺利地访问、修改对象的数据成员、调用对象的方法。归纳起来,this的使用场合有下述三种:
(1)用来访问当前对象的数据成员,其使用形式如下:
this.数据成员
(2)用来访问当前对象的成员方法,其使用形式如下:
this.成员方法(参数)
(3)用来调用另一个构造方法,其使用形式如下:
this(参数)
【例4-7】用this访问当前对象的成员方法,计算圆的面积和周长。
//演示this的使用方法
classCircle
{
doubler; //定义半径
finaldoublePI=3 //定义圆周率
publicCircle(doubler) //类的构造方法
{this.r=r;
//用来访问当前对象的数据成员
}
//计算圆面积的方法
doublearea()
{
returnPI*r*r;
//通过构造方法给r赋值
}
//计算圆周长的方法
doubleperimeter()
{return2*(this.area()/r);
//使用this变量获取圆的面积
}
}
//主程序
publicclassCircleDemo
{
publicstaticvoidmain(String[]args)
{
doublex;Circlecircle=newCircle(5.0);
x=circle.area();
System.out.println(“圆的面积=”+x);
x=circle.perimeter();
System.out.println(“圆的周长=”+x);
}
}
程序运行结果:
圆的面积=78.53981633974999
圆的周长=31.415926535899995
2.super的使用场合
super表示的是当前对象的直接父类对象。所谓直接父类,是相对于当前对象的其他“祖先”类而言。例如,假设类A派生出子类B,类B又派生出自己的子类C,则B是C的直接父类,而A是C的祖先类。super代表的就是直接父类。若子类的数据成员或成员方法名与父类的数据成员或成员方法名相同,当要调用父类的同名方法或使用父类的同名数据成员时,则可用关键字super来指明父类的数据成员或方法。
super的使用方法有三种:
(1)用来访问直接父类隐藏的数据成员,其使用形式如下:
super.数据成员
(2)用来调用直接父类中被覆盖的成员方法,其使用形式如下:
super.成员方法(参数)
(3)用来调用直接父类中的构造方法,其使用形式如下:
super(参数)
【例4-8】访问直接父类隐藏的数据成员和被覆盖的成员方法。
//演示super的用法
classSuperPrint
{
intx=4;inty=1;
publicvoidprintme()
{System.out.println("x="+x+"y="+y);
System.out.println("classname:"+this.getClass().getName());
}
}
//定义子类
publicclassSubPrintextendsSuperPrint
{
intx;
publicvoidprintme()
{intz=super.x+6; //引用父类(即SuperPrint类)的数据成员
super.printme();
//调用父类(即SuperPrint类)的成员方法
System.out.println(“Iaman”+this.getClass().getName());
x=5;
System.out.println(“z=”+z+“x=”+x);
//打印子类的数据成员
}
//主程序
publicstaticvoidmain(String[]args)
{intk;
SuperPrintp1=newSuperPrint();
SubPrintp2=newSubPrint();
p1.printme();
p2.printme();
//super.Printme();
//错,在static方法中不能引用非static成员方法
//k=super.x+23;
//错,在static方法中不能引用非static数据成员
}
}程序运行结果如下:
x=4y=1
classname:SuperPrint
x=4y=1
classname:SubPrint
IamanSubPrint
z=10x=54.3.1最终类与final
继承机制允许对现有的类进行扩充,但有些时候,需要把一个类的功能固定下来,不再允许定义它的子类,Java语言称这样的类为最终类(finalclass),例如基本数据类型的包装类(Byte、Double等)都是最终类。4.3最终类与抽象类把一个类定义为最终类,只要在声明时用保留字final修饰即可,例如:
publicfinalclassMyFinalClass
{
...
}
如果把最终类声明为某个类的父类,编译程序将报告错误。程序员也可以把一个(普通)类的某些成员方法声明为最终方法,这使得该方法不可以被后代类重定义,例如:
publicclassMyClass
{
publicfinalvoidmyFinalMethod()
{...}
}
子类中可定义与父类中常量(即用final修饰的成员数据)同名的成员数据,因为实质上,成员数据的重定义不具有成员方法重定义的功能,它只是用来屏蔽父类的成员数据。4.3.2抽象类与abstract
最终类是类层次结构的叶子节点,它们不能再被继承和扩充。Java语言还有一种特殊的类,它们只能被继承和扩充,而不能用于创建自己的对象实例,这种类称为抽象类(abstractclass)。抽象类用于建模现实世界中一些没有具体对象的纯粹抽象的概念。例如,“鸟”可以认为是一个纯粹抽象的概念,它本身没有任何具体的对象实例,任何具体的鸟儿都是由“鸟”经过特殊化形成的某个具体的种类的对象,也就是说,“鸟”是具体的鸟儿经过抽象形成的一个概念。因此,抽象类可以从现有的一些类中抽取共同特性而得到。什么时候进行这种抽象呢?Eiffel语言的设计者BertrandMeyer提供了所谓的单选原则(singlechoiceprinciple):当软件系统要解决的问题有一个可供选择的集合时,只在一个软件构建中进行这种选择。例如,一个图形处理软件,要处理各种各样的图形,三角形、矩形、圆等,那么应该设计一个抽象类——“图形”作为进行选择的软件构件,也就是说,其他的软件构件可以通过这个构件访问所有图形的共同特性。再例如,一个文本编辑软件,要支持文本编辑的许多命令,插入、删除、移动等,那么应该设计一个抽象类——“命令”描述这些命令的共同特性。
1.抽象类
Java语言在声明一个类时使用保留字abstract修饰使得它成为抽象类,例如:
publicabstractclassMyAbstractClass
{
...
}
使用抽象类创建对象实例,编译器会报告错误,例如下述语句不能通过编译:
MyAbstractClassobj=newMath();//编译错误
2.抽象方法
Java语言也可以定义某些成员方法为抽象方法,例如:
publicabstractclassMyAbstractClass
{
publicabstractvoidmyAbstractMethod();
//没有方法体
...
}抽象方法没有方法体,直接跟分号表示结束。注意,只能将抽象类的成员方法定义为抽象方法,不能将非抽象类的成员方法定义为抽象方法。从某种意义上,正是因为抽象类有一些成员方法是抽象方法,没有方法体,不能直接调用,所以抽象类才不能创建对象实例。当然抽象类中也可以没有抽象方法,全是非抽象的方法,但这多少有些不自然。显然不能将一个类或方法同时使用final和abstract修饰,因为最终类禁止类被派生,同时禁止方法被重定义,而抽象类只能被派生才有可能创建对象实例,抽象方法只有被重定义才能给出其方法体。
抽象类不能创建对象实例,因此它通常作为某些类的父类。抽象类的子类应该重定义抽象类的抽象方法,给出它们的具体实现,这时这个子类方法不再用abstract修饰。如果抽象类的某个抽象方法没有被它的子类重定义给出具体实现,则这个子类也是抽象类,在声明这个子类时必须使用abstract修饰。
3.应用举例
这里举一个比较完整的例子来说明抽象类的使用。假定要为某个公司编写雇员工资支付程序。这个公司有各种类型的雇员,不同类型的雇员按不同的方式支付工资:经理(Manager)每月获得一份固定的工资;销售人员(Salesman)在基本工资的基础上每月还有销售提成;一般工人(Worker)则按每月工作的天数计算工资。按照单选原则,应该设计一个类——雇员(Employee)来描述所有雇员的共同特性,例如姓名(name)等。这个类还应提供一个计算工资的抽象方法computeSalary()使得可以通过这个类计算所有雇员的工资,这个方法是抽象方法,因为程序无法为一个没有明确类型的雇员计算工资。经理、销售人员、一般工人对应的类都继承这个父类,并重定义计算工资的方法computeSalary(),给出它的具体实现。例4-9定义了父类Employee,例4-9、例4-10、例4-11分别给出了子类Manager、Salesman和Worker的定义。为简单起见,这些类都采用了最简单的设计。【例4-9】Employee.java程序。
//抽象类——雇员
publicabstractclassEmployee
{
privateStringname;
publicEmployee(Stringname)
{
=name;
}
publicStringgetName(){
returnname;
}
//计算雇员月工资的抽象方法
publicabstractdoublecomputeSalary();
}【例4-10】Manager.java程序。
//经理是雇员中的一类人群
publicclassManagerextendsEmployee
{
privatedoublemonthSalary;//月工资额
publicManager(Stringname,doublemonthSalary)
{
super(name);//调用父类Employee的构造方法this.monthSalary=monthSalary;
}
//重定义父类的抽象方法computeSalary,此处不能再使用abstract修饰,下面几个类的方法类似。注意,方法重定义只要求方法的返回类型与参数列表完全一致
publicdoublecomputeSalary()
{
returnmonthSalary;
}
}【例4-11】Salesman.java程序。
//销售人员也是雇员中的一类人群
publicclassSalesmanextendsEmployee
{
privatedoublebaseSalary; //基本工资额
privatedoublecommision; //每件产品的提成额
privateintquantity; //销售的产品数量
publicSalesman(Stringname,doublebaseSalary,doublecommision,intquantity)
{
super(name);
//调用父类Employee的构造方法
this.baseSalary=baseSalary;
mision=commision;
this.quantity=quantity;
}
publicdoublecomputeSalary()
{
returnbaseSalary+commision*quantity;
}
}【例4-12】Worker.java程序。
//工人类同样是雇员中的一类人群
publicclassWorkerextendsEmployee
{
privatedoubledailySalary;//每天工资额
privateintdays; //每月工作的天数
publicWorker(Stringname,doubledailySalary,intdays){
super(name); //调用父类Employee的构造方法
this.dailySalary=dailySalary;
this.days=days;
}
publicdoublecomputeSalary()
{
returndailySalary*days;
}
}【例4-13】EmployeeDemo.java程序。
//演示类
publicclassEmployeeDemo
{
publicstaticvoidmain(String[]args)
{
//以共同的父类声明记录雇员的数组Employee[]data=newEmployee[4];
//创建一些雇员对象,这时雇员数组是一个多态数据结构,可以存放各种类型的雇员
data[0]=newManager("Manager",10000);
data[1]=newSalesman("Salesman",3000,200,12);
data[2]=newWorker("WorkerZhang",200,25);
data[3]=newWorker("WorkerLi",250,26);
displaySalary(data);
}
//显示所有雇员的工资额,不同种类的雇员都放在数组data中,因为Employee抽象了所有雇员的
//共同特性,因此这里只要以Employee作为参数即可,而无需关心具体种类的雇员
publicstaticvoiddisplaySalary(Employee[]data)
{
for(inti=0;i<data.length;i++)
{
//下面调用data[i].computeSalary()将根据data[i]所引用的对象实例调用
//相应的computeSalary()方法来计算该雇员的月工资额
System.out.println("雇员"+data[i].getName()+"的月工资是:"+data[i].
computeSalary());
}
}
}程序运行的结果如下:
雇员Manager的月工资是:10000.0
雇员Salesman的月工资是:5400.0
雇员WorkerZhang的月工资是:5000.0
雇员WorkerLi的月工资是:6500.0这里雇员数组data是一个典型的多态数据结构,里面可存放各种类型的雇员(实际上是雇员对象实例的引用),在displaySalary()中调用data[i].computeSalary()会根据data[i]的动态类型调用相应雇员的computeSalary()方法计算其月工资额。 这种动态多态性使得程序的扩充十分方便,假设要将一般雇员再细分为计时雇员(DailyWorker)和计件雇员(PieceWorker)两类,计时雇员按天计算工资额,而计件雇员按其生产的产品件数计算工资额,那么可取消类Worker,重新派生两个类DailyWorker和PieceWorker,重定义其中的computeSalary()方法,再重新编译这两个类即可,无需重新编译调用computeSalary()的displaySalary()方法,当data数组中存放这两种类型的雇员时,仍可正确计算其工资额。4.4.1引例——“郭敬问题”
郭敬是一名高校教师,他正读在职研究生,请用Java语言描述像郭敬同样情况的人。
1.问题剖析
读在职研究生的教师既可作为学生的子类又可作为教师的子类,这正是因为我们既可以从学生的角度观察这一类人,也可以从教师的角度观察这一类人。4.4接口在C++语言中,可以先定义一个Person类描述学生与教师共有的特征,再定义一个Student类和Teacher类,分别描述学生和教师各自独有的特征,然后定义一个在职研究生类(StudentTeacher),同时继承上述三个类即可描述郭敬既是学生又是教师的身份。然而Java语言不支持类的多重继承,无法用类似C++的办法解决。
Java出于安全性、简化程序结构的考虑,不支持类间的多重继承而只支持单继承。然而在解决实际问题的过程中,在很多情况下仅仅依靠单继承不能将复杂的问题描述清楚,像要描述4.4.1中的郭敬身份的问题,就遇到了困难。为了使Java程序的类间层次结构更加合理,更符合实际问题的本质,Java语言提供接口来实现多重继承机制。实际上,在Java语言中,可以先定义一个Person类描述学生与教师共有的特征,如姓名、年龄等,再定义一个AsStudent接口和一个AsTeacher接口,表示分别从学生和教师各自不同的角度观察人的行为和属性,然后定义一个在职研究生类InService,继承上述Person类,同时实现AsStudent接口和AsTeacher接口即可描述郭敬既是学生又是教师的身份。2.声明接口
1)声明接口的语法格式
[修饰符]interface接口名[extends父接口名列表]
{
常量数据成员声明
抽象方法声明
}
2)定义接口注意事项
定义接口要注意以下几点:
(1)接口定义用关键字interface,而不是用class。
(2)接口名要求符合Java标识符规定。
(3)修饰符有两种:public和默认。public修饰的接口是公共接口,可以被所有的类和接口使用;默认修饰符的接口只能被同一个程序包中的其他类和接口使用。
(4)父接口列表:接口也具有继承性。定义一个接口时可以通过extends关键字声明该接口是某个已经存在的父接口的派生接口,它将继承父接口的所有属性和方法。与类的继承不同的是一个接口可以有一个以上的父接口,它们之间用逗号分隔。
(5)常量数据成员声明:常量数据成员前可以有也可以没有修饰符。修饰符是publicfinalstatic或finalstatic,接口中的数据成员都是用final修饰的常量,且必须初始化。书写格式如下:
[修饰符]数据成员类型数据成员名=常量值或
数据成员名=常量值
例如:
publicfinalstaticdoublePI=3.14159;
finalstaticinta=9;
intSUM=100;(等价于finalstaticintSUM=100;)但下面的定义是错误的:
publicinterfaceControllable
{
privateintOFF=0; //错误,接口不能声明私有的成员
protectedintON=1; //错误,接口不能声明受保护的成员
...
}
接口的公有静态常量都必须使用常量表达式进行初始化,否则会出现编译错误:
publicinterfaceControllable
{
publicintOFF;
//错误,接口的数据成员必须初始化
...
}
(6)抽象方法声明:接口中的方法都是用abstract修饰的抽象方法。在接口中只能给出这些抽象方法的方法名、返回值和参数列表,而不能定义方法体,即这些接口仅仅是规定了一组信息交换、传输和处理的“接口”。格式如下:
返回值类型方法名(参数列表);其中:接口中的方法默认为publicabstract方法。例如,下述声明是错误的:
publicinterfaceControllable{
privatevoidmethodOne();
//错误,接口不能声明私有的成员
finalvoidmethodTwo();
//错误,接口不能声明最终方法成员
staticvoidmethodTwo();
//错误,接口不能声明静态方法成员
…
}接口中的方法都是抽象方法,因此不能给出任何实现体。接口相当于纯抽象类,可以声明类型为某个接口的引用变量,但不能使用该引用变量创建对象实例,例如:
Controllablerefer; //正确,定义接口也定义了相应的类型
refer=newControllable();
//错误,不能创建接口的对象实例
从上面定义接口的语法格式可以看出,定义接口与定义类非常相似。实际上完全可以把接口理解成为一种由常量和抽象方法组成的特殊类。一个类只能有一个父类,但是它可以同时实现若干个接口。这种情况下,如果把接口理解成特殊的类,那么这个类利用接口就获得了多个父类,即实现了多重继承。定义接口仅仅是规定了一组实现特定功能的对外接口和规范,而不能真正地实现这个功能,这个功能的真正实现是在“继承”这个接口的各个类中完成的,即要由这些类来具体定义接口中各抽象方法的方法体。因而在Java中,通常称接口功能的“继承”为“实现(implementation)”。
3.问题的解决
有了上述知识准备,就可以用例4-14~例4-17的程序模拟多重继承,描述郭敬既是学生又是老师的双重身份。
【例4-14】Person.java程序。
//父类Person描述学生和教师共有的特征
publicclassPerson
{
privateStringname; //人的姓名属性定义
privateintage; //定义人的年龄属性publicPerson(Stringname,intage)
{
=name;
this.age=age;
}
publicStringgetName()
{
returnname;
}
publicintgetAge(){
returnage;
}
publicvoidsetAge(intnewAge)
{
age=newAge;
}
}
例4-15和例4-16给出了接口AsStudent和AsTeacher的定义,分别从学生和教师的角度观察某个类。
【例4-15】AsStudent.java程序。
//接口AsStudent表示从学生的角度观察人的行为和属性
publicinterfaceAsStudent
{
intMAX_COURSE_NUMBER=30;
//学生学习的课程数目的上限
String[]getStudyCourse(); //查看学生学习的课程
int[]getStudyScore(); //查看学生学习的每门课
程的成绩
StringgetStudyDepartment(); //查看学生所在系voidsetStudyCourse(String[]course);
//设置学生学习的课程
voidsetStudyScore(int[]score);
//设置学生学习的每门课程的成绩
voidsetStudyDepartment(Stringdept);
//设置学生所在的系
}
【例4-16】AsTeacher.java程序。
//接口AsTeacher表示从教师的角度观察人的行为和属性
publicinterfaceAsTeacher
{
intMAX_COURSE_NUMBER=30;String[]getTeachCourse(); //查看教师讲授的课程
doublegetTeachWage(); //查看教师的工资
StringgetTeachDepartment(); //查看教师所在的系
voidsetTeachCourse(String[]course);
//设置教师教授的课程
voidsetTeachWage(doublewage); //设置教师的工资
voidsetTeachDepartment(Stringdept);
//设置教师所在的系
}【例4-17】InService.java程序。
//定义InService类继承Person类实现AsStudent和AsTeacher两个接口
publicclassInServiceextendsPersonimplementsAsStudent,AsTeacher
{
privateString[]studyCourse=newString[AsStudent.MAX_COURSE_NUMBER];
privateint[]studyScore=newint[AsStudent.MAX_COURSE_NUMBER];
privateStringstudyDepartment;privateString[]teachCourse=newString[AsTeacher.MAX_COURSE_NUMBER];
privatedoubleteachWage;
privateStringteachDepartment;
publicInService(Stringname,intage)
{
super(name,age);
}//查看学生学习的课程
publicString[]getStudyCourse()
{
//由于数组本身不是不变对象,所以为了安全,拷贝一份课程数组返回给使用者,而不是将引用变量studyCourse直接暴露给使用者,以免被使用者改变
String[]course=newString[AsStudent.MAX_COURSE_NUMBER];
for(inti=0;i<course.length&&i<studyCourse.length;i++)
{//因为JavaAPI提供的String是不变类,因此这里采用浅复制策略,
//只复制数组元素,并不复制元素引用的字符串
course[i]=studyCourse[i];
}
returncourse;
}
//查看学生学习的每门课程的成绩
publicint[]getStudyScore(){
int[]score=newint[AsStudent.MAX_COURSE_NUMBER];
for(inti=0;i<score.length&&i<studyScore.length;i++)
{
score[i]=studyScore[i];
}
returnscore;
}
//查看学生所在的系
publicStringgetStudyDepartment(){
returnstudyDepartment;
}
//设置学生学习的课程
publicvoidsetStudyCourse(String[]course)
{
for(inti=0;i<course.length&&i<studyCourse.length;i++)
{
studyCourse[i]=course[i];
}
}//设置学生学习的每门课程的成绩
publicvoidsetStudyScore(int[]score)
{
for(inti=0;i<score.length&&i<studyScore.length;i++)
{
studyScore[i]=score[i];
}
}
//设置学生所在的系
publicvoidsetStudyDepartment(Stringdept){
studyDepartment=dept;
}
//查看教师讲授的课程
publicString[]getTeachCourse()
{
String[]course=newString[AsTeacher.MAX_COURSE_NUMBER];
for(inti=0;i<course.length&&i<teachCourse.length;i++){
course[i]=teachCourse[i];
}
returncourse;
}
//查看教师的工资
publicdoublegetTeachWage()
{
returnteachWage;}
//查看教师所在的系
publicStringgetTeachDepartment()
{
returnteachDepartment;
}
//设置教师教授的课程
publicvoidsetTeachCourse(String[]course)
{
for(inti=0;i<course.length&&i<teachCourse.length;i++){
teachCourse[i]=course[i];
}
}
//设置教师的工资
publicvoidsetTeachW
温馨提示
- 1. 本站所有资源如无特殊说明,都需要本地电脑安装OFFICE2007和PDF阅读器。图纸软件为CAD,CAXA,PROE,UG,SolidWorks等.压缩文件请下载最新的WinRAR软件解压。
- 2. 本站的文档不包含任何第三方提供的附件图纸等,如果需要附件,请联系上传者。文件的所有权益归上传用户所有。
- 3. 本站RAR压缩包中若带图纸,网页内容里面会有图纸预览,若没有图纸预览就没有图纸。
- 4. 未经权益所有人同意不得将文件中的内容挪作商业或盈利用途。
- 5. 人人文库网仅提供信息存储空间,仅对用户上传内容的表现方式做保护处理,对用户上传分享的文档内容本身不做任何修改或编辑,并不能对任何下载内容负责。
- 6. 下载文件中如有侵权或不适当内容,请与我们联系,我们立即纠正。
- 7. 本站不保证下载资源的准确性、安全性和完整性, 同时也不承担用户因使用这些下载资源对自己和他人造成任何形式的伤害或损失。
最新文档
- 2024-2025学年徐州市邳州市三上数学期末调研模拟试题含解析
- 2024-2025学年新疆维吾尔巴音郭楞蒙古自治州尉犁县数学三年级第一学期期末达标测试试题含解析
- 2025年氢能源项目申请报告模板
- 2025年水处理阻垢分散剂系列项目规划申请报告模范
- 2021教师辞职报告(15篇)
- 《乌鸦喝水》教案范文汇编5篇
- 高中语文教研工作计划锦集5篇
- 员工年终总结体会10篇
- 有关高中语文周记四篇
- 少年宫活动计划集锦9篇
- 发生输血反应时应急预案及程序
- 《工程制图与CAD》期末考试题库(含答案)
- 厦门市2024届高三年级第二次质量检测(二检)生物试卷
- 医药代表销售技巧培训 (2)课件
- Python语言程序设计全套教学课件
- 全球钽铌矿产资源开发利用现状及趋势
- 《进制及进制转换》课件
- 药物过敏性休克急救指南
- 骑手站长述职报告
- 2023年游学销售主管年终业务工作总结
- CityEngine城市三维建模入门教程 课件全套 第1-7章 CityEngine概述-使用Python脚本语言
评论
0/150
提交评论