Merge pull request #4 from LingCoder/master

获取更新
This commit is contained in:
1326670425
2019-06-27 02:49:19 -05:00
committed by GitHub

View File

@@ -13,7 +13,7 @@
1. 第一种方式直接了当。在新类中创建现有类的对象。这种方式叫做“组合”Composition通过这种方式复用代码的功能而非其形式。
2. 第二种方式更为微妙。通过现有类创建新类。照字面理解采用现有类形式又无需在编码时改动其代码这种方式就叫做“继承”Inheritance。**继承**是面向对象编程OOP的重要基础之一。更多功能相关将在[多态](./09-Polymorphism.md)Polymorphism章节中介绍。
2. 第二种方式更为微妙。创建现有类类型的新类。照字面理解采用现有类形式又无需在编码时改动其代码这种方式就叫做“继承”Inheritance,编译器会做大部分的工作。**继承**是面向对象编程OOP的重要基础之一。更多功能相关将在[多态](./09-Polymorphism.md)Polymorphism章节中介绍。
组合与继承的语法、行为上有许多相似的地方(这其实是有道理的,毕竟都是基于现有类型构建新的类型)。在本章中,你会学到这两种代码复用的方法。
@@ -21,7 +21,7 @@
## 组合语法
在前面的学习中“组合”Composition已经被多次使用。你仅需要把对象的引用object references放置在一个新的类里这就使用了组合。例如假设你需要一个对象其中内置了几个 **String** 对象两个基本类型primitives的属性字段一个自定义 Class 的对象。对于非基本类型对象,将引用直接放置在 Class 中,对于基本类型属性字段则仅进行声明。
在前面的学习中“组合”Composition已经被多次使用。你仅需要把对象的引用object references放置在一个新的类里这就使用了组合。例如假设你需要一个对象其中内置了几个 **String** 对象两个基本类型primitives的属性字段一个其他类的对象。对于非基本类型对象,将引用直接放置在新类中,对于基本类型属性字段则仅进行声明。
```java
// reuse/SprinklerSystem.java
@@ -68,13 +68,13 @@ i = 0 f = 0.0 source = Constructed
```
这两个类中定义的一个方法是特殊的: **toString()**。每个非原始类型对象都有一个 **toString()** 方法,在编译器需要字符串但它有对象的特殊情况下调用该方法。因此,在[1]中,编译器看到你试图“添加”一个 **WaterSource** 类型字符串对象 。因为你只能将一个字符串添加到另一个字符串,所以它说,“我将通过调用 **toString()****source** 转换成一个字符串。然后,它可以组合这两个字符串并将结果字符串传递给 **System.out.println()**。要对创建的任何类允许这种行为,只需要编写一个 **toString()** 方法。在 **toString()** 上使用 **@Override** 注释来告诉编译器,以确保正确地覆盖。**@Override** 是可选的,但它有助于验证你没有拼写错误 (或者更微妙地说,大小写字母输入错误)。类中的基本类型字段自动初始化为零,正如 **object Everywhere** 一章中所述。但是对象引用被初始化为 **null**,如果你尝试调用其任何一个方法,你将得到一个异常(一个运行时错误)。
这两个类中定义的一个方法是特殊的: `toString()`。每个非基本类型对象都有一个 `toString()` 方法,在编译器需要字符串但它有对象的特殊情况下调用该方法。因此,在 [1] 中,编译器看到你试图“添加”一个 **WaterSource** 类型字符串对象 。因为字符串只能拼接另一个字符串,所以它就先会调用 `toString()`**source** 转换成一个字符串。然后,它可以拼接这两个字符串并将结果字符串传递给 `System.out.println()`。要对创建的任何类允许这种行为,只需要编写一个 **toString()** 方法。在 `toString()` 上使用 **@Override** 注释来告诉编译器,以确保正确地覆盖。**@Override** 是可选的,但它有助于验证你没有拼写错误 (或者更微妙地说,大小写字母输入错误)。类中的基本类型字段自动初始化为零,正如 **object Everywhere** 一章中所述。但是对象引用被初始化为 **null**,如果你尝试调用其任何一个方法,你将得到一个异常(一个运行时错误)。方便的是,打印 **null** 引用却不会得到异常。
编译器不只是为每个引用创建一个默认对象,这是有意义的,因为在许多情况下,这会导致不必要的开销。初始化引用有四种方法:
编译器不为每个引用创建一个默认对象,这是有意义的,因为在许多情况下,这会导致不必要的开销。初始化引用有四种方法:
1. 当对象被定义时。这意味着它们总是在调用构造函数之前初始化。
2. 在该类的构造函数中。
3. 在实际使用对象之前。这通常称为延迟初始化。在对象创建开销大且不需要每次都创建对象的情况下,它可以减少开销。
3. 在实际使用对象之前。这通常称为*延迟初始化*。在对象创建开销大且不需要每次都创建对象的情况下,它可以减少开销。
4. 使用实例初始化。
以上四种实例创建的方法例子在这:
@@ -144,9 +144,9 @@ castille = Constructed
```
**Bath** 构造函数中,在初始化发生。当你不在定义处初始化时,仍然不能保证在向对象引用发送消息之前执行任何初始化——如果你试图对未初始化的引用调用方法,则未初始化的引用将产生运行时异常。
**Bath** 构造函数中,有一行语句在所有初始化发生前就已经执行了。当你不在定义处初始化时,仍然不能保证在向对象引用发送消息之前执行任何初始化——如果你试图对未初始化的引用调用方法,则未初始化的引用将产生运行时异常。
当调用 **toString()** 时,它将赋值 s4以便在使用字段的时候进行初始化。
当调用 `toString()` 时,它将赋值 s4以便在使用字段的时候所有的属性都已被初始化。
<!-- Inheritance Syntax -->
@@ -208,17 +208,17 @@ Cleanser dilute() apply() scrub()
```
这演示了一些特性。首先,在 **Cleanser append()** 方法中,使用 += 操作符将字符串连接到 **s**,这是 Java 设计人员“重载”来处理字符串的操作符之一 (还有 + )。
这演示了一些特性。首先,在 **Cleanser**`append()` 方法中,使用 `+=` 操作符将字符串连接到 **s**,这是 Java 设计人员“重载”来处理字符串的操作符之一 (还有 + )。
第二,**Cleanser** 和 **Detergent** 都包含一个 **main()** 方法。你可以为每个类创建一个 main() ; 这允许对每个类进行简单的测试。当你完成测试时,不需要删除 **main()** ; 你可以将其留在以后的测试中。即使程序中有很多类,惟一 运行的 **main()** 也是在命令行上调用的 **main()**。这里,当你使用 **java Detergent** 时候,调用了 **Detergent.main()**。但是你也可以使用 **java Cleanser** 来调用 **Cleanser.main()** ,即使 **Cleanser** 不是一个公共类。即使类具只有包访问权,也可以访问 **public main()**
第二,**Cleanser** 和 **Detergent** 都包含一个 `main()` 方法。你可以为每个类创建一个 `main()` ; 这允许对每个类进行简单的测试。当你完成测试时,不需要删除 `main()`; 你可以将其留在以后的测试中。即使程序中有很多类都有 `main()` 方法,惟一运行的只有在命令行上调用的 `main()`。这里,当你使用 **java Detergent** 时候,调用了 `Detergent.main()`。但是你也可以使用 **java Cleanser** 来调用 `Cleanser.main()`,即使 **Cleanser** 不是一个公共类。即使类具只有包访问权,也可以访问 `public main()`
在这里,**Detergent.main() **显式地调用 **Cleanser.main()** ,从命令行传递相同的参数(当然,你可以传递任何字符串数组)。
在这里,`Detergent.main()` 显式地调用 `Cleanser.main()`,从命令行传递相同的参数(当然,你可以传递任何字符串数组)。
**Cleanser** 中的所有方法都是公开的。请记住,如果不使用任何访问说明符,则成员默认为包访问,这只允许访问包成员。因此,在这个包中,如果没有访问说明符,任何人都可以使用这些方法。例如,**Detergent** 就没有问题。但是,如果要从 **Cleanser** 继承其他包中的类,则该类只能访问公共成员。因此,为了允许继承,一般规则是所有字段为私有,所有方法为公共。(受保护成员也允许派生类访问;你以后会知道的。)在特定的情况下,你必须进行调整,但这是一个有用的指南。
**Cleanser** 中的所有方法都是公开的。请记住,如果不使用任何访问修饰符,则成员默认为包访问权限,这只允许包内成员访问。因此,如果没有访问修饰符,那么包内的任何人都可以使用这些方法。例如,**Detergent** 就没有问题。但是,如果其他包中的类继承 **Cleanser**,则该类只能访问 **Cleanser**公共成员。因此,为了允许继承,一般规则是所有字段为私有,所有方法为公共。(受保护成员也允许派生类访问;你以后会知道的。)在特定的情况下,你必须进行调整,但这是一个有用的指南。
**Cleanser** 的接口中有一组方法: **append()**、**dilute()**、**apply()**、**scrub()****toString()**。因为 **Detergent** 是从 **Cleanser** 派生的(通过 **extends** 关键字),所以它会在其接口中自动获取所有这些方法,即使你没有在 **Detergent** 中看到所有这些方法的显式定义。你那么,可以把继承看作是用类。如在 **scrub()** 中所见,可以使用基类中定义的方法并修改它。在这里,你可以从新版本中的基类中调用该方法。但是在 **scrub()** 内部,不能简单地调用 **scrub()**因为这会产生递归调用。为了解决这个问题Java的 **super** 关键字引用了当前类继承的“超类”(基类)。因此表达式 **super.scrub()** 调用方法 **scrub()** 的基类版本。
**Cleanser** 的接口中有一组方法: `append()``dilute()``apply()``scrub()``toString()`。因为 **Detergent** 是从 **Cleanser** 派生的(通过 **extends** 关键字),所以它会在其接口中自动获取所有这些方法,即使你没有在 **Detergent** 中看到所有这些方法的显式定义。你那么,可以把继承看作是用类。如在 `scrub()` 中所见,可以使用基类中定义的方法并修改它。在这里,你可以在新类中调用基类的该方法。但是在 `scrub()` 内部,不能简单地调用 `scrub()`因为这会产生递归调用。为了解决这个问题Java的 **super** 关键字引用了当前类继承的“超类”(基类)。因此表达式 `super.scrub()` 调用方法 `scrub()` 的基类版本。
继承时,你不受限于使用基类的方法。你还可以像向类添加任何方法一样向派生类添加新方法:只需定义它。方法 **foam()** 就是一个例子。**Detergent.main()** 中可以看到,对于 **Detergent** 对象,你可以调用 **Cleanser****Detergent** 中可用的所有方法 (如 **foam()** )。
继承时,你不受限于使用基类的方法。你还可以像向类添加任何方法一样向派生类添加新方法:只需定义它。方法 `foam()` 就是一个例子。`Detergent.main()` 中可以看到,对于 **Detergent** 对象,你可以调用 **Cleanser****Detergent** 中可用的所有方法 (如 `foam()` )。
<!-- Initializing the Base Class -->
@@ -269,7 +269,7 @@ Cartoon constructor
### 带参数的构造函数
上面的所有例子中构造函数都是无参数的 ; 编译器很容易调用这些构造函数,因为不需要参数。如果没有无参数的基类构造函数,或者必须调用具有参数的基类构造函数,则必须使用 super 关键字和适当的参数列表显式地编写对基类构造函数的调用:
上面的所有例子中构造函数都是无参数的 ; 编译器很容易调用这些构造函数,因为不需要参数。如果没有无参数的基类构造函数,或者必须调用具有参数的基类构造函数,则必须使用 **super** 关键字和适当的参数列表显式地编写对基类构造函数的调用:
```java
// reuse/Chess.java
@@ -308,9 +308,7 @@ Chess constructor
```
如果没有在 **BoardGame** 构造函数中调用基类构造函数,编译器就会报错找不到 **Game()** 的构造函数。此外,对基类构造函数的调用必须是派生类构造函数中的第一个操作。(如果你写错了,编译器会提醒你。)
如果没有在 **BoardGame** 构造函数中调用基类构造函数,编译器就会报错找不到 `Game()` 的构造函数。此外,对基类构造函数的调用必须是派生类构造函数中的第一个操作。(如果你写错了,编译器会提醒你。)
<!-- Delegation -->
@@ -361,7 +359,7 @@ DerivedSpaceShip extends SpaceShipControls {
```
然而, **DerivedSpaceShip** 并不是真正的“一种” **SpaceShipControls** ,即使你“告诉” **DerivedSpaceShip** 调用 **forward()** 。更准确地说,一艘宇宙飞船包含了 **SpaceShipControls **,同时 **SpaceShipControls** 中的所有方法都暴露在宇宙飞船中。委托解决了这个难题:
然而, **DerivedSpaceShip** 并不是真正的“一种” **SpaceShipControls** ,即使你“告诉” **DerivedSpaceShip** 调用 `forward()`。更准确地说,一艘宇宙飞船包含了 **SpaceShipControls **,同时 **SpaceShipControls** 中的所有方法都暴露在宇宙飞船中。委托解决了这个难题:
```java
// reuse/SpaceShipDelegation.java
@@ -509,7 +507,7 @@ PlaceSetting constructor
### 保证适当的清理
Java 没有 c++ 中析构函数的概念析构函数是在对象被销毁时自动调用的方法。原因可能是在Java中实践只会忘记对象而不是销毁它们,从而允许垃圾收集器根据需要回收内存。通常这是可以的,但是有时你的类可能在其生命周期中执行一些需要清理的活动。Housekeeping 章节解释说,你不能知道何时调用垃圾收集器,甚至不能知道是否调用了它。因此,如果你想为类清理一些东西,你必须显式地编写一个特殊的方法来完成它,并确保客户程序员知道他们必须调用这个方法。最重要的是——正如在 Exceptions 章节中描述的——你必须通过在 **finally **子句中放置此类清理来防止异常。
Java 没有 C++ 中析构函数的概念析构函数是在对象被销毁时自动调用的方法。原因可能是在Java中通常是忘掉而不是销毁对象,从而允许垃圾收集器根据需要回收内存。通常这是可以的,但是有时你的类可能在其生命周期中执行一些需要清理的活动。初始化和清理章节提到,你不能知道垃圾收集器何时会被调用,甚至它是否会被调用。因此,如果你想为类清理一些东西,你必须显式地编写一个特殊的方法来完成它,并确保客户程序员知道他们必须调用这个方法。最重要的是——正如在"异常"章节中描述的——你必须通过在 **finally **子句中放置此类清理来防止异常。
请考虑一个在屏幕上绘制图片的计算机辅助设计系统的例子:
@@ -633,13 +631,13 @@ Shape dispose
```
这个系统中的所有东西都是某种 **Shape** (它本身是一种 **Object**,因为它是从根类隐式继承的) 。除了使用 **super** 调用该方法的基类版本外,每个类还覆盖 **Shapes dispose()** 方法。特定的 **Shape** 类——Circle、**Triangle** 和 **Line**,都有 “draw” 构造函数,尽管在对象的生命周期中调用的任何方法都可以负责做一些需要清理的事情。每个类都有自己的 **dispose()** 方法来将非内存的内容恢复到对象存在之前的状态。
这个系统中的所有东西都是某种 **Shape** (它本身是一种 **Object**,因为它是从根类隐式继承的) 。除了使用 **super** 调用该方法的基类版本外,每个类还覆盖 `dispose()` 方法。特定的 **Shape** 类——**Circle**、**Triangle** 和 **Line**,都有 “draw” 构造函数,尽管在对象的生命周期中调用的任何方法都可以负责做一些需要清理的事情。每个类都有自己的 `dispose()` 方法来将非内存的内容恢复到对象存在之前的状态。
**main()** 中,有两个关键字是你以前没有见过的,在 exception 一章之前不会详细解释: **try****finally**。try关键字表示后面的块 (用花括号分隔 )是一个受保护的区域,这意味着它得到了特殊处理。其中一个特殊处理是,无论 **try** 块如何退出,在这个保护区域之后的 **finally** 子句中的代码总是被执行。(通过异常处理,可以用许多不同寻常的方式留下 **try** 块。)这里,**finally** 子句的意思是,“无论发生什么,始终调用 **dispose()** for **x**。”
`main()` 中,有两个关键字是你以前没有见过的,在"异常"一章之前不会详细解释: **try****finally****try** 关键字表示后面的块 (用花括号分隔 )是一个受保护的区域,这意味着它得到了特殊处理。其中一个特殊处理是,无论 **try** 块如何退出,在这个保护区域之后的 **finally** 子句中的代码总是被执行。(通过异常处理,可以用许多不同寻常的方式留下 **try** 块。)这里,**finally** 子句的意思是,“无论发生什么,始终调用 `x.dispose()`。”
在清理方法 (在本例中是 **dispose()** ) 中,还必须注意基类和成员对象清理方法的调用顺序,以防一个子对象依赖于另一个子对象。首先,按创建的相反顺序执行特定于类的所有清理工作。(一般来说,这要求基类元素仍然是可访问的。) 然后调用基类清理方法,如这所示。
在清理方法 (在本例中是 `dispose()` ) 中,还必须注意基类和成员对象清理方法的调用顺序,以防一个子对象依赖于另一个子对象。首先,按创建的相反顺序执行特定于类的所有清理工作。(一般来说,这要求基类元素仍然是可访问的。) 然后调用基类清理方法,如这所示。
在很多情况下,清理问题不是问题;你只需要让垃圾收集器来完成这项工作。但是,当你必须执行显式清理时,就需要勤奋和注意,因为在垃圾收集方面没有什么可以依赖的。可能永远不会调用垃圾收集器。如果,它可以按照它想要的任何顺序回收对象。除了内存回收外,你不能依赖垃圾收集来做任何事情。如果希望进行清理,可以使用自己的清理方法,不要使用 **finalize()**
在很多情况下,清理问题不是问题;你只需要让垃圾收集器来完成这项工作。但是,当你必须执行显式清理时,就需要多做努力,更加细心,因为在垃圾收集方面没有什么可以依赖的。可能永远不会调用垃圾收集器。如果调用,它可以按照它想要的任何顺序回收对象。除了内存回收外,你不能依赖垃圾收集来做任何事情。如果希望进行清理,可以使用自己的清理方法,不要使用 `finalize()`
<!-- Name Hiding -->
@@ -692,7 +690,7 @@ doh(Milhouse)
````
**Bart** 的所有重载方法在 **Bart** 中都是可用的,尽管 **Bart** 引入了一种新的重载方法。在下一章中你将看到,使用与基类中完全相同的签名和返回类型覆盖相同名称的方法要常见得多。否则就会令人困惑。
**Homer** 的所有重载方法在 **Bart** 中都是可用的,尽管 **Bart** 引入了一种新的重载方法。在下一章中你将看到,使用与基类中完全相同的签名和返回类型覆盖相同名称的方法要常见得多。否则就会令人困惑。
你已经看到了Java 5 **@Override **注释,它不是关键字,但是可以像使用关键字一样使用它。当你打算重写一个方法时,你可以选择添加这个注释,如果你不小心用了重载而不是重写,编译器会产生一个错误消息:
@@ -717,27 +715,82 @@ class Lisa extends Homer {
## 组合与继承的选择
组合和继承都允许在新类中放置子对象(组合是显式的,而继承是隐式的)。你或许想知道这二者之间的区别,以及怎样在二者间做选择。
当你想在新类中包含一个已有类的功能时,使用组合,而非继承。也就是说,在新类中嵌入一个对象(通常是私有的),以实现其功能。新类的使用者看到的是你所定义的新类的接口,而非嵌入对象的接口。
有时让类的用户直接访问到新类中的组合成分是有意义的。只需将成员对象声明为 **public** 即可(可以把这当作“半委托”的一种)。成员对象隐藏了具体实现,所以这是安全的。当用户知道你正在组装一组部件时,会使得接口更加容易理解。下面的 car 对象是个很好的例子:
```java
// reuse/Car.java
// Composition with public objects
class Engine {
public void start() {}
public void rev() {}
public void stop() {}
}
class Wheel {
public void inflate(int psi) {}
}
class Window {
public void rollup() {}
pubilc void rolldown() {}
}
class Door {
public Window window = new Window();
public void open() {}
public void close() {}
}
public class Car {
public Engine engine = new Engine();
public Wheel[] wheel = new Wheel[4];
public Door left = new Door(), right = new Door(); // 2-door
public Car() {
for (int i = 0; i < 4; i++) {
wheel[i] = new Wheel();
}
}
public static void main(String[] args) {
Car car = new Car();
car.left.window.rollup();
car.wheel[0].inflate(72);
}
}
```
因为在这个例子中 car 的组合也是问题分析的一部分(不是底层设计的部分),所以声明成员为 **public** 有助于客户端程序员理解如何使用类,且降低了类创建者面临的代码复杂度。但是,记住这是一个特例。通常来说,属性还是应该声明为 **private**
当使用继承时,使用一个现有类并开发出它的新版本。通常这意味着使用一个通用类,并为了某个特殊需求将其特殊化。稍微思考下,你就会发现,用一个交通工具对象来组成一部车是毫无意义的——车不包含交通工具,它就是交通工具。这种“是一个”的关系是用继承来表达的,而“有一个“的关系则用组合来表达。
<!-- protected -->
## protected
<!-- Upcasting -->
## 向上转型
<!-- The final Keyword -->
## final关键字
<!-- Initialization and Class Loading -->
## 类初始化和加载
<!-- Summary -->
## 本章小结
<!-- 分页 -->
<div style="page-break-after: always;"></div>