第四章 继承
一、继承的定义
继承是在一个已有类的基础上构建新类,新类除了继承已有类的属性和方法,还可以根据需要增加新的属性和方法。新类称作子类(或派生类),已有类称作父类(或超类)。子类可以从父类继承成员变量和成员方法。如果想声明一个类继承另一个类,使用extends关键字,格式为:
1 | class 子类名 extends 父类名 { |
父类可以是自己编写的类,也可以是已有的类或java类库中的类。继承有利于实现代码的复用,子类只需添加新的代码,父类已有的代码不必重写。Java不支持多重继承,一个子类只能用extends声明一个父类。子类从父类继承的成员变量和成员方法就和子类自己声明的一样。
如果一个类没有声明继承某个父类,那么这个类默认是Object的子类。也就是说,class A {……}与class A extends Object{……}是等价的。Java中所有的类都直接或间接的继承Object类,Object是所有Java类的根父类。
1 | class Father{ //父亲 |
注意:Java只支持单继承,一个类只能有一个父类,但是多个类可以继承同一个父类;
继承关系可以传递:C继承B,B继承A,则C也间接继承A,是A的子类(间接子类),A也是C的父类(间接父类)。
-
继承的内容
子类的成员有一部分是自己定义的,另一部分是从父类继承的。子类可以继承父类的实例变量和实例方法,但是子类不继承父类的构造方法
。
子类也可以继承父类的类变量和类方法。被继承的类变量和类方法,除了可以通过子类对象或父类对象访问,还可以通过"子类名.类方法/类变量"或者"父类名.类方法/类变量"的方式来访问。
子类从父类继承的成员变量和成员方法的访问权限不变。
-
子类和父类在同一包中的继承性
如果子类和父类在同一个包中,那么子类能够继承父类的非private的成员变量和成员方法。任何情况下,父类的private的成员变量和成员方法都不能被继承。
-
子类和父类不在同一包中的继承性
如果子类和父类不在同一个包中,那么子类只能继承父类的protected和public成员,不能继承父类的友好成员和private成员。 -
访问修饰符protected的进一步说明
如果一个类B要访问另一个类A的protected成员,则权限如下所述:
- 如果类A的protected成员是自己定义的,则类B和类A在同一个包中就可以访问,否则不能访问。
- 如果类A的protected成员是继承的,那就要追溯到定义该protected成员的那个祖先类,如果类B和类A的那个祖先类在同一个包中,就能访问该protected成员,否则不能访问。
- 子类对象的创建
子类对象是按照下面的顺序创建的:
(1) 创建并初始化父类的静态成员变量(只在第一次使用父类时执行一次)
(2) 创建并初始化子类的静态成员变量(只在第一次使用子类时执行一次)
(3) 创建并初始化父类的实例成员变量 (不管是否被继承
)。
(4) 调用父类的构造方法;
(5) 创建并初始化子类的实例成员变量
(6) 调用子类的构造方法。
子类可以通过继承父类的方法来访问父类不能被继承的那些成员,看下面的例子:
1 | class A{ |
二、成员变量的隐藏和成员方法的重写
- 成员变量的隐藏
当子类的成员变量和从父类继承的
成员变量同名时(类型不必相同,可以是实例变量也可以是类变量),子类会隐藏父类的同名成员变量,也就是说,在子类中访问的同名成员变量是子类自己定义的变量。不能被继承的父类成员变量不存在隐藏问题。
注意:当子类和父类有同名成员变量时,在子类方法中访问的是子类的成员变量,在父类方法中访问的是父类的成员变量。
在下面的例子中,子类的int型成员变量y隐藏了从父类继承的double型成员变量y,但是子类可以使用从父类继承的方法f()访问被隐藏的父类成员变量y。
1 | class A{ |
2.方法重写
在子类中定义一个方法,这个方法的名字、参数、返回值类型与从父类继承的某个方法完全相同,这就是子类对父类方法的重写(或者叫覆盖)。子类隐藏父类被重写的方法,当通过子类对象或在子类内部
调用同名方法时,调用的总是子类重写的方法。如果子类对象或在子类内部想使用被重写的父类方法,可以使用关键字super。
重写是针对实例方法,类方法不存在重写问题。不能被继承的父类方法也不存在重写问题。
重写父类方法时,不能降低子类方法的访问权限。例如,若父类方法的访问权限protected,则子类重写方法的访问权限不能低于protected级别,可以是protected或public。
如果子类的某个方法与继承的父类的某个方法同名,但参数不同,则不是重写。这种情况下,子类拥有名字相同而参数不同的两个方法,这是方法的重载。
下面这个例子可以帮助我们更好的理解方法重写:
1 | class A{ |
三、Super关键字
super关键字表示当前的父类,有两种用法:(1)在子类的构造方法中,使用super关键字指定调用父类的某个构造方法;(2) 在子类中通过super关键字访问父类的成员变量和成员方法。
super关键字只能用于构造方法和实例方法,不能用于类方法。
- 使用super调用父类的构造方法
在子类的构造方法中,通过"super(参数列表);"的形式,来指定调用父类的某个构造方法,由参数列表确定具体调用哪个构造方法。super语句必须是第一条语句。如果子类构造方法中没有super语句,系统默认调用父类不带参数的构造方法,这种情况下要确保父类有不带参数的构造方法(或者自己定义,或者由系统默认添加),否则编译出错。
注意:构造方法中的super语句只是起到占位符的作用,用于确定调用父类的哪个构造方法,并不是先执行super语句然后再执行子类构造方法中剩余语句的意思,而是:先创建父类成员变量,再调用父类构造方法,然后创建子类成员变量,再调用子类构造方法。
- 使用super访问父类的成员变量和成员方法
如果子类想访问父类被隐藏的成员变量以及被重写的成员方法,可以使用关键字super。访问父类其它的成员变量和成员方法,也可以使用super关键字,只是这种情况下一般省略super。
1 | class A{ |
1 | class A{ |
四、Final类与Final方法
用final关键字修饰的类称为final类。final类不能被继承,即不能有子类。按照如下方式定义的类A就是一个final类。
1 | final class A{ |
有时候出于安全性的考虑,将一些类修饰为final类。例如,Java提供的String类,它对于编译器和解释器的正常运行有很重要的作用,对它不能轻易改变,因此它被修饰为final类。
用final关键字修饰的方法称为final方法。如果一个方法被修饰为final方法,则这个方法可以被继承,但不能被重写,即不允许子类重写父类的final方法。
上机作业:
编写一个父类A,该类有个方法int f1(int a, int b),计算a和b的最大公约数。再编写一个子类B,该类有个方法int f2(int a, int b),计算a和b的最小公倍数,在该方法中调用父类的方法f1()完成最小公倍数的计算,提示:最大公约数 * 最小公倍数 = a * b。在应用程序的主类C中,创建一个子类B的对象,分别计算两个数的最大公约数和最小公倍数,并输出结果。
程序大致如下,不要再问我、或让我写程序、或呆坐在那里!最大公约数的计算代码,上网查,但不要用递归程序实现!
1 | public class A{ |
五、上转型与多态
1. 上转型
设类B是类A的子类(直接或间接子类),将子类B的对象赋值给父类A的变量:A a = new B(),这称为子类对象的上转型,a称为上转型对象。上转型可以自动将子类对象转换为父类对象,不需要强制转换。
子类对象上转型后,不能再使用子类新增的属性和方法,只能使用从父类继承的属性和方法。对于父类被子类重写的方法,实际访问的是子类的方法,但访问权限还是按照父类方法的访问权限。
对于上转型,需要注意以下几点:
- 调用一个方法时,把子类对象作为实参传给父类的形参,这也是上转型。
- 若子类重写了父类的方法,则上转型对象在调用同名方法时,调用的是子类重写的方法。
- 若子类隐藏了父类的属性,则上转型对象在访问同名属性时,访问的是父类被隐藏的属性。
- 父类不能被子类继承的属性和方法,则上转型对象不能访问。
注意:如果两种类型之间没有继承关系,那么Java编译器不允许在两者间进行类型转换,例如:
1 | Dog dog = new Dog(); |
下转型是把父类对象赋值给子类变量:B b = (B)a; b称为下转型对象。下转型需要强制转换,分两种情况:
- 把一个父类对象强制转换为子类对象。这是不允许的,尽管编译能通过,但不能运行。
- 先将子类对象上转型,然后再下转型。这是允许的,上转型对象通过下转型又转换回原来的子类对象,可以使用子类的全部属性和方法。
总结:把子类对象赋值给父类变量叫上转型,上转型不用强制转换;把父类对象赋值给子类变量叫下转型,要强制转换。当出现下转型时,一定要在此之前先执行过上转型,然后再执行下转型。
2. 多态
Java的多态有两种实现方式:
- 静态多态:重载(Overloading);
- 动态多态:和重写(Overwriting)相关。
父类的方法被不同的子类重写时,表现出不同的行为。例如,哺乳动物是父类,具有行为"叫",而子类狗的"叫" 是"汪汪…,子类猫的"叫" 是"喵喵…"。
一个父类有多个子类,这些子类都重写了父类的方法,通过上转型对象访问父类被重写的方法,则不同子类的上转型对象表现出不同的行为,这就是多态。
多态的实现条件:继承—重写—上转型。看下面的例子:
1 | class Animal{ |
六、抽象类和抽象方法
用关键字abstract修饰的类称为抽象类(abstract类),定义格式如下:
1 | abstract class A{ |
abstract类不能用来创建对象实例,但是可以被继承,也可以用来声明变量。不允许使用final和abstract同时修饰一个类。
用关键字abstract修饰的成员方法称为抽象方法(abstract方法),抽象方法只有声明没有实现,也就是没有方法体。抽象方法必须是实例方法。抽象方法可以被子类重写(在子类中实现方法体),重写的方法不再是抽象方法。如果子类不重写父类的抽象方法,则继承该抽象方法。同样不允许使用final和abstract同时修饰一个方法。有abstract方法的类一定是abstract类,而abstract类不一定有abstract方法。
抽象类和抽象方法的例子:
1 | abstract class A{ //抽象类 |
子类如果不重写abstract父类的abstract方法,则会继承该abstract方法,从而子类也成abstract类。
思考:
- 构造方法可以是抽象方法吗?
- 抽象类不能用new操作符创建对象,那么有构造方法吗?
1 | public abstract class Geometry{ //抽象类 |
上机作业:
编写一个Java程序,除了主类TestGeometry外,该程序还有一个抽象类Geometry、一个类Rectangle和一个类Circle。要求:
(1) 抽象类Geometry有两个抽象方法:public abstract double computeArea(),用于计算面积;public abstract double computeLength(),用于计算周长。
(2) Rectangle类继承Geometry,重写父类计算面积和周长的方法,并打印输出面积和周长,格式为:矩形的面积 = xxx; 矩形的周长 = xxx; 有两个私有成员变量分别保存矩形的长和宽,一个构造方法用于初始化这两个成员变量。
(3) Circle类继承Geometry,重写父类计算面积和周长的方法,并打印输出面积和周长,格式为:圆的面积 = xxx; 园的周长 = xxx; 有一个私有成员变量用于保存圆的半径,一个构造方法用于初始化这个成员变量。圆周率PI = 3.142定义为常量。
(4) 在TestGeometry类的main方法中分别创建Rectangle类和Circle类的对象,从键盘输入矩形的长和宽,以及圆的半径.。通过上转型对象计算并打印输出各个对象的面积和周长。
七、内部类
所谓的内部类,就是定义在一个类内部的类,包括常规内部类、匿名内部类、静态内部类(不要求掌握)
、局部内部类(不要求掌握)
。
1. 常规内部类
定义在类体中(不是方法体中)且没有用static关键字修饰的类。
定义常规内部类时可以使用private、protected、public、友好等访问权限,与用于成员变量和成员方法时的规则相同。也就是说,可以将常规内部类看作是外部类的一个成员。
在常规内部类中可以访问外部类的成员,用"外部类名.this.外部类成员"的形式访问外部类的实例成员,用"外部类名.外部类成员"的形式访问外部类的静态成员。在不引起歧义的情况下,前缀"外部类名.this"和"外部类名"可以省略。
在外部类的实例方法和构造方法中可以直接声明内部类的变量,以及创建内部类的对象:
内部类名 变量名 = new 内部类名(…)。
在外部类的实例方法和构造方法之外的地方创建常规内部类的对象:
- 声明内部类的变量: 外部类名.内部类名 变量名;
- 创建内部类的对象: 外部类对象.new 内部类名(…);
也就是要先创建外部类对象,然后通过外部类对象创建内部类对象。
1 | class MyOuter { |
2. 匿名内部类(简称匿名类)
在一个类的内部定义,在定义类的同时创建对象并且类没有名字,这种类称为匿名内部类,定义格式:
1 | new 类或接口(…){ |
匿名类必须继承一个类或实现一个接口,new后面的名字是匿名类的父类或要实现的接口,new返回的是所创建的匿名类对象。匿名类的定义和使用都在同一个地方,如果某个类的对象只使用一次,可以考虑使用匿名类。
由于匿名类没有名字,所以不能定义构造方法。思考:为什么?
匿名类可以访问外部类的成员变量和成员方法:
- 如果匿名类定义在外部类的静态方法中,那么只能访问外部类的静态成员;如果匿名类定义在外部类的实例方法中,则外部类的静态成员和非静态成员都可以访问。
- 如果匿名类定义在外部类的类体中,那就看把匿名类对象赋值给外部类的静态成员变量还是实例成员变量。如果是赋值给静态成员变量,则只能访问外部类的静态成员;如果赋值给实例成员变量,则外
部类的静态的和非静态成员都可以访问。 - 可以用"外部类名.this.成员"的形式来访问外部类的实例成员,用"外部类名.成员"的形式来访问外部类的静态成员。在不引起歧义的情况下,前缀"外部类名.this"和"外部类名"可以省略。
定义形式
- new ClassName(…) {…}; //匿名类继承父类ClassName,圆括号内如果有参数,是传给父类ClassName的构造方法的参数
- new InterfaceName() {…}; //匿名类实现接口InterfaceName,圆括号内不能有参数。
常用方式
- someMethod(new Xxx(){ … }); //将匿名类对象作为方法的参数
- return new Xxx(){ … }; //将匿名类对象作为方法的返回值
- Xxx a = new Xxx(…) {…}; //匿名类对象上转型
1 | class MyOuter { |
3. 静态内部类(不要求掌握)
定义在类体中并且用static关键字修饰的内部类。
静态内部类可以直接访问外部类的static成员,但不能直接访问外部类的实例成员。同样可以用权限修饰符限制静态内部类的访问权限。
在static内部类中不能使用"外部类名.this"访问外部类的成员。
创建静态内部类的对象:
- 声明静态内部类的对象变量:外部类名.内部类名 对象变量;
- 创建静态内部类的对象实例:new 外部类名.内部类名(…);
静态内部类可以定义在接口中,定义在接口中的任何类都自动成为public和static的。
1 | public class MyOuter { |
4. 局部内部类(不要求掌握)
在方法或语句块中定义的类称为局部内部类。
- 局部内部类只能在定义它的方法或语句块内可以使用。
- 局部内部类同局部变量一样不能使用private、protected、public等访问修饰符,也不能使用static修饰。
- 局部内部类可以访问外部类的成员(static成员和实例成员),也可以通过"外部类名.this"访问外部类对象及成员。
1 | public class MyOuter{ |
上机作业:
- 抽象类Geometry有两个成员变量double width和double height(保存矩形的宽和高)。
- 一个构造方法Geometry(double width, double height)用于给两个成员变量赋初值。
- 有两个抽象方法:double computeArea()计算矩形面积,double computeLength()计算矩形周长。
- 主类TestClass有一个Geometry类型的成员变量ge。
- 有一个构造方法TestClass(double width, double height),width和height分别指定矩形的宽和高。
- 在构造方法中定义一个父类为Geometry的匿名类,在匿名类中重写Geometry的抽象方法double computeArea()和double computeLength(),创建匿名类对象所需的宽和高由构造方法TestClass(double width, double height)的参数提供,将匿名类对象赋值给变量ge,从而成上转型对象。
- 在TestClass中定义两个实例方法public double computeArea()和public double computeLength(),分别使用匿名类的上转型对象ge计算周长和面积,并打印输出,格式为:矩形的面积 = xxx; 矩形的周长 = xxx。
- 在类TestClass的main方法中创建一个TestClass对象,调用该对象的方法完成周长和面积的计算及打印输出。