【Java编程思想】7.复用类
Java 中复用代码的方法:
- 组合:只需在新的类中产生现有类的对象。
- 继承:按照现有类的类型来创建新的类,无需改变现有类的形式。
- 代理:继承和组合之间的方式
7.1 组合语法
其实就是在一个类中引入其他类作为属性/域。
类中域为基本类型时会被自动初始化为0或 false,对象会被初始化为 null。
初始化对象的引用,可以在代码中的下列位置中进行:
- 在定义对象的地方-->意味着他们总是能够在构造器被调用前被初始化
- 在类的构造器中
- 在正要使用这些对象之前(惰性初始化/懒汉式/懒加载)
- 使用实例初始化
7.2 继承语法
所有的类,都在显式或隐式的继承标准根类 Object。
基类中,约定俗成将数据成员都指定为 private,所有方法都指定为 public。
继承不只是复制基类的接口,当创建了一个导出类的对象时,该对象包含了一个基类的子对象。这个子对象与使用基类直接创建的对象时一样的,二者区别在于,直接创建的对象来自于外部,而基类的子对象被包装在导出类对象内部。
在继承中,初始化对象时,构建的过程是从基类”向外“扩散的,所以基类在导出类构造器可以访问它之前,就已经完成了初始化。
如果没有默认的基类构造器,或者需要调用一个带参数的基类构造器,就必须使用 super 关键字显式地编写调用基类构造器的语句,并配以适当的参数列表。
7.3 代理
如下
public class SpaceShipControls {
void up(int velocity) {}
void down(int velocity) {}
void turboBoost() {}
}
SpaceShipDelegation 实现了 SpaceShipControls 的代理
public class SpaceShipDelegation {
private String name;
private SpaceShipControls controls = new SpaceShipControls();
public SpaceShipDelegation(String name) {
this.name = name;
}
// Delegated methods:
public void down(int velocity) {
controls.down(velocity);
}
public void up(int velocity) {
controls.up(velocity);
}
public void turboBoost() {
controls.turboBoost();
}
public static void main(String[] args) {
SpaceShipDelegation protector = new SpaceShipDelegation("NSEA Protector");
protector.forward(100);
}
}
7.4 结合使用组合和继承
Java 中没有 C++ 中析构函数的概念。析构函数是一种在对象被销毁时可以被自动调用的函数。
有时类需要在其生命周期内执行一些必须的清理活动,这是必须显式的编写一个特殊方法,来保证客户端程序员知道他们必须调用这种方法。因此可以将这一清理动作置于 finally 子句中预防异常的出现。
上面说的这种方法,是适合写在被继承的基类中供各种导出类使用。
在清理类的过程中,执行类的所有特定的清理动作,其顺序与生成类对象的顺序相反(要求基类元素依然存活)。
一个不懂的点?
对于清理这个动作,最好是除了内存以外,不去依赖垃圾回收期做任何事。如果需要进行清理,最好是编写自己的清理方法,而不是使用finalize()
方法。
如果 Java 的基类拥有某个已被多次重载的方法名称,那么在导出类中重新定义该方法名称,并不会屏蔽其在基类中的任何版本。也就是说,无论在该导出类或者其基类中对方法进行定义,重载机制都会正常工作。
针对上述情况,Java SE5 新增了 @override
注解,当需要覆写某个方法时,可以选择添加此注解,用来区别于在导出类重载基类的方法(即同名不同参数的方法)。
@override
注解可以防止在不想重载时而意外的进行重载(这就和 C++ 一致了)。
7.5 在组合和继承之间选择
组合和继承的区别在于:
- 组合:
- 组合显式的在新的类中放置子对象
- 组合通常用于想在新类中使用现有类的功能而非他的接口的情形,即在新类中嵌入某个对象并让其实现所需的功能。
- has-a(有一个)关系用组合来表达
- 继承:
- 继承隐式的在新的类中放置子对象
- 继承的时候,使用某个现有类,并会开发一个特殊的版本。这更像是因为需求而去定制的特别实现。
- is-a(是一个)关系用继承来表达
一个比较清晰的判定方法:
在使用组合或继承前,仔细考虑是否需要从新类向基类进行向上转型,如果必须向上转型,则继承才是必要的。
7.6 protected 关键字
使用 protected 的判定:在使用继承的时候,如果希望某些成员可以对外部隐藏起来,而仍可以被导出类所访问。
就是说,对类用户而言是 private 的,而对继承于此类或者任何位于同一个包内的类来说是 public 的。
7.7 向上转型
在继承的关系中,可以确保基类中的所有方法在导出类中也同样有效,所以,能够像基类发送的信息,应该也是同样也可以向导出类发送的。
class Instrument {
public void play() {}
static void tune(Instrument i) {
// ...
i.play();
}
}
// Wind objects are instruments
// because they have the same interface:
public class Wind extends Instrument {
public static void main(String[] args) {
Wind flute = new Wind();
Instrument.tune(flute); // Upcasting
}
}
上面的例子中,tune()
可以接受 Instrument
的引用(自身的引用)。而且 Wind.main()
中传递给 tune()
方法的参数,是 Wind
的引用。
像这种将 Wind
的引用转换为 Instrument
的引用的动作,就称之为向上转型。
这种命名方式的起源,可能是来自于传统的 UML 图中关于类继承关系的表示。如下:
向上转型是从一个较专用类型,向较通用类型转换,因此总是很安全的。
就是说导出类是基类的超集,导出类会具备基类中的方法。因此在向上转型的过程中,唯一可能发生的事是丢失方法。
Java 中是有向下转型的,后面会有介绍。
总结来说,就是对象既可以作为他自己本身的类型使用,也可以作为他的基本类型使用,把这种对某个对象的引用视为对其基类类型的引用的做法称之为向上转型(因为在 UML 继承树的画法中,基类是在上方的)。
7.8 final 关键字
通常关键字 final 就是代表着“无法改变”的含义。
对于数据来说,有两种情况需要使用 final:
- 一个永不改变的编译时常量(必须是基本数据类型,且在定义时必须进行赋值)。
- 一个在运行时被初始化的值,不希望他被改变。
一个既是 static 又是 final 的域,只占据一段不能被改变的存储空间。
当对象引用使用 final 时,代表其引用恒定不变,这意味着将无法把它改为指向另一个对象;然而对象自身确是可以被修改的。这种情况也适用于数组,数组也是对象。
final 不意味着某数据可以在编译时就知道它的值,只能说这个数据在被赋值之后,就再也不能去修改了(基本类型是不能修改值,对象是不能修改引用)。
Java 允许生成“空白 final”(就是声明为 final 但是为给定初始值的域),但是编译器会确保空白 final 在使用前一定会被初始化。
对于参数来说,使用 final 以为这无法在方法中更改参数引用所指向的对象。
这一特性主要用来向匿名内部类传递数据。
对于方法来说,有两种情况需要使用 final:
- 需要把方法锁定,以防止任何继承类修改它的含义。
- 另一个原因(过去)是因为效率。早起 Java 的实现中,方法指明为 final,就是同意编译器将针对该方法的调用都转为内嵌调用。
针对第二个原因,对于早期提高效率的更具体的描述,是说当编译器发现一个 final 方法调用命令是,会根据自己的判断,调过插入程序代码这种正常的方式,而去执行方法调用机制(将参数压入栈,跳至方法代码处并执行,然后跳回并清理栈中的参数,处理返回值),并且以方法体重的实际代码的副本代替方法调用。这样会消除方法调用的开销。
这样当一个方法很大的时候,带来的程序代码膨胀,是不会因为内嵌而提高性能的(相当于被内嵌的方法阻塞住了)。
最新的虚拟机的 hotspot 技术可以探测到这种情况,并优化去掉这些效率降低的内嵌调用,所以不需要使用final 方法来进行优化了。
关于 final 和 private
类中的 private 方法其实都隐式地被指定了 final:无法取用 private 的方法是无法被覆盖的,因此跟被 final 修饰的效果是相同的。
但是当强行覆盖一个 private 方法时,编译器是不会报错的。
这是因为“覆盖“这种情况只有在某方法是基类接口的一部分的时候才会出现,即,必须能将对象向上转型为它的基类类型,并调用相同的方法。当基类的某个方法时 private 时,对于外部导出类来说这个方法并不是基类接口的一部分,他只是基类中隐藏的一部分代码而已。
因此不需要特别在意 private 和 final 的区别,两者对方法的造成结果都是一致的。
类被 final 修饰时,代表该类不会被继承。实际上使用的情况:
- 类永远不需要进行任何改动。
- 处于安全考虑不允许该类被继承。
被 final 修饰的类中的域依然是可以自定义其是不是 final 的。然而 final 类 中的域和方法其实都是默认是 final 的。。。(这 tm 不是废话吗)
7.9 初始化以及类的加载
每个类的编译代码,都存在于自己的独立的文件中,该文件只在需要使用程序代码是才会被加载。
这通常是指加载发生于创建类的第一个对象只是,但是当访问 static 域或 static 方法时,也会发生加载(构造器也是 static 方法,某种程度的隐式的,因此更准确的讲,类实在其任何 static 成员被访问时加载的,说的就是你”构造器“)。
class Insect {
private int i = 9;
protected int j;
Insect() {
print("i = " + i + ", j = " + j); // 4
j = 39;
}
private static int x1 =
printInit("static Insect.x1 initialized"); // 1
static int printInit(String s) {
print(s);
return 47;
}
}
public class Beetle extends Insect {
private int k = printInit("Beetle.k initialized"); // 5
public Beetle() {
print("k = " + k); // 6
print("j = " + j); // 7
}
private static int x2 =
printInit("static Beetle.x2 initialized"); // 2
public static void main(String[] args) {
print("Beetle constructor"); // 3
Beetle b = new Beetle();
}
}
1 static Insect.x1 initialized
2 static Beetle.x2 initialized
3 Beetle constructor
4 i = 9, j = 0
5 Beetle.k initialized
6 k = 47
7 j = 39
可以看上面一段程序,运行程序时顺序如下:
- 访问 static 的
Beetle.main()
方法 - 加载器开始启动,找出 Beetle.class 文件之中的编译代码。
- 在进行加载的过程中,编译器由 extends 关键字找到他有基类,于是继续加载 Insect.class 文件之中的编译代码。
- 抛开这个例子,如果基类还有自身的基类,那么第二个基类会被继续加载,如此类推向上。
- 根基类中的 static 初始化开始被执行,然后是下一个导出类,如此类推向下。(这种方式确保了带出类的 static 初始化会依赖于基类成员能够被正确初始化)
- 至此为止,必要的类已经加载完毕,可以开始创建对象了。
- 对象中所有基本类型被设置为默认值,对象引用设置为 null(通过将对象内存设为二进制零值而生成的)。
- 基类的构造器(其静态 static 属性再次暴露)被调用。例子中被自动调用,也可以用 super 来指定对基类构造器的调用。
- 基类构造器和导出类的构造器一样,以相同的顺序经历相同过程。
- 基类构造器完成后,实例变量按照次序被初始化。
- 构造器其余部分被执行。