6: Reusing Classes(复用类)

第一种非常简单:在新的类里直接创建旧的类的对象。这被称为合成(compostion)。
第二种方法更为精妙。它会创建一个新的,与原来那个类同属一种类型的类。你全盘接受了就类的形式,在没有对它做修改的情况下往里面添加了新的代码。这种神奇的做法被称为继承(inheritance)。

合成所使用的语法
如果你想对reference进行初始化,那么可以在以下几个时间进行:
1. 在定义对象的时候。这就意味着在构造函数调用之前,它们已经初始化完毕了。
2. 在这个类的构造函数里。
3. 在即将使用那个对象之前。这种做法通常被称为“偷懒初始化(lazy initalization)”。如果碰到创建对象的代价很高,或者不是每次都需要创建对象的时候,这种做法就能降低程序的开销了。

继承所使用的语法
继承设计方面有一条通用准则,那就是把数据都设成private的,把方法都设成public的。当然碰到特殊情况还要进行调整,但是这还是一条非常有用的准则。
基类的初始化
构造行为是从基类“向外”发展的,所以基类会在派生类的构造函数访问它之前先进行初始化。
带参数的构造函数
如果类没有默认的构造函数(也就是无参数的构造函数),或者你要调用的基类构造函数是带参数的,你就必须用super关键词以及合适的参数明确地调用基类的构造函数。
对派生类构造函数而言,调用基类的构造函数应该是它做的第一件事。
捕获基类构造函数抛出的异常
编译器会强制你将基类构造函数的调用放在派生类的构造函数的最前面。

把合成和继承结合起来
虽然编译器会强制你对基类进行初始化,并且会要求你在构造函数的开始部分完成初始化,但是它不会检查你是不是进行了成员对象的初始化,因此你只能自己留神了。
确保进行妥善地清理
先按照创建对象的相反顺序进行类的清理。(一般来说,这要求留着基类对象以供访问。)然后调用基类的清理方法。
最好不要依赖垃圾回收器去做任何与内存回收无关的事情。如果你要进行清理,一定要自己写清理方法,别去用finalize()。
名字的遮盖

用合成还是继承
合成与继承都能让你将子对象植入新的类(合成是显式的,继承是隐含的)。
合成用于新类要使用旧类的功能,而不是其接口的场合。也就是说,把对象嵌进去,用它来实现新类的功能,但是用户看到的是新类的接口,而不是嵌进去的对象的接口。因此,你得在新类里嵌入private得就类对象。
有时,让用户直接访问新类的各个组成部分也是合乎情理的;这就是说,将成员对象定义成public。
继承要表达的是一种“是(is-a)”关系,而合成表达的是“有(has-a)”关系。

protected
对用户而言,它是private的,但是如果你想继承这个类,或者开发一个也属于这个package的类的话,就可以访问它了。(Java的protected也提供package的权限。)
最好的做法是,将数据成员设成private的;你应该永远保留修改底层实现的权利,然后用protected权限的方法来控制继承类的访问权限。

渐进式的开发

上传
为什么叫“上传”?

把派生类传给基类就是沿着继承图往上送,因此被称为“上传(upcasting)”。上传总是安全的,因为你是把一个较具体的类型转换成较为一般的类型。也就是说派生类是基类的超集(superset)。它可能会有一些基类所没有的方法,但是它最少要有基类的方法。在上传过程中,类的接口只会减小,不会增大。
合成还是继承,再深讨
运用继承的时候,你应该尽可能的保守,只有在它能带来很明显的好处的时候,你才能用。在判断该使用合成还是继承的时候,有一个简单的办法,就是问一下你是不是会把新类上传给基类。如果你必须上传,那么继承就是必须的,如果不需要上传,那么就该再看看是不是应该用继承了。

final关键词
设计和效率
final的三种用途:数据(data),方法(method)和类(class)
final的数据
常量能用于下列两个情况:
1. 可以是“编译时的常量(compile-time constant)”,这样就再也不能改了。
2. 也可以是运行时初始化的值,这个值你以后就不想再改了。
如果是编译时的常量,编译器会把常量放到算式里面;这样编译的时候就能进行计算,因此也就降低了运行时的开销。在Java中这种常量必须是primitive型的,而且要用final关键词表示。这种常量的赋值必须在定义的时候进行。
一个既是static又是final的数据成员会只占据一段内存,并且不可修改。
对primitive来说,final会将这个值定义成常量,但是对于对象的reference而言,final的意思则是这个reference是常量。初始化的时候,一旦将reference连到了某个对象,那么它就再也不能指别的对象了。但是这个对象本身是可以修改的;Java没有提供将某个对象作成常量的方法。(但是你可以自己写一个类,这样就可以把类当作常量了。)这种局限性也体现在数组上,因为它也是一个对象。
空白的final的数据(Blank finals)
Java能让你创建“空白的final数据(blank finals)”,也就是说把数据成员声明成final的,但却没给初始化的值。碰到这种情况,你必须先进行初始化,在世用空白的final数据成员,而且编译器会强制你这么做。不过,空白的final数据也提供了一种更为灵活的运用final关键词方法,比方说,现在对象里的final数据就能在保持不变性的同时又有所不同了。
你一定得为final数据赋值,要么是在定义数据的时候用一个表达式赋值,要么是在构造函数里面进行赋值。为了确保final数据在使用之前已经进行了初始化,这一要求是强制的。
Final的参数
Java允许你在参数表中声明参数是final的,这样参数也变成final了。也就是说,你不能在方法里让参数 reference指向另一个对象了。
Final方法
使用final方法的目的有二。第一,为方法上“锁”,进制派生类进行修改。第二个原因就是效率。
final和private
如果方法是private的,那它就不属于基类的接口。它只能算是被类隐藏起来的,正好有着相同的名字的代码。如果你在派生类里创建了同名的public或protected,或package权限的方法,那么它们同基类中可能同名的方法,没有任何联系。你并没有覆写那个方法,你只是创建了一个新的方法。
Final类
把整个类都定义成final的(把final关键词放到类的定义部分的前面)就等于在宣布,你不会去继承这个类,你也不允许别人去继承这个类。
final类的数据可以是final的,也可以不是final的,这要由你来决定。无论类是不是final的,这一条都适用于“将final用于数据的”场合。但是,由于final类精致了继承,覆写方法已经不可能了,因此所有的方法都隐含地变成final了。
小心使用final

初始化与类地装载
第一次使用static数据的时候也是进行初始化的时候。装载的时候,static对象和static代码段会按照它们字面的顺序(也就是在程序中出现的顺序)进行初始化。当然static数据只会初始化一次。
继承情况下的初始化
1. 装载程序。先装载派生类,然后装载基类。
2. 执行“根基类(root base class)”。先是基类的static初始化,然后是派生类的static初始化。
3. 创建对象。首先,primitive都会被设成它们的缺省值,而reference也会被设成null。然后,调用基类的构造函数,再调用派生类的构造函数。

总结

练习

「读书笔记」Thinking in Java 3rd Edition - 7: Polymorphism