**
**
继承和多态是 Java 语言面向对象程序设计的两个特征,本章将着重讲述继承和多态的概
念和用法,以及 final、static 关键字的使用等。
本章学习要点如下:
⚫ 继承的概念和使用
⚫ 多态
⚫ 初始化块
⚫ final 关键字的使用
⚫ 抽象类和接口
继承,封装,多态是面向对象的三大特征之一
Java的继承具有单继承的特点
Java 的继承通过 extends 关键字来实现。实现继承的类称为子类(派生类)
被继承的类称为父类(基类或超类)。父类和子类的关系,是一种一般和特殊的关系。
子类继承父类的语法格式如下:
[修饰符] class 子类名 extends 父类名{
//子类代码部分
}
在子类中构造一个与父类方法相同的方法,在子类中对其进行重写这种子类包含与父类
同名方法的现象被称为方法重写,也称为方法覆盖(Override)。可以说子类重写了父
类的方法,也可以说子类覆盖了父类的方法。
注意:方法的重写要遵循“三同一小一大”规则,“三同”即方法名相同,形参列表相同
返回值类型相同。“一小”指的是子类方法声明抛出的异常类应比父类方法声明抛出的异
常类更小或相等。“一大”指的子类方法的访问权限应比父类方法更大或相等,尤其需要
指出的是,覆盖方法和被覆盖方法要么都是类方法,要么都是实例方法,不能一个是类方
法,一个是实例方法
当子类覆盖了父类方法后,子类的对象将无法访问父类中被覆盖的方法,但仍可以在子类
方法中调用父类中被覆盖的方法。如需要在子类方法中调用父类中被覆盖方法,可以使用
super或者父类类名作为调用
如果父类方法具有 private 访问权限,则该方法对其子类是隐藏的,因此其子类无法访问
该方法,也就无法重写该方法。如果子类中定义了一个与父类 private 方法具有相同方法名,相
同形参列表,相同返回值类型的方法,依然不是重写,只是在子类中重新定义了一个新方法。
5.1.3 父类实例的 super 引用
如果需要在子类方法中调用父类被覆盖的实例方法,可以用 super 作为调用者进行调用。
如果被覆盖的是类属性,在子类的方法中则可以通过父类名调用访问被覆盖的类属性。如
果子类里没有包含和父类同名的属性,则子类将直接继承到父类属性。如果在子类实例方法中
访问该属性时,则无须显示使用 super 或父类名作为调用者。因此,如果我们在某个方法中访
问名为 a 的属性,但没有显示指定调用者,则系统查找 a 的顺序如下。
⚫ 查找该方法中是否有名为 a 的局部变量。
⚫ 查找当前类中是否包含名为 a 的属性。
⚫ 查找 a 的直接父类中是否包含名为 a 的属性,依次上溯 a 的父类,直到 java.lang.Object
类,如果最终不能找到名为 a 的属性,则系统出现编译错误。
子类不会获得父类的构造方法,但有些时候子类构造方法却需要调用父类构造方法的初始
化代码,就如前面所介绍的一个构造方法需要调用另一个重载的构造方法一样。
在一个构造方法中调用另一个重载的构造方法,一般使用 this 调用来实现;在子类构造方
法中调用父类构造方法,一般使用 super 调用来实现
不管我们是否使用 super 调用来执行父类构造方法的初始化代码,子类构造方法总会调用
父类的构造方法一次。子类构造方法调用父类构造方法分如下几种情况:
⚫ 子类构造方法执行体的第一行代码使用 super 显示调用父类构造方法,系统将根据
super 调用里传入的实参列表调用父类对应的构造方法。
⚫ 子类构造方法执行体的第一行代码使用 this 显式调用本类中重载的构造方法,系统将
根据 this 调用里传入的实参列表调用本类另一个构造方法。执行本类中另一个构造方
法时即会调用父类的构造方法。
⚫ 子类构造方法执行体中既没有 super 调用,也没有 this 调用,系统将会在执行子类构
造方法之前,隐式调用父类无参数的构造方法。
不管上面哪种情况,当调用子类构造方法来初始化子类对象时,父类构造方法总会在子类
构造方法之前执行。不仅如此,执行父类构造方法时,系统会再次上溯执行其父类的构造方
法……依此类推,创建任何 Java 对象,最先执行的总是 java.lang.Object 类的构造方法
多态是指同一操作作用于不同的对象,可以有不同的解释,产生不同的执行结果。它是面
向对象程序设计(OOP)的一个重要特征。
在 Java 中,为了实现多态,允许使用一个父类的变量来引用一个子类的对象。根据被引用
子类对象特征的不同,得到不同的运行结果。
在父类的引用变量引用了 父类 类型的对象时,通过该引用变量调用 方法,调
用的是 父类 类型中定义的 方法。而当 父类类型的引用变量引用了 父类的
子类的对象时,通过该引用变量调用 这个方法,则调用的是子类中定义的 的方法。
作用:
多态不仅解决了方法同名的问题,而且还使得程序变得更加灵活,从而有效地提高了程
序的可扩展行和可维护性。
此时我们把子类的对象当成父类的对象去看待,当编译器检查到“an1.sleep();”
时,发现 Animal 类中并没有定义 sleep()方法,则报错。但该对象的本质是子类类型,
这时如果恢复子类对象的身份,则可以调用 sleep()方法,即
Animal an1=new Dog();
a1.sleep();//报错的
Dog d=(Dog)an1;
d.sleep();
这实际上是将父类当做子类使用的情况,在 Java 语言环境中被称为“向上转型”。
需要注意的是,这种向上转型也可能出现错误。
在向上转型前需要判定指定对象是否为某个类型的实例或者子类的实例,可以使
用 instanceof 关键字来实现。
public static void func(Animal an){
an.shout();
if(an instanceof Dog){
Dog d=(Dog)an;
d.sleep();
} }
强制轉換;在 func()方法中,可以使用 instanceof 关键字判断方法中传入的对象是
否为 Dog 类型,如果是 Dog 类就进行强制类型转换,然后再调用子类中定义的 sleep()方法。
Java 中使用构造方法来对单个对象进行初始化操作。与之非常类似的是初始化块,它也可
以对对象进行初始化操作。
初始化块是 Java 类里可以出现的第 4 种成员(前面 3 个依次有属性、方法和构造方法),
一个类里可以有多个初始化块,相同类型的初始化块之间,前面定义的初始化块先执行,后面
定义的初始化块后执行,初始化块的语法格式如下:
[修饰符]{
//初始化块的可执行性代码
}
初始化块的修饰符只能是 static,被称为静态初始化块。其中的代码可以包含任何可执行
性语句,如定义局部变量、调用其他对象的方法,以及使用分支、循环语句等。
初始化块和构造方法的作用非常相似,它们都用于对 Java 对象执行指定的初始化操作,
但两者之间仍然存在着差异。具体差异在于:初始化块是一段固定的执行代码,它不能接受任
何参数,因此初始化块对同一个类内的属性所进行的初始化处理完全相同。基于这个原因,不
难发现初始化块的用法,如果多个构造方法里有相同的初始化代码,这些初始化代码无需接受
参数,那就可以把它们放在初始化块中定义。通过把多个构造方法中相同的代码提取到初始化
块中定义,能更好的提高初始化块的复用,提高整个应用的可维护性。
与构造方法类似,创建一个 Java 对象时,不仅会执行该类的初始化块和构造方法,系统
还会一直追溯到 java.lang.Object 类,先执行 java.lang.Object 类的初始化块,执行 java.lang.Object
的构造方法,然后依次向下执行父类的的初始化块,执行父类的构造方法…,最后才执行该类
的初始化块和构造方法,返回该类的对象。
如果定义初始化块时使用了 static 修饰符,则这个初始化块就变成了静态初始化块。静态
初始化块是类相关的,系统将在类初始化阶段执行静态初始化块,而不是在创建对象时才执行,
因此静态初始块总是比普通初始化块先执行
静态初始块通常用于对整个类进行初始化处理,它可对类属性执行初始化,但不能对实例
属性进行初始化。静态初始块也被称为类初始化块,属于类的静态成员,同样需要遵循静态成
员不能访问非静态成员的规则,因此静态初始块不能访问实例属性和实例方法。
与普通初始化块类似的是,系统在类初始化阶段执行静态初始化时,不仅会执行本类的静
态初始化块,还会一直上溯到 java.lang.Object 类(如果它包含静态初始化块),先执行
java.lang.Object 类的静态初始化块,然后执行其父类的静态初始化块,经过这个过程,才完成
了对类的初始化过程。只有当类初始化完成后,才可以在系统中使用这个类,包括访问这个类
的方法、类属性,或者用这个类来创建实例。
注意:静态初始块和声明静态属性时所指定的初始值都是该类的初始化代码,其执行顺序
与源程序中排列顺序相同。
final 关键字可用于修饰类、变量和方法,被其修饰的类、变量和方法将无法发生改变。
final 修饰变量时,表示该变量一旦获得了初始值就不会再被改变。final 既可修饰成员变量
(包括类变量和实例变量),也可以修饰局部变量、形参。final 修饰成员变量和修饰局部变量
时有一些不同。
成员变量是随类初始化或对象初始化而初始化的。当类初始化时,系统会为该类的类属性
分配内存,并赋默认值;当创建对象时,系统会为该对象的实例属性分配内存,并赋默认值。
也就是说,当执行静态初始化块时可以对类属性赋初始值,当执行普通初始块、构造方法时可
对实例属性赋初始值。因此,成员变量的初始值可以在定义该变量时指定默认值,或在初始化
块、构造方法中指定初始值;否则,成员变量的初始值将由系统自动分配。
对于 final 修饰的成员变量而言,一旦有了初始值就不能被重新赋值,因此不可以在普通
方法中对成员变量重新赋值。成员变量只能在定义该成员变量时指定默认值,或者在静态初始
化块、初始化块和构造方法为成员变量指定初始值。如果既没有在定义成员变量时指定默认值,
也没有在初始化块、构造方法中为成员变量指定初始值,那么这些成员变量的值一直是 0、 ‘\u000’
、false 或 null,它们也就失去了存在的意义。
当使用 final 修饰成员变量的时候,要么在定义成员变量时候指定其初始值,要么在
初始化块、构造方法中为成员变量赋初始值。如果在定义该成员变量时指定了默认值,则不能
在初始化块、构造方法中为该属性重新赋值。归纳起来,final 修饰的类属性、实例属性能指定
初始值的地方如下。
⚫ 类属性:可在静态初始化块中或声明该属性时指定初始值。
⚫ 实例属性:可在非静态初始化块中或声明该属性时或构造方法中指定初始值。
注意:final 修饰的实例属性,要么在定义该属性时指定初始值,要么在普通初始化块或构
造方法中为该属性指定初始值。实例属性不能在静态初始化块中指定初始值,因为静态初始
化块是静态成员,不可访问实例属性—非静态成员;类属性不能在普通初始化块中指定初始
值,因为类属性在类初始化阶段已经被初始化了,普通初始化块不能对其重新赋值
由于系统不会对局部变量进行初始化,局部变量必须由程序员手动赋值。因此使用 final 修
饰局部变量时既可以在定义时指定默认值,也可以不指定默认值。如果 final 修饰的局部变量
在定义时没有指定默认值,则可以在后面代码中对该 final 变量赋初始值,但只能一次,不能
重复赋值;如果 final 修饰的局部变量在定义时已经指定默认值,则后面代码中不能再对该变
量赋值
当使用 final 修饰基本类型变量时,不能对基本类型变量重新赋值,因此基本类型变量不
能被改变。但引用类型的变量保存的仅仅是一个引用,当使用 final 修饰时,只保证这个引用
所引用的地址不会改变,即一直引用同一个对象,但对象本身的内容却完全可以发生改变。
使用 final 修饰的引用类型变量不能被重新赋值,但引用型变量
所引用的对象内容却可以改变。例如,p 变量使用了 final 修饰,表明 p 变量不能被重新赋值,
但 p 变量所引用 Person 对象的属性 age 却是可以被改变的。
注意:如果 final 修饰的变量为基本数据类型,且在编译时就可以确定该变量的值,则可以
把该变量当成常量处理。
子类继承父类时将可以访问到父类内部数据,并可通过重写父类方法来改变父类方法的实
现细节,从而导致一些不安全的因素。因此,当需要保证某个类无法被继承时,可以使用 final
修饰这个类
在面向对象的概念中,所有的对象都是通过类来描述的,但并不是所有的类都是用来描述
对象的。如果一个类中没有包含足够的信息来描绘一个具体的对象时,这样的类就是抽象类。
抽象类往往用来描述我们在对问题进行分析、设计中得出的抽象概念,是对一系列看上去不同
但本质上相同的具体概念的抽象。
抽象方法是只有方法签名,没有方法实现的部分。
Java 中用关键字 abstract 修饰的类称为抽象类,它拥有所有子类的共同属性和方法。用关
键字 abstract 修饰的方法称为抽象方法,该方法只有方法签名,没有方法体,方法签名后面以
分号(;)结束。抽象方法必须定义在抽象类中,但抽象类中的方法不一定都是抽象方法,也可
包含实现了的具体方法。抽象类不能被实例化,只能被子类继承,子类继承抽象类时,如果子
类不再是抽象类,必须实现从抽象类继承来的所有抽象方法。子类在实现从抽象父类继承来的
抽象方法时,其返回值类型、方法名、参数列表必须和父类相同。但不同的是子类有方法体,
且不同的子类可以有不同的方法体。
抽象方法和抽象类必须使用 abstract 修饰符进行定义,有抽象方法的类只能被定义成抽象
类,抽象类里可以没有抽象方法。抽象方法和抽象类的规则如下:
⚫ 抽象类必须使用 abstract 修饰符来修饰,抽象方法也必须使用 abstract 修饰符来修饰,
抽象方法不能有方法体。
⚫ 抽象类不能被实例化,无法使用 new 关键字来调用抽象类的构造方法创建抽象类的
实例。即使抽象类里不包含抽象方法,这个抽象类也不能创建实例。
⚫ 抽象类可以包含属性、方法(普通方法和抽象方法都可以)、构造方法、初始化块、内
部类、枚举类 6 种成分。抽象类的构造方法不能用于创建实例,主要是用于被其子类
调用。
⚫ 含有抽象方法的类(包括直接定义了一个抽象方法;继承了一个抽象父类,但没有完
全实现父类包含的抽象方法;以及实现了一个接口,但没有完全实现接口包含的抽象
方法 3 种情况)只能被定义成抽象类。
根据上面定义规则,不难发现:抽象类同样能包含和普通类相同的成员,但不能用于创建
实例;普通类不能包含抽象方法,而抽象类可以包含抽象方法。定义抽象方法只需在普通方法
前增加 abstract 修饰符,将其方法体(也就是方法后花括号括起来的部分)全部去掉,并在方
法后增加分号即可。
抽象方法和空方法体的方法不是同一个概念,例如 public abstract void test();是一个抽象方
法,它根本没有方法体,即方法定义后面没有一对花括号;但 public void test(){}方法是一个普
通方法,它已经定义了方法体,只是方法体为空,即它的方法体什么也不做,因此这个方法不
可以使用 abstract 来修饰。定义抽象类只需在普通类上增加 abstract 修饰符即可。抽象类中可
以包含抽象方法,也可以没有抽象方法
需要注意以下几个方面:
⚫ 当 abstract 修饰类时,表明这个类只能被继承;当 abstract 修饰方法时,表明这个方法
必须由子类提供实现(即重写)。而 final 修饰的类不能被继承,final 修饰的方法不能
被重写。因此,final 和 abstract 永远不能同时使用。
⚫ abstract 不能用于修饰属性,不能用于修饰局部变量,即没有抽象变量、抽象属性等说
法;abstract 也不能用于修饰构造方法,即没有抽象构造方法的概念,抽象类里定义的
构造方法只能是普通构造方法。
⚫ 当使用 static 来修饰一个方法时,表明这个方法属于当前类,即该方法可以通过类来
调用,如果该方法被定义成抽象方法,则将导致通过该类来调用该方法时出现错误(调
用了一个没有方法体的方法肯定会引起错误),因此 static 和 abstract 不能同时修饰某
个方法,即没有所谓的类抽象方法。
⚫ abstract 关键字修饰的方法必须被其子类重写才有意义,否则这个方法将永远不会有
方法体,因此 abstract 方法不能定义为 private 访问权限,即 private 和 abstract 不能同
时使用。
使用模板模式的简单
规则:
⚫ 抽象父类可以只定义需要使用的某些方法,其余则留给其子类实现。
⚫ 父类中可以包含已经实现的具体方法,通常这些方法只是定义了一个通用算法,其实
现并不需要完全由自身实现,而可以借助于子类实现。
在 Java 中,类与类之间只能单继承,而不能多继承。多继承虽然能使子类同时拥有多个父
类的特征,但是其缺点也是很显著的,主要体现在两个方面:一方面,如果在一个子类继承的
多个父类中拥有相同名称的变量,子类在引用该变量时将产生歧义,无法判断应该使用哪个父
类的变量。另一方面,如果在一个子类继承的多个父类中拥有相同方法,子类中又没有覆盖该
方法,那么调用该方法时将产生歧义,无法判断应该调用哪个父类的方法。
接口的概念可以借鉴计算机中的 USB 口来理解。很多人认为 USB 接口等同于主机板上的
插口,这其实是一种错误的认识。当我说到 USB 接口时,其实指的是主机板上那个插口是遵
守了 USB 规范的一个具体的实例。接口里不能包含普通方法,而只有抽象方法。和抽象类一
样接口是从多个相似类中抽象出来的,和抽象类的区别是接口只是规范,不提供任何实现。
和类定义不同,定义接口时不再使用 class 关键字,而是使用 interface 关键字,接口定义
的基本语法格式如下:
[修饰符] interface 接口名 extends 父接口 1,父接口 2…{
零个或多个常量定义…
零个或多个抽象方法定义… }
说明:
⚫ 修饰符可以是 public,说明接口可以被任何其他接口或类访问;修饰符若是缺省的,
说明接口只能被同一个包中的其他接口或类访问。
⚫ 接口的名称可以是任意有效标识符;
⚫ 接口中的成员变量都是公有的、静态的、最终的常量;
⚫ 接口中的方法都是公有的、抽象的方法,仅有方法签名,没有方法体;
⚫ 接口可以继承,通过关键字 extends 描述继承关系,子接口可以继承父接口的属性和
方法,Java 接口与类的继承不同,接口支持多继承,多个父接口之间用逗号分隔。
接口和抽象类都包括抽象方法,但两者存在很大的不同。
⚫ 抽象类可以有实例变量,而接口不能有实例变量,而只能是静态(static)的常量(final)。
⚫ 抽象类可以有非抽象方法,而接口只能有抽象方法。
⚫ 抽象类只支持单继承,接口支持多继承。
对于接口里定义的方法而言,它们只能是抽象方法,因此系统自动会为其增加 abstract 修
饰符;且接口里不允许定义静态方法,即不可使用 static 修饰接口中的方法。因此,不管接口
中方法时是否使用 public abstract 修饰符,其效果都是一样的。
接口的继承和类继承不一样,接口完全支持多继承,即一个接口可以有多个直接父接口。
和类继承相似,子接口扩展某个父接口,将会获得父接口里定义的所有抽象方法、常量属性、
内部类和枚举类定义。
一个接口继承多个父接口时,多个父接口排在 extends 关键字之后,其间以英文逗号(.)
隔开。
5.6.4 使用接口
接口不能用于创建实例,但接口可以用于声明引用类型的变量。当使用接口来声明引用类
型的变量时,这个引用类型的变量必须引用到其实现类的对象。除此之外,接口的主要用途就
是被实现类实现。
一个类可以实现一个或多个接口,使用 implements 关键字,这也是 Java 为单继承灵活性
不足所做的补充。类实现接口的语法格式如下:
[修饰符] class 类名 extends 父类 implements 接口 1,接口 2…{
类体部分
}
实现接口与继承父类类似,一样可以获得所实现接口里常量属性、抽象方法、内部类和枚
举类定义。类实现接口需要在类定义后增加 implements 部分,当需要实现多个接口时,多个
接口之间以英文逗号(,)隔开。一个类可以继承一个父类,并同时实现多个接口,implements
部分必需放在 extends 部分之后。
一个类实现了一个或多个接口之后,这个类必须完全实现这些接口里定义的全部抽象方法
(也就是重写这些抽象方法);否则,该类将保留从父接口那里继承到的抽象方法,该类也必
须定义成抽象类。
注意:实现接口方法时,必须使用 public 访问控制修饰符,因为接口里的方法都是 public
的,而子类(相当于实现类)重写父类方法时访问权限只能更大或者相等,所以实现类实现接
口里的方法时只能使用 public 访问控制权限。
接口和抽象类很像,它们都具有如下特征。
⚫ 接口和抽象类都不能被实例化,它们都位于继承树的顶端,用于被其他类实现和继承。
⚫ 接口和抽象类都可以包含抽象方法,实现接口或继承抽象类的普通子类都必须实现这
些抽象方法。
但接口和抽象类之间的差别非常大,这种差别主要体现在两者的设计目的上
接口和抽象类在用法上也存在如下差别:
⚫ 接口里只能包含抽象方法,不包含已经提供实现的方法;抽象类中则完全可以包含普
通方法。
⚫ 接口里不能定义静态方法;抽象类可以定义静态方法。
⚫ 接口里只能定义静态常量属性,不能定义普通属性;而抽象类里则既可以定义普通属
性,也可以定义静态常量属性。
⚫ 接口不包含构造方法;抽象类里可以包含构造方法,但并不是用于创建对象,而是让
其子类调用这些构造方法来完成属于抽象类的初始化操作。
⚫ 接口里不能包含初始化块;但抽象类则完全可以包含初始化块。
⚫ 一个类最多只能有一个直接父类,包括抽象类;但一个类可以直接实现多个接口,通 过这些接口弥补 Java 单继承方面的不足。
大部分时候,我们把类定义成一个独立的程序单元。但在某些情况下,也会将一个类放在
另一个类的内部定义,这个定义在其他类内部的类被称为内部类(有时也叫嵌套类),包含内
部类的类则称为外部类(有时也叫宿主类)。
其作用为:
⚫ 内部类提供了更好的封装,可以把内部类隐藏在外部类之内,不允许同一个包中的其
他类访问该类。假设需要创建 Cow 这个类,Cow 类需要组合一个 CowLeg 属性,
CowLeg 类只有在 Cow 类里才有效,离开了 Cow 类之后没有任何意义。这种情况下,
就可把 CowLeg 定义成 Cow 的内部类,不允放其他类访问 CowLeg。
⚫ 内部类成员可以直接访问外部类的私有数据,因为内部类被当成其外部类成员,同一
个类的成员之间可以互相访问。但外部类不能访问内部类的实现细节,例如内部类的
属性。
⚫ 没有名字的内部类称为匿名内部类,适用于创建那些仅需要一次使用的内部类,在图
形用户界面的事件处理中经常使用匿名内部类。
⚫ 内部类可以声明为 static 或非 static,也可以使用 private、protected、public 以及缺省
的各种访问控制修饰符。使用 static 修饰的内部类称为静态内部类,而没有使用 static
修饰的内部类简称内部类。
⚫ 和普通的外部类不同,内部类是其所在的外部类的一个成员,内部类对象不能单独存
在,它必许依赖一个外部类对象。
使用内部类时应注意:
⚫ 一个内部类的对象能够访问创建它的外部类对象的所有属性及方法(包括私有部分)。
⚫ 对于同一个包中的其它类来说,内部类能够隐藏起来(将内部类用 private 修饰即可)。
⚫ 内部类也可定义在方法或语句块中,称为局部内部类,局部内部类与成员类的基本特
性相同,例如局部内部类实例必属于其外部类的一个实例,可通过 OuterClass.this 引
用其外部类实例等,但它只能使用方法或语句块中的 final 常量。
⚫ 内部类可以被定义为抽象类或接口,但必须被其他的内部类继承或实现。
⚫ 非静态内部类不能声明 static 成员,只有静态的内部类可以声明 static 成员。
匿名类就是没有名称的内部类,它将类和类的方法定义在一个表达式中。
匿名类定义和使用的语法格式如下:
new 父类构造方法名([实参列表])| 实现的接口名(){
//匿名类的类体部分
}
从上面的定义中可以看出,匿名类必须继承一个父类,或实现一个接口,但最多只能继承
一个父类,或实现一个接口。
关于匿名类还有如下两条规则:
⚫ 匿名类不能是抽象类,因为系统在创建匿名类的时候,会立即创建匿名类的对象。因
此不允许将匿名类定义成抽象类。
⚫ 匿名类不能定义构造方法(因为匿名类没有类名,所以无法定义构造方法),但它可以
定义实例初始化块,通过实例初始化块来完成构造方法需要完成的事情。