一、组合语法
在新类中产生现有类的对象。
每个非基本类型的对象都有一个toString()
方法。
在不抛出异常的情况下仍旧可以打印一个null
引用(打印出null
),但不可调用它的任何方法。
初始化引用的位置:
- 定义对象的地方
- 类的构造器中
- 使用这些对象之前(惰性初始化)
- 使用实例初始化(块)
二、继承语法(extends)
1、初始化基类
当创建了一个导出类的对象时,该对象包含了一个基类的子对象,这个子对象与基类直接创建对象(来自于外部)是一样的。
只能在构造器中调用基类构造器来执行初始化(仅有的方式),构造过程为从基类“向外”扩散的。
(Java会自动在导出类的构造器中插入对基类构造器的调用)
2、带参数的构造器
必须用super()
显式调用基类的构造器。
三、代理(模式)
Java并未直接支持(是一种设计模式)
继承与组合之间的中庸之道。
class A {
void up() {}
void down() {}
}
class B {
private A a = new A();
public void up() {}
public void down() {}
}
继承也可以实现此类功能,但B
其实不是A
的子类型,所以不应该使用继承。
使用代理能拥有更多的控制力,可以选择性提供成员方法的某个子集。
四、结合使用组合和继承
编译器会强制你去初始化基类,并要求你要在构造器的起始处就这么做,但它并不监督你必须将成员对象也初始化。
1、确保正确清除
定义特殊的方法(如dispose()
等)清理一些东西。
try{...}finally{dispose()}
2、名字屏蔽
如果Java的基类拥有某个已被多次重载的方法名称,那么在导出类中重新定义该方法名称并不会屏蔽其在基类中的任何版本(该方法的参数列表与基类中的同名方法的参数列表不同,否则即为重载)。
在C++中若要完成这项工作需要屏蔽基类方法。
@Override
注解可以防止你在不想要重载时而意外地进行了重载。
五、在组合与继承之间选择
is-a 继承 has-a 组合
组合中的域一般为private
的,但如果某组合也是问题分析的一部分,使成员成为public
有助于客户的程序员了解如何使用类(需要调用成员对象的方法),可降低代码复杂度,但这仅是特例。
六、protected关键字
就类用户而言,是private
的,但对于任何继承于此类的导出类或者其它任何位于同一包呢的类来说,它却是可以访问的。
七、向上转型
1、为什么称为向上转型
由导出类转型成基类(导出类是基类的一个超集)
2、再论组合与继承
是否需要从新类向基类进行向上转型是是否选用继承(慎用)的依据。
八、final关键字
数据、方法和类三种情况(设计与效率)
1、final数据
一个永不改变的编译时常量;
一个在运行时被初始化的值,而你不希望它被改变。
定义static
强调只有一份(存储),定义final
说明它是常量。
对于final
的对象引用(数组),对象自身可被修改,但引用恒定不变。
- 空白
final
:被声明为final但又未给定初始值的域(没有默认的初始化值),编译器会确保空白final
在使用前被初始化(需要自行初始化)。 final
参数(匿名内部类,第十章)无法在方法中更改参数引用所指向的对象(作为引用的拷贝,参数在方法体能不能再引用新对象,但可以对象自身可以改变)。
2、final方法
把方法锁定,以防任何继承类修改(重载)其含义(设计);
内嵌调用,消除方法调用开销(效率) 早期做法,现代虚拟机等可以探测这些情况。
final
与private
关键字:private
都隐式地指定为final
3、final类:无法继承
不论类是否为final
,final
的域可选择是否定义为final
,但final
类中的所有方法都隐式地定义为final
(无法继承)。
4、有关final的忠告
预见类是如何复用一般是很困难的,特别是对于一个通用类而言更是如此(标准类库中的Vector
→List
,Hashtable
→HashTable
等),不要轻易定义final
。
九、初始化及类的加载
C++中(程序作为启动过程的一部分被加载,而后是初始化,紧接着程序运行)如果某个static
期望另一个static
在被初始化之前就能有效地使用它就会报错。
Java中类的代码在初次使用时才加载,在访问static
域或static
方法时也会被加载,初次使用之处即static
初始化发生之处。
继承与初始化
你在类Beetle
上运行Java时的过程:
- 试图访问
Beetle.main()
(一个static
方法), - 加载器开始启动并找出
Beetle
类被编译的程序代码(class
的文件)。 - 如果有个基类(这是由关键字
extends
告知的),于是它继续进行加载基类。不管你是否打算产生一个该基类的对象,这都要发生。 - 该基类还有其自身的基类,那么第二个基类就会被加载,依此类推。
- 根基类中的静态初始化即会被执行
- 然后是下一个导出类,以此类推。(这种方式很重要,因为导出类的静态初始化可能会依赖于基类成员能否被正确初始化的。)
- 至此为止,必要的类都已加载完毕,对象就可以被创建了。
- 对象中所有的原始类型都会被设为缺省值,对象引用被设为零(这是通过将对象内存设为二进制零值而一举生成的。)
- 基类的构造器会被调用。可能会自动调用的,也可以用
super
来指定对基类构造器的调用。基类构造器和导出类的构造器一样,以相同的顺序来经历相同的过程。 - 在基类构造器完成之后,实例变量(instance variables)按其次序被初始化。
- 最后,构造器的其余部分被执行。
十、总结
继承和组合都能从现有类型生成新类型。组合一般将现有类型作为新类型的底层实现的一部分加以复用,而继承复用的是接口。
尽管面向对象编程对继承极力强调,但在你开始设计时,一般你应优先选择使用组合,只在确实必要时才使用继承。因为组合更具灵活性。此外,通过对成员类型使用继承技术,你可以在运行期就改变那些成员对象的类型和行为。因此,你可以在运行期改变组合而成的对象的行为。
一个系统中的每个类都有具体的用途,既不是太大(包含太多的功能而难以复用),也不能太小(不添加其他功能就无法使用)。
继承与组合是在面向对象程序设计中最基本的两个工具。