ThinkingInJava笔记-类再生(第六章)

类再生分为两种方式:

  1. 合成,在新类里简单创建原有类的对象。
  2. 继承,它创建一个新类,将其视作现有类的一个“类型”,我们可以原样采取现有类的形式,并在其中加入新代码,同时不会对现有类产生影响。

由于这儿涉及到两个类——基础类及衍生类,而不再是以前的一个,所以在想象衍生类的结果对象时,可能会产生一些迷惑。从外部看,似乎新类拥有与基础类相同的接口,而且可包含一些额外的方法和字段。但继承并非仅仅简单地复制基础类的接口了事。创建衍生类的一个对象时,它在其中包含了基础类的一个“子对象”。这个子对象就象我们根据基础类本身创建了它的一个对象。从外部看,基础类的子对象已封装到衍生类的对象里了。
当然,基础类子对象应该正确地初始化,而且只有一种方法能保证这一点:在构建器中执行初始化,通过调
用基础类构建器,后者有足够的能力和权限来执行对基础类的初始化。在衍生类的构建器中,Java 会自动插
入对基础类构建器的调用。下面这个例子向大家展示了对这种三级继承的应用:

public class Art {
	Art(){
		System.out.println("art");
	}
}

public class Drawing extends Art {

	Drawing(){
		System.out.println("drawing");
	}
	public static void main(String[] args) {
		Drawing drawing = new Drawing();
	}
}

 

输出结果为:

art
drawing

 

上述例子有自己默认的构建器;也就是说,它们不含任何自变量。编译器可以很容易地调用它们,因为不存
在具体传递什么自变量的问题。如果类没有默认的自变量,或者想调用含有一个自变量的某个基础类构建
器,必须明确地编写对基础类的调用代码。这是用 super 关键字以及适当的自变量列表实现的,如下所示:

class Game {
	Game(int i) {
		System.out.println("Game constructor");
	}
}

class BoardGame extends Game {
	BoardGame(int i) {
		super(i);
		System.out.println("BoardGame constructor");
	}
}

class Chess extends BoardGame {
	Chess() {
		super(11);
		System.out.println("Chess constructor");
	}

	public static void main(String[] args) {
		Chess x = new Chess();
	}
}
 

输出结果为:

Game constructor
BoardGame constructor
Chess constructor

尽管编译器会强迫我们对基础类进行初始化,并要求我们在构建器最开头做这一工作,但它并不会监视我们
是否正确初始化了成员对象。所以对此必须特别加以留意。

 

继承的一个好处是它支持“累积开发”,允许我们引入新的代码,同时不会为现有代码造成错误。这样可将
新错误隔离到新代码里。通过从一个现成的、功能性的类继承,同时增添成员新的数据成员及方法(并重新
定义现有方法),我们可保持现有代码原封不动(另外有人也许仍在使用它),不会为其引入自己的编程错
误。一旦出现错误,就知道它肯定是由于自己的新代码造成的。这样一来,与修改现有代码的主体相比,改
正错误所需的时间和精力就可以少很多。

 

继承最值得注意的地方就是它没有为新类提供方法。继承是对新类和基础类之间的关系的一种表达。可这样
总结该关系:“新类属于现有类的一种类型”。
这种表达并不仅仅是对继承的一种形象化解释,继承是直接由语言提供支持的。作为一个例子,大家可考虑
一个名为Instrument 的基础类,它用于表示乐器;另一个衍生类叫作Wind。由于继承意味着基础类的所有
方法亦可在衍生出来的类中使用,所以我们发给基础类的任何消息亦可发给衍生类。若Instrument 类有一个
play()方法,则Wind 设备也会有这个方法。这意味着我们能肯定地认为一个Wind 对象也是Instrument的一
种类型。下面这个例子揭示出编译器如何提供对这一概念的支持:

 

public class Instrument {
	public void play(){
		System.out.println("hello");
	}
	
	static void tune(Instrument i){
		i.play();
	}
}

public class Wind extends Instrument {

	public static void main(String[] args) {
		Wind wind = new Wind();
		Instrument.tune(wind);
	}
}

 运行结果:

hello

这个例子中最有趣的无疑是tune()方法,它能接受一个Instrument句柄。但在 Wind.main()中,tune()方法
是通过为其赋予一个Wind 句柄来调用的。由于Java 对类型检查特别严格,所以大家可能会感到很奇怪,为
什么接收一种类型的方法也能接收另一种类型呢?但是,我们一定要认识到一个Wind 对象也是一个
Instrument对象。而且对于不在Wind 中的一个Instrument(乐器),没有方法可以由tune()调用。在
tune()中,代码适用于Instrument以及从 Instrument 衍生出来的任何东西。在这里,我们将从一个Wind 句
柄转换成一个Instrument 句柄的行为叫作“上溯造型”。


