文章目录
第八章 复用
代码复用是面向对象编程(OOP)最具魅力的原因之一。
不污染源代码的前提下使用现存代码是需要技巧的。
- 第一种方式直接了当。在新类中创建现有类的对象。这种方式叫做“组合”(Composition),通过
这种方式复用代码的功能,而非其形式。
- 第二种方式更为微妙。创建现有类类型的新类。照字面理解:采用现有类形式,又无需在编码时
改动其代码,这种方式就叫做“继承”(Inheritance),编译器会做大部分的工作。继承是面向对象
编程(OOP)的重要基础之一。
在本章中,你会学到这两种代码复用的方法。
1. 组合语法
仅需要把对象的引用(object references)放置在一个新的类里,这就使用了组合。
—PS:就是在一个类里面 new 一个其他类的对象
初始化引用有四种方法:
-
当对象被定义时。这意味着它们总是在调用构造函数之前初始化。
-
在该类的构造函数中。
-
在实际使用对象之前。这通常称为延迟初始化。在对象创建开销大且不需要每次都创建对象的情
况下,它可以减少开销。
- 使用实例初始化。
class Soap {
private String s;
Soap() {
System.out.println("Soap()");
s = "Constructed";
}
@Override
public String toString() {
return s;
}
}
public class Bath {
private String // Initializing at point of definition:
// PS:s1,s2 为 1.当对象被定义时 初始化
s1 = "Happy",
s2 = "Happy",
s3,
s4;
private Soap castille;
private int i;
private float toy;
public Bath() {
System.out.println("Inside Bath()");
// PS:s3,toy,castille 为 2.在该类的构造函数中 初始化
s3 = "Joy";
toy = 3.14f;
castille = new Soap();
}
// Instance initialization:
// PS:i 为 4.使用实例初始化
{
i = 47;
}
@Override
public String toString() {
if (s4 == null) // Delayed initialization:
// PS:s4 为 3.在实际使用对象之前。这通常称为延迟初始化。
s4 = "Joy";
return "s1 = " + s1 + "\n" +
"s2 = " + s2 + "\n" +
"s3 = " + s3 + "\n" +
"s4 = " + s4 + "\n" +
"i = " + i + "\n" +
"toy = " + toy + "\n" +
"castille = " + castille;
}
public static void main(String[] args) {
Bath b = new Bath();
System.out.println(b);
}
}
/* Output:
Inside Bath()
Soap()
s1 = Happy
s2 = Happy
s3 = Joy
s4 = Joy
i = 47
toy = 3.14
castille = Constructed
*/
2. 继承语法
继承是所有面向对象语言的一个组成部分。事实证明,在创建类时总是要继承,因为除非显式地继承其他类,否则就隐式地继承 Java 的标准根类对象(Object)。
“这个新类与那个旧类类似。你可以在类主体的左大括号前的代码中声明这一点,使用关键字 extends 后跟基类的名称。
—PS:extends 崽种直视我
Java的 super 关键字引用了当前类继承的“超类”(基类)。
2.1 初始化基类
Java 自动在派生类构造函数中插入对基类构造函数的调用。
2.2 带参数的构造函数
如果没有无参数的基类构造函数,或者必须调用具有参数的基类构造函数,则必须使用 super 关键字和适当
的参数列表显式地编写对基类构造函数的调用:
class Game {
Game(int i) {
System.out.println("Game constructor");
}
}
class BoardGame extends Game {
BoardGame(int i) {
super(i);
System.out.println("BoardGame constructor");
}
}
public class Chess extends BoardGame {
Chess() {
super(11);
System.out.println("Chess constructor");
}
public static void main(String[] args) {
Chess chess = new Chess();
}
}
/* Output:
Game constructor
BoardGame constructor
Chess constructor
*/
如果没有在 BoardGame 构造函数中调用基类构造函数,编译器就会报错找不到 Game() 的构造函数。此外,对基类构造函数的调用必须是派生类构造函数中的第一个操作。(如果你写错了,编译器会提醒你。)
—PS:super(i) 是爹,需要在第一行
3. 委托
Java不直接支持的第三种重用关系称为委托。这介于继承和组合之间,因为你将一个成员对象放在正在构建的类中(比如组合),但同时又在新类中公开来自成员对象的所有方法(比如继承)。
public class SpaceShipControls {
void up(int velocity) {
}
void down(int velocity) {
}
void left(int velocity) {
}
void right(int velocity) {
}
void forward(int velocity) {
}
void back(int velocity) {
}
void turboBoost() {
}
}
public class SpaceShipDelegation {
private String name;
private SpaceShipControls controls = new SpaceShipControls();
public SpaceShipDelegation(String name) {
this.name = name;
}
// Delegated methods:
// PS:委托方法
public void back(int velocity) {
controls.back(velocity);
}
public void down(int velocity) {
controls.down(velocity);
}
public void forward(int velocity) {
controls.forward(velocity);
}
public void left(int velocity) {
controls.left(velocity);
}
public void right(int velocity) {
controls.right(velocity);
}
public void turboBoost() {
controls.turboBoost();
}
public void up(int velocity) {
controls.up(velocity);
}
public static void main(String[] args) {
SpaceShipDelegation protector = new SpaceShipDelegation("NSEA Protector");
// PS:forward 被转发到了 最底层 SpaceShipControls
// PS:SpaceShipControls 是 new 出来的,它的所有方法都在这里重新实现了
// 这也就是委托的那句话 :你将一个成员对象放在正在构建的类中(比如组合),但同时又在新类中公开来自成员对象的所有方法(比如继承)。
protector.forward(100);
}
}
4. 结合组合与继承
class Utensil {
Utensil(int i) {
System.out.println("Utensil constructor");
}
}
class Spoon extends Utensil {
Spoon(int i) {
super(i);
System.out.println("Spoon constructor");
}
}
class Custom {
Custom(int i) {
System.out.println("Custom constructor");
}
}
public class PlaceSetting extends Custom {
private Spoon sp;
PlaceSetting(int i) {
super(i + 1);
sp = new Spoon(i + 2);
System.out.println("PlaceSetting constructor");
}
public static void main(String[] args) {
PlaceSetting x = new PlaceSetting(9);
}
}
/* Output:
Custom constructor
Utensil constructor
Spoon constructor
PlaceSetting constructor
*/
—PS:简单的讲就是一个新类继承了某个类,它的某个成员变量是其他类(这个类也可能继承了别的类)
4.1 保证适当的清理
无论 try 块如何退出,在这个保护区域之后的 finally 子句中的代码总是被执行。
在很多情况下,清理问题不是问题;你只需要让垃圾收集器来完成这项工作。但是,当你必须执行显式清理时,就需要多做努力,更加细心,因为在垃圾收集方面没有什么可以依赖的。可能永远不会调用垃圾收集器。如果调用,它可以按照它想要的任何顺序回收对象。除了内存回收外,你不能依赖垃圾收集来做任何事情。如果希望进行清理,可以使用自己的清理方法,不要使用 finalize() 。
—PS:可能是我段位低,没有用过。不要写死循环一般内存时没有问题的
4.2 名称隐藏
如果 Java 基类的方法名多次重载,则在派生类中重新定义该方法名不会隐藏任何基类版本。
—PS:@Override 注释
5. 组合与继承的选择
当你想在新类中包含一个已有类的功能时,使用组合,而非继承。也就是说,在新类中嵌入一个对象(通常是私有的),以实现其功能。新类的使用者看到的是你所定义的新类的接口,而非嵌入对象的接口。
当使用继承时,使用一个现有类并开发出它的新版本。通常这意味着使用一个通用类,并为了某个特殊需求将其特殊化。稍微思考下,你就会发现,用一个交通工具对象来组成一部车是毫无意义的—— 车不包含交通工具,它就是交通工具。这种“是一个”的关系是用继承来表达的,而“有一个“的关系则用组合来表达。
—PS:extends 继承 “是一个”
6. protected
关键字 protected 就起这个作用。它表示“就类的用户而言,这是 private 的。但对于任何继承它的子类或在同一包中的类,它是可访问的。”(protected 也提供了包访问权限)
7. 向上转型
继承最重要的方面不是为新类提供方法。它是新类与基类的一种关系。简而言之,这种关系可以表述为“新类是已有类的一种类型”。
class Instrument {
public void play() {
}
static void tune(Instrument i) {
// ...
i.play();
}
}
public class Wind extends Instrument {
public static void main(String[] args) {
// PS:书中在此的代码为 Wind flute = new Wind();
// Wind flute = new Wind();
// PS:改造成了 Instrument flute = new Wind();
Instrument flute = new Wind();
Instrument.tune(flute); // Upcasting
}
}
派生类转型为基类是向上的,所以通常称作向上转型。
—PS:父类的引用指向子类的对象
7.1 再论组合和继承
因此尽管在教授 OOP 的过程中我们多次强调继承,但这并不意味着要尽可能使用它。恰恰相反,尽量少使用它,除非确实使用继承是有帮助的。一种判断使用组合还是继承的最清晰的方法是问一问自己是否需要把新类向上转型为基类。如果必须向上转型,那么继承就是必要的,但如果不需要,则要进一步考虑是否该采用继承。
8. final关键字
根据上下文环境,Java 的关键字 final 的含义有些微的不同,但通常它指的是“这是不能被改变的”。防止改变有两个原因:设计或效率。因为这两个原因相差很远,所以有可能误用关键字 final。以下几节讨论了可能使用 final 的三个地方:数据、方法和类。
8.1 final数据
static 强调只有一个,final 说明是一个常量。
按照惯例,带有恒定初始值的 final static 基本变量(即编译时常量)命名全部使用大写,单词之间用下划线分隔。
public static final int VALUE_THREE = 39;
在 Java 中,这类常量必须是基本类型,而且用关键字 final 修饰。你必须在定义常量的时候进行赋值。
一个被 static 和 final 同时修饰的属性只会占用一段不能改变的存储空间。
当用 final 修饰对象引用而非基本类型时,其含义会有一点令人困惑。对于基本类型,final 使数值恒定不变,而对于对象引用,final 使引用恒定不变。一旦引用被初始化指向了某个对象,它就不能改为指向其他对象。但是,对象本身是可以修改的,Java 没有提供将任意对象设为常量的方法。
—PS:final 修饰对象引用而非基本类型时没啥用,最多不能重新 new 而已。
8.2 空白final
空白 final 指的是没有初始化值的 final 属性。编译器确保空白 final 在使用前必须被初始化。这样既能使一个类的每个对象的 final 属性值不同,也能保持它的不变性。
—PS:如果空白 final 没有初始化,会有报错提示:需要在构造器中初始化。是的,编译器通过报错确保的。
class Poppet {
private int i;
Poppet(int ii) {
i = ii;
}
}
public class BlankFinal {
private final int i = 0;
private final int j;// PS:空白 final
private final Poppet p;// PS:空白 final
public BlankFinal() {
j = 11;
p = new Poppet(1);
}
public BlankFinal(int x) {
j = x;
p = new Poppet(x);
}
public static void main(String[] args) {
new BlankFinal();
new BlankFinal(47);
}
}
8.3 final参数
在参数列表中,将参数声明为 final 意味着在方法中不能改变参数指向的对象或基本变量:
//void f(final int i) { i++; } // Can't change
// You can only read from a final primitive
int g(final int i) {
return i + 1;
}
这个特性主 要用于传递数据给匿名内部类。
8.4 final方法
使用 final 方法的原因有两个。第一个原因是给方法上锁,防止子类通过覆写改变方法的行为。这是出
于继承的考虑,确保方法的行为不会因继承而改变。
过去建议使用 final 方法的第二个原因是效率。
…
有很长一段时间,使用 final 来提高效率都被阻止。你应该让编译器和 JVM 处理性能问题,只有在为了明确禁止覆写方法时才使用 final。
—PS:所以 final 方法就是给方法上锁,防止子类通过覆写改变方法
8.5 final和private
类中所有的 private 方法都隐式地指定为 final。因为不能访问 private 方法,所以不能覆写它。可以给private 方法添加 final 修饰,但是并不能给方法带来额外的含义。
—PS:可以但没必要
8.6 final类
当说一个类是 final (final 关键字在类定义之前),就意味着它不能被继承。之所以这么做,是因为类的设计就是永远不需要改动,或者是出于安全考虑不希望它有子类。
final 类的属性可以根据个人选择是或不是 final。这同样适用于不管类是否是 final 的内部 final 属性。然而,由于 final 类禁止继承,类中所有的方法都被隐式地指定为 final,所以没有办法覆写它们。你可以在 final 类中的方法加上 final 修饰符,但不会增加任何意义。
—PS:这个我知道,“可以但没必要”
8.7 final忠告
在设计类时将一个方法指明为 final 看上去是明智的。你可能会觉得没人会覆写那个方法。有时这是对的。
但请留意你的假设。通常来说,预见一个类如何被复用是很困难的,特别是通用类。如果将一个方法指定为 final,可能会防止其他程序员的项目中通过继承来复用你的类,而这仅仅是因为你没有想到它被以那种方式使用。
9. 类初始化和加载
一般可以说“类的代码在首次使用时加载“。这通常是指创建类的第一个对象,或者是访问了类的 static 属性或方法。构造器也是一个 static 方法尽管它的 static 关键字是隐式的。因此,准确地说,一个类当它任意一个 static 成员被访问时,就会被加载。首次使用时就是 static 初始化发生时。所有的 static 对象和 static 代码块在加载时按照文本的顺序(在类中定义的顺序)依次初始化。static 变量只被初始化一次。
—PS:static 就是导火线
9.1 继承和初始化
class Insect {
private int i = 9;
protected int j;
Insect() {
System.out.println("i = " + i + ", j = " + j);
j = 39;
}
private static int x1 = printInit("static Insect.x1 initialized");
static {
System.out.println("Insect static");
}
static int printInit(String s) {
System.out.println(s);
return 47;
}
static void say() {
System.out.println("测试一下");
}
}
public class Beetle extends Insect {
private int k = printInit("Beetle.k.initialized");
private int k2 = printInit("Beetle.k2.initialized");
public Beetle() {
System.out.println("k = " + k);
System.out.println("j = " + j);
}
private int k3 = printInit("Beetle.k3.initialized");
static {
System.out.println("Beetle static");
}
private static int x2 = printInit("static Beetle.x2 initialized");
public static void main(String[] args) {
System.out.println("Beetle constructor");
Beetle b = new Beetle();
System.out.println("again");
new Beetle();
}
}
输出:
static Insect.x1 initialized
Insect static
Beetle static
static Beetle.x2 initialized
Beetle constructor
i = 9, j = 0
Beetle.k.initialized
Beetle.k2.initialized
Beetle.k3.initialized
k = 47
j = 39
again
i = 9, j = 0
Beetle.k.initialized
Beetle.k2.initialized
Beetle.k3.initialized
k = 47
j = 39
—PS:这个例子是在原来的 demo 上加了一些东西,正好用来串联第六章的初始化,下面开始解析:
- 想执行访问 Beetle 类的 main() 方法,就需要先加载 Beetle 类。
- 编译器在加载的过程中,发现 Beetle 类存在基类(Insect),就去加载 Insect 类
- 加载 Insect 类过程中,会顺序执行 static 修饰的成员变量或代码块,此时输出结果集中的第1-2行
- 基类(Insect)加载完成后,接着加载 Beetle 类,同样会顺序执行 static 修饰的成员变量或代码块,此时输出结果集中的第3-4行
- 所有类加载完成后,执行 Beetle 类的 main() 方法,进入 main() 方法,第一行代码执行,输出结果集中的第5行
- 接着执行 Beetle b = new Beetle(); 因为调用派生类的构造方法前会先调用基类的构造方法,另根据“在类中变量定义的顺序决定了它们初始化的顺序。即使变量定义散布在方法定义之间,它们仍会在任何方法(包括构造器)被调用之前得到初始化。”,所以此时先进行成员变量(i j)的初始化,i 被赋值为9,j 为默认值0。所以经过 Insect()
- Insect() {
System.out.println("i = " + i + ", j = " + j);
j = 39;
} - 输出结果集中的第6行,并为 j 赋值为39
- 接着调用Beetle 类的构造方法,但是在此之前需要先为 Beetle 类的成员变量初始化,即会输出结果集中的第7-9行
- 接着执行两行输出语句,即结果集中的第10-11行。
- 接着执行 Beetle 类的 main() 方法中的第3行代码,即会输出结果集中的第12行
- 接着执行 Beetle 类的 main() 方法中的第4行代码,再次 new Beetle 对象。所以会重新执行一遍步骤 6-10,即会输出结果集中的第13-18行
10. 本章小结
继承和组合都是从已有类型创建新类型。组合将已有类型作为新类型底层实现的一部分,继承复用的是接口。
使用继承时,派生类具有基类接口,因此可以向上转型为基类,这对于多态至关重要。
尽管在面向对象编程时极力强调继承,但在开始设计时,优先使用组合(或委托),只有当确实需要
时再使用继承。组合更具灵活性。
在设计一个系统时,目标是发现或创建一系列类,每个类有特定的用途,而且既不应太大(包括太多
功能难以复用),也不应太小(不添加其他功能就无法使用)。如果设计变得过于复杂,通过将现有
类拆分为更小的部分而添加更多的对象。
当开始设计一个系统时,记住程序开发是一个增量过程,正如人类学习。它依赖实验,你可以尽可能
多做分析,然而在项目开始时仍然无法知道所有的答案。如果把项目视作一个有机的,进化着的生命
去培养,而不是视为像摩天大楼一样快速见效,就能获得更多的成功和更迅速的反馈。
继承和组合正是可以让你执行如此实验的面向对象编程中最基本的两个工具。
自我学习总结:
- 通过组合和继承可以从已有类的基础上创建新类,已达到复用已有类的目的
- 多用组合,少用继承
- extends 继承 具有 “是一个” 的含义
- final 修饰的变量不能被修改,修饰的方法不能被重写,修饰的类不能被继承
- 类初次加载时,会执行 static 修饰的代码
- 调用构造方法前会先进行类的成员变量初始化
- 调用派生类的构造方法前会先调用基类的构造方法