摘要:
本文结合Java的类的复用对面向对象两大特征继承和多态进行了全面的介绍。
首先,我们介绍了继承的实质和意义,并探讨了继承,组合和代理在类的复用方面的异同。紧接着,我们依据继承引入了多态。介绍了它的实现机制和详细应用。此外,为了更好地理解继承和多态。我们对final关键字进行了全面的介绍。
在此基础上。我们介绍了Java中类的载入及初始化顺序。最后。我们对面向对象设计中三个十分重要的概念–重载、覆盖与隐藏进行了详细的说明。
要点:
- 继承
- 组合,继承,代理
- 多态
- final 关键字
- 类载入及初始化顺序
- 重载、覆盖与隐藏
一. 继承
继承是全部OOP语言必不可少的部分,在java中,使用extends关键字来表示继承关系。
当创建一个类时,总是在继承。假设没有明白指出要继承的类,就总是隐式地从根类 Object 进行继承。假设两个类存在继承关系,则子类会自己主动继承父类的方法和变量,在子类中能够直接调用父类的方法和变量。须要指出的是。在java中。仅仅同意单继承,也就是说,一个类最多仅仅能显式地继承于一个父类。
可是,一个类却能够被多个类继承,也就是说,一个类能够拥有多个子类。此外,我们须要特别注意以下几点:
1、 成员变量的继承
当子类继承了某个类之后。便能够使用父类中的成员变量,可是并非全然继承父类的全部成员变量。
详细的原则例如以下:
子类能够继承父类的 public 和 protected 成员变量 。不能够继承父类的 private 成员变量,但能够通过父类对应的getter/setter方法进行訪问。
对于父类的包訪问权限成员变量。假设子类和父类在同一个包下,则子类能够继承。否则,子类不能够继承。
对于子类能够继承的父类成员变量,假设在子类中出现了同名称的成员变量,则会发生 隐藏 现象。即子类的成员变量会屏蔽掉父类的同名成员变量。假设要在子类中訪问父类中同名成员变量,须要使用super关键字来进行引用。
2、 成员方法的继承
同样地。当子类继承了某个类之后。便能够使用父类中的成员方法,可是子类并非全然继承父类的全部方法。详细的原则例如以下:
子类能够继承父类的 public和protected成员方法 ,不能够继承父类的 private成员方法。
对于父类的包訪问权限成员方法,假设子类和父类在同一个包下,则子类能够继承。否则,子类不能够继承。
对于子类能够继承的父类成员方法。假设在子类中出现了同名称的成员方法,则称为 覆盖 ,即子类的成员方法会覆盖掉父类的同名成员方法。假设要在子类中訪问父类中同名成员方法,须要使用super关键字来进行引用。
程序演示样例:
class Person {
public String gentle = "Father";
}
public class Student extends Person {
public String gentle = "Son";
public String print(){
return super.gentle; // 在子类中訪问父类中同名成员变
}
public static void main(String[] args) throws ClassNotFoundException {
Student student = new Student();
System.out.println("##### " + student.gentle);
Person p = student;
System.out.println("***** " + p.gentle); //隐藏:编译时决定。不会发生多态
System.out.println("----- " + student.print());
System.out.println("----- " + p.print()); //Error:Person 中没有定义该方法
}
}/* Output:
##### Son
***** Father
----- Father
*///:~
隐藏和覆盖是不同的。 隐藏 是 针对成员变量和静态方法 的,而 覆盖 是 针对普通方法 的。
3、 基类的初始化与构造器
我们知道。导出类就像是一个与基类具有同样接口的新类,也许还会有一些额外的方法和域。
可是,继承并不仅仅是复制基类的接口。当创建一个导出类对象时。该对象会包括一个基类的子对象。这个子对象与我们用基类直接创建的对象是一样的。
二者的差别在于,后者来自于外部。而基类的子对象被包装在导出类对象的内部。
因此,对基类子对象的正确初始化是至关重要的,而且Java也提供了对应的方法来保证这一点: 导出类必须在构造器中调用基类构造器来执行初始化,而基类构造器具有执行基类初始化所需的全部知识和能力。
当基类含有默认构造器时,Java会自己主动在导出类的构造器插入对该基类默认构造器的调用。由于编译器不必考虑要传递什么样的參数的问题。可是,若父类不含有默认构造器。或者导出类想调用一个带參数的父类构造器,那么在导出类的构造器中就必须使用 super 关键字显式的进行调用对应的基类的构造器。而且该调用语句必是导出类构造器的第一条语句。
二. 组合,继承,代理
在Java中,组合、继承和代理三种技术都能够实现代码的复用。
(1) 组合(has-a)
通过在新的类中加入现有类的对象就可以实现组合。即。新的类是由现有类的对象所组成。该技术通经常使用于想在新类中使用现有类的功能而非它的接口这样的情形。也就是说,在新类中嵌入某个对象,让其实现所须要的功能。但新类的用户看到的仅仅是为新类所定义的接口,而非所嵌入对象的接口。
(2) 继承(is-a)
继承能够使我们依照现有类的类型来创建新类。即。我们採用现有类的形式并在当中加入新代码。通常。这意味着我们在使用一个通用类。并为了某种特殊须要而将其特殊化。本质上,组合和继承都同意在新的类中放置子对象,组合是显式地这样做,而继承则是隐式地做。
(3) 代理(继承与组合之间的一种中庸之道:像组合一样使用已有类的功能,同一时候像继承一样使用已有类的接口)
代理是继承与组合之间的一种中庸之道。Java并没有提供对它的直接支持。在代理中,我们将一个成员对象置于所要构造的类中(就像组合),但与此同一时候我们在新类中暴露了该成员对象的接口/方法(就像继承)。
程序演示样例:
// 控制模块
public class SpaceShipControls {
void up(int velocity) {
}
void down(int velocity) {
}
void left(int velocity) {
}
void right(int velocity) {
}
void forward(int velocity) {
}
void back(int velocity) {
}
void turboBoost() {
}
}
太空船须要一个控制模块,那么,构造太空船的一种方式是使用继承:
public class SpaceShip extends SpaceShipControls {
private String name;
public SpaceShip(String name) { this.name = name; }
public String toString() { return name; }
public static void main(String[] args) {
SpaceShip protector = new SpaceShip("NSEA Protector");
protector.forward(100);
}
}
然而。SpaceShip 并非真正的 SpaceShipControls 类型,即便你能够“告诉” SpaceShip 向前运动(forward())。
更准确的说,SpaceShip 包括 SpaceShipControls ,与此同一时候, SpaceShipControls 的全部方法在 SpaceShip 中都暴露出来。
代理(SpaceShip 的运动行为由 SpaceShipControls 代理完毕) 正好能够解决这样的问题:
// SpaceShip 的行为由 SpaceShipControls 代理完毕
public class SpaceShipDelegation {
private String name;
private SpaceShipControls controls = new SpaceShipControls();
public SpaceShipDelegation(String name) {
this.name = name;
}
// 代理方法:
public void back(int velocity) {
controls.back(velocity);
}
public void down(int velocity) {
controls.down(velocity);
}
public void forward(int velocity) {
controls.forward(velocity);
}
public void left(int velocity) {
controls.left(velocity);
}
public void right(int velocity) {
controls.right(velocity);
}
public void turboBoost() {
controls.turboBoost();
}
public void up(int velocity) {
controls.up(velocity);
}
public static void main(String[] args) {
SpaceShipDelegation protector = new SpaceShipDelegation("NSEA Protector");
protector.forward(100);
}
}
实际上,我们使用代理时能够拥有很多其它的控制力。由于我们能够选择仅仅提供在成员对象中方法的某个子集。
三. final关键字
很多编程语言都须要某种方法来向编译器告知一块数据是恒定不变的。有时。数据的恒定不变是非常实用的。比方:
- 一个永不改变的编译时常量。
-
一个在执行时被初始化的值。而你不希望它被改变。
对于编译期常量这样的情况。编译器能够将该常量值带入不论什么可能用到它的计算式中。也即是说,能够在编译时执行计算式,这减轻了一些执行时负担。在Java中,这类常量必须满足两个条件:
是基本类型。而且用final修饰;
-
在对这个常量进行定义的时候,必须对其进行赋值。
此外。当用final修饰对象引用时,final使其引用恒定不变。一旦引用被初始化指向一个对象,就无法再把它指向还有一个对象。然而,对象本身是能够被改动的,这同样适用于数组,由于它也是对象。
特别须要注意的是。我们不能由于某数据是final的。就觉得在编译时就能够知道它的值。比如:
public class Test {
final int i4 = rand.nextInt(20);
}
1、空白final
Java同意生成 空白final , 即:声明final但又未给定初值的域。但不管什么情况,编译器都会确保空白final在使用前被初始化。可是,空白final在关键字final的使用上提供了更大的灵活性: 一个类中的 final域 就能够做到依据对象而有所不同,却又保持其恒定不变的特性。
比如。
必须在域的定义处或者每一个构造器中使用表达式对final进行赋值。这正是 final域 在使用前总是被初始化的原因所在。
2、final參数
final參数 主要应用于局部内部类和匿名内部类中,很多其它详细介绍请移步我的还有一篇文章:Java 内部类综述。
3、final方法
final关键字作用域方法时。用于锁定方法,以防不论什么继承类改动它的含义。
这是出于设计的考虑:想要确保在继承中使方法行为保持不变,而且不会被覆盖。
对于成员方法,仅仅有在明白禁止覆盖时。才将方法设为final的。
4、final类
当将某个类定义为final时,就表明你不打算继承该类。而且也不同意别人这样做。换句话说,出于某种考虑,你对该类的设计永不须要做不论什么变动,或者出于安全考虑,你不希望它有子类。
须要注意的是,final类的域能够依据实际情况选择是否为final的。不论是否被定义为final。同样的规则都适用于定义final的域。
然而。由于final类禁止继承,所以final类中的全部方法都隐式指定为final的,由于无法覆盖它们。在final类中能够给方法加入final修饰。但这不会增添不论什么意义。
5、 final与private
类中全部的private方法都隐式地指定为final的。由于无法取用private方法,所以也就无法覆盖它。能够对private方法加入final修饰,但这并不会给该方法加入不论什么额外的意义。
特别须要注意的是,覆盖仅仅有在某方法是基类接口的一部分时才会出现。假设一个方法是private的,它就不是基类接口中的一部分,而仅仅是一些隐藏于类中的程序代码。但若在导出类中以同样的名称生成一个非private方法,此时我们并没有覆盖该方法。仅仅是生成了一个新的方法。
由于private方法无法触及而且能有效隐藏,所以除了把它看成是由于它所归属的类的组织结构的原因而存在外,其它不论什么情况都不须要考虑它。
6、 final 与 static
static 修饰变量时。其 具有默认值, 且 可改变, 且其 仅仅能修饰成员变量和成员方法。
-
一个 static final域 仅仅占领一段不能改变的存储空间,且仅仅能在声明时进行初始化。
由于其是 final 的,因而没有默认值;且又是static的。因此在类没有实例化时。其已被赋值,所以仅仅能在声明时初始化。
四. 多态
我们知道 继承同意将对象视为它自己本身的类型或其基类型加以处理,从而使同一份代码能够毫无差别地执行在这些不同的类型之上。当中,多态方法调用同意一种类型表现出与其它类似类型之间的差别,仅仅要这些类型由同一个基类所导出。所以,多态的作用主要体如今两个方面:
- 多态通过分离做什么和怎么做,从还有一个角度将接口和实现分离开来,从而实现将改变的事物与未变的事物分离开来;
- 消除类型之间的耦合关系(类似的,在Java中。泛型也被用来消除类或方法与所使用的类型之间的耦合关系)。
1、实现机制
我们知道方法的覆盖非常好的体现了多态,可是当使用一个基类引用去调用一个覆盖方法时,究竟该调用哪个方法才正确呢?
将一个方法调用同一个方法主体关联起来被称作绑定。
若在程序执行前进行绑定。叫做 前期绑定 。
可是。显然,这样的机制并不能解决上面的问题。由于在编译时编译器并不知道上述基类引用究竟指向哪个对象。解决的办法就是后期绑定(动态绑定/执行时绑定):在执行时依据对象的详细类型进行绑定。
其实,在Java中,除了static方法和final方法(private方法属于final方法)外,其它全部的方法都是后期绑定。
这样。一个方法声明为final后,能够防止其它人覆盖该方法。但更重要一点是:这样做能够有效地关闭动态绑定。或者说,告诉编译器不须要对其进行动态绑定。以便为final方法调用生成更有效的代码。
基于动态绑定机制,我们就能够编写仅仅与基类打交道的代码了,而且这些代码对全部的导出类都能够正确执行。或者说,发送消息给某个对象。让该对象去断定该做什么事情。
2、向下转型与执行时类型识别
由于向上转型会丢失详细的类型信息,所以我们可能会想,通过向下转型也应该能够获取类型信息。
然而。我们知道向上转型是安全的,由于基类不会具有大于导出类的接口。
因此,我们通过基类接口发送的消息都能被接受。可是对于向下转型。我们就无法保证了。
要解决问题,必须有某种方法来确保向下转型的正确性,使我们不至于贸然转型到一种错误的类型,进而发出该对象无法接受的消息。
在Java中,执行时类型识别(RTTI)机制能够处理这个问题,它保证Java中全部的转型都会得到检查。
所以,即使我们仅仅是进行一次普通的加括弧形式的类型转换,再进入执行期时仍会对其进行检查,以便保证它的确是我们希望的哪种类型。
假设不是。我们就会得到一个类型转换异常:ClassCastException。
3、多态的应用举例
- 策略模式。
- 适配器模式;
五. 类载入及初始化顺序
首先,必须指出类载入及初始化顺序为:父类静态代码块->子类静态代码块->父类非静态代码块->父类构造函数->子类非静态代码块->子类构造函数
即,首先。初始化父类中的静态成员变量和静态代码块,依照在程序中出现的顺序初始化。然后。初始化子类中的静态成员变量和静态代码块,依照在程序中出现的顺序初始化;其次,初始化父类的普通成员变量和代码块,再执行父类的构造方法。最后,初始化子类的普通成员变量和代码块,再执行子类的构造方法。
我们通过以下一段程序说明:
class SuperClass {
private static String STR = "Super Class Static Variable";
static {
System.out.println("Super Class Static Block:" + STR);
}
public SuperClass() {
System.out.println("Super Class Constructor Method");
}
{
System.out.println("Super Class Block");
}
}
public class ObjectInit extends SuperClass {
private static String STR = "Class Static Variable";
static {
System.out.println("Class Static Block:" + STR);
}
public ObjectInit() {
System.out.println("Constructor Method");
}
{
System.out.println("Class Block");
}
public static void main(String[] args) {
@SuppressWarnings("unused")
ObjectInit a = new ObjectInit();
}
}/* Output:
Super Class Static Block:Super Class Static Variable
Class Static Block:Class Static Variable
Super Class Block
Super Class Constructor Method
Class Block
Constructor Method
*///:~
在执行该程序时,所发生的第一件事就是试图訪问 ObjectInit.main() 方法(一个static方法),于是载入器開始启动并载入 ObjectInit类 。在对其载入时,编译器注意到它有一个基类(这由关键字extends得知),于是先进行载入其基类。假设该基类还有其自身的基类,那么先载入这个父父基类,如此类推(本例中是先载入 Object类 ,再载入 SuperClass类 ,最后载入 ObjectInit类 )。接下来,根基类中的 static域 和 static代码块 会被执行,然后是下一个导出类。以此类推这样的方式非常重要。由于导出类的static初始化可能会依赖于基类成员是否能被正确初始化。到此为止。全部的类都已载入完毕。对象就能够创建了。
首先,初始化根基类全部的普通成员变量和代码块,然后执行根基类构造器以便创建一个基对象,然后是下一个导出类。依次类推。直到初始化完毕。
六. 重载、覆盖与隐藏
1、重载与覆盖
(1) 定义与差别
重载:假设在一个类中定义了多个同名的方法,但它们有不同的參数(包括三方面:參数个数、參数类型和參数顺序),则称为方法的重载。当中。不能通过訪问权限、返回类型和抛出异常进行重载。
覆盖:子类中定义的某个方法与其父类中某个方法具有同样的方法签名(包括同样的名称和參数列表),则称为方法的覆盖。
子类对象使用这种方法时,将调用该方法在子类中的定义,对它而言,父类中该方法的定义被屏蔽了。
总的来说。重载和覆盖是Java多态性的不同表现。前者是一个类中多态性的一种表现,后者是父类与子类之间多态性的一种表现。
(2) 实现机制
重载是一种參数多态机制,即通过方法參数的差异实现多态机制。而且,其属于一种 静态绑定机制,在编译时已经知道详细执行哪个方法。
覆盖是一种动态绑定的多态机制。即,在父类与子类中具有同样签名的方法具有不同的详细实现。至于终于执行哪个方法 依据执行时的实际情况而定。
(3) 总结
我们应该注意以下几点:
final 方法不能被覆盖;
-
子类不能覆盖父类的private方法。否则,仅仅是在子类中定义了一个与父类重名的全新的方法,而不会有不论什么覆盖效果。
其它须要注意的地方例如以下图所看到的:
2、覆盖与隐藏
(1) 定义
覆盖:指 执行时系统调用当前对象引用 执行时类型 中定义的方法 ,属于 执行期绑定。
隐藏:指执行时系统调用当前对象引用 编译时类型 中定义的方法。即 被声明或者转换为什么类型就调用对应类型中的方法或变量,属于编译期绑定。
(2) 范围
覆盖:仅仅针对实例方法;
隐藏:仅仅针对静态方法和成员变量.
(3) 小结
子类的实例方法不能隐藏父类的静态方法,同样地。子类的静态方法也不能覆盖父类的实例方法。否则编译出错;
不管静态成员还是实例成员,都能被子类同名的成员变量所隐藏。
以下的程序演示样例非常好地诠释了重载,覆盖和隐藏三个概念:
引用
《Java 编程思想》
Java:类与继承