通过创建新类来重复使用代码,但却用不着重新创建,可以直接使用别人已建好并调试好的现成类。
- 第一个最简单:在新类里简单地创建原有类的对象。我们把这种方法叫作“组合”,因为新类由现有类的对象合并而成。
- 第二种方法叫作“继承”(Inheritance),涉及的大多数工作都是由编译器完成的。
1.组合的语法
为进行组合,我们只需在新类里简单地置入对象句柄即可。
System.out.println("source = " + source) ;
试图将一个WaterSource添加给一个String对象("source =")。因为我们只能将一个字串“添加”到另一个字串,所以它会说:“我要调用toString(),把source转换成字串!”经这样处理后,它就能编译两个字串,并将结果字串传递给一个System.out.println()。每次随同自己创建的一个类允许这种行为的时候,都只需要写一个toString()方法。
在类内作为字段使用的基本数据会初始化成零。但对象句柄会初始化成null。
如希望句柄得到初始化,可在下面这些地方进行:
- (1) 在对象定义的时候。这意味着它们在构建器调用之前肯定能得到初始化。
- (2) 在那个类的构建器中。
- (3) 紧靠在要求实际使用那个对象之前。这样做可减少不必要的开销——假如对象并不需要创建的话。
2.继承的语法
在类主体的起始花括号之前,需要放置一个关键字extends,在后面跟随“基础类”的名字。若采取这种做法,就可自动获得基础类的所有数据成员以及方法。
父类(基类)和子类都有scrub(),但在子类的scrub()里,不可只是简单地发出对scrub()的调用。那样便造成了递归调用,我们不愿看到这一情况。为解决这个问题,Java提供了一个super关键字,它引用当前类已从中继承的一个“超类”(Superclass)。所以表达式super.scrub()调用的是方法scrub()的基础类版本。
2.1 初始化基类
创建子类的一个对象时,它在其中包含了基础类的一个“子对象”。这个子对象就象我们根据基础类本身创建了它的一个对象。
在子类的构建器中,Java会自动插入对基础类构建器的调用。
2.1.1 含有自变量的构建器
如果类没有默认的自变量,或者想调用含有一个自变量的某个基础类构建器,必须明确地编写对基础类的调用代码。这是用super关键字以及适当的自变量列表实现的。
在子类构建器中,对基础类构建器的调用是必须做的第一件事情。
2.1.2 捕获基类构建器的异常
编译器会强迫我们在子类构建器的主体中首先设置对基础类构建器的调用。这意味着在它之前不能出现任何东西。这同时也会防止子类构建器捕获来自一个基础类的任何违例事件。显然,这有时会为我们造成不便。
3.组合与继承的结合
尽管编译器会强迫我们对基础类进行初始化,并要求我们在构建器最开头做这一工作,但它并不会监视我们是否正确初始化了成员对象。所以对此必须特别加以留意。
3.1 保证正确的清除
必须将这样的清除代码置于一个finally从句中,从而防范任何可能出现的异常事
件。
3.2 名字的隐藏(验证)
如果Java基础类有一个方法名被重载使用多次,在衍生类里对那个方法名的重新定义就不会隐藏任何基础类的版本。所以无论方法在这一级还是在一个基础类中定义,重载都会生效。
很少会用与基础类里完全一致的签名和返回类型来覆盖同名的方法,否则会使人感到迷惑。
4.到底选择组合还是继承
不能用一个车辆对象来合成一辆汽车——汽车并不“包含”车辆;相反,它“属于”车辆的一种类别。
“属于”关系是用继承来表达的,而“包含”关系是用合成来表达的。
5.protected
protected的意思是“它本身是私有的,但可由从这个类继承的任何东西或者同一个包内的其他任何东西访问”。
6.累积开发
继承的一个好处是它支持“累积开发”,允许我们引入新的代码,同时不会为现有代码造成错误。这样可将新错误隔离到新代码里。
继承是对一种特殊关系的表达,意味着“这个新类属于那个旧类的一种类型”。我们的程序不应纠缠于一些细树末节,而应着眼于创建和操作各种类型的对象,用它们表达出来自“问题空间”的一个模型。
7.向上转型(Upcasting)
继承:子类属于父类的一种类型。
向上转型:将子类句柄转换成一个父类句柄。
7.1 何谓向上转型?
之所以叫作这个名字,类继承图的画法是根位于最顶部,再逐渐向下扩展。
上溯造型(向上转型)肯定是安全的,因为我们是从一个更特殊的类型到一个更常规的类型。
7.1.1 再论组合与继承
使用继承时要特别慎重。只有在清楚知道继承在所有方法中最有效的前提下,才可考虑它。
为判断自己到底应该选用组合还是继承,一个最简单的办法就是考虑是否需要从新类上溯造型回基础类。若必须上溯,就需要继承。
8.final关键字
final声明“这个东西不能改变”。之所以要禁止改变,可能是考虑到两方面的因素:设计或效率。
8.1 final数据
常数主要应用于下述两个方面:
(1) 编译期常数,它永远不会改变
在Java中,这些形式的常数必须属于基本数据类型(Primitives),而且要用final关键字进行表达。在对这样的一个常数进行定义的时候,必须给出一个值。
但对于对象句柄,final会将句柄变成一个常数。进行声明时,必须将句柄初始化到一个具体的对象。而且永远不能将句柄变成指向另一个对象。然而,对象本身是可以修改的。这一限制也适用于数组,它也属于对象。(2) 在运行期初始化的一个值,我们不希望它发生变化
不能由于某样东西的属性是final,就认定它的值能在编译时期知道。
将final值设为static和非static之间的差异。只有当值在运行期间初始化的前提下,这种差异才会揭示出来。因为编译期间的值被编译器认为是相同的。
8.1.1 Blank finals
尽管被声明成final,但却未得到一个初始值。无论在哪种情况下,空白final都必须在实际使用前得到正确的初始化。
现在强行要求我们对final进行赋值处理——要么在定义字段时使用一个表达 式,要么在每个构建器中。这样就可以确保final字段在使用前获得正确的初始化。
8.1.2 final自变量
意味着在一个方法的内部,我们不能改变自变量句柄指向的东西或其值(基本类型)。
8.2 final方法
之所以要使用final方法,可能是出于对两方面理由的考虑。
第一个是为方法“上锁”,防止任何继承类改变它的本来含义。设计程序时,若希望一个方法的行为在继承期间保持不变,而且不可被覆盖或改写,就可以采取这种做法。
采用final方法的第二个理由是程序执行的效率。将一个方法设成final后,编译器就可以把对那个方法的所有调用都置入“嵌入”调用里。只要编译器发现一个final方法调用,就会(根据它自己的判断)忽略为执行方法调用机制而采取的常规代码插入方法(将自变量压入堆栈;跳至方法代码并执行它;跳回来;清除堆栈自变量;最后对返回值进行处理)。相反,它会用方法主体内实际代码的一个副本来替换方法调用。这样做可避免方法调用时的系统开销。
通常,只有在方法的代码量非常少,或者想明确禁止方法被覆盖的时候,才应考虑将一个方法设为final。
类内所有private方法都自动成为final。
8.3 final类
不允许从这个类继承。
将类定义成final后,结果只是禁止进行继承——没有更多的限制。然而,由于它禁止了继承,所以一个final类中的所有方法都默认为final。
8.4 final的注意事项
我们很难预测一个类以后会以什么样的形式再生或重复利用。常规用途的类尤其如此。若将一个方法定义成final,就可能杜绝了在其他程序员的项目中对自己的类进行继承的途径。
9.初始化和类装载
每个对象的代码都存在于独立的文件中。除非真的需要代码,否则那个文件是不会载入的。
9.1 继承初始化
在装载过程中,装载程序注意它有一个基础类(即extends关键字要表达的意思),所以随之将其载入。无论是否准备生成那个基础类的一个对象,这个过程都会发生。
若基础类含有另一个基础类,则另一个基础类随即也会载入,以此类推。接下来,会在根基础类执行static初始化,再在下一个衍生类执行,以此类推。保证这个顺序是非常关键的,因为子类的初始化可能要依赖于对基础类成员的正确初始化。
此时,必要的类已全部装载完毕,所以能够创建对象。首先,这个对象中的所有基本数据类型都会设成它们的默认值,而将对象句柄设为null。随后会调用基础类构建器。在这种情况下,调用是自动进行的。但也完全可以用super来自行指定构建器调用。基础类的构建采用与衍生类构建器完全相同的处理过程。基础类构建器完成以后,实例变量会按本来的顺序得以初始化。最后,执行构建器剩余的主体部分。