资源描述
实验4 继承、多态、抽象类、接口
注意:有些程序由于Word的关系,复制后,tab缩位可能会变成其它符号。需要你去调整一下,删除缩位,重新Tab
一、实验目的 1
二、实验要求 1
三、实验内容 2
1. 类的继承与覆盖练习 2
1.0 父类的哪些成员可以被继承? 2
1.1 父类Student(学生)与子类HiStudent(大学生) 2
1.2 实例方法为什么需要覆盖 4
1.3 验证成员方法的覆盖方式 5
1.4 this、super和super()的使用 6
1.5 变量、静态方法的隐藏。 9
1.6 隐藏与覆盖的综合性实例 10
2. 类的多态性练习 11
2.1 普通方法的重载(实验3,本实验略) 11
2.2 构造方法的重载(实验3,本实验略) 11
2.3 运行时多态与动态绑定 11
3. 抽象类 13
4. 接口 14
一、实验目的
通过编程和上机实验理解Java语言的面向对象特性,了解类的继承性和多态性的作用,掌握它们的实现方法,了解数据域和静态方法的隐藏,了解抽象类和接口的作用。
二、实验要求
1、 编写体现类的继承性(成员变量、成员方法、成员变量隐藏)的程序;
2、 编写体现类的多态性的程序;
3、 编写体现抽象类和接口功能的程序。
三、实验内容
1. 类的继承与覆盖练习
例如,圆是一种形状。圆类Circle是形状类Shape的子类。父类也叫基类、超类,子类也叫次类、扩展类、派生类。子类可从父类中产生,保留父类的成员变量和方法,并可根据需要对它们加以修改。新类还可添加新的变量和方法。这种现象就称为类的继承。
当建立一个父类时,不必写出全部成员变量和成员方法。只要简单地声明这个类是从一个已定义的类继承下来的,就可以引用被继承类的全部成员。
Java 提供了一个庞大的类库让开发人员继承和使用。设计这些类是出于公用的目的,因此,很少有某个类恰恰满足你的需要。你必须设计自己的能处理实际问题的类,如果你设计的这个类仅仅实现了继承,和父类一样,那样没什么用。所以,通常要对父类进行扩展,即添加新的属性和方法。这使得子类要比父类大,但更具特殊性,代表着一组更具体的对象。继承的意义就在于此。
1.0 父类的哪些成员可以被继承?
可以访问的成员才能被继承。
具体分2种情况:
* 当父类和子类在同一个包时,子类可以继承父类中的public/protected/无修饰 级别的变量和方法;
* 当父类和子类不在同一个包时,且父类是public的,那么子类可以继承父类的public/protected 级别的变量和方法。如果父类前没有public,那么子类无法访问父类,更谈不上继承的问题。
1.1 父类Student(学生)与子类HiStudent(大学生)
(1) Student.java程序源代码如下。
public class Student //学生类
{
protected String xm; //姓名
protected int xh; //学号
void setData(String m,int h) //设置姓名和学号
{
xm =m;
xh = h;
}
public void print() //输出姓名和学号
{
System.out.println(xm+", "+xh);
}
}
(2) HiStudent.java的描述和代码如下
程序功能:通过Student类产生子类HiStudent,其不仅具有父类的成员变量xm(姓名)、xh(学号),还定义了新成员变量xy(学院)、xi(系)。在程序中调用了父类的print方法,同时可以看出子类也具有该方法。
程序源代码如下
public class HiStudent extends Student{ //大学生类继承自学生类
protected String xy;//所在学院或大学名称
protected String xi;//所在系的名称
public static void main(String args[]){
Student s1 = new Student();
s1.setdata("李四",12321) ;
s1.print();
HiStudent s2 = new HiStudent() ;
s2.setdata("张三",12345); //调用父类的成员方法
s2.xy="XX大学"; //访问本类的成员变量
s2.xi="计算机系"; //访问本类的成员变量
s2.print();//调用父类的方法,就象调用它自己的一样
System.out.print(s2.xm+", "+s2.xy+", "+s2.xi);
}
}
(3)编译并运行,结果如下图所示。
1.2 实例方法为什么需要覆盖
上例中,s2.print();语句调用父类的方法,只能输出父类的数据(姓名和学号)。如果想用函数输出姓名、学号、学院、系,需要在子类中另外添加一个函数。如果这个函数也叫print(),(嗯,这样好记),并且如果形参、返回类型都一样,并且其可见性修饰符和父类print()方法的一样甚至更开放,并且它们都不是静态的,并且它所抛出的异常必须和被覆盖方法的所抛出的异常一致,或者是其子类,或者什么也不抛出,那么,我们可以说子类的print()方法覆盖(override)了父类的print()方法。子类中添加的print()可以这样写:
public void print() //输出姓名、学号、学院、系
{
System.out.println(xm+", "+xh+", "+xy+", "+xi);
}
可见,子类的覆盖方法重写(修改)了父类的被覆盖方法。实现了不同的功能。当然,父类的被覆盖方法仍然存在,在子类中可以用super.print()来引用。
嗯,如果参数不一样,那也叫重载。如果返回类型不一样,或者一个静态一个非静态,或者子类方法的开放性比父类被覆盖方法的更低,都会编译错。如果两个方法其它一样,又都是静态的,这没问题,叫做隐藏,而非覆盖。另外,父类中用final修饰的方法不能被覆盖。
再重复一下方法覆盖的概念:如果在子类中定义一个方法,其名称、返回类型及参数签名正好与父类中某个方法的名称、返回类型及参数签名相匹配,那么可以说,子类的方法覆盖了父类的方法。
覆盖的说明:
· 子类的方法名称返回类型及参数签名 必须与父类的一致
· 子类方法不能缩小父类方法的访问权限
· 子类方法不能抛出比父类方法更多的异常
· 方法覆盖只存在于子类和父类之间,同一个类中只能重载
· 父类的静态方法不能被子类覆盖为非静态方法
· 子类可以定义于父类的静态方法同名的静态方法,以便在子类中隐藏父类的静态方法(满足覆盖约束),
· Java虚拟机把静态方法和所属的类绑定,而把实例方法和所属的实例绑定。
· 父类的非静态方法不能被子类覆盖为静态方法
· 父类的私有方法不能被子类覆盖
· 父类的抽象方法可以被子类通过两种途径覆盖(即实现和覆盖)
· 父类的非抽象方法可以被覆盖为抽象方法
1.3 验证成员方法的覆盖方式
方法覆盖为子类提供了修改父类实例方法的能力。例如,Object类的toString方法返回的字符串是“类名@随机码”这样的。子类可以修改层层继承下来的Object 根类的toString 方法,让它输出一些更有用的信息。下面的程序显示了在子类Circle 中添加toString 方法,用来返回圆半径和圆面积信息。
(1) 编写Circle类,覆盖Object 类的toString方法,Circle类和Test类源代码如下。
class Circle {
private int radius;
Circle(int r) {
setRadius(r);
}
public void setRadius(int r) {
radius=r;
}
public int getRadius() {
return radius;
}
public double area() {
return Math.PI*radius*radius;
}
public String toString() {//覆盖继承自Object的toString()方法
return "圆半径:"+getRadius()+" 圆面积:"+area();
}
}
public class Test{
public static void main(String args[]) {
Circle c=new Circle(10);
System.out.println(c.toString());
}
}
1.4 this、super和super()的使用
(1) 程序功能:说明this.、this()、super. 和super()的用法。程序首先定义Point(点)类,然后创建点的子类Line(线)。Line继承了Point的点,它自己又定义了一个点。最后通过Test类输出线段的长度。Line对象程序中通过super(a,b)调用父类Point 的构造方法为父类的x和y赋值。在子类Line 的setLine方法中,因为参数名和成员变量名相同,为给成员变量赋值,使用this 引用,告诉编译器是为当前类的成员变量赋值。在length 和toString 方法中使用父类成员变量时,使用super 引用,告诉编译器使用的是父类的成员变量。
(2) 程序源代码如下。
class Point {
protected int x, y;
Point(int a, int b) {
setPoint(a, b);
}
public void setPoint(int a, int b) {
x=a;
y=b;
}
}
class Line extends Point {
protected int x, y;
Line(int a, int b) {
super(a, b);
setLine(a, b);
}
public void setLine(int x, int y) {
this.x=x+x;
this.y=y+y;
}
public double length() {
int x1=super.x, y1=super.y, x2=this.x, y2=this.y;//this.可省略
return Math.sqrt((x2-x1) * (x2-x1) + (y2-y1) * (y2-y1));
}
public String toString() {
return "直线端点:[" + super.x + "," + super.y + "] [" +x + "," + y + "] 直线长度:" + this.length();
}
}
public class Test{
public static void main(String args[]) {
Line line=new Line(50, 50);
System.out.println(line.toString());
}
}
说明:这个例子只为验证super(),super.,this.等的用法。当然,线并不是一种点,所以用Line继承Point不合适。用聚合的形式更合适些。例如可以如下修改:
class Point {
protected int x, y;
Point(int x, int y) {
setPoint(x, y);
}
public void setPoint(int x, int y) {
this.x = x;
this.y = y;
}
}
class Line{
Point a, b;
public double length() {
return Math.sqrt((a.x-b.x) * (a.x-b.x) + (a.y-b.y) * (a.y-b.y));
}
public Line(){}
public Line(Point a, Point b){
this.a = a;
this.b = b;
}
public Line(int x1, int y1, int x2, int y2){
a = new Point(x1, y1);
b = new Point(x2, y2);
}
}
public class Test{
public static void main(String args[]) {
Line line=new Line(1,1,2,0);
System.out.println("直线端点:[" + line.a.x + "," + line.a.y + "] - [" +line.b.x + "," + line.b.y
+ "] 直线长度:" +line.length());
}
}
1.5 变量、静态方法的隐藏。
父类和子类定义了同名的变量,或者相同的静态方法。
(1)父类和子类有相同的变量。父类变量被隐藏。将B类中的x所在行屏蔽起来或者不屏蔽,比较输出结果有何变化。
class A{
int x=1;
}
class B extends A{
//int x = 2;
void printX(){//输出父类对象的x值
System.out.println(super.x);//如果B类没有声明x,那么super.可省略。但如果B类也声明了变量x,那么父类的x被隐藏,要访问父类对象的成员,super.不能省略
}
}
public class Test{
public static void main(String []s){
B p = new B();
System.out.println(p.x);
p.printX();
//System.out.println(p.super.x);//语法错误。要用super.x引用B类的父类对象的成员,可以在B类中进行
}
}
(2)父类和子类有相同的静态方法。父类静态方法被隐藏。下面B类中的print()方法,屏蔽或不屏蔽,输出结果有何不同?
class A{
static void print(){
System.out.println("A");
}
}
class B extends A{
/*
static void print(){
System.out.println("B");
}
*/
}
public class Test{
public static void main(String []s){
B p = new B();
p.print();
}
}
【说明】上面A、B两类中的print()方法都是static的,称为静态方法隐藏。如果都是非静态的,则称为覆盖。如果其中一个是静态的,另一个非静态,则编译错,提示“无法覆盖”。
1.6 隐藏与覆盖的综合性实例
注意看蓝色或红色的注释。其中,红色的注释是容易出错的地方。
class A{
int x = 1;//被隐藏
void print(){//被覆盖
System.out.println("这里是父类方法,x="+x);//父类A的方法中访问的变量必然是A类或A的父类的,不可能访问B类的。
m();//父类A的方法中调用的实例方法m()是子类B的,由于发生了覆盖
}
void m(){//被覆盖
System.out.println("这里是父类的实例方法m()");
}
static void m2(){//被隐藏
System.out.println("这里是父类的静态方法m2()");
}
}
class B extends A{
int x = 2;
void print(){
System.out.println("这里是子类方法,x="+x);//子类方法访问的变量是子类对象的(当然条件是子类中声明了这个变量)
System.out.println("这里是子类方法,super.x="+super.x);//super.x是父类对象的
super.print();//调用父类的print()方法
m();//调用本对象的m()方法
}
void m(){
System.out.println("这里是子类的实例方法m()");
}
static void m2(){
System.out.println("这里是子类的静态方法m2()");
}
}
public class Test{
public static void main(String []s){
A p = new B();
System.out.println(p.x);//通过引用变量p来访问变量或静态方法,要看p的声明类型。所以x是A类的。
p.m2();//同上。静态方法m2()是A类的。
p.print();//通过引用变量p来访问实例方法,要看p指向的对象的实际类型。由于覆盖,调用的print()方法是子类的。
}
}
2. 类的多态性练习
c++允许多重继承(多个直接父类),功能强大,但复杂的继承关系也给c++开发者带来了很大麻烦。为了规避风险,java只允许单重继承,即,只能有一个直接父类,所以继承关系简单明了。但在功能上又给开发者提出了难题。因此,Java用多态性、抽象类、接口的概念来弥补此项不足。
多态性: 在程序中同一符号或名字在不同情况下具有不同解释
Ø 编译时多态性: 指在程序编译阶段即可确定下来的多态性。重载发生时,编译器根据方法签名来确定未来会调用哪个方法。
Ø 运行时多态性: 指必须等到程序动态运行时才可确定的多态性,JVM根据引用变量指向的实际对象来绑定方法。
2.1 普通方法的重载
(参考实验3,本实验略)
2.2 构造方法的重载
(参考实验3,本实验略)
2.3 运行时多态与动态绑定
一组方法,在多个类中都有定义。由于继承与覆盖,到底调用哪个方法,需要根据不同的对象来进行动态绑定。因此结果呈现多态性。
如果A是父类,B是子类,那么A p = new B();是合法的。p的类型被声明成A类型,但p实际上指向一个B类对象。
(1) 如果A、B两类中都有实例方法m(),那么p.m()调用的是B中的m()。
(2) 如果A中有而B中没有,那么p.m()调用的是B类对象从A继承而来的m()方法。这是动态绑定。
(3)如果A中没有m()方法,则无论B中有没有,都会编译错。因为编译并非执行,编译器发现p是A类型的,而A类型以及A的父类都没有m()方法,也就是说,A类没有这个方法,也没有通过继承获得这个方法。
另外,关于静态方法:
(1)如果m()方法在A、B两个类中,一个静态一个非静态,则编译错。(无法覆盖)
(2)如果m()方法在A中是静态的,在B中没有,或者也是静态的,那么p.m()调用的是A类的静态方法。编译的时候就把p.m()换成A.m()。读1.6隐藏与覆盖的综合性实例。
(3)如果A中没有静态方法m(),那么编译器要看A的父类有没有。静态方法虽然无法覆盖,但也可继承。如果其父类也没有,则会编译错。
下面的程序,Test类中函数showInformation的形参是Person类型的引用变量x。实参如果是其子类对象,那么就是让x指向子类对象,这和A p = new B()的原理是一样的。
//代码Test.java
class Person{
protected String name; //姓名
Person(){}
Person(String name){
this.name = name;
}
String information(){
return "类Person: "+name;
}
}
class Student extends Person{ //学生类
protected int id; //学号
Student(){}
Student(String name, int id){
super(name);
this.id = id;
}
/*
String information(){
return "类Student: "+name+id;
}
*/
}
class HiStudent extends Student{ //大学生类继承自学生类
protected String school;//大学名称
protected String department;//系的名称
HiStudent(){}
HiStudent(String name, int id, String school, String department){
super(name, id);
this.school = school;
this.department = department;
}
String information(){
return "类HiStudent: "+name+id+school+department;
}
}
public class Test{
public static void main(String []as){
showInformation(new Person("张三"));
showInformation(new Student("张三", 201122));//Student类没有information方法,因此showInformation将调用Student对象继承来的information方法。这是动态绑定。
showInformation(new HiStudent("张三", 201122, "西南大学", "计科系"));
}
static void showInformation(Person x){
System.out.println(x.information());
}
}
问题:
1、如果在Student类中,将对information()方法的屏蔽取消,结果有何变化?
2、Test类showInformation方法的形参类型是Person。将其改为Object,编译能通过吗?
3. 抽象类
将父类设计得非常抽象,让它包含所有子类的共同属性、方法,以至于它没有具体的实例。如果子类不是抽象类,就必须实现(覆盖)抽象父类中的所有抽象方法。这其实也就规定了子类必须实现的的共同行为。
abstract class GeometricShape{//几何形状类
public abstract double getArea();//不知道具体形状,无法计算,只能定义为抽象方法
public abstract double getPerimeter();
}
class Circle extends GeometricShape{
private double radius;
public Circle(double radius){
this.radius = radius;
}
public double getArea(){
return Math.PI*radius*radius;
}
public double getPerimeter(){
return 2*Math.PI*radius;
}
}
class Rect extends GeometricShape{
private double width, height;
public Rect(double w, double h){
width = h;
height = h;
}
public double getArea(){
return width*height;
}
public double getPerimeter(){
return 2*(width+height);
}
}
public class Test{
public static void main(String []s){
GeometricShape g1 = new Circle(5);
GeometricShape g2 = new Rect(5, 3);
System.out.println("面积相同吗?" + equalArea(g1,g2));
printGeometricShape(g1);
printGeometricShape(g2);
}
public static boolean equalArea(GeometricShape g1, GeometricShape g2){
return g1.getArea()==g2.getArea();
}
public static void printGeometricShape(GeometricShape x){
System.out.println();
if(x instanceof Circle) System.out.println("这是一个圆");
else if(x instanceof Rect) System.out.println("这是一个矩形");
else return;
System.out.println("面积是" + x.getArea());
System.out.println("周长是" + x.getPerimeter());
}
}
问:如果将抽象类GeometricShape中的抽象方法删除,编译能通过吗?
下面是关于抽象类的说明:
l 注意:
– 1 抽象类前需加修饰符abstract
– 2 不能使用new方法进行实例化,故,抽象类必须被继承
– 3 抽象类可包含常规类能够包含的任何东西,例如构造方法等非抽象方法,其构造方法在子类的构造方法中调用
– 4 没有抽象方法的类也可被声明为抽象类
– 5 包含抽象方法的类必须声明为抽象类
– 6 若子类没有实现父类的全部抽象方法,它也必须声明为抽象类
– 7 抽象方法必须是非静态的,子类中的实现也必须是非静态的,否则无法覆盖
– 8 即便父类是具体类,子类也可能是抽象类
– 9 抽象类虽然无法用new实例化,但可以用作数据类型
例:抽象类 x = new 子类();
4. 接口
接口允许我们在看起来不相干的对象之间定义共同属性和共同行为。
下面的例子中,电视机TV和人Person看似不相干,但它们有相同的行为,比如,都有年龄信息。它们都实现了年龄接口Age。注意:TV和Person都不是Age的子类,但它们又可以看做是Age的子类,还可以直接访问Age中的常量。
更多的接口学习,将在以后的图形界面和事件的章节中学习。这里学习接口的基本知识。
interface Age{ //定义年龄接口
int month = 3;//int前面自动隐含了public static final
String ageInformation();//void前面自动隐含了public abstract
//String ageInformation(){}//编译错,接口中的方法不能有{}主体
}
class TV implements Age{//电视机有年龄,TV要实现Age接口
public String ageInformation(){//实现接口中的抽象方法pringAge(),这也是覆盖
return "这台电视机的年龄是5岁"+month+"个月";//可访问接口中的常量month
}
}
class Person implements Age{//人有年龄,Person要实现Age接口
public String ageInformation(){//实现接口中的抽象方法pringAge(),这也是覆盖
return "这个人的年龄是20岁"+month+"个月";//可访问接口中的常量month
}
}
public class Test{
static void print(Age x){//Age是接口,虽然无法用new来建立对象,但它可作为类型,相当于是TV和Person的父类
System.out.println(x.ageInformation());
}
public static void main (String args[]){
print(new TV());
print(new Person());
}
}
编译并运行。
展开阅读全文