复用代码是Java众多引人注目的功能之一。但要想成为极具革命性的语言,仅仅能够复制代码并对之加以改变是不够的,它还必须做更多的事情。
使用类而不破坏程序代码:
- 在新类中产生现有对象。由于新的类是由现有类的对象组成,所有这种方法称为组合。
- 按照现有类的类型来创建新类。无需改变现有类的形式,采用现有类的形式并在其中添加新代码。这种方法是继承。
7.1 组合语法
组合只需要将对象引用置于新类中就可以了。
但编译器并不是简单的为每一个引用都创建默认对象。如果想初始化这些引用,可以在代码中下列位置进行:
- 在定义对象的地方。
- 在类的构造器中。
- 就在正要使用这些对象之前,这种方式称为惰性初始化。
- 使用实例初始化。
7.2 继承语法
除非已明确指出要从其他类中继承,否则就是在隐式的从Java的标准根中类Object进行继承。
这种声明是通过在类主体的左边花括号之前,书写后面紧随基类名称的关键字extends而实现的。当这么做时,会自动得到基类中所有域和方法。
Javay用super关键字表示超类的意思。C#中用base。
7.2.1 初始化基类
当创建了一个导出类对象时,该对象包含了一个基类的子对象。这个子对象与你用基类直接创建的对象是一样的。二者区别在于,后者来自于外部,而基类的子对象被包装在导出类对象内部。
在构造器中调用基类构造器来执行初始化,而基类构造器具有执行基类初始化所需要的所有知识和能力。Java会自动在导出类的构造器中插入对基类构造器的调用。
即使你不为导出类创建构造器,编译器也会为你合成一个默认的构造器,该构造器将调用基类的构造器。
带参数的构造器
如果没有默认的基类构造器,或者想调用一个带参数的基类构造器,就必须使用关键字super显式的编写调用基类构造器的语句,并且配以适当的参数列表。
7.3 代理
这是继承于组合之间的中庸之道,因为我们将一个成员对象置于所要构造的类中,但与此同时我们在新类中暴露了该成员对象的所有方法。
public class SpaceShipDelegation {
SpaceShipControls spaceShipControls=null;
public SpaceShipDelegation(SpaceShipControls controls){
this.spaceShipControls=controls;
}
public void up(int i){
spaceShipControls.up(i);
}
public void down(int i){
spaceShipControls.down(i);
}
}
class SpaceShipControls{
void up(int velocity){}
void down(int i){}
}
7.4 结合使用组合和继承
7.4.1 确保正确清理
Java中没有C++中析构函数的概念。析构函数是一种在对象被销毁时可以被自动调用的函数。
在清理方法中,还必须注意对基类的清理方法和成员对象清理方法的调用顺序,以防止某个对象依赖于另一个子对象情形发生。首先,执行类的所有特定的清理动作,其顺序同生成顺序相反。然后,调用基类的清理方法。
一旦涉及垃圾回收,能够信赖的事就不会很多了。垃圾回收器可能永远也无法被调用,即使被调用,它也可能以任何它想要的顺序来回收对象。最好的办法是除了内存外,不能依赖垃圾回收器去做任何事。如果需要进行清理,最好事编写自己的清理方法,但不要使用finalize()。
7.4.2 名字屏蔽
如果Java的基类拥有某个已被多次重载的方法名称,那么在导出类中重新定义该方法名称并不会屏蔽在基类中的任何版本。无论是在该层的基类中对方法进行定义,重载机制都可以正常工作。
当你想要覆写某个方法时,可以使用@Override这个注解。它可以防止你在不需要进行重载时意外进行了重载。
7.5 在组合与继承之间选择
组合和继承都允许在新的类中放置子对象,组合是显式的这样做,而继承是隐式的做。
组合技术通常用于想在新类中使用现有类的功能,但新类的用户看到的只是为新类所定义的接口,而非所嵌入对象的接口。
7.6 protected关键字
对于任何继承于此类的导出类或其他任何位于同一包内的类来说,它是可以访问的。
7.7 向上转型
将导出类转换成基类。
导出类是基类的一个超集,它可能比基类含有更多的方法,但它必须至少具备基类中所含有的方法。
7.7.2 组合与继承
该用组合还是继承,一个最清晰的判断办法就是是否需要从新类向基类进行向上转换。
final关键字
final可以用于三种情况:数据、方法、类
7.8.1 final数据
使数据恒定不变是有用的:
- 1.一个永不改变的编译时常量
- 2.一个再运行时被初始化的值,而你不希望它被改变
在编译期常量这种情况,编译器可以将该常量值带入任何可能用到它的计算式中,也就是可以在编译时执行计算式,这就减轻了运行时的负担。在对这个常量进行定义时,必须初始化。
一个是static又是final的域只占据一段不能改变的存储空间。
对于对象引用,final引用恒定不变,对象自身可以被修改。
空白final
Java允许生成"空白final",空白final指被声明为final但又未给定初始值的域。无论什么情况,编译器都确保final在使用前必须被初始化。
public class BlankFinal {
private final int i=0;
private final int j;//必须进行初始化
private final Poppet p;
public BlankFinal(){
j=1;
p=new Poppet(1);
}
public BlankFinal(int x){
j=x;
p=new Poppet(x);
}
public static void main(String[] args){
new BlankFinal();
new BlankFinal(2);
}
}
class Poppet{
private int i;
Poppet(int ii){
i=ii;
}
}
final参数
Java允许在参数列表中以声明的方式将参数指明为final,这就意味着你无法在方法中更改参数引用所指向的对象。
public class FinalArguments {
void with(final Gizmo g){
//g=new Gizmo();不能赋值。
}
int without(final int i){
//i+=1;不能改变
return i+1;
}
}
class Gizmo{
public void spin(int i){
System.out.println(i);
}
}
7.8.2 final方法
使用final方法的原因有两个:
- 把方法锁定,以防止任何继承类修改它的含义。保持继承中使方法保持不变,并且不会被覆盖。
- 效率。将一个方法指定为final,就是同意将针对该方法的所有调用都转为内嵌调用。但在现在的虚拟机中,不需要使用final方法来进行优化了。
final和private关键字
private方法都隐式的指定是final。
7.8.3 final类
当将某个类整体定义为final时,就表面此类不能被继承。
7.9 初始化及类的加载
在对导出类进行加载时,会先对基类进行加载。如果该基类还有自身的基类,那么第二个基类就会被加载。