代码的复用有两种方法————1就是直接在新类中生成旧有的类的对象 (合成)2是创建一个新的类 这个类会全盘接受旧有的类 并且在不对旧有类修改的前提下 添加新的代码(继承)
合成所使用的方法
只要把对象的引用reference放到新的类里面就可以了。当primitive数据作为类的成员时候会自动初始化为零,对象的reference也会为null,这时候调用这个reference会抛出nullpoint异常。
reference的初始化有以下三种方式:
1 在定义对象的时候。这就意味着在调用构造函数之前,他们已经初始化完毕了。
2在这个类的构造函数里。
3在即将使用那个对象之前。如果碰到创建对象的代价很高,或者不是每次都需要创建对象的时候,这种做法就能降低程序的开销。
继承所使用的方法
继承的时候 必须使用extends关键词和基类的名字。之后新类就会自动获得基类的全部成员和方法。继承不会限定只能使用基类的方法。也可以往派生类里加入新的方法。
基类的初始化
当创建一个派生类的对象的时候,这个对象立门还有一个基类的对象。这个对象同基类自己创建的对象没有任何的差别。只是从外面看来这个对象是被包裹在派生类的对象里面。
基类的初始化只有一个方法能保证:调用基类的构造函数来进行初始化,只有这样才能掌握怎样才能正确地进行初始化的信息和权限。java会让派生类的构造函数自动地调用基类的构造函数。
带参数的构造函数
调用带参数的构造函数必须使用super关键词以及合适的参数明确调用基类的构造函数。
确保进行妥善地清理
一般情况下java会将需要清理的对象留给垃圾回收器去处理,它不会主动的进行清理。由于不知道垃圾回收器甚么时候启动,也不知道会不会启动,所有需要进行清理,就必须明确地写一个专门干这件事情的方法,为了应付异常,还要将这个方法放入finally子句里面去。不要去调用finalize方法。
用合成还是继承
一般来说,合成用于新类要使用旧类的功能,而不是其接口的场合,也就是说把对象嵌进去,用它来实现新类的功能,但用户看到的是新类的接口,而不是嵌进去的对象的接口,因此在新类里嵌入private的旧类对象,有时为了让用户直接访问新类的各个组成部分也可以嵌入public的旧类对象。通常都应该将基类的成员数据定义为privite类型。继承表达的是一种“是(is-a)”关系,合成表达的是“有(has-a)”关系,对于一些需要对外部世界隐藏,但要对它的继承类开放的基类 可以使用protected来定义那些开放的部分。最好的做法是,将数据成员设成private 的;你应该永远保留修改底层实现的权利。然后用protected 权限的方法来控制继承类的访问权限。
渐进式的开发
继承的优点之一就是,它支持渐进式的开发(incremental develop)。添加新的代码的时候,不会给老代码带来bug;实际上新的bug 全都被圈在新代码里。通过继承已有的,已经能正常工作的类,然后再添加一些数据成员和方法(以及重新定义一些原有的方法),你可以不去修改那些可能还有人在用的老代码,因而也就不会造成bug 了。一旦发现了bug,你就知道它肯定是在新代码里。相比要去修改老代码,新代码会短很多,读起来也更简单。继承实质上是在表达这样一种关系:“新的类是一种旧的类”。
上传
继承最重要的特征不在于它为新类提供了方法,而是它表达了新类同基类之间的关系。这种关系可以被归纳为一句话“新类就是一种原有的类。” (upcasting)”。上传总是安全的,因为你是把一个较具体的类型转换成较为一般的类型。也就是说派生类是基类的超集(superset)。它可能会有一些基类所没有的方法,但是它最少要有基类的方法。在上传过程中,类的接口只会减小,不会增大。这就是为什么编译器会允许你不作任何明确的类型转换或特殊表示就进行上传的原因了。
合成还是继承,再探讨
在判断该使用合成还是继承的时候,有一个最简单的办法,就是问一下你是不是会把新类上传给基类。如果你必须上传,那么继承就是必须的,如果不需要上传,那么就该再看看是不是应该用继承了。
final 关键词
final 的三种用途:数据(data),方法(method)和类(class)。
Final 的数据
很多编程语言都有通知编译器“这是段『常量(constant)』 数据”的手段。常量能用于下列两种情况:
1. 可以是“编译时的常量(compile-time constant)”,这样就再也不能改了。
2. 也可以是运行时初始化的值,这个值你以后就不想再改了。
如果是编译时的常量,编译器会把常量放到算式里面;这样编译的时候就能进行计算,因此也就降低了运行时的开销。在Java 中这种常量必须是primitive 型的,而且要用final 关键词表示。这种常量的赋值必须在定义的时候进行。
一个既是static 又是final 的数据成员会只占据一段内存,并且不可修改。
当final 不是指primitive,而是用于对象的reference 的时候,意思就有点搞了。对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 的,那么编译器就会把调用转换成“内联的(inline)”。当编译器看到要调用final 方法的时候,它就会(根据判断)舍弃普通的,“插入方法调用代码的”编译机制(将参数压入栈,然后跳去执行要调用的方法的代码,再跳回来清空栈,再处理返回值),相反它会用方法本身的拷贝来代替方法的调用。当然如果方法很大,那么程序就会膨胀得很快,于是内联也不会带来什么性能的改善,因为这种改善相比程序处理所耗用的时间是微不足道的。Java 的设计者们暗示过,Java 的编译器有这个功能,可以智能地判断是不是应该将final 方法做成内联的。不过,最好还是把效率问题留给编译器和JVM去处理,而只把final 用于要明确地禁止覆写的场合。
final 和private
private 方法都隐含有final 的意思。由于你不能访问private 的方法,因此你也不能覆写它。你可以给private 方法加一个final 修饰符,但是这样做什么意义也没有。这个问题有可能会造成混乱,因为即使你覆写了一个private 方法(它隐含有final 的意思),看上去它还是可以运行的,而且编译器也不会报错。
只有是基类接口里的东西才能被“覆写”。也就是说,对象应该可以被上传到基类,然后再调用同一个方法(这一点要到下一章才能讲得更清楚。)如果方法是private 的,那它就不属于基类的接口。它只能算是被类隐藏起来的,正好有着相同的名字的代码。如果你在派生类里创建了同名的public 或protected,或package 权限的方法,那么它们同基类中可能同名的方法,没有任何联系。你并没有覆写那个方法,你只是创建了一个新的方法。由于private 方法是无法访问的,实际上是看不见的,因此这么作除了会影响类的代码结构,其它什么意义都没有。
Final 类
把整个类都定义成final 的(把final 关键词放到类的定义部分的前面)就等于在宣布,你不会去继承这个类,你也不允许别人去继承这个类。换言之,出于类的设计考虑,它再也不需要作修改了,或者从安全角度出发,你不希望它再生出子类。
注意,final 类的数据可以是final 的,也可以不是final 的,这要由你来决定。无论类是不是final 的,这一条都适用于“将final 用于数据的”场合。但是,由于final 类禁止了继承,覆写方法已经不可能了,因此所有的方法都隐含地变成final 了。你可以为final 类的方法加一个final 修饰符,但是这一样没什么意义。
小心使用final
看来,设计类的时候将方法定义成final 的,会是一个很明智的决定。可能你会觉得没人会要覆写你的方法。有时确实是这样。