由于造型的方向是从衍生类到基础类,箭头朝上,所以通常把它叫作“上溯造型 ”,即Upcasting。上溯造
型肯定是安全的,因为我们是从一个更特殊的类型到一个更常规的类型。换言之,衍生类是基础类的一个超
集。它可以包含比基础类更多的方法,但它至少包含了基础类的方法。进行上溯造型的时候,类接口可能出
现的唯一一个问题是它可能丢失方法,而不是赢得这些方法。这便是在没有任何明确的造型或者其他特殊标
注的情况下,编译器为什么允许上溯造型的原因所在。

 

继承中对于final关键字的解释:

  1. final数据
    (1) 编译期常数,它永远不会改变
    (2) 在运行期初始化的一个值,我们不希望它发生变化
    final int s = 1;//s为常数
    final Object obj = new Object();//obj为不可变的句柄,但是obj内部变量可以变
    final int s;//s为常量,但是使用前必须初始化
    void method(final int s){}//当调用方法时,s得到一个值,但这个值在方法内只读
     
  2. final方法
    之所以要使用final 方法,可能是出于对两方面理由的考虑。第一个是为方法“上锁”,防止任何继承类改变它的本来含义。设计程序时,若希望一个方法的行为在继承期间保持不变,而且不可被覆盖或改写,就可以采取这种做法。
    采用final 方法的第二个理由是程序执行的效率。将一个方法设成 final 后,编译器就可以把对那个方法的所有调用都置入“嵌入”调用里。只要编译器发现一个final 方法调用,就会(根据它自己的判断)忽略为执行方法调用机制而采取的常规代码插入方法(将自变量压入堆栈;跳至方法代码并执行它;跳回来;清除堆栈自变量;最后对返回值进行处理)。相反,它会用方法主体内实际代码的一个副本来替换方法调用。这样做可避免方法调用时的系统开销。当然,若方法体积太大,那么程序也会变得雍肿,可能受到到不到嵌入代码所带来的任何性能提升。因为任何提升都被花在方法内部的时间抵消了。Java 编译器能自动侦测这些情况,并颇为“明智”地决定是否嵌入一个final 方法。然而,最好还是不要完全相信编译器能正确地作出所有判断。通常,只有在方法的代码量非常少,或者想明确禁止方法被覆盖的时候,才应考虑将一个方法设为final。
    类内所有private方法都自动成为final。由于我们不能访问一个private方法,所以它绝对不会被其他方法覆盖(若强行这样做,编译器会给出错误提示)。可为一个 private方法添加final 指示符,但却不能为那个方法提供任何额外的含义。
  3. final类
    将类定义成final后,结果只是禁止进行继承——没有更多的限制。然而,由于它禁止了继承,所以一个 final类中的所有方法都默认为final。因为此时再也无法覆盖它们。所以与我们将一个方法明确声明为final 一样,编译器此时有相同的效率选择。 可为final 类内的一个方法添加final 指示符,但这样做没有任何意义。

类装载顺序:

若基础类含有另一个基础类,则另一个基础类随即也会载入,以此类推。接下来,会在根基础类执行 static 初始化,再在下一个衍生类执行,以此类推。保证这个顺序是非常关键的,因为衍生类的初始化可能要依赖于对基础类成员的正确初始化。

 

总结:

无论继承还是合成,我们都可以在现有类型的基础上创建一个新类型。但在典型情况下,我们通过合成来实现现有类型的“再生”或“重复使用”,将其作为新类型基础实施过程的一部分使用。但如果想实现接口的“再生”,就应使用继承。由于衍生或派生出来的类拥有基础类的接口,所以能够将其“上溯造型”为基础类。对于下一章要讲述的多形性问题,这一点是至关重要的。
尽管继承在面向对象的程序设计中得到了特别的强调,但在实际启动一个设计时,最好还是先考虑采用合成技术。只有在特别必要的时候,才应考虑采用继承技术(下一章还会讲到这个问题)。合成显得更加灵活。但是,通过对自己的成员类型应用一些继承技巧,可在运行期准确改变那些成员对象的类型,由此可改变它们的行为。
尽管对于快速项目开发来说,通过合成和继承实现的代码再生具有很大的帮助作用。但在允许其他程序员完全依赖它之前,一般都希望能重新设计自己的类结构。我们理想的类结构应该是每个类都有自己特定的用途。它们不能过大(如集成的功能太多,则很难实现它的再生),也不能过小(造成不能由自己使用,或者不能增添新功能)。最终实现的类应该能够方便地再生。

你可能感兴趣的:(继承,java语言,extends)