From 2633b672b0f2839e0d774bda6f2bab73f9aa4557 Mon Sep 17 00:00:00 2001
From: dingpeilong <77676182@qq.com>
Date: Mon, 13 Jan 2020 18:50:33 +0800
Subject: [PATCH 001/224] =?UTF-8?q?=E4=BF=AE=E6=94=B9=E7=AC=AC=E5=8D=81?=
=?UTF-8?q?=E4=B9=9D=E7=AB=A0=E7=9A=84=E4=B8=80=E4=BA=9B=E6=8B=BC=E5=86=99?=
=?UTF-8?q?=E9=94=99=E8=AF=AF?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
docs/book/19-Type-Information.md | 23 +++++++++++------------
1 file changed, 11 insertions(+), 12 deletions(-)
diff --git a/docs/book/19-Type-Information.md b/docs/book/19-Type-Information.md
index 75dfa68e..dd6514ce 100644
--- a/docs/book/19-Type-Information.md
+++ b/docs/book/19-Type-Information.md
@@ -1361,12 +1361,12 @@ import java.util.regex.*;
public class ShowMethods {
private static String usage =
- "usage:n" +
- "ShowMethods qualified.class.namen" +
- "To show all methods in class or:n" +
- "ShowMethods qualified.class.name wordn" +
- "To search for methods involving 'word'";
- private static Pattern p = Pattern.compile("\w+\.");
+ "usage:\n" +
+ "ShowMethods qualified.class.name\n" +
+ "To show all methods in class or:\n" +
+ "ShowMethods qualified.class.name word\n" +
+ "To search for methods involving 'word'";
+ private static Pattern p = Pattern.compile("\\w+\\.");
public static void main(String[] args) {
if (args.length < 1) {
@@ -1648,7 +1648,7 @@ class SelectingMethods {
SomeMethods proxy =
(SomeMethods) Proxy.newProxyInstance(
SomeMethods.class.getClassLoader(),
- new Class[]{Interface.class},
+ new Class[]{ SomeMethods.class },
new MethodSelector(new Implementation()));
proxy.boring1();
proxy.boring2();
@@ -2078,7 +2078,7 @@ public class NullRobot {
newNullRobot(Class extends Robot> type) {
return (Robot) Proxy.newProxyInstance(
NullRobot.class.getClassLoader(),
- new Class,
+ new Class[] { Null.class, Robot.class },
new NullRobotProxyHandler(type));
}
@@ -2375,8 +2375,7 @@ class AnonymousA {
private void w() {
System.out.println("private C.w()");
}
- }
- ;
+ };
}
}
@@ -2432,8 +2431,8 @@ public class ModifyingPrivateFields {
Field f = pf.getClass().getDeclaredField("i");
f.setAccessible(true);
System.out.println(
- "f.getInt(pf): " + f.getint(pf));
- f.setint(pf, 47);
+ "f.getInt(pf): " + f.getInt(pf));
+ f.setInt(pf, 47);
System.out.println(pf);
f = pf.getClass().getDeclaredField("s");
f.setAccessible(true);
From 6362f0a6bc84c96a037125bbb4e6de8c7186a22b Mon Sep 17 00:00:00 2001
From: Joe <736777445@qq.com>
Date: Tue, 14 Jan 2020 21:51:54 +0800
Subject: [PATCH 002/224] =?UTF-8?q?=E8=A1=A5=E5=9B=9E=E8=AF=AF=E5=88=A0?=
=?UTF-8?q?=E9=99=A4=E7=9A=84=E5=86=85=E5=AE=B9?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
docs/book/08-Reuse.md | 486 +++++++++++++++++++++++++++++++++++++++++-
1 file changed, 485 insertions(+), 1 deletion(-)
diff --git a/docs/book/08-Reuse.md b/docs/book/08-Reuse.md
index 5c81f346..baad9ac2 100644
--- a/docs/book/08-Reuse.md
+++ b/docs/book/08-Reuse.md
@@ -777,4 +777,488 @@ public class Car {
关键字 **protected** 就起这个作用。它表示“就类的用户而言,这是 **private** 的。但对于任何继承它的子类或在同一包中的类,它是可访问的。”(**protected** 也提供了包访问权限)
-尽管可以创建 **protected** 属性,但是最好的方式是将属性声明为 **private** 以一直保留更改底层实现的权利。
+尽管可以创建 **protected** 属性,但是最好的方式是将属性声明为 **private** 以一直保留更改底层实现的权利。然后通过 **protected** 控制类的继承者的访问权限。
+
+```java
+// reuse/Orc.java
+// The protected keyword
+class Villain {
+ private String name;
+
+ protected void set(String nm) {
+ name = nm;
+ }
+
+ Villain(String name) {
+ this.name = name;
+ }
+
+ @Override
+ public String toString() {
+ return "I'm a Villain and my name is " + name;
+ }
+}
+
+public class Orc extends Villain {
+ private int orcNumber;
+
+ public Orc(String name, int orcNumber) {
+ super(name);
+ this.orcNumber = orcNumber;
+ }
+
+ public void change(String name, int orcNumber) {
+ set(name); // Available because it's protected
+ this.orcNumber = orcNumber;
+ }
+
+ @Override
+ public String toString() {
+ return "Orc " + orcNumber + ": " + super.toString();
+ }
+
+ public static void main(String[] args) {
+ Orc orc = new Orc("Limburger", 12);
+ System.out.println(orc);
+ orc.change("Bob", 19);
+ System.out.println(orc);
+ }
+}
+```
+
+输出:
+
+```
+Orc 12: I'm a Villain and my name is Limburger
+Orc 19: I'm a Villain and my name is Bob
+```
+
+`change()` 方法可以访问 `set()` 方法,因为 `set()` 方法是 **protected**。注意到,类 **Orc** 的 `toString()` 方法也使用了基类的版本。
+
+
+
+## 向上转型
+
+继承最重要的方面不是为新类提供方法。它是新类与基类的一种关系。简而言之,这种关系可以表述为“新类是已有类的一种类型”。
+
+这种描述并非是解释继承的一种花哨方式,这是直接由语言支持的。例如,假设有一个基类 **Instrument** 代表音乐乐器和一个派生类 **Wind**。 因为继承保证了基类的所有方法在派生类中也是可用的,所以任意发送给该基类的消息也能发送给派生类。如果 **Instrument** 有一个 `play()` 方法,那么 **Wind** 也有该方法。这意味着你可以准确地说 **Wind** 对象也是一种类型的 **Instrument**。下面例子展示了编译器是如何支持这一概念的:
+
+```java
+// reuse/Wind.java
+// Inheritance & upcasting
+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** 引用。鉴于 Java 对类型检查十分严格,一个接收一种类型的方法接受了另一种类型看起来很奇怪,除非你意识到 **Wind** 对象同时也是一个 **Instrument** 对象,而且 **Instrument** 的 `tune` 方法一定会存在于 **Wind** 中。在 `tune()` 中,代码对 **Instrument** 和 所有 **Instrument** 的派生类起作用,这种把 **Wind** 引用转换为 **Instrument** 引用的行为称作*向上转型*。
+
+该术语是基于传统的类继承图:图最上面是根,然后向下铺展。(当然你可以以任意方式画你认为有帮助的类图。)于是,**Wind.java** 的类图是:
+
+
+
+继承图中派生类转型为基类是向上的,所以通常称作*向上转型*。因为是从一个更具体的类转化为一个更一般的类,所以向上转型永远是安全的。也就是说,派生类是基类的一个超集。它可能比基类包含更多的方法,但它必须至少具有与基类一样的方法。在向上转型期间,类接口只可能失去方法,不会增加方法。这就是为什么编译器在没有任何明确转型或其他特殊标记的情况下,仍然允许向上转型的原因。
+
+也可以执行与向上转型相反的向下转型,但是会有问题,对于该问题会放在下一章和“类型信息”一章进行更深入的探讨。
+
+### 再论组合和继承
+
+在面向对象编程中,创建和使用代码最有可能的方法是将数据和方法一起打包到类中,然后使用该类的对象。也可以使用已有的类通过组合来创建新类。继承其实不太常用。因此尽管在教授 OOP 的过程中我们多次强调继承,但这并不意味着要尽可能使用它。恰恰相反,尽量少使用它,除非确实使用继承是有帮助的。一种判断使用组合还是继承的最清晰的方法是问一问自己是否需要把新类向上转型为基类。如果必须向上转型,那么继承就是必要的,但如果不需要,则要进一步考虑是否该采用继承。“多态”一章提出了一个使用向上转型的最有力的理由,但是只要记住问一问“我需要向上转型吗?”,就能在这两者中作出较好的选择。
+
+
+
+## final关键字
+
+根据上下文环境,Java 的关键字 **final** 的含义有些微的不同,但通常它指的是“这是不能被改变的”。防止改变有两个原因:设计或效率。因为这两个原因相差很远,所以有可能误用关键字 **final**。
+
+以下几节讨论了可能使用 **final** 的三个地方:数据、方法和类。
+
+### final 数据
+
+许多编程语言都有某种方法告诉编译器有一块数据是恒定不变的。恒定是有用的,如:
+
+1. 一个永不改变的编译时常量。
+2. 一个在运行时初始化就不会改变的值。
+
+对于编译时常量这种情况,编译器可以把常量带入计算中;也就是说,可以在编译时计算,减少了一些运行时的负担。在 Java 中,这类常量必须是基本类型,而且用关键字 **final** 修饰。你必须在定义常量的时候进行赋值。
+
+一个被 **static** 和 **final** 同时修饰的属性只会占用一段不能改变的存储空间。
+
+当用 **final** 修饰对象引用而非基本类型时,其含义会有一点令人困惑。对于基本类型,**final** 使数值恒定不变,而对于对象引用,**final** 使引用恒定不变。一旦引用被初始化指向了某个对象,它就不能改为指向其他对象。但是,对象本身是可以修改的,Java 没有提供将任意对象设为常量的方法。(你可以自己编写类达到使对象恒定不变的效果)这一限制同样适用数组,数组也是对象。
+
+下面例子展示了 **final** 属性的使用:
+
+```java
+// reuse/FinalData.java
+// The effect of final on fields
+import java.util.*;
+
+class Value {
+ int i; // package access
+
+ Value(int i) {
+ this.i = i;
+ }
+}
+
+public class FinalData {
+ private static Random rand = new Random(47);
+ private String id;
+
+ public FinalData(String id) {
+ this.id = id;
+ }
+ // Can be compile-time constants:
+ private final int valueOne = 9;
+ private static final int VALUE_TWO = 99;
+ // Typical public constant:
+ public static final int VALUE_THREE = 39;
+ // Cannot be compile-time constants:
+ private final int i4 = rand.nextInt(20);
+ static final int INT_5 = rand.nextInt(20);
+ private Value v1 = new Value(11);
+ private final Value v2 = new Value(22);
+ private static final Value VAL_3 = new Value(33);
+ // Arrays:
+ private final int[] a = {1, 2, 3, 4, 5, 6};
+
+ @Override
+ public String toString() {
+ return id + ": " + "i4 = " + i4 + ", INT_5 = " + INT_5;
+ }
+
+ public static void main(String[] args) {
+ FinalData fd1 = new FinalData("fd1");
+ //- fd1.valueOne++; // Error: can't change value
+ fd1.v2.i++; // Object isn't constant
+ fd1.v1 = new Value(9); // OK -- not final
+ for (int i = 0; i < fd1.a.length; i++) {
+ fd1.a[i]++; // Object isn't constant
+ }
+ //- fd1.v2 = new Value(0); // Error: Can't
+ //- fd1.VAL_3 = new Value(1); // change reference
+ //- fd1.a = new int[3];
+ System.out.println(fd1);
+ System.out.println("Creating new FinalData");
+ FinalData fd2 = new FinalData("fd2");
+ System.out.println(fd1);
+ System.out.println(fd2);
+ }
+}
+```
+
+输出:
+
+```
+fd1: i4 = 15, INT_5 = 18
+Creating new FinalData
+fd1: i4 = 15, INT_5 = 18
+fd2: i4 = 13, INT_5 = 18
+```
+
+因为 **valueOne** 和 **VALUE_TWO** 都是带有编译时值的 **final** 基本类型,它们都可用作编译时常量,没有多大区别。**VALUE_THREE** 是一种更加典型的常量定义的方式:**public** 意味着可以在包外访问,**static** 强调只有一个,**final** 说明是一个常量。
+
+按照惯例,带有恒定初始值的 **final** **static** 基本变量(即编译时常量)命名全部使用大写,单词之间用下划线分隔。(源于 C 语言中定义常量的方式。)
+
+我们不能因为某数据被 **final** 修饰就认为在编译时可以知道它的值。由上例中的 **i4** 和 **INT_5** 可以看出,它们在运行时才会赋值随机数。示例部分也展示了将 **final** 值定义为 **static** 和非 **static** 的区别。此区别只有当值在运行时被初始化时才会显现,因为编译器对编译时数值一视同仁。(而且编译时数值可能因优化而消失。)当运行程序时就能看到这个区别。注意到 **fd1** 和 **fd2** 的 **i4** 值不同,但 **INT_5** 的值并没有因为创建了第二个 **FinalData** 对象而改变,这是因为它是 **static** 的,在加载时已经被初始化,并不是每次创建新对象时都初始化。
+
+**v1** 到 **VAL_3** 变量说明了 **final** 引用的意义。正如你在 `main()` 中所见,**v2** 是 **final** 的并不意味着你不能修改它的值。因为它是引用,所以只是说明它不能指向一个新的对象。这对于数组具有同样的意义,数组只不过是另一种引用。(我不知道有什么方法能使数组引用本身成为 **final**。)看起来,声明引用为 **final** 没有声明基本类型 **final** 有用。
+
+### 空白 final
+
+空白 final 指的是没有初始化值的 **final** 属性。编译器确保空白 final 在使用前必须被初始化。这样既能使一个类的每个对象的 **final** 属性值不同,也能保持它的不变性。
+
+```java
+// reuse/BlankFinal.java
+// "Blank" final fields
+class Poppet {
+ private int i;
+
+ Poppet(int ii) {
+ i = ii;
+ }
+}
+
+public class BlankFinal {
+ private final int i = 0; // Initialized final
+ private final int j; // Blank final
+ private final Poppet p; // Blank final reference
+ // Blank finals MUST be initialized in constructor
+ public BlankFinal() {
+ j = 1; // Initialize blank final
+ p = new Poppet(1); // Init blank final reference
+ }
+
+ public BlankFinal(int x) {
+ j = x; // Initialize blank final
+ p = new Poppet(x); // Init blank final reference
+ }
+
+ public static void main(String[] args) {
+ new BlankFinal();
+ new BlankFinal(47);
+ }
+}
+```
+
+你必须在定义时或在每个构造器中执行 final 变量的赋值操作。这保证了 final 属性在使用前已经被初始化过。
+
+### final 参数
+
+在参数列表中,将参数声明为 final 意味着在方法中不能改变参数指向的对象或基本变量:
+
+```java
+// reuse/FinalArguments.java
+// Using "final" with method arguments
+class Gizmo {
+ public void spin() {
+
+ }
+}
+
+public class FinalArguments {
+ void with(final Gizmo g) {
+ //-g = new Gizmo(); // Illegal -- g is final
+ }
+
+ void without(Gizmo g) {
+ g = new Gizmo(); // OK -- g is not final
+ g.spin();
+ }
+
+ //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;
+ }
+
+ public static void main(String[] args) {
+ FinalArguments bf = new FinalArguments();
+ bf.without(null);
+ bf.with(null);
+ }
+}
+```
+
+方法 `f()` 和 `g()` 展示了 **final** 基本类型参数的使用情况。你只能读取而不能修改参数。这个特性主要用于传递数据给匿名内部类。这将在”内部类“章节中详解。
+
+### final 方法
+
+使用 **final** 方法的原因有两个。第一个原因是给方法上锁,防止子类通过覆写改变方法的行为。这是出于继承的考虑,确保方法的行为不会因继承而改变。
+
+过去建议使用 **final** 方法的第二个原因是效率。在早期的 Java 实现中,如果将一个方法指明为 **final**,就是同意编译器把对该方法的调用转化为内嵌调用。当编译器遇到 **final** 方法的调用时,就会很小心地跳过普通的插入代码以执行方法的调用机制(将参数压栈,跳至方法代码处执行,然后跳回并清理栈中的参数,最终处理返回值),而用方法体内实际代码的副本替代方法调用。这消除了方法调用的开销。但是如果一个方法很大代码膨胀,你也许就看不到内嵌带来的性能提升,因为内嵌调用带来的性能提高被花费在方法里的时间抵消了。
+
+在最近的 Java 版本中,虚拟机可以探测到这些情况(尤其是 *hotspot* 技术),并优化去掉这些效率反而降低的内嵌调用方法。有很长一段时间,使用 **final** 来提高效率都被阻止。你应该让编译器和 JVM 处理性能问题,只有在为了明确禁止覆写方法时才使用 **final**。
+
+### final 和 private
+
+类中所有的 **private** 方法都隐式地指定为 **final**。因为不能访问 **private** 方法,所以不能覆写它。可以给 **private** 方法添加 **final** 修饰,但是并不能给方法带来额外的含义。
+
+以下情况会令人困惑,当你试图覆写一个 **private** 方法(隐式是 **final** 的)时,看上去奏效,而且编译器不会给出错误信息:
+
+```java
+// reuse/FinalOverridingIllusion.java
+// It only looks like you can override
+// a private or private final method
+class WithFinals {
+ // Identical to "private" alone:
+ private final void f() {
+ System.out.println("WithFinals.f()");
+ }
+ // Also automatically "final":
+ private void g() {
+ System.out.println("WithFinals.g()");
+ }
+}
+
+class OverridingPrivate extends WithFinals {
+ private final void f() {
+ System.out.println("OverridingPrivate.f()");
+ }
+
+ private void g() {
+ System.out.println("OverridingPrivate.g()");
+ }
+}
+
+class OverridingPrivate2 extends OverridingPrivate {
+ public final void f() {
+ System.out.println("OverridingPrivate2.f()");
+ }
+
+ public void g() {
+ System.out.println("OverridingPrivate2.g()");
+ }
+}
+
+public class FinalOverridingIllusion {
+ public static void main(String[] args) {
+ OverridingPrivate2 op2 = new OverridingPrivate2();
+ op2.f();
+ op2.g();
+ // You can upcast:
+ OverridingPrivate op = op2;
+ // But you can't call the methods:
+ //- op.f();
+ //- op.g();
+ // Same here:
+ WithFinals wf = op2;
+ //- wf.f();
+ //- wf.g();
+ }
+}
+```
+
+输出:
+
+```
+OverridingPrivate2.f()
+OverridingPrivate2.g()
+```
+
+"覆写"只发生在方法是基类的接口时。也就是说,必须能将一个对象向上转型为基类并调用相同的方法(这一点在下一章阐明)。如果一个方法是 **private** 的,它就不是基类接口的一部分。它只是隐藏在类内部的代码,且恰好有相同的命名而已。但是如果你在派生类中以相同的命名创建了 **public**,**protected** 或包访问权限的方法,这些方法与基类中的方法没有联系,你没有覆写方法,只是在创建新的方法而已。由于 **private** 方法无法触及且能有效隐藏,除了把它看作类中的一部分,其他任何事物都不需要考虑到它。
+
+### final 类
+
+当说一个类是 **final** (**final** 关键字在类定义之前),就意味着它不能被继承。之所以这么做,是因为类的设计就是永远不需要改动,或者是出于安全考虑不希望它有子类。
+
+```java
+// reuse/Jurassic.java
+// Making an entire class final
+class SmallBrain {}
+
+final class Dinosaur {
+ int i = 7;
+ int j = 1;
+ SmallBrain x = new SmallBrain();
+
+ void f() {}
+}
+
+//- class Further extends Dinosaur {}
+// error: Cannot extend final class 'Dinosaur'
+public class Jurassic {
+ public static void main(String[] args) {
+ Dinosaur n = new Dinosaur();
+ n.f();
+ n.i = 40;
+ n.j++;
+ }
+}
+```
+
+**final** 类的属性可以根据个人选择是或不是 **final**。这同样适用于不管类是否是 **final** 的内部 **final** 属性。然而,由于 **final** 类禁止继承,类中所有的方法都被隐式地指定为 **final**,所以没有办法覆写它们。你可以在 final 类中的方法加上 **final** 修饰符,但不会增加任何意义。
+
+### final 忠告
+
+在设计类时将一个方法指明为 final 看上去是明智的。你可能会觉得没人会覆写那个方法。有时这是对的。
+
+但请留意你的假设。通常来说,预见一个类如何被复用是很困难的,特别是通用类。如果将一个方法指定为 **final**,可能会防止其他程序员的项目中通过继承来复用你的类,而这仅仅是因为你没有想到它被以那种方式使用。
+
+Java 标准类库就是一个很好的例子。尤其是 Java 1.0/1.1 的 **Vector** 类被广泛地使用,而且从效率考虑(这近乎是个幻想),如果它的所有方法没有被指定为 **final**,可能会更加有用。很容易想到,你可能会继承并覆写这么一个基础类,但是设计者们认为这么做不合适。有两个讽刺的原因。第一,**Stack** 继承自 **Vector**,就是说 **Stack** 是个 **Vector**,但从逻辑上来说不对。尽管如此,Java 设计者们仍然这么做,在用这种方式创建 **Stack** 时,他们应该意识到了 **final** 方法过于约束。
+
+第二,**Vector** 中的很多重要方法,比如 `addElement()` 和 `elementAt()` 方法都是同步的。在“并发编程”一章中会看同步会导致很大的执行开销,可能会抹煞 **final** 带来的好处。这加强了程序员永远无法正确猜到优化应该发生在何处的观点。如此笨拙的设计却出现在每个人都要使用的标准库中,太糟糕了。庆幸的是,现代 Java 容器用 **ArrayList** 代替了 **Vector**,它的行为要合理得多。不幸的是,仍然有很多新代码使用旧的集合类库,其中就包括 **Vector**。
+
+Java 1.0/1.1 标准类库中另一个重要的类是 **Hashtable**(后来被 **HashMap** 取代),它不含任何 **final** 方法。本书中其他地方也提到,很明显不同的类是由不同的人设计的。**Hashtable** 就比 **Vector** 中的方法名简洁得多,这又是一条证据。对于类库的使用者来说,这是一个本不应该如此草率的事情。这种不规则的情况造成用户需要做更多的工作——这是对粗糙的设计和代码的又一讽刺。
+
+
+
+## 类初始化和加载
+
+在许多传统语言中,程序在启动时一次性全部加载。接着初始化,然后程序开始运行。必须仔细控制这些语言的初始化过程,以确保 **statics** 初始化的顺序不会造成麻烦。在 C++ 中,如果一个 **static** 期望使用另一个 **static**,而另一个 **static** 还没有初始化,就会出现问题。
+
+Java 中不存在这样的问题,因为它采用了一种不同的方式加载。因为 Java 中万物皆对象,所以加载活动就容易得多。记住每个类的编译代码都存在于它自己独立的文件中。该文件只有在使用程序代码时才会被加载。一般可以说“类的代码在首次使用时加载“。这通常是指创建类的第一个对象,或者是访问了类的 **static** 属性或方法。构造器也是一个 **static** 方法尽管它的 **static** 关键字是隐式的。因此,准确地说,一个类当它任意一个 **static** 成员被访问时,就会被加载。
+
+首次使用时就是 **static** 初始化发生时。所有的 **static** 对象和 **static** 代码块在加载时按照文本的顺序(在类中定义的顺序)依次初始化。**static** 变量只被初始化一次。
+
+### 继承和初始化
+
+了解包括继承在内的整个初始化过程是有帮助的,这样可以对所发生的一切有全局性的把握。考虑下面的例子:
+
+```java
+// reuse/Beetle.java
+// The full process of initialization
+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 int printInit(String s) {
+ System.out.println(s);
+ return 47;
+ }
+}
+
+public class Beetle extends Insect {
+ private int k = printInit("Beetle.k.initialized");
+
+ public Beetle() {
+ System.out.println("k = " + k);
+ System.out.println("j = " + j);
+ }
+
+ private static int x2 = printInit("static Beetle.x2 initialized");
+
+ public static void main(String[] args) {
+ System.out.println("Beetle constructor");
+ Beetle b = new Beetle();
+ }
+}
+```
+
+输出:
+
+```
+static Insect.x1 initialized
+static Beetle.x2 initialized
+Beetle constructor
+i = 9, j = 0
+Beetle.k initialized
+k = 47
+j = 39
+```
+
+当执行 **java Beetle**,首先会试图访问 **Beetle** 类的 `main()` 方法(一个静态方法),加载器启动并找出 **Beetle** 类的编译代码(在名为 **Beetle.class** 的文件中)。在加载过程中,编译器注意到有一个基类,于是继续加载基类。不论是否创建了基类的对象,基类都会被加载。(可以尝试把创建基类对象的代码注释掉证明这点。)
+
+如果基类还存在自身的基类,那么第二个基类也将被加载,以此类推。接下来,根基类(例子中根基类是 **Insect**)的 **static** 的初始化开始执行,接着是派生类,以此类推。这点很重要,因为派生类中 **static** 的初始化可能依赖基类成员是否被正确地初始化。
+
+至此,必要的类都加载完毕,可以创建对象了。首先,对象中的所有基本类型变量都被置为默认值,对象引用被设为 **null** —— 这是通过将对象内存设为二进制零值一举生成的。接着会调用基类的构造器。本例中是自动调用的,但是你也可以使用 **super** 调用指定的基类构造器(在 **Beetle** 构造器中的第一步操作)。基类构造器和派生类构造器一样以相同的顺序经历相同的过程。当基类构造器完成后,实例变量按文本顺序初始化。最终,构造器的剩余部分被执行。
+
+
+
+## 本章小结
+
+继承和组合都是从已有类型创建新类型。组合将已有类型作为新类型底层实现的一部分,继承复用的是接口。
+
+使用继承时,派生类具有基类接口,因此可以向上转型为基类,这对于多态至关重要,在下一章你将看到。
+
+尽管在面向对象编程时极力强调继承,但在开始设计时,优先使用组合(或委托),只有当确实需要时再使用继承。组合更具灵活性。另外,通过对成员类型使用继承的技巧,可以在运行时改变成员的类型和行为。因此,可以在运行时改变组合对象的行为。
+
+在设计一个系统时,目标是发现或创建一系列类,每个类有特定的用途,而且既不应太大(包括太多功能难以复用),也不应太小(不添加其他功能就无法使用)。如果设计变得过于复杂,通过将现有类拆分为更小的部分而添加更多的对象,通常是有帮助的。
+
+当开始设计一个系统时,记住程序开发是一个增量过程,正如人类学习。它依赖实验,你可以尽可能多做分析,然而在项目开始时仍然无法知道所有的答案。如果把项目视作一个有机的,进化着的生命去培养,而不是视为像摩天大楼一样快速见效,就能获得更多的成功和更迅速的反馈。继承和组合正是可以让你执行如此实验的面向对象编程中最基本的两个工具。
+
+
+
+
+
From da805f405455be426db209450007ffb9a06c9d5e Mon Sep 17 00:00:00 2001
From: Joe <736777445@qq.com>
Date: Tue, 14 Jan 2020 21:52:14 +0800
Subject: [PATCH 003/224] =?UTF-8?q?=E8=A1=A5=E5=9B=9E=E8=AF=AF=E5=88=A0?=
=?UTF-8?q?=E9=99=A4=E7=9A=84=E5=86=85=E5=AE=B9?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
From 15c089fc5adb59d17598c1dfa604a052d90ea689 Mon Sep 17 00:00:00 2001
From: Pic
Date: Fri, 17 Jan 2020 10:34:51 +0800
Subject: [PATCH 004/224] Update 23-Annotations.md (#353)
Modify word errors
---
docs/book/23-Annotations.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/docs/book/23-Annotations.md b/docs/book/23-Annotations.md
index 9d0454ae..2e38bd9e 100644
--- a/docs/book/23-Annotations.md
+++ b/docs/book/23-Annotations.md
@@ -306,7 +306,7 @@ public class Member {
}
```
-类注解 **@DBTable** 注解给定了元素值 MEMBER,它将会作为标的名字。类的属性 **firstName** 和 **firstName** 都被注解为 **@SQLString** 类型并且给了默认元素值分别为 30 和 50。这些注解都有两个有趣的地方:首先,他们都使用了嵌入的 **@Constraints** 注解的默认值;其次,它们都是用了快捷方式特性。如果你在注解中定义了名为 **value** 的元素,并且在使用该注解时,**value** 为唯一一个需要赋值的元素,你就不需要使用名—值对的语法,你只需要在括号中给出 **value** 元素的值即可。这可以应用于任何合法类型的元素。这也限制了你必须将元素命名为 **value**,不过在上面的例子中,这样的注解语句也更易于理解:
+类注解 **@DBTable** 注解给定了元素值 MEMBER,它将会作为标的名字。类的属性 **firstName** 和 **lastName** 都被注解为 **@SQLString** 类型并且给了默认元素值分别为 30 和 50。这些注解都有两个有趣的地方:首先,他们都使用了嵌入的 **@Constraints** 注解的默认值;其次,它们都是用了快捷方式特性。如果你在注解中定义了名为 **value** 的元素,并且在使用该注解时,**value** 为唯一一个需要赋值的元素,你就不需要使用名—值对的语法,你只需要在括号中给出 **value** 元素的值即可。这可以应用于任何合法类型的元素。这也限制了你必须将元素命名为 **value**,不过在上面的例子中,这样的注解语句也更易于理解:
```java
@SQLString(30)
From 9fd1aa7079c32426870a6747c4a4ad5aa60cd9bd Mon Sep 17 00:00:00 2001
From: LeonTheProfessional
Date: Fri, 17 Jan 2020 14:31:49 +0800
Subject: [PATCH 005/224] =?UTF-8?q?=E6=9B=B4=E6=94=B9=E4=BA=86=E7=A7=BB?=
=?UTF-8?q?=E4=BD=8D=E8=BF=90=E7=AE=97=E7=AC=A6=E4=B8=AD=E8=BE=83=E6=9C=89?=
=?UTF-8?q?=E8=AF=AF=E5=AF=BC=E6=80=A7=E7=9A=84=E6=8F=8F=E8=BF=B0=20(#354)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
* 更改了移位运算符中较有误导性的描述
“仅使用右侧的5 个低阶位” 中的“右侧”之前主语不明确,容易引起歧义。改为“仅使用右值的5 个低阶位” 则明确表示为等号右侧值的5个低阶位,使语义更清晰。
* 翻译勘误
”数字的二进制表示称为有符号的两个补数。“这句的原文为“The binary representation of the numbers is referred to as signed twos complement”。这里的“2's complement” 为计算机术语“补码”,所以这句话应翻译为“数字的二进制表示形式是带符号的补码”。
---
docs/book/04-Operators.md | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/docs/book/04-Operators.md b/docs/book/04-Operators.md
index 1cfeb312..d9f83652 100644
--- a/docs/book/04-Operators.md
+++ b/docs/book/04-Operators.md
@@ -654,7 +654,7 @@ float f4 = 1e-43f; //10 的幂数
移位运算符面向的运算对象也是二进制的“位”。它们只能用于处理整数类型(基本类型的一种)。左移位运算符 `<<` 能将其左边的运算对象向左移动右侧指定的位数(在低位补 0)。右移位运算符 `>>` 则相反。右移位运算符有“正”、“负”值:若值为正,则在高位插入 0;若值为负,则在高位插入 1。Java 也添加了一种“不分正负”的右移位运算符(>>>),它使用了“零扩展”(zero extension):无论正负,都在高位插入 0。这一运算符是 C/C++ 没有的。
-如果移动 **char**、**byte** 或 **short**,则会在移动发生之前将其提升为 **int**,结果为 **int**。仅使用右侧的 5 个低阶位。这可以防止我们移动超过 **int** 范围的位数。若对一个 **long** 值进行处理,最后得到的结果也是 **long**。
+如果移动 **char**、**byte** 或 **short**,则会在移动发生之前将其提升为 **int**,结果为 **int**。仅使用右值(rvalue)的 5 个低阶位。这可以防止我们移动超过 **int** 范围的位数。若对一个 **long** 值进行处理,最后得到的结果也是 **long**。
移位可以与等号 `<<=` 或 `>>=` 或 `>>>=` 组合使用。左值被替换为其移位运算后的值。但是,问题来了,当无符号右移与赋值相结合时,若将其与 **byte** 或 **short** 一起使用的话,则结果错误。取而代之的是,它们被提升为 **int** 型并右移,但在重新赋值时被截断。在这种情况下,结果为 -1。下面是代码示例:
@@ -805,7 +805,7 @@ i >>> 5, int: 97591828, binary:
...
```
-结尾的两个方法 `printBinaryInt()` 和 `printBinaryLong()` 分别操作一个 **int** 和 **long** 值,并转换为二进制格式输出,同时附有简要的文字说明。除了演示 **int** 和 **long** 的所有位运算符的效果之外,本示例还显示 **int** 和 **long** 的最小值、最大值、+1 和 -1 值,以便我们了解它们的形式。注意高位代表符号:0 表示正,1 表示负。上面显示了 **int** 部分的输出。数字的二进制表示称为有符号的两个补数。
+结尾的两个方法 `printBinaryInt()` 和 `printBinaryLong()` 分别操作一个 **int** 和 **long** 值,并转换为二进制格式输出,同时附有简要的文字说明。除了演示 **int** 和 **long** 的所有位运算符的效果之外,本示例还显示 **int** 和 **long** 的最小值、最大值、+1 和 -1 值,以便我们了解它们的形式。注意高位代表符号:0 表示正,1 表示负。上面显示了 **int** 部分的输出。以上数字的二进制表示形式是带符号的补码(2's complement)。
## 三元运算符
From d3fea25992534d1f44c37e7517efcae0cf189c7b Mon Sep 17 00:00:00 2001
From: crimson <1291463831@qq.com>
Date: Sat, 18 Jan 2020 07:31:39 +0800
Subject: [PATCH 006/224] =?UTF-8?q?Fix=20issue=20#112:=E7=BF=BB=E8=AF=91?=
=?UTF-8?q?=E5=B0=8F=E8=8A=82=EF=BC=9A=E6=9E=84=E9=80=A0=E5=99=A8=E9=9D=9E?=
=?UTF-8?q?=E7=BA=BF=E7=A8=8B=E5=AE=89=E5=85=A8?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
docs/book/24-Concurrent-Programming.md | 231 +++++++++++++++++++++++++
1 file changed, 231 insertions(+)
diff --git a/docs/book/24-Concurrent-Programming.md b/docs/book/24-Concurrent-Programming.md
index 8faec42b..9b2b4ca1 100644
--- a/docs/book/24-Concurrent-Programming.md
+++ b/docs/book/24-Concurrent-Programming.md
@@ -2226,7 +2226,238 @@ public class DiningPhilosophers {
## 构造函数非线程安全
+当你在脑子里想象一个对象构造的过程,你会很容易认为这个过程是线程安全的。毕竟,在对象初始化完成前没人能见到这个对象,所以又怎么会产生对于这个对象的争议呢?确实,java语言规范(JLS)自信满满地陈述道:“没有必要使构造器同步,因为它会锁定正在构造的对象,而这通常会使得该对象直到其所有构造器完成所有工作后,才对其他线程可见。”
+不幸的是,对象构造过程像其他任何事物一样容易受到共享内存并发问题的影响,只是作用机制可能更微妙罢了。
+考虑使用静态字段为每个对象自动创建唯一标识符的过程。为了测试其不同的实现过程,我们从一个接口开始:
+```java
+//concurrent/HasID.java
+public interface HasID {
+ int getID();
+}
+```
+
+然后StaticIDField类以显式方式实现该接口:
+
+```java
+// concurrent/StaticIDField.java
+public class StaticIDField implements HasID {
+ private static int counter = 0;
+ private int id = counter++;
+ public int getID() { return id; }
+}
+```
+
+正如您所想的,此类是一个简单无害的类,它甚至没有一个显式的构造器来引发问题。当我们运行多个用于创建此类对象的线程时,究竟会发生什么,为了搞清楚这点,我们做了以下测试:
+
+```java
+// concurrent/IDChecker.java
+import java.util.*;
+import java.util.function.*;
+import java.util.stream.*;
+import java.util.concurrent.*;
+import com.google.common.collect.Sets;
+public class IDChecker {
+ public static final int SIZE = 100_000;
+
+ static class MakeObjects implements
+ Supplier> {
+ private Supplier gen;
+
+ MakeObjects(Supplier gen) {
+ this.gen = gen;
+ }
+
+ @Override public List get() {
+ return Stream.generate(gen)
+ .limit(SIZE)
+ .map(HasID::getID)
+ .collect(Collectors.toList());
+ }
+ }
+
+ public static void test(Supplier gen) {
+ CompletableFuture>
+ groupA = CompletableFuture.supplyAsync(new
+ MakeObjects(gen)),
+ groupB = CompletableFuture.supplyAsync(new
+ MakeObjects(gen));
+
+ groupA.thenAcceptBoth(groupB, (a, b) -> {
+ System.out.println(
+ Sets.intersection(
+ Sets.newHashSet(a),
+ Sets.newHashSet(b)).size());
+ }).join();
+ }
+}
+```
+
+MakeObjects类是一个供应者类,包含一个能够产生List\类型的列表对象的get()方法。通过从每个HasID对象提取ID并放入列表中来生成这个列表对象,而test()方法则创建了两个并行的CompletableFuture对象,用于运行MakeObjects供应者类,然后获取运行结果。
+使用Guava库中的Sets.intersection()方法,计算出这两个返回的List\对象中有多少相同的ID(使用谷歌Guava库里的方法比使用官方的retainAll()方法速度快得多)。
+
+现在我们可以测试上面的StaticIDField类了:
+
+```java
+// concurrent/TestStaticIDField.java
+public class TestStaticIDField {
+
+ public static void main(String[] args) {
+ IDChecker.test(StaticIDField::new);
+ }
+}
+/* Output:
+ 13287
+*/
+```
+
+结果中出现了很多重复项。很显然,纯静态int用于构造过程并不是线程安全的。让我们使用AtomicInteger来使其变为线程安全的:
+
+```java
+// concurrent/GuardedIDField.java
+import java.util.concurrent.atomic.*;
+public class GuardedIDField implements HasID {
+ private static AtomicInteger counter = new
+ AtomicInteger();
+
+ private int id = counter.getAndIncrement();
+
+ public int getID() { return id; }
+
+ public static void main(String[] args) { IDChecker.test(GuardedIDField::new);
+ }
+}
+/* Output:
+ 0
+*/
+```
+
+构造器有一种更微妙的状态共享方式:通过构造器参数:
+
+```java
+// concurrent/SharedConstructorArgument.java
+import java.util.concurrent.atomic.*;
+interface SharedArg{
+ int get();
+}
+
+class Unsafe implements SharedArg{
+ private int i = 0;
+
+ public int get(){
+ return i++;
+ }
+}
+
+class Safe implements SharedArg{
+ private static AtomicInteger counter = new AtomicInteger();
+
+ public int get(){
+ return counter.getAndIncrement();
+ }
+}
+
+class SharedUser implements HasID{
+ private final int id;
+
+ SharedUser(SharedArg sa){
+ id = sa.get();
+ }
+
+ @Override
+ public int getID(){
+ return id;
+ }
+}
+
+public class SharedConstructorArgument{
+ public static void main(String[] args){
+ Unsafe unsafe = new Unsafe();
+ IDChecker.test(() -> new SharedUser(unsafe));
+
+ Safe safe = new Safe();
+ IDChecker.test(() -> new SharedUser(safe));
+ }
+}
+/* Output:
+ 24838
+ 0
+*/
+```
+
+在这里,SharedUser构造器实际上共享了相同的参数。即使SharedUser以完全无害且合理的方式使用其自己的参数,其构造器的调用方式也会引起冲突。SharedUser甚至不知道它是以这种方式调用的,更不必说控制它了。
+同步构造器并不被java语言所支持,但是通过使用同步语块来创建你自己的同步构造器是可能的(请参阅附录:Low-Level Concurrency,来进一步了解同步关键字——synchronized)。尽管JLS(java语言规范)这样陈述道:“……它会锁定正在构造的对象”,但这并不是真的——构造器实际上只是一个静态方法,因此同步构造器实际上会锁定该类的Class对象。我们可以通过创建自己的静态对象并锁定它,来达到与同步构造器相同的效果:
+
+```java
+// concurrent/SynchronizedConstructor.java
+
+import java.util.concurrent.atomic.*;
+
+class SyncConstructor implements HasID{
+ private final int id;
+ private static Object constructorLock =
+ new Object();
+
+ SyncConstructor(SharedArg sa){
+ synchronized (constructorLock){
+ id = sa.get();
+ }
+ }
+
+ @Override
+ public int getID(){
+ return id;
+ }
+}
+
+public class SynchronizedConstructor{
+ public static void main(String[] args){
+ Unsafe unsafe = new Unsafe();
+ IDChecker.test(() -> new SyncConstructor(unsafe));
+ }
+}
+/* Output:
+ 0
+*/
+
+```
+
+Unsafe类的共享使用现在就变得安全了。另一种方法是将构造器设为私有(因此可以防止继承),并提供一个静态Factory方法来生成新对象:
+
+```java
+// concurrent/SynchronizedFactory.java
+import java.util.concurrent.atomic.*;
+
+final class SyncFactory implements HasID{
+ private final int id;
+
+ private SyncFactory(SharedArg sa){
+ id = sa.get();
+ }
+
+ @Override
+ public int getID(){
+ return id;
+ }
+
+ public static synchronized SyncFactory factory(SharedArg sa){
+ return new SyncFactory(sa);
+ }
+}
+
+public class SynchronizedFactory{
+ public static void main(String[] args){
+ Unsafe unsafe = new Unsafe();
+ IDChecker.test(() -> SyncFactory.factory(unsafe));
+ }
+}
+/* Output:
+ 0
+*/
+```
+
+通过同步静态工厂方法,可以在构造过程中锁定Class对象。
+这些示例充分表明了在并发Java程序中检测和管理共享状态有多困难。即使您采取“不共享任何内容”的策略,也很容易产生意外的共享事件。
## 复杂性和代价
From fb9581dec28ef9065e3d20444e80944865724e11 Mon Sep 17 00:00:00 2001
From: LingCoder
Date: Sat, 18 Jan 2020 07:43:34 +0800
Subject: [PATCH 007/224] =?UTF-8?q?=E4=BF=AE=E6=94=B9=20Predicate=20?=
=?UTF-8?q?=E7=9A=84=E7=BF=BB=E8=AF=91=E4=B8=BA=20=E8=B0=93=E8=AF=8D?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
docs/book/13-Functional-Programming.md | 12 ++++++------
docs/book/14-Streams.md | 6 +++---
2 files changed, 9 insertions(+), 9 deletions(-)
diff --git a/docs/book/13-Functional-Programming.md b/docs/book/13-Functional-Programming.md
index 4ede66bf..3ddd4493 100644
--- a/docs/book/13-Functional-Programming.md
+++ b/docs/book/13-Functional-Programming.md
@@ -646,7 +646,7 @@ public class FunctionalAnnotation {
4. 如果返回值类型与参数类型一致,则是一个运算符:单个参数使用 `UnaryOperator`,两个参数使用 `BinaryOperator`。
-5. 如果接收两个参数且返回值为布尔值,则是一个断言(Predicate)。
+5. 如果接收两个参数且返回值为布尔值,则是一个谓词(Predicate)。
6. 如果接收的两个参数类型不同,则名称中有一个 `Bi`。
@@ -1306,9 +1306,9 @@ public class AnonymousClosure {
| :----- | :----- |
| `andThen(argument)`
根据参数执行原始操作 | **Function
BiFunction
Consumer
BiConsumer
IntConsumer
LongConsumer
DoubleConsumer
UnaryOperator
IntUnaryOperator
LongUnaryOperator
DoubleUnaryOperator
BinaryOperator** |
| `compose(argument)`
根据参数执行原始操作 | **Function
UnaryOperator
IntUnaryOperator
LongUnaryOperator
DoubleUnaryOperator** |
-| `and(argument)`
短路**逻辑与**原始断言和参数断言 | **Predicate
BiPredicate
IntPredicate
LongPredicate
DoublePredicate** |
-| `or(argument)`
短路**逻辑或**原始断言和参数断言 | **Predicate
BiPredicate
IntPredicate
LongPredicate
DoublePredicate** |
-| `negate()`
该断言的**逻辑否**断言| **Predicate
BiPredicate
IntPredicate
LongPredicate
DoublePredicate** |
+| `and(argument)`
短路**逻辑与**原始谓词和参数谓词 | **Predicate
BiPredicate
IntPredicate
LongPredicate
DoublePredicate** |
+| `or(argument)`
短路**逻辑或**原始谓词和参数谓词 | **Predicate
BiPredicate
IntPredicate
LongPredicate
DoublePredicate** |
+| `negate()`
该谓词的**逻辑否**谓词| **Predicate
BiPredicate
IntPredicate
LongPredicate
DoublePredicate** |
下例使用了 `Function` 里的 `compose()`和 `andThen()`。代码示例:
@@ -1374,9 +1374,9 @@ foobar
foobaz
```
-`p4` 获取到了所有断言并组合成一个更复杂的断言。解读:如果字符串中不包含 `bar` 且长度小于 5,或者它包含 `foo` ,则结果为 `true`。
+`p4` 获取到了所有谓词并组合成一个更复杂的谓词。解读:如果字符串中不包含 `bar` 且长度小于 5,或者它包含 `foo` ,则结果为 `true`。
-正因它产生如此清晰的语法,我在主方法中采用了一些小技巧,并借用了下一章的内容。首先,我创建了一个字符串对象的流,然后将每个对象传递给 `filter()` 操作。 `filter()` 使用 `p4` 的断言来确定对象的去留。最后我们使用 `forEach()` 将 `println` 方法引用应用在每个留存的对象上。
+正因它产生如此清晰的语法,我在主方法中采用了一些小技巧,并借用了下一章的内容。首先,我创建了一个字符串对象的流,然后将每个对象传递给 `filter()` 操作。 `filter()` 使用 `p4` 的谓词来确定对象的去留。最后我们使用 `forEach()` 将 `println` 方法引用应用在每个留存的对象上。
从输出结果我们可以看到 `p4` 的工作流程:任何带有 `foo` 的东西都会留下,即使它的长度大于 5。 `fongopuckey` 因长度超出和不包含 `bar` 而被丢弃。
diff --git a/docs/book/14-Streams.md b/docs/book/14-Streams.md
index b75adc65..764d3ac9 100644
--- a/docs/book/14-Streams.md
+++ b/docs/book/14-Streams.md
@@ -1959,7 +1959,7 @@ Lambda 表达式中的第一个参数 `fr0` 是上一次调用 `reduce()` 的结
- `anyMatch(Predicate)`:如果流中的任意一个元素根据提供的 **Predicate** 返回 true 时,结果返回为 true。在第一个 false 是停止执行计算。
- `noneMatch(Predicate)`:如果流的每个元素根据提供的 **Predicate** 都返回 false 时,结果返回为 true。在第一个 true 时停止执行计算。
-我们已经在 `Prime.java` 中看到了 `noneMatch()` 的示例;`allMatch()` 和 `anyMatch()` 的用法基本上是等同的。下面我们来探究一下短路行为。为了消除冗余代码,我们创建了 `show()`。首先我们必须治到如何统一地描述这三个匹配器的操作,然后再将其转换为 **Matcher** 接口。代码示例:
+我们已经在 `Prime.java` 中看到了 `noneMatch()` 的示例;`allMatch()` 和 `anyMatch()` 的用法基本上是等同的。下面我们来探究一下短路行为。为了消除冗余代码,我们创建了 `show()`。首先我们必须知道如何统一地描述这三个匹配器的操作,然后再将其转换为 **Matcher** 接口。代码示例:
```java
// streams/Matching.java
@@ -2001,9 +2001,9 @@ public class Matching {
1 2 3 4 5 6 7 8 9 true
```
-**BiPredicate** 是一个二元断言,它只能接受两个参数且只返回 true 或者 false。它的第一个参数是我们要测试的流,第二个参数是一个断言 **Predicate**。**Matcher** 适用于所有的 **Stream::\*Match** 方法,所以我们可以传递每一个到 `show()` 中。`match.test()` 的调用会被转换成 **Stream::\*Match** 函数的调用。
+**BiPredicate** 是一个二元谓词,它只能接受两个参数且只返回 true 或者 false。它的第一个参数是我们要测试的流,第二个参数是一个谓词 **Predicate**。**Matcher** 适用于所有的 **Stream::\*Match** 方法,所以我们可以传递每一个到 `show()` 中。`match.test()` 的调用会被转换成 **Stream::\*Match** 函数的调用。
-`show()` 获取两个参数,**Matcher** 匹配器和用于表示断言测试 **n < val** 中最大值的 **val**。这个方法生成一个1-9之间的整数流。`peek()` 是用于向我们展示测试在短路之前的情况。从输出中可以看到每次都发生了短路。
+`show()` 获取两个参数,**Matcher** 匹配器和用于表示谓词测试 **n < val** 中最大值的 **val**。这个方法生成一个1-9之间的整数流。`peek()` 是用于向我们展示测试在短路之前的情况。从输出中可以看到每次都发生了短路。
### 查找
From 86bf1010e41bbc30f6fe4ee62230118f43b7d662 Mon Sep 17 00:00:00 2001
From: crimson <1291463831@qq.com>
Date: Sat, 18 Jan 2020 09:48:19 +0800
Subject: [PATCH 008/224] =?UTF-8?q?Fix=20issue#112:=E7=BF=BB=E8=AF=91?=
=?UTF-8?q?=E5=B0=8F=E8=8A=82=E5=A4=8D=E6=9D=82=E6=80=A7=E5=92=8C=E4=BB=A3?=
=?UTF-8?q?=E4=BB=B7?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
docs/book/24-Concurrent-Programming.md | 350 +++++++++++++++++++++++++
1 file changed, 350 insertions(+)
diff --git a/docs/book/24-Concurrent-Programming.md b/docs/book/24-Concurrent-Programming.md
index 9b2b4ca1..15cdfef3 100644
--- a/docs/book/24-Concurrent-Programming.md
+++ b/docs/book/24-Concurrent-Programming.md
@@ -2461,8 +2461,358 @@ public class SynchronizedFactory{
## 复杂性和代价
+假设您正在做披萨,我们把从整个流程的当前步骤到下一个步骤所需的工作量,在这里一一表示为枚举变量的一部分:
+
+
+
+```java
+// concurrent/Pizza.java import java.util.function.*;
+
+import onjava.Nap;
+public class Pizza{
+ public enum Step{
+ DOUGH(4), ROLLED(1), SAUCED(1), CHEESED(2),
+ TOPPED(5), BAKED(2), SLICED(1), BOXED(0);
+ int effort;// Needed to get to the next step
+
+ Step(int effort){
+ this.effort = effort;
+ }
+
+ Step forward(){
+ if (equals(BOXED)) return BOXED;
+ new Nap(effort * 0.1);
+ return values()[ordinal() + 1];
+ }
+ }
+
+ private Step step = Step.DOUGH;
+ private final int id;
+
+ public Pizza(int id){
+ this.id = id;
+ }
+
+ public Pizza next(){
+ step = step.forward();
+ System.out.println("Pizza " + id + ": " + step);
+ return this;
+ }
+
+ public Pizza next(Step previousStep){
+ if (!step.equals(previousStep))
+ throw new IllegalStateException("Expected " +
+ previousStep + " but found " + step);
+ return next();
+ }
+
+ public Pizza roll(){
+ return next(Step.DOUGH);
+ }
+
+ public Pizza sauce(){
+ return next(Step.ROLLED);
+ }
+
+ public Pizza cheese(){
+ return next(Step.SAUCED);
+ }
+
+ public Pizza toppings(){
+ return next(Step.CHEESED);
+ }
+
+ public Pizza bake(){
+ return next(Step.TOPPED);
+ }
+
+ public Pizza slice(){
+ return next(Step.BAKED);
+ }
+
+ public Pizza box(){
+ return next(Step.SLICED);
+ }
+
+ public boolean complete(){
+ return step.equals(Step.BOXED);
+ }
+
+ @Override
+ public String toString(){
+ return "Pizza" + id + ": " + (step.equals(Step.BOXED) ? "complete" : step);
+ }
+}
+```
+
+这只算得上是一个简单的状态机,就像Machina类一样。
+
+制作一个披萨,当披萨饼最终被放在盒子中时,就算完成最终任务了。 如果一个人在做一个披萨饼,那么所有步骤都是线性进行的,即一个接一个地进行:
+
+```java
+// concurrent/OnePizza.java
+
+import onjava.Timer;
+
+public class OnePizza{
+ public static void main(String[] args){
+ Pizza za = new Pizza(0);
+ System.out.println(Timer.duration(() -> {
+ while (!za.complete()) za.next();
+ }));
+ }
+}
+/* Output:
+Pizza 0: ROLLED
+Pizza 0: SAUCED
+Pizza 0: CHEESED
+Pizza 0: TOPPED
+Pizza 0: BAKED
+Pizza 0: SLICED
+Pizza 0: BOXED
+ 1622
+*/
+```
+
+时间以毫秒为单位,加总所有步骤的工作量,会得出与我们的期望值相符的数字。 如果您以这种方式制作了五个披萨,那么您会认为它花费的时间是原来的五倍。 但是,如果这还不够快怎么办? 我们可以从尝试并行流方法开始:
+
+```java
+// concurrent/PizzaStreams.java
+// import java.util.*; import java.util.stream.*;
+
+import onjava.Timer;
+
+public class PizzaStreams{
+ static final int QUANTITY = 5;
+
+ public static void main(String[] args){
+ Timer timer = new Timer();
+ IntStream.range(0, QUANTITY)
+ .mapToObj(Pizza::new)
+ .parallel()//[1]
+ .forEach(za -> { while(!za.complete()) za.next(); }); System.out.println(timer.duration());
+ }
+}
+/* Output:
+Pizza 2: ROLLED
+Pizza 0: ROLLED
+Pizza 1: ROLLED
+Pizza 4: ROLLED
+Pizza 3:ROLLED
+Pizza 2:SAUCED
+Pizza 1:SAUCED
+Pizza 0:SAUCED
+Pizza 4:SAUCED
+Pizza 3:SAUCED
+Pizza 2:CHEESED
+Pizza 1:CHEESED
+Pizza 0:CHEESED
+Pizza 4:CHEESED
+Pizza 3:CHEESED
+Pizza 2:TOPPED
+Pizza 1:TOPPED
+Pizza 0:TOPPED
+Pizza 4:TOPPED
+Pizza 3:TOPPED
+Pizza 2:BAKED
+Pizza 1:BAKED
+Pizza 0:BAKED
+Pizza 4:BAKED
+Pizza 3:BAKED
+Pizza 2:SLICED
+Pizza 1:SLICED
+Pizza 0:SLICED
+Pizza 4:SLICED
+Pizza 3:SLICED
+Pizza 2:BOXED
+Pizza 1:BOXED
+Pizza 0:BOXED
+Pizza 4:BOXED
+Pizza 3:BOXED
+1739
+*/
+```
+
+现在,我们制作五个披萨的时间与制作单个披萨的时间就差不多了。 尝试删除标记为[1]的行后,你会发现它花费的时间是原来的五倍。 你还可以尝试将QUANTITY更改为4、8、10、16和17,看看会有什么不同,并猜猜看为什么会这样。
+
+PizzaStreams类产生的每个并行流在它的forEach()内完成所有工作,如果我们将其各个步骤用映射的方式一步一步处理,情况会有所不同吗?
+
+```java
+// concurrent/PizzaParallelSteps.java
+
+import java.util.*;
+import java.util.stream.*;
+import onjava.Timer;
+
+public class PizzaParallelSteps{
+ static final int QUANTITY = 5;
+
+ public static void main(String[] args){
+ Timer timer = new Timer();
+ IntStream.range(0, QUANTITY)
+ .mapToObj(Pizza::new)
+ .parallel()
+ .map(Pizza::roll)
+ .map(Pizza::sauce)
+ .map(Pizza::cheese)
+ .map(Pizza::toppings)
+ .map(Pizza::bake)
+ .map(Pizza::slice)
+ .map(Pizza::box)
+ .forEach(za -> System.out.println(za));
+ System.out.println(timer.duration());
+ }
+}
+/* Output:
+Pizza 2: ROLLED
+Pizza 0: ROLLED
+Pizza 1: ROLLED
+Pizza 4: ROLLED
+Pizza 3: ROLLED
+Pizza 1: SAUCED
+Pizza 0: SAUCED
+Pizza 2: SAUCED
+Pizza 3: SAUCED
+Pizza 4: SAUCED
+Pizza 1: CHEESED
+Pizza 0: CHEESED
+Pizza 2: CHEESED
+Pizza 3: CHEESED
+Pizza 4: CHEESED
+Pizza 0: TOPPED
+Pizza 2: TOPPED
+Pizza 1: TOPPED
+Pizza 3: TOPPED
+Pizza 4: TOPPED
+Pizza 1: BAKED
+Pizza 2: BAKED
+Pizza 0: BAKED
+Pizza 4: BAKED
+Pizza 3: BAKED
+Pizza 0: SLICED
+Pizza 2: SLICED
+Pizza 1: SLICED
+Pizza 3: SLICED
+Pizza 4: SLICED
+Pizza 1: BOXED
+Pizza1: complete
+Pizza 2: BOXED
+Pizza 0: BOXED
+Pizza2: complete
+Pizza0: complete
+Pizza 3: BOXED
+Pizza 4: BOXED
+Pizza4: complete
+Pizza3: complete
+1738
+*/
+```
+
+答案是“否”,事后看来这并不奇怪,因为每个披萨都需要按顺序执行步骤。因此,没法通过分步执行操作来进一步提高速度,就像上文的PizzaParallelSteps.java里面展示的一样。
+
+我们可以使用CompletableFutures重写这个例子:
+
+```java
+// concurrent/CompletablePizza.java
+
+import java.util.*;
+import java.util.concurrent.*;
+import java.util.stream.*;
+import onjava.Timer;
+
+public class CompletablePizza{
+ static final int QUANTITY = 5;
+
+ public static CompletableFuture makeCF(Pizza za){
+ return CompletableFuture
+ .completedFuture(za)
+ .thenApplyAsync(Pizza::roll)
+ .thenApplyAsync(Pizza::sauce)
+ .thenApplyAsync(Pizza::cheese)
+ .thenApplyAsync(Pizza::toppings)
+ .thenApplyAsync(Pizza::bake)
+ .thenApplyAsync(Pizza::slice)
+ .thenApplyAsync(Pizza::box);
+ }
+
+ public static void show(CompletableFuture cf){
+ try{
+ System.out.println(cf.get());
+ } catch (Exception e){
+ throw new RuntimeException(e);
+ }
+ }
+
+ public static void main(String[] args){
+ Timer timer = new Timer();
+ List> pizzas =
+ IntStream.range(0, QUANTITY)
+ .mapToObj(Pizza::new)
+ .map(CompletablePizza::makeCF)
+ .collect(Collectors.toList());
+ System.out.println(timer.duration());
+ pizzas.forEach(CompletablePizza::show);
+ System.out.println(timer.duration());
+ }
+}
+/* Output:
+169
+Pizza 0: ROLLED
+Pizza 1: ROLLED
+Pizza 2: ROLLED
+Pizza 4: ROLLED
+Pizza 3: ROLLED
+Pizza 1: SAUCED
+Pizza 0: SAUCED
+Pizza 2: SAUCED
+Pizza 4: SAUCED
+Pizza 3: SAUCED
+Pizza 0: CHEESED
+Pizza 4: CHEESED
+Pizza 1: CHEESED
+Pizza 2: CHEESED
+Pizza 3: CHEESED
+Pizza 0: TOPPED
+Pizza 4: TOPPED
+Pizza 1: TOPPED
+Pizza 2: TOPPED
+Pizza 3: TOPPED
+Pizza 0: BAKED
+Pizza 4: BAKED
+Pizza 1: BAKED
+Pizza 3: BAKED
+Pizza 2: BAKED
+Pizza 0: SLICED
+Pizza 4: SLICED
+Pizza 1: SLICED
+Pizza 3: SLICED
+Pizza 2: SLICED
+Pizza 4: BOXED
+Pizza 0: BOXED
+Pizza0: complete
+Pizza 1: BOXED
+Pizza1: complete
+Pizza 3: BOXED
+Pizza 2: BOXED
+Pizza2: complete
+Pizza3: complete
+Pizza4: complete
+1797
+*/
+```
+
+并行流和CompletableFutures是Java并发工具箱中最先进发达的技术。 您应该始终首先选择其中之一。 当一个问题很容易并行处理时,或者说,很容易把数据分解成相同的、易于处理的各个部分时,使用并行流方法处理最为合适(而如果您决定不借助它而由自己完成,您就必须撸起袖子,深入研究Spliterator的文档)。
+
+而当工作的各个部分内容各不相同时,使用CompletableFutures是最好的选择。比起面向数据,CompletableFutures更像是面向任务的。
+
+对于披萨问题,结果似乎也没有什么不同。实际上,并行流方法看起来更简洁,仅出于这个原因,我认为并行流作为解决问题的首次尝试方法更具吸引力。
+
+由于制作披萨总需要一定的时间,无论您使用哪种并发方法,你能做到的最好情况,是在制作一个披萨的相同时间内制作n个披萨。 在这里当然很容易看出来,但是当您处理更复杂的问题时,您就可能忘记这一点。 通常,在项目开始时进行粗略的计算,就能很快弄清楚最大可能的并行吞吐量,这可以防止您因为采取无用的加快运行速度的举措而忙得团团转。
+
+使用CompletableFutures或许可以轻易地带来重大收益,但是在尝试更进一步时需要倍加小心,因为额外增加的成本和工作量会非常容易远远超出你之前拼命挤出的那一点点收益。
+
## 本章小结
[^1]:例如,Eric-Raymond在“VIIX编程艺术”(Addison-Wesley,2004)中提出了一个很好的案例。
From 4b09a31a4ec8e4f489104e75af891338cd8f412a Mon Sep 17 00:00:00 2001
From: LingCoder
Date: Sat, 18 Jan 2020 13:09:32 +0800
Subject: [PATCH 009/224] =?UTF-8?q?=E5=88=9D=E6=AD=A5=E6=A0=A1=E8=AE=A2=20?=
=?UTF-8?q?=E5=B9=B6=E5=8F=91=E7=BC=96=E7=A8=8B?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
docs/book/24-Concurrent-Programming.md | 491 +++++++++++++++----------
1 file changed, 300 insertions(+), 191 deletions(-)
diff --git a/docs/book/24-Concurrent-Programming.md b/docs/book/24-Concurrent-Programming.md
index 15cdfef3..5a82b1e6 100644
--- a/docs/book/24-Concurrent-Programming.md
+++ b/docs/book/24-Concurrent-Programming.md
@@ -19,7 +19,7 @@
而不是单一的意识流叙事,我们在同时多条故事线进行的间谍小说里。一个间谍在一个特殊的岩石下李璐下微缩胶片,当第二个间谍来取回包裹时,它可能已经被第三个间谍带走了。但是这部特别的小说并没有把事情搞得一团糟;你可以轻松地走到尽头,永远不会弄明白什么。
-构建并发应用程序非常类似于游戏[Jenga](https://en.wikipedia.org/wiki/Jenga),每当你拉出一个块并将其放置在塔上时,一切都会崩溃。每个塔楼和每个应用程序都是独一无二的,有自己的作用。您从构建系统中学到的东西可能不适用于下一个系统。
+构建并发应用程序非常类似于游戏[Jenga](https://en.wikipedia.org/wiki/Jenga),每当你拉出一个块并将其放置在塔上时,一切都会崩溃。每个塔楼和每个应用程序都是独一无二的,有自己的作用。你从构建系统中学到的东西可能不适用于下一个系统。
本章是对并发性的一个非常基本的介绍。虽然我使用了最现代的Java 8工具来演示原理,但这一章远非对该主题的全面处理。我的目标是为你提供足够的基础知识,使你能够解决问题的复杂性和危险性,从而安全的通过这些鲨鱼肆虐的困难水域。
@@ -64,7 +64,7 @@ _并行_
我开始怀疑是否真的有高度抽象。当编写这些类型的程序时,你永远不会被底层系统和工具屏蔽,甚至关于CPU缓存如何工作的细节。最后,如果你非常小心,你创作的东西在特定的情况下起作用,但它在其他情况下不起作用。有时,区别在于两台机器的配置方式,或者程序的估计负载。这不是Java特有的-它是并发和并行编程的本质。
-您可能会认为[纯函数式](https://en.wikipedia.org/wiki/Purely_functional)语言没有这些限制。实际上,纯函数式语言解决了大量并发问题,所以如果你正在解决一个困难的并发问题,你可以考虑用纯函数语言编写这个部分。但最终,如果你编写一个使用队列的系统,例如,如果它没有正确调整并且输入速率要么没有被正确估计或被限制(并且限制意味着,在不同情况下不同的东西具有不同的影响),该队列将填满并阻塞或溢出。最后,您必须了解所有细节,任何问题都可能会破坏您的系统。这是一种非常不同的编程方式
+你可能会认为[纯函数式](https://en.wikipedia.org/wiki/Purely_functional)语言没有这些限制。实际上,纯函数式语言解决了大量并发问题,所以如果你正在解决一个困难的并发问题,你可以考虑用纯函数语言编写这个部分。但最终,如果你编写一个使用队列的系统,例如,如果它没有正确调整并且输入速率要么没有被正确估计或被限制(并且限制意味着,在不同情况下不同的东西具有不同的影响),该队列将填满并阻塞或溢出。最后,你必须了解所有细节,任何问题都可能会破坏你的系统。这是一种非常不同的编程方式
### 并发的新定义
@@ -75,14 +75,14 @@ _并行_
这实际上是一个相当多的声明,所以我将其分解:
- 这是一个集合:有许多不同的方法来解决这个问题。这是使定义并发性如此具有挑战性的问题之一,因为技术差别很大
-- 这些是性能技术:就是这样。并发的关键点在于让您的程序运行得更快。在Java中,并发是非常棘手和困难的,所以绝对不要使用它,除非你有一个重大的性能问题 - 即使这样,使用最简单的方法产生你需要的性能,因为并发很快变得无法管理。
-- “减少等待”部分很重要而且微妙。无论(例如)你运行多少个处理器,你只能在等待某个地方时产生结果。如果您发起I/O请求并立即获得结果,没有延迟,因此无需改进。如果您在多个处理器上运行多个任务,并且每个处理器都以满容量运行,并且任何其他任务都没有等待,那么尝试提高吞吐量是没有意义的。并发的唯一形式是如果程序的某些部分被迫等待。等待可以以多种形式出现 - 这解释了为什么存在如此不同的并发方法。
+- 这些是性能技术:就是这样。并发的关键点在于让你的程序运行得更快。在Java中,并发是非常棘手和困难的,所以绝对不要使用它,除非你有一个重大的性能问题 - 即使这样,使用最简单的方法产生你需要的性能,因为并发很快变得无法管理。
+- “减少等待”部分很重要而且微妙。无论(例如)你运行多少个处理器,你只能在等待某个地方时产生结果。如果你发起I/O请求并立即获得结果,没有延迟,因此无需改进。如果你在多个处理器上运行多个任务,并且每个处理器都以满容量运行,并且任何其他任务都没有等待,那么尝试提高吞吐量是没有意义的。并发的唯一形式是如果程序的某些部分被迫等待。等待可以以多种形式出现 - 这解释了为什么存在如此不同的并发方法。
值得强调的是,这个定义的有效性取决于等待这个词。如果没有什么可以等待,那就没有机会了。如果有什么东西在等待,那么就会有很多方法可以加快速度,这取决于多种因素,包括系统运行的配置,你要解决的问题类型以及其他许多问题。
## 并发的超能力
-想象一下,你是一部科幻电影。您必须在高层建筑中搜索一个精心巧妙地隐藏在建筑物的一千万个房间之一中的单个物品。您进入建筑物并移动走廊。走廊分开了。
+想象一下,你是一部科幻电影。你必须在高层建筑中搜索一个精心巧妙地隐藏在建筑物的一千万个房间之一中的单个物品。你进入建筑物并移动走廊。走廊分开了。
你自己完成这项任务需要一百个生命周期。
@@ -94,36 +94,36 @@ _并行_
我很想能够说,“你在科幻小说中的超级大国?这就是并发性。“每当你有更多的任务要解决时,它就像分裂两个一样简单。问题是我们用来描述这种现象的任何模型最终都是抽象的
-以下是其中一个漏洞:在理想的世界中,每次克隆自己时,您还会复制硬件处理器来运行该克隆。但当然不会发生这种情况 - 您的机器上可能有四个或八个处理器(通常在写入时)。您可能还有更多,并且仍有许多情况只有一个处理器。在抽象的讨论中,物理处理器的分配方式不仅可以泄漏,甚至可以支配您的决策
+以下是其中一个漏洞:在理想的世界中,每次克隆自己时,你还会复制硬件处理器来运行该克隆。但当然不会发生这种情况 - 你的机器上可能有四个或八个处理器(通常在写入时)。你可能还有更多,并且仍有许多情况只有一个处理器。在抽象的讨论中,物理处理器的分配方式不仅可以泄漏,甚至可以支配你的决策
让我们在科幻电影中改变一些东西。现在当每个克隆搜索者最终到达一扇门时,他们必须敲门并等到有人回答。如果我们每个搜索者有一个处理器,这没有问题 - 处理器只是空闲,直到门被回答。但是如果我们只有8个处理器和数千个搜索者,那么只是因为搜索者恰好是因为处理器闲置了被锁,等待一扇门被接听。相反,我们希望将处理器应用于搜索,在那里它可以做一些真正的工作,因此需要将处理器从一个任务切换到另一个任务的机制。
-许多型号能够有效地隐藏处理器的数量,并允许您假装您的数量非常大。但是有些情况会发生故障的时候,你必须知道处理器的数量,以便你可以解决这个问题。
+许多型号能够有效地隐藏处理器的数量,并允许你假装你的数量非常大。但是有些情况会发生故障的时候,你必须知道处理器的数量,以便你可以解决这个问题。
-其中一个最大的影响取决于您是单个处理器还是多个处理器。如果你只有一个处理器,那么任务切换的成本也由该处理器承担,将并发技术应用于你的系统会使它运行得更慢。
+其中一个最大的影响取决于你是单个处理器还是多个处理器。如果你只有一个处理器,那么任务切换的成本也由该处理器承担,将并发技术应用于你的系统会使它运行得更慢。
-这可能会让您决定,在单个处理器的情况下,编写并发代码时没有意义。然而,有些情况下,并发模型会产生更简单的代码,实际上值得让它运行得更慢以实现。
+这可能会让你决定,在单个处理器的情况下,编写并发代码时没有意义。然而,有些情况下,并发模型会产生更简单的代码,实际上值得让它运行得更慢以实现。
在克隆体敲门等待的情况下,即使单处理器系统也能从并发中受益,因为它可以从等待(阻塞)的任务切换到准备好的任务。但是如果所有任务都可以一直运行那么切换的成本会降低一切,在这种情况下,如果你有多个进程,并发通常只会有意义。
在接听电话的客户服务部门,你只有一定数量的人,但是你可以拨打很多电话。那些人(处理器)必须一次拨打一个电话,直到完成电话和额外的电话必须排队。
-在“鞋匠和精灵”的童话故事中,鞋匠做了很多工作,当他睡着时,一群精灵来为他制作鞋子。这里的工作是分布式的,但即使使用大量的物理处理器,在制造鞋子的某些部件时会产生限制 - 例如,如果鞋底需要制作鞋子,这会限制制鞋的速度并改变您设计解决方案的方式。
+在“鞋匠和精灵”的童话故事中,鞋匠做了很多工作,当他睡着时,一群精灵来为他制作鞋子。这里的工作是分布式的,但即使使用大量的物理处理器,在制造鞋子的某些部件时会产生限制 - 例如,如果鞋底需要制作鞋子,这会限制制鞋的速度并改变你设计解决方案的方式。
-因此,您尝试解决的问题驱动解决方案的设计。打破一个“独立运行”问题的高级[原文:lovely ]抽象,然后就是实际发生的现实。物理现实不断侵入和震撼,这种抽象。
+因此,你尝试解决的问题驱动解决方案的设计。打破一个“独立运行”问题的高级[原文:lovely ]抽象,然后就是实际发生的现实。物理现实不断侵入和震撼,这种抽象。
这只是问题的一部分。考虑一个制作蛋糕的工厂。我们不知何故在工人中分发了蛋糕制作任务,但是现在是时候让工人把蛋糕放在盒子里了。那里有一个盒子,准备收到蛋糕。但是,在工人将蛋糕放入盒子之前,另一名工人投入并将蛋糕放入盒子中!我们的工人已经把蛋糕放进去了,然后就开始了!这两个蛋糕被砸碎并毁了。这是常见的“共享内存”问题,产生我们称之为竞争条件的问题,其结果取决于哪个工作人员可以首先在框中获取蛋糕(通常使用锁定机制来解决问题,因此一个工作人员可以先抓住框并防止蛋糕砸)。
当“同时”执行的任务相互干扰时,会出现问题。他可以以如此微妙和偶然的方式发生,可能公平地说,并发性“可以说是确定性的,但实际上是非确定性的。”也就是说,你可以假设编写通过维护和代码检查正常工作的并发程序。然而,在实践中,编写仅看起来可行的并发程序更为常见,但是在适当的条件下,将会失败。这些情况可能会发生,或者很少发生,你在测试期间从未看到它们。实际上,编写测试代码通常无法为并发程序生成故障条件。由此产生的失败只会偶尔发生,因此它们以客户投诉的形式出现。
这是推动并发的最强有力的论据之一:如果你忽略它,你可能会被咬。
-因此,并发似乎充满了危险,如果这让你有点害怕,这可能是一件好事。尽管Java 8在并发性方面做出了很大改进,但仍然没有像编译时验证或检查异常那样的安全网来告诉您何时出现错误。通过并发,您可以自己动手,只有知识渊博,可疑和积极,才能用Java编写可靠的并发代码。
+因此,并发似乎充满了危险,如果这让你有点害怕,这可能是一件好事。尽管Java 8在并发性方面做出了很大改进,但仍然没有像编译时验证或检查异常那样的安全网来告诉你何时出现错误。通过并发,你可以自己动手,只有知识渊博,可疑和积极,才能用Java编写可靠的并发代码。
## 并发为速度而生
-在听说并发编程的问题之后,你可能会想知道它是否值得这么麻烦。答案是“不,除非你的程序运行速度不够快。”并且在决定它没有之前你会想要仔细思考。不要随便跳进并发编程的悲痛之中。如果有一种方法可以在更快的机器上运行您的程序,或者如果您可以对其进行分析并发现瓶颈并在该位置交换更快的算法,那么请执行此操作。只有在显然没有其他选择时才开始使用并发,然后仅在孤立的地方。
+在听说并发编程的问题之后,你可能会想知道它是否值得这么麻烦。答案是“不,除非你的程序运行速度不够快。”并且在决定它没有之前你会想要仔细思考。不要随便跳进并发编程的悲痛之中。如果有一种方法可以在更快的机器上运行你的程序,或者如果你可以对其进行分析并发现瓶颈并在该位置交换更快的算法,那么请执行此操作。只有在显然没有其他选择时才开始使用并发,然后仅在孤立的地方。
速度问题一开始听起来很简单:如果你想要一个程序运行得更快,将其分解成碎片并在一个单独的处理器上运行每个部分。由于我们能够提高时钟速度流(至少对于传统芯片),速度的提高是出现在多核处理器的形式而不是更快的芯片。为了使你的程序运行得更快,你必须学习利用那些超级处理器,这是并发性给你的一个建议。
@@ -139,11 +139,11 @@ _并行_
有些人甚至提倡将进程作为并发的唯一合理方法[^1],但不幸的是,通常存在数量和开销限制,以防止它们在并发频谱中的适用性(最终你习惯了标准的并发性克制,“这种方法适用于一些情况但不适用于其他情况”)
-一些编程语言旨在将并发任务彼此隔离。这些通常被称为_函数式语言_,其中每个函数调用不产生其他影响(因此不能与其他函数干涉),因此可以作为独立的任务来驱动。Erlang就是这样一种语言,它包括一个任务与另一个任务进行通信的安全机制。如果您发现程序的一部分必须大量使用并发性并且您在尝试构建该部分时遇到了过多的问题,那么您可能会考虑使用专用并发语言创建程序的那一部分。
+一些编程语言旨在将并发任务彼此隔离。这些通常被称为_函数式语言_,其中每个函数调用不产生其他影响(因此不能与其他函数干涉),因此可以作为独立的任务来驱动。Erlang就是这样一种语言,它包括一个任务与另一个任务进行通信的安全机制。如果你发现程序的一部分必须大量使用并发性并且你在尝试构建该部分时遇到了过多的问题,那么你可能会考虑使用专用并发语言创建程序的那一部分。
Java采用了更传统的方法[^2],即在顺序语言之上添加对线程的支持而不是在多任务操作系统中分配外部进程,线程在执行程序所代表的单个进程中创建任务交换。
-并发性会带来成本,包括复杂性成本,但可以通过程序设计,资源平衡和用户便利性的改进来抵消。通常,并发性使您能够创建更加松散耦合的设计;否则,您的代码部分将被迫明确标注通常由并发处理的操作。
+并发性会带来成本,包括复杂性成本,但可以通过程序设计,资源平衡和用户便利性的改进来抵消。通常,并发性使你能够创建更加松散耦合的设计;否则,你的代码部分将被迫明确标注通常由并发处理的操作。
## 四句格言
@@ -167,7 +167,7 @@ Java采用了更传统的方法[^2],即在顺序语言之上添加对线程的
证明并发性的唯一因素是速度。如果你的程序运行速度不够快 - 在这里要小心,因为只是希望它运行得更快是不合理的 - 首先应用一个分析器(参见代码校验章中分析和优化)来发现你是否可以执行其他一些优化。
-如果您被迫进行并发,请采取最简单,最安全的方法来解决问题。使用众所周知的库并尽可能少地编写自己的代码。有了并发性,就没有“太简单了”。自负才是你的敌人。
+如果你被迫进行并发,请采取最简单,最安全的方法来解决问题。使用众所周知的库并尽可能少地编写自己的代码。有了并发性,就没有“太简单了”。自负才是你的敌人。
### 2.没有什么是真的,一切可能都有问题
@@ -175,41 +175,41 @@ Java采用了更传统的方法[^2],即在顺序语言之上添加对线程的
在并发领域,有些事情可能是真的而有些事情却不是,你必须认为没有什么是真的。你必须质疑一切。即使将变量设置为某个值也可能或者可能不会按预期的方式工作,并且从那里开始走下坡路。我已经很熟悉的东西,认为它显然有效但实际上并没有。
-在非并发程序中你可以忽略的各种事情突然变得非常重要。例如,您必须知道处理器缓存以及保持本地缓存与主内存一致的问题。您必须了解对象构造的深度复杂性,以便您的构造对象不会意外地将数据暴露给其他线程进行更改。问题还有很多。
+在非并发程序中你可以忽略的各种事情突然变得非常重要。例如,你必须知道处理器缓存以及保持本地缓存与主内存一致的问题。你必须了解对象构造的深度复杂性,以便你的构造对象不会意外地将数据暴露给其他线程进行更改。问题还有很多。
-虽然这些主题太复杂,无法为您提供本章的专业知识(再次参见Java Concurrency in Practice),但您必须了解它们。
+虽然这些主题太复杂,无法为你提供本章的专业知识(再次参见Java Concurrency in Practice),但你必须了解它们。
### 3.它起作用,并不意味着它没有问题
-您可以轻松编写一个似乎可以工作,但实际上是有问题的并发程序,并且该问题仅在最极限的条件下显示出来 - 在您部署程序后不可避免地会出现用户问题。
+我们很容易编写出一个看似完美实则有问题的并发程序,并且往往问题直在极端情况下才暴露出来 - 在程序部署后不可避免地会出现用户问题。
- 你不能证明并发程序是正确的,你只能(有时)证明它是不正确的。
- 大多数情况下你甚至不能这样做:如果它有问题,你可能无法检测到它。
-- 您通常不能编写有用的测试,因此您必须依靠代码检查结合深入的并发知识来发现错误。
+- 你通常不能编写有用的测试,因此你必须依靠代码检查结合深入的并发知识来发现错误。
- 即使是有效的程序也只能在其设计参数下工作。当超出这些设计参数时,大多数并发程序会以某种方式失败。
在其他Java主题中,我们培养了一种感觉-决定论。一切都按照语言的承诺(或隐含)进行,这是令人欣慰和期待的 - 毕竟,编程语言的目的是让机器做我们想要的。从确定性编程的世界进入并发编程领域,我们遇到了一种称为[Dunning-Kruger](https://en.wikipedia.org/wiki/Dunning%E2%80%93Kruger_effect)效应的认知偏差,可以概括为“你知道的越多,你认为你知道得越多。”这意味着“......相对不熟练的人拥有着虚幻的优越感,错误地评估他们的能力远高于实际。
-我自己的经验是,无论你是多么确定你的代码是线程安全的,它可能已经无效了。你可以很容易地了解所有的问题,然后几个月或几年后你会发现一些概念让你意识到你编写的大多数内容实际上都容易受到并发错误的影响。当某些内容不正确时,编译器不会告诉您。为了使它正确,你必须在研究代码时掌握前脑的所有并发问题。
+我自己的经验是,无论你是多么确定你的代码是线程安全的,它可能已经无效了。你可以很容易地了解所有的问题,然后几个月或几年后你会发现一些概念让你意识到你编写的大多数内容实际上都容易受到并发错误的影响。当某些内容不正确时,编译器不会告诉你。为了使它正确,你必须在研究代码时掌握前脑的所有并发问题。
在Java的所有非并发领域,“没有明显的错误和没有明显的编译错误”似乎意味着一切都好。对于并发,它没有任何意义。你可以在这个情况下做的最糟糕的事情是“自信”。
### 4.你必须仍然理解
-在格言1-3之后,您可能会对并发性感到害怕,并且认为,“到目前为止,我已经避免了它,也许我可以继续保留它。
+在格言1-3之后,你可能会对并发性感到害怕,并且认为,“到目前为止,我已经避免了它,也许我可以继续保留它。
-这是一种理性的反应。您可能知道其他编程语言更好地设计用于构建并发程序 - 甚至是在JVM上运行的程序(从而提供与Java的轻松通信),例如Clojure或Scala。为什么不用这些语言编写并发部分并将Java用于其他所有部分呢?
+这是一种理性的反应。你可能知道其他编程语言更好地设计用于构建并发程序 - 甚至是在JVM上运行的程序(从而提供与Java的轻松通信),例如Clojure或Scala。为什么不用这些语言编写并发部分并将Java用于其他所有部分呢?
唉,你不能轻易逃脱:
- 即使你从未明确地创建一个线程,你可能使用的框架 - 例如,Swing图形用户界面(GUI)库,或者像**Timer** clas那样简单的东西。
-- 这是最糟糕的事情:当您创建组件时,您必须假设这些组件可能在多线程环境中重用。即使你的解决方案是放弃并声明你的组件“不是线程安全的”,你仍然必须知道这样的声明是重要的,它是什么意思?
+- 这是最糟糕的事情:当你创建组件时,你必须假设这些组件可能在多线程环境中重用。即使你的解决方案是放弃并声明你的组件“不是线程安全的”,你仍然必须知道这样的声明是重要的,它是什么意思?
人们有时会认为并发性太难,不能包含在介绍该语言的书中。他们认为并发是一个可以独立对待的独立主题,并且它在日常编程中出现的少数情况(例如图形用户界面)可以用特殊的习语来处理。如果你可以避免它,为什么要介绍这样的复杂的主题。
-唉,如果只是这样的话,那就太好了。但不幸的是,您无法选择何时在Java程序中出现线程。仅仅你从未写过自己的线程,并不意味着你可以避免编写线程代码。例如,Web系统是最常见的Java应用程序之一,本质上是多线程的Web服务器通常包含多个处理器,而并行性是利用这些处理器的理想方式。就像这样的系统看起来那么简单,你必须理解并发才能正确地编写它。
+唉,如果只是这样的话,那就太好了。但不幸的是,你无法选择何时在Java程序中出现线程。仅仅你从未写过自己的线程,并不意味着你可以避免编写线程代码。例如,Web系统是最常见的Java应用程序之一,本质上是多线程的Web服务器通常包含多个处理器,而并行性是利用这些处理器的理想方式。就像这样的系统看起来那么简单,你必须理解并发才能正确地编写它。
-Java是一种多线程语言,如果您了解它们是否存在并发问题。因此,有许多Java程序正在使用中,或者只是偶然工作,或者大部分时间工作并且不时地发生问题,因为。有时这种问题是相对良性的,但有时它意味着丢失有价值的数据,如果你没有意识到并发问题,你最终可能会把问题放在其他地方而不是你的代码中。如果将程序移动到多处理器系统,则可以暴露或放大这些类型的问题。基本上,了解并发性使您意识到正确的程序可能会表现出错误的行为。
+Java是一种多线程语言,如果你了解它们是否存在并发问题。因此,有许多Java程序正在使用中,或者只是偶然工作,或者大部分时间工作并且不时地发生问题,因为。有时这种问题是相对良性的,但有时它意味着丢失有价值的数据,如果你没有意识到并发问题,你最终可能会把问题放在其他地方而不是你的代码中。如果将程序移动到多处理器系统,则可以暴露或放大这些类型的问题。基本上,了解并发性使你意识到正确的程序可能会表现出错误的行为。
## 残酷的真相
@@ -220,13 +220,13 @@ Java是一种多线程语言,如果您了解它们是否存在并发问题。
有了这种根本性的人类变化,看到许多破坏和失败的实验并不令人惊讶。实际上,进化依赖于无数的实验,其中大多数都失败了。这些实验是向前发展的必要条件
-Java是在充满自信,热情和睿智的氛围中创建的。在发明一种编程语言时,很容易就像语言的初始可塑性会持续存在一样,你可以把某些东西拿出来,如果不能解决问题,那么就修复它。编程语言以这种方式是独一无二的 - 它们经历了类似水的改变:气态,液态和最终的固态。在气体相位期间,灵活性似乎是无限的,并且很容易认为它总是那样。一旦人们开始使用您的语言,变化就会变得更加严重,环境变得更加粘稠。语言设计的过程本身就是一门艺术。
+Java是在充满自信,热情和睿智的氛围中创建的。在发明一种编程语言时,很容易就像语言的初始可塑性会持续存在一样,你可以把某些东西拿出来,如果不能解决问题,那么就修复它。编程语言以这种方式是独一无二的 - 它们经历了类似水的改变:气态,液态和最终的固态。在气体相位期间,灵活性似乎是无限的,并且很容易认为它总是那样。一旦人们开始使用你的语言,变化就会变得更加严重,环境变得更加粘稠。语言设计的过程本身就是一门艺术。
紧迫感来自互联网的最初兴起。它似乎是一场比赛,第一个通过起跑线的人将“获胜”(事实上,Java,JavaScript和PHP等语言的流行程度可以证明这一点)。唉,通过匆忙设计语言而产生的认知负荷和技术债务最终会赶上我们。
[Turing completeness](https://en.wikipedia.org/wiki/Turing_completeness)是不足够的;语言需要更多的东西:它们必须能够创造性地表达,而不是用不必要的东西来衡量我们。解放我们的心理能力只是为了扭转并再次陷入困境,这是毫无意义的。我承认,尽管存在这些问题,我们已经完成了令人惊奇的事情,但我也知道如果没有这些问题我们能做得更多。
-热情使原始Java设计师因为看起来有必要而投入功能。信心(以及原始语言的气味)让他们认为任何问题都可以解决。在时间轴的某个地方,有人认为任何加入Java的东西是固定的和永久性的 - 这是非常有信心,相信第一个决定永远是正确的,因此我们看到Java的体系中充斥着糟糕的决策。其中一些决定最终没有什么后果;例如,您可以告诉人们不要使用Vector,但保留了对之前版本的支持。
+热情使原始Java设计师因为看起来有必要而投入功能。信心(以及原始语言的气味)让他们认为任何问题都可以解决。在时间轴的某个地方,有人认为任何加入Java的东西是固定的和永久性的 - 这是非常有信心,相信第一个决定永远是正确的,因此我们看到Java的体系中充斥着糟糕的决策。其中一些决定最终没有什么后果;例如,你可以告诉人们不要使用Vector,但保留了对之前版本的支持。
线程包含在Java 1.0中。当然,并发性是影响语言远角的基本语言设计决策,很难想象以后添加它。公平地说,当时并不清楚基本的并发性是多少。像C这样的其他语言能够将线程视为一个附加功能,因此Java设计师也纷纷效仿,包括一个Thread类和必要的JVM支持(这比你想象的要复杂得多)。
@@ -240,36 +240,36 @@ Java实验告诉我们,结果是悄然灾难性的。程序员很容易陷入
尽管有这些基本的不可修复的缺陷,但令人印象深刻的是它还有多远。Java的后续版本添加了库,以便在使用并发时提升抽象级别。事实上,我根本不会想到有可能在Java 8中进行改进:并行流和**CompletableFutures** - 这是惊人的史诗般的变化,我会惊奇地重复的查看它[^3]。
-这些改进非常有用,我们将在本章重点介绍并行流和**CompletableFutures**。虽然它们可以大大简化您对并发和后续代码的思考方式,但基本问题仍然存在:由于Java的原始设计,代码的所有部分仍然容易受到攻击,您仍然必须理解这些复杂和微妙的问题。Java中的线程绝不是简单或安全的;那种经历必须降级为另一种更新的语言。
+这些改进非常有用,我们将在本章重点介绍并行流和**CompletableFutures**。虽然它们可以大大简化你对并发和后续代码的思考方式,但基本问题仍然存在:由于Java的原始设计,代码的所有部分仍然容易受到攻击,你仍然必须理解这些复杂和微妙的问题。Java中的线程绝不是简单或安全的;那种经历必须降级为另一种更新的语言。
## 本章其余部分
-这是我们将在本章的其余部分介绍的内容。请记住,本章的重点是使用最新的高级Java并发结构。使用这些使得您的生活比旧的替代品更加轻松。但是,您仍会在遗留代码中遇到一些低级工具。有时,你可能会被迫自己使用其中的一些。附录:[并发底层原理](./Appendix-Low-Level-Concurrency.md)包含一些更原始的Java并发元素的介绍。
+这是我们将在本章的其余部分介绍的内容。请记住,本章的重点是使用最新的高级Java并发结构。使用这些使得你的生活比旧的替代品更加轻松。但是,你仍会在遗留代码中遇到一些低级工具。有时,你可能会被迫自己使用其中的一些。附录:[并发底层原理](./Appendix-Low-Level-Concurrency.md)包含一些更原始的Java并发元素的介绍。
- Parallel Streams(并发流)
-到目前为止,我已经强调了Java 8 Streams提供的改进语法。现在您对该语法(作为一个粉丝,我希望)感到满意,您可以获得额外的好处:您可以通过简单地将parallel()添加到表达式来并行化流。这是一种简单,强大,坦率地说是利用多处理器的惊人方式
+到目前为止,我已经强调了Java 8 Streams提供的改进语法。现在你对该语法(作为一个粉丝,我希望)感到满意,你可以获得额外的好处:你可以通过简单地将parallel()添加到表达式来并行化流。这是一种简单,强大,坦率地说是利用多处理器的惊人方式
添加parallel()来提高速度似乎是微不足道的,但是,唉,它就像你刚刚在[残酷的真相](#The-Brutal-Truth)中学到的那样简单。我将演示并解释一些盲目添加parallel()到Stream表达式的缺陷。
- 创建和运行任务
-任务是一段可以独立运行的代码。为了解释创建和运行任务的一些基础知识,本节介绍了一种比并行流或CompletableFutures:Executor更复杂的机制。执行者管理一些低级Thread对象(Java中最原始的并发形式)。您创建一个任务,然后将其交给Executorto运行。
+任务是一段可以独立运行的代码。为了解释创建和运行任务的一些基础知识,本节介绍了一种比并行流或CompletableFutures:Executor更复杂的机制。执行者管理一些低级Thread对象(Java中最原始的并发形式)。你创建一个任务,然后将其交给Executorto运行。
有多种类型的Executor用于不同的目的。在这里,我们将展示规范形式,代表创建和运行任务的最简单和最佳方法。
- 终止长时间运行的任务
任务独立运行,因此需要一种机制来关闭它们。典型的方法使用了一个标志,这引入了共享内存的问题,我们将使用Java的“Atomic”库来回避它。
- Completable Futures
-当您将衣服带到干洗店时,他们会给您一张收据。你继续完成其他任务,最终你的衣服很干净,你可以拿起它。收据是您与干洗店在后台执行的任务的连接。这是Java 5中引入的Future的方法。
+当你将衣服带到干洗店时,他们会给你一张收据。你继续完成其他任务,最终你的衣服很干净,你可以拿起它。收据是你与干洗店在后台执行的任务的连接。这是Java 5中引入的Future的方法。
Future比以前的方法更方便,但你仍然必须出现并用收据取出干洗,等待任务没有完成。对于一系列操作,Futures并没有真正帮助那么多。
-Java 8 CompletableFuture是一个更好的解决方案:它允许您将操作链接在一起,因此您不必将代码写入接口排序操作。有了CompletableFuture完美的结合,就可以更容易地做出“采购原料,组合成分,烹饪食物,提供食物,清理菜肴,储存菜肴”等一系列链式操作。
+Java 8 CompletableFuture是一个更好的解决方案:它允许你将操作链接在一起,因此你不必将代码写入接口排序操作。有了CompletableFuture完美的结合,就可以更容易地做出“采购原料,组合成分,烹饪食物,提供食物,清理菜肴,储存菜肴”等一系列链式操作。
- 死锁
某些任务必须去**等待 - 阻塞**来获得其他任务的结果。被阻止的任务有可能等待另一个被阻止的任务,等待另一个被阻止的任务,等等。如果被阻止的任务链循环到第一个,没有人可以取得任何进展,你就会陷入僵局。
-如果在运行程序时没有立即出现死锁,则会出现最大的问题。您的系统可能容易出现死锁,并且只会在某些条件下死锁。程序可能在某个平台上运行正常,例如您的开发机器,但是当您将其部署到不同的硬件时会开始死锁。
+如果在运行程序时没有立即出现死锁,则会出现最大的问题。你的系统可能容易出现死锁,并且只会在某些条件下死锁。程序可能在某个平台上运行正常,例如你的开发机器,但是当你将其部署到不同的硬件时会开始死锁。
死锁通常源于细微的编程错误;一系列无辜的决定,最终意外地创建了一个依赖循环。本节包含一个经典示例,演示了死锁的特性。
@@ -279,7 +279,7 @@ Java 8 CompletableFuture是一个更好的解决方案:它允许您将操作
## 并行流
-Java 8流的一个显着优点是,在某些情况下,它们可以很容易地并行化。这来自仔细的库设计,特别是流使用内部迭代的方式 - 也就是说,它们控制着自己的迭代器。特别是,他们使用一种特殊的迭代器,称为Spliterator,它被限制为易于自动分割。这产生了相当神奇的结果,即能够简单用parallel()然后流中的所有内容都作为一组并行任务运行。如果您的代码是使用Streams编写的,那么并行化以提高速度似乎是一种琐事
+Java 8流的一个显着优点是,在某些情况下,它们可以很容易地并行化。这来自仔细的库设计,特别是流使用内部迭代的方式 - 也就是说,它们控制着自己的迭代器。特别是,他们使用一种特殊的迭代器,称为Spliterator,它被限制为易于自动分割。这产生了相当神奇的结果,即能够简单用parallel()然后流中的所有内容都作为一组并行任务运行。如果你的代码是使用Streams编写的,那么并行化以提高速度似乎是一种琐事
例如,考虑来自Streams的Prime.java。查找质数可能是一个耗时的过程,我们可以看到该程序的计时:
@@ -311,17 +311,20 @@ public class ParallelPrime {
Files.write(Paths.get("primes.txt"), primes, StandardOpenOption.CREATE);
}
}
- /*
+```
+
+输出结果:
+
+```
Output:
1224
- */
```
请注意,这不是微基准测试,因为我们计时整个程序。我们将数据保存在磁盘上以防止过激的优化;如果我们没有对结果做任何事情,那么一个高级的编译器可能会观察到程序没有意义并且消除了计算(这不太可能,但并非不可能)。请注意使用nio2库编写文件的简单性(在[文件](./17-Files.md)一章中有描述)。
当我注释掉[1] parallel()行时,我的结果大约是parallel()的三倍。
-并行流似乎是一个甜蜜的交易。您所需要做的就是将编程问题转换为流,然后插入parallel()以加快速度。实际上,有时候这很容易。但遗憾的是,有许多陷阱。
+并行流似乎是一个甜蜜的交易。你所需要做的就是将编程问题转换为流,然后插入parallel()以加快速度。实际上,有时候这很容易。但遗憾的是,有许多陷阱。
- parallel()不是灵丹妙药
@@ -367,11 +370,15 @@ public class Summing {
// .limit(SZ+1).sum());
}
}
-/* Output:5000000050000000
+```
+
+输出结果:
+
+```
+5000000050000000
Sum Stream: 167ms
Sum Stream Parallel: 46ms
Sum Iterated: 284ms
-*/
```
**CHECK**值是使用Carl Friedrich Gauss在1700年代后期仍在小学时创建的公式计算出来的.
@@ -420,13 +427,17 @@ public class Summing2 {
});
}
}
-/* Output:200000010000000
+```
+
+输出结果:
+
+```
+200000010000000
Array Stream
Sum: 104ms
Parallel: 81ms
Basic Sum: 106ms
parallelPrefix: 265ms
-*/
```
第一个限制是内存大小;因为数组是预先分配的,所以我们不能创建几乎与以前版本一样大的任何东西。并行化可以加快速度,甚至比使用 **basicSum()** 循环更快。有趣的是, **Arrays.parallelPrefix()** 似乎实际上减慢了速度。但是,这些技术中的任何一种在其他条件下都可能更有用 - 这就是为什么你不能做出任何确定性的声明,除了“你必须尝试一下”。”
@@ -464,13 +475,17 @@ public class Summing3 {
});
}
}
-/* Output:50000005000000
+```
+
+输出结果:
+
+```
+50000005000000
Long Array
Stream Reduce: 1038ms
Long Basic
Sum: 21ms
Long parallelPrefix: 3616ms
-*/
```
现在可用的内存量大约减半,并且所有情况下所需的时间都会很长,除了**basicSum()**,它只是循环遍历数组。令人惊讶的是, **Arrays.parallelPrefix()** 比任何其他方法都要花费更长的时间。
@@ -493,9 +508,13 @@ public class Summing4 {
.reduce(0L,Long::sum));
}
}
-/* Output:50000005000000
+```
+
+输出结果:
+
+```
+50000005000000
Long Parallel: 1014ms
-*/
```
它比非parallel()版本略快,但并不显着。
@@ -524,7 +543,7 @@ Long Parallel: 1008ms**
- parallel()/limit()交点
-使用parallel()时会有更复杂的问题。从其他语言中吸取的流是围绕无限流模型设计的。如果您拥有有限数量的元素,则可以使用集合以及为有限大小的集合设计的关联算法。如果您使用无限流,则使用针对流优化的算法。
+使用parallel()时会有更复杂的问题。从其他语言中吸取的流是围绕无限流模型设计的。如果你拥有有限数量的元素,则可以使用集合以及为有限大小的集合设计的关联算法。如果你使用无限流,则使用针对流优化的算法。
Java 8将两者合并起来。例如,**Collections**没有内置的**map()**操作。在Collection和Map中唯一类似流的批处理操作是**forEach()**。如果要执行**map()**和**reduce()**等操作,必须首先将Collection转换为存在这些操作的Stream:
@@ -547,7 +566,11 @@ public class CollectionIntoStream {
System.out.println(result);
}
}
-/* Output:btpen
+```
+
+输出结果:
+
+```
pccux
szgvg
meinn
@@ -558,7 +581,6 @@ ygpoa
lkljl
bynxt
:PENCUXGVGINNLOZVEWPPCPOALJLNXT
-*/
```
**Collection**确实有一些批处理操作,如**removeAll()**,**removeIf()**和**retainAll()**,但这些都是破坏性的操作.**ConcurrentHashMap**对**forEachand**和**reduce**操作有特别广泛的支持。
@@ -628,15 +650,17 @@ public class ParallelStreamPuzzle2 {
Files.write(Paths.get("PSP2.txt"), trace);
}
}
-/*
-Output:
+```
+
+输出结果:
+
+```
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
-*/
```
-current是使用线程安全的**AtomicInteger**类定义的,可以防止竞争条件;**parallel()**允许多个线程调用**get()**。
+current是使用线程安全的 **AtomicInteger** 类定义的,可以防止竞争条件;**parallel()**允许多个线程调用**get()**。
-在查看**PSP2.txt**。**IntGenerator.get()**被调用1024次时,您可能会感到惊讶。
+在查看 **PSP2.txt**.**IntGenerator.get()** 被调用1024次时,你可能会感到惊讶。
**0: main
1: ForkJoinPool.commonPool-worker-1
@@ -685,7 +709,11 @@ public class ParallelStreamPuzzle3 {
System.out.println(x);
}
}
-/* Output:
+```
+
+输出结果:
+
+```
8: main
6: ForkJoinPool.commonPool-worker-5
3: ForkJoinPool.commonPool-worker-7
@@ -697,12 +725,11 @@ public class ParallelStreamPuzzle3 {
7: ForkJoinPool.commonPool-worker-1
9: ForkJoinPool.commonPool-worker-2
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
-*/
```
为了表明**parallel()**确实有效,我添加了一个对**peek()**的调用,这是一个主要用于调试的流函数:它从流中提取一个值并执行某些操作但不影响从流向下传递的元素。注意这会干扰线程行为,但我只是尝试在这里做一些事情,而不是实际调试任何东西。
-您还可以看到boxed()的添加,它接受int流并将其转换为Integer流。
+你还可以看到boxed()的添加,它接受int流并将其转换为Integer流。
现在我们得到多个线程产生不同的值,但它只产生10个请求的值,而不是1024个产生10个值。
@@ -710,20 +737,20 @@ public class ParallelStreamPuzzle3 {
- 并行流只看起来很容易
-实际上,在许多情况下,并行流确实可以毫不费力地更快地产生结果。但正如您所见,只需将**parallel()**打到您的Stream操作上并不一定是安全的事情。在使用**parallel()**之前,您必须了解并行性如何帮助或损害您的操作。有个错误认识是认为并行性总是一个好主意。事实上并不是。Stream意味着您不需要重写所有代码以便并行运行它。流什么都不做的是取代理解并行性如何工作的需要,以及它是否有助于实现您的目标。
+实际上,在许多情况下,并行流确实可以毫不费力地更快地产生结果。但正如你所见,只需将**parallel()**打到你的Stream操作上并不一定是安全的事情。在使用**parallel()**之前,你必须了解并行性如何帮助或损害你的操作。有个错误认识是认为并行性总是一个好主意。事实上并不是。Stream意味着你不需要重写所有代码以便并行运行它。流什么都不做的是取代理解并行性如何工作的需要,以及它是否有助于实现你的目标。
## 创建和运行任务
-如果无法通过并行流实现并发,则必须创建并运行自己的任务。稍后您将看到运行任务的理想Java 8方法是CompletableFuture,但我们将使用更基本的工具介绍概念。
+如果无法通过并行流实现并发,则必须创建并运行自己的任务。稍后你将看到运行任务的理想Java 8方法是CompletableFuture,但我们将使用更基本的工具介绍概念。
Java并发的历史始于非常原始和有问题的机制,并且充满了各种尝试的改进。这些主要归入附录:[低级并发(Appendix: Low-Level Concurrency)](./Appendix-Low-Level-Concurrency.md)。在这里,我们将展示一个规范形式,表示创建和运行任务的最简单,最好的方法。与并发中的所有内容一样,存在各种变体,但这些变体要么降级到该附录,要么超出本书的范围。
- Tasks and Executors
-在Java的早期版本中,您通过直接创建自己的Thread对象来使用线程,甚至将它们子类化以创建您自己的特定“任务线程”对象。你手动调用了构造函数并自己启动了线程。
+在Java的早期版本中,你通过直接创建自己的Thread对象来使用线程,甚至将它们子类化以创建你自己的特定“任务线程”对象。你手动调用了构造函数并自己启动了线程。
-创建所有这些线程的开销变得非常重要,现在不鼓励采用实际操作方法。在Java 5中,添加了类来为您处理线程池。您可以将任务创建为单独的类型,然后将其交给ExecutorService以运行该任务,而不是为每种不同类型的任务创建新的Thread子类型。ExecutorService为您管理线程,并且在运行任务后重新循环线程而不是丢弃线程。
+创建所有这些线程的开销变得非常重要,现在不鼓励采用实际操作方法。在Java 5中,添加了类来为你处理线程池。你可以将任务创建为单独的类型,然后将其交给ExecutorService以运行该任务,而不是为每种不同类型的任务创建新的Thread子类型。ExecutorService为你管理线程,并且在运行任务后重新循环线程而不是丢弃线程。
首先,我们将创建一个几乎不执行任务的任务。它“sleep”(暂停执行)100毫秒,显示其标识符和正在执行任务的线程的名称,然后完成:
@@ -770,7 +797,7 @@ public class Nap {
```
为了消除异常处理的视觉噪声,这被定义为实用程序。第二个构造函数在超时时显示一条消息
-对**TimeUnit.MILLISECONDS.sleep()**的调用获取“当前线程”并在参数中将其置于休眠状态,这意味着该线程被挂起。这并不意味着底层处理器停止。操作系统将其切换到其他任务,例如在您的计算机上运行另一个窗口。OS任务管理器定期检查**sleep()**是否超时。当它执行时,线程被“唤醒”并给予更多处理时间。
+对**TimeUnit.MILLISECONDS.sleep()**的调用获取“当前线程”并在参数中将其置于休眠状态,这意味着该线程被挂起。这并不意味着底层处理器停止。操作系统将其切换到其他任务,例如在你的计算机上运行另一个窗口。OS任务管理器定期检查**sleep()**是否超时。当它执行时,线程被“唤醒”并给予更多处理时间。
你可以看到**sleep()**抛出一个已检查的**InterruptedException**;这是原始Java设计中的一个工件,它通过突然断开它们来终止任务。因为它往往会产生不稳定的状态,所以后来不鼓励终止。但是,我们必须在需要或仍然发生终止的情况下捕获异常。
@@ -798,7 +825,11 @@ public class SingleThreadExecutor {
}
}
}
-/* Output:
+```
+
+输出结果:
+
+```
All tasks submitted
main awaiting termination
main awaiting termination
@@ -821,7 +852,6 @@ main awaiting termination
NapTask[8] pool-1-thread-1
main awaiting termination
NapTask[9] pool-1-thread-1
-*/
```
首先请注意,没有**SingleThreadExecutor**类。**newSingleThreadExecutor()**是**Executors**中的工厂,它创建特定类型的[^4]
@@ -846,7 +876,11 @@ public class SingleThreadExecutor2 {
exec.shutdown();
}
}
-/* Output:
+```
+
+输出结果:
+
+```
NapTask[0] pool-1-thread-1
NapTask[1] pool-1-thread-1
NapTask[2] pool-1-thread-1
@@ -857,7 +891,6 @@ NapTask[6] pool-1-thread-1
NapTask[7] pool-1-thread-1
NapTask[8] pool-1-thread-1
NapTask[9] pool-1-thread-1
-*/
```
一旦你callexec.shutdown(),尝试提交新任务将抛出RejectedExecutionException。
@@ -878,17 +911,19 @@ public class MoreTasksAfterShutdown {
}
}
}
-/* Output:
-java.util.concurrent.RejectedExecutionException: TaskNapTask[99] rejected from java.util.concurrent.ThreadPoolExecutor@4e25154f[Shutting down, pool size = 1,active threads = 1, queued tasks = 0, completed tasks =0]NapTask[1] pool-1-thread-1
-*/
+```
+
+输出结果:
+```
+java.util.concurrent.RejectedExecutionException: TaskNapTask[99] rejected from java.util.concurrent.ThreadPoolExecutor@4e25154f[Shutting down, pool size = 1,active threads = 1, queued tasks = 0, completed tasks =0]NapTask[1] pool-1-thread-1
```
**exec.shutdown()**的替代方法是**exec.shutdownNow()**,它除了不接受新任务外,还会尝试通过中断任务来停止任何当前正在运行的任务。同样,中断是错误的,容易出错并且不鼓励。
- 使用更多线程
-使用线程的重点是(几乎总是)更快地完成任务,那么我们为什么要限制自己使用SingleThreadExecutor呢?查看执行**Executors**的Javadoc,您将看到更多选项。例如CachedThreadPool:
+使用线程的重点是(几乎总是)更快地完成任务,那么我们为什么要限制自己使用SingleThreadExecutor呢?查看执行**Executors**的Javadoc,你将看到更多选项。例如CachedThreadPool:
```java
// concurrent/CachedThreadPool.java
@@ -904,7 +939,11 @@ public class CachedThreadPool {
exec.shutdown();
}
}
-/* Output:
+```
+
+输出结果:
+
+```
NapTask[7] pool-1-thread-8
NapTask[4] pool-1-thread-5
NapTask[1] pool-1-thread-2
@@ -915,8 +954,6 @@ NapTask[2] pool-1-thread-3
NapTask[9] pool-1-thread-10
NapTask[6] pool-1-thread-7
NapTask[5] pool-1-thread-6
-*/
-
```
当你运行这个程序时,你会发现它完成得更快。这是有道理的,而不是使用相同的线程来顺序运行每个任务,每个任务都有自己的线程,所以它们都并行运行。似乎没有缺点,很难看出为什么有人会使用SingleThreadExecutor。
@@ -958,7 +995,11 @@ public class CachedThreadPool2 {
exec.shutdown();
}
}
-/* Output:
+```
+
+输出结果:
+
+```
0 pool-1-thread-1 200
1 pool-1-thread-2 200
4 pool-1-thread-5 300
@@ -969,8 +1010,6 @@ public class CachedThreadPool2 {
7 pool-1-thread-8 800
3 pool-1-thread-4 900
6 pool-1-thread-7 1000
-*/
-
```
输出不是我们所期望的,并且从一次运行到下一次运行会有所不同。问题是所有的任务都试图写入val的单个实例,并且他们正在踩着彼此的脚趾。我们说这样的类不是线程安全的。让我们看看SingleThreadExecutor会发生什么:
@@ -989,7 +1028,11 @@ public class SingleThreadExecutor3 {
exec.shutdown();
}
}
-/* Output:
+```
+
+输出结果:
+
+```
0 pool-1-thread-1 100
1 pool-1-thread-1 200
2 pool-1-thread-1 300
@@ -1000,8 +1043,6 @@ public class SingleThreadExecutor3 {
7 pool-1-thread-1 800
8 pool-1-thread-1 900
9 pool-1-thread-1 1000
-*/
-
```
现在我们每次都得到一致的结果,尽管**InterferingTask**缺乏线程安全性。这是SingleThreadExecutor的主要好处 - 因为它一次运行一个任务,这些任务不会相互干扰,因此强加了线程安全性。这种现象称为线程限制,因为在单线程上运行任务限制了它们的影响。线程限制限制了加速,但可以节省很多困难的调试和重写。
@@ -1035,7 +1076,7 @@ public class CountingTask implements Callable {
**call()完全独立于所有其他CountingTasks生成其结果**,这意味着没有可变的共享状态
-**ExecutorService**允许您使用**invokeAll()**启动集合中的每个Callable:
+**ExecutorService**允许你使用**invokeAll()**启动集合中的每个Callable:
```java
// concurrent/CachedThreadPool3.java
@@ -1066,7 +1107,11 @@ public class CachedThreadPool3 {
exec.shutdown();
}
}
-/* Output:
+```
+
+输出结果:
+
+```
1 pool-1-thread-2 100
0 pool-1-thread-1 100
4 pool-1-thread-5 100
@@ -1078,11 +1123,10 @@ public class CachedThreadPool3 {
6 pool-1-thread-7 100
7 pool-1-thread-8 100
sum = 1000
-*/
```
-只有在所有任务完成后,**invokeAll()**才会返回一个**Future**列表,每个任务一个**Future**。**Future**是Java 5中引入的机制,允许您提交任务而无需等待它完成。在这里,我们使用**ExecutorService.submit()**:
+只有在所有任务完成后,**invokeAll()**才会返回一个**Future**列表,每个任务一个**Future**。**Future**是Java 5中引入的机制,允许你提交任务而无需等待它完成。在这里,我们使用**ExecutorService.submit()**:
```java
// concurrent/Futures.java
@@ -1099,10 +1143,13 @@ public class Futures {
exec.shutdown();
}
}
-/* Output:
+```
+
+输出结果:
+
+```
99 pool-1-thread-1 100
100
-*/
```
- [1] 当你的任务尚未完成的**Future**上调用**get()**时,调用会阻塞(等待)直到结果可用。
@@ -1111,7 +1158,7 @@ public class Futures {
还要注意在**CachedThreadPool3.java.get()**中抛出异常,因此**extractResult()**在Stream中执行此提取。
-因为当你调用**get()**时,**Future**会阻塞,所以它只能解决等待任务完成的问题。最终,**Futures**被认为是一种无效的解决方案,现在不鼓励,支持Java 8的**CompletableFuture**,我们将在本章后面探讨。当然,您仍会在遗留库中遇到Futures
+因为当你调用**get()**时,**Future**会阻塞,所以它只能解决等待任务完成的问题。最终,**Futures**被认为是一种无效的解决方案,现在不鼓励,支持Java 8的**CompletableFuture**,我们将在本章后面探讨。当然,你仍会在遗留库中遇到Futures
我们可以使用并行Stream以更简单,更优雅的方式解决这个问题:
@@ -1131,7 +1178,11 @@ public class CountingStream {
.reduce(0, Integer::sum));
}
}
-/* Output:
+```
+
+输出结果:
+
+```
1 ForkJoinPool.commonPool-worker-3 100
8 ForkJoinPool.commonPool-worker-2 100
0 ForkJoinPool.commonPool-worker-6 100
@@ -1143,16 +1194,15 @@ public class CountingStream {
5 ForkJoinPool.commonPool-worker-2 100
3 ForkJoinPool.commonPool-worker-3 100
1000
-*/
```
这不仅更容易理解,我们需要做的就是将**parallel()**插入到其他顺序操作中,然后一切都在同时运行。
- Lambda和方法引用作为任务
-使用lambdas和方法引用,您不仅限于使用**Runnables**和**Callables**。因为Java 8通过匹配签名来支持lambda和方法引用(即,它支持结构一致性),所以我们可以将notRunnables或Callables的参数传递给ExecutorService:
+使用lambdas和方法引用,你不仅限于使用**Runnables**和**Callables**。因为Java 8通过匹配签名来支持lambda和方法引用(即,它支持结构一致性),所以我们可以将notRunnables或Callables的参数传递给ExecutorService:
-使用lambdas和方法引用,您不仅限于使用**Runnables**和**Callables**。因为Java 8通过匹配签名来支持lambda和方法引用(即,它支持结构一致性),所以我们可以将不是**Runnables**或**Callables**的参数传递给**ExecutorService**:
+使用lambdas和方法引用,你不仅限于使用**Runnables**和**Callables**。因为Java 8通过匹配签名来支持lambda和方法引用(即,它支持结构一致性),所以我们可以将不是**Runnables**或**Callables**的参数传递给**ExecutorService**:
```java
// concurrent/LambdasAndMethodReferences.java
@@ -1182,23 +1232,26 @@ public class LambdasAndMethodReferences {
exec.shutdown();
}
}
-/* Output:
+```
+
+输出结果:
+
+```
Lambda1
NotCallable
NotRunnable
Lambda2
-*/
```
-这里,前两个**submit()**调用可以改为调用**execute()**。所有**submit()**调用都返回**Futures**,您可以在后两次调用的情况下提取结果。
+这里,前两个**submit()**调用可以改为调用**execute()**。所有**submit()**调用都返回**Futures**,你可以在后两次调用的情况下提取结果。
## 终止耗时任务
-并发程序通常使用长时间运行的任务。可调用任务在完成时返回值;虽然这给它一个有限的寿命,但仍然可能很长。可运行的任务有时被设置为永远运行的后台进程。您经常需要一种方法在正常完成之前停止**Runnable**和**Callable**任务,例如当您关闭程序时。
+并发程序通常使用长时间运行的任务。可调用任务在完成时返回值;虽然这给它一个有限的寿命,但仍然可能很长。可运行的任务有时被设置为永远运行的后台进程。你经常需要一种方法在正常完成之前停止**Runnable**和**Callable**任务,例如当你关闭程序时。
-最初的Java设计提供了中断运行任务的机制(为了向后兼容,仍然存在);中断机制包括阻塞问题。中断任务既乱又复杂,因为您必须了解可能发生中断的所有可能状态,以及可能导致的数据丢失。使用中断被视为反对模式,但我们仍然被迫接受。
+最初的Java设计提供了中断运行任务的机制(为了向后兼容,仍然存在);中断机制包括阻塞问题。中断任务既乱又复杂,因为你必须了解可能发生中断的所有可能状态,以及可能导致的数据丢失。使用中断被视为反对模式,但我们仍然被迫接受。
InterruptedException,因为设计的向后兼容性残留。
@@ -1238,7 +1291,7 @@ public class QuittableTask implements Runnable {
- [1]:只要运行标志为true,此任务的run()方法将继续。
- [2]: 显示仅在任务退出时发生。
-需要**running AtomicBoolean**证明编写Java program并发时最基本的困难之一是,如果**running**是一个普通的布尔值,你可能无法在执行程序中看到问题。实际上,在这个例子中,你可能永远不会有任何问题 - 但是代码仍然是不安全的。编写表明该问题的测试可能很困难或不可能。因此,您没有任何反馈来告诉您已经做错了。通常,您编写线程安全代码的唯一方法就是通过了解事情可能出错的所有细微之处。
+需要**running AtomicBoolean**证明编写Java program并发时最基本的困难之一是,如果**running**是一个普通的布尔值,你可能无法在执行程序中看到问题。实际上,在这个例子中,你可能永远不会有任何问题 - 但是代码仍然是不安全的。编写表明该问题的测试可能很困难或不可能。因此,你没有任何反馈来告诉你已经做错了。通常,你编写线程安全代码的唯一方法就是通过了解事情可能出错的所有细微之处。
作为测试,我们将启动很多QuittableTasks然后关闭它们。尝试使用较大的COUNT值
@@ -1262,9 +1315,13 @@ public class QuittingTasks {
tasks.forEach(QuittableTask::quit); es.shutdown();
}
}
-/* Output:24 27 31 8 11 7 19 12 16 4 23 3 28 32 15 20 63 60 68 6764 39 47 52 51 55 40 43 48 59 44 56 36 35 71 72 83 10396 92 88 99 100 87 91 79 75 84 76 115 108 112 104 107111 95 80 147 120 127 119 123 144 143 116 132 124 128
+```
+
+输出结果:
+
+```
+24 27 31 8 11 7 19 12 16 4 23 3 28 32 15 20 63 60 68 6764 39 47 52 51 55 40 43 48 59 44 56 36 35 71 72 83 10396 92 88 99 100 87 91 79 75 84 76 115 108 112 104 107111 95 80 147 120 127 119 123 144 143 116 132 124 128
136 131 135 139 148 140 2 126 6 5 1 18 129 17 14 13 2122 9 10 30 33 58 37 125 26 34 133 145 78 137 141 138 6274 142 86 65 73 146 70 42 149 121 110 134 105 82 117106 113 122 45 114 118 38 50 29 90 101 89 57 53 94 4161 66 130 69 77 81 85 93 25 102 54 109 98 49 46 97
-*/
```
我使用**peek()**将**QuittableTasks**传递给**ExecutorService**,然后将这些任务收集到**List.main()**中,只要任何任务仍在运行,就会阻止程序退出。即使为每个任务按顺序调用quit()方法,任务也不会按照它们创建的顺序关闭。独立运行的任务不会确定性地响应信号。
@@ -1295,7 +1352,12 @@ public class QuittingCompletable {
cfutures.forEach(CompletableFuture::join);
}
}
-/* Output:7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 2526 27 28 29 30 31 32 33 34 6 35 4 38 39 40 41 42 43 4445 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 6263 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 8081 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 9899 100 101 102 103 104 105 106 107 108 109 110 111 1121 113 114 116 117 118 119 120 121 122 123 124 125 126127 128 129 130 131 132 133 134 135 136 137 138 139 140141 142 143 144 145 146 147 148 149 5 115 37 36 2 3*/
+```
+
+输出结果:
+
+```
+7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 2526 27 28 29 30 31 32 33 34 6 35 4 38 39 40 41 42 43 4445 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 6263 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 8081 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 9899 100 101 102 103 104 105 106 107 108 109 110 111 1121 113 114 116 117 118 119 120 121 122 123 124 125 126127 128 129 130 131 132 133 134 135 136 137 138 139 140141 142 143 144 145 146 147 148 149 5 115 37 36 2 3
```
任务是一个**List **,就像在**QuittingTasks.java**中一样,但是在这个例子中,没有**peek()**将每个**QuittableTask**提交给**ExecutorService**。相反,在创建cfutures期间,每个任务都交给**CompletableFuture::runAsync**。这执行**VerifyTask.run(**)并返回**CompletableFuture **。因为**run()**不返回任何内容,所以在这种情况下我只使用**CompletableFuture**调用**join()**来等待它完成。
@@ -1386,18 +1448,20 @@ public class CompletableApply {
cf4.thenApply(Machina::work);
}
}
-/* Output:
+```
+
+输出结果:
+
+```
Machina0: ONE
Machina0: TWO
Machina0: THREE
Machina0: complete
-*/
-
```
**thenApply()**应用一个接受输入并产生输出的函数。在这种情况下,**work()**函数产生与它相同的类型,因此每个得到的**CompletableFuture**仍然被输入为**Machina**,但是(类似于**Streams**中的**map()**)**Function**也可以返回不同的类型,这将反映在返回类型
-您可以在此处看到有关**CompletableFutures**的重要信息:它们会在您执行操作时自动解包并重新包装它们所携带的对象。这样你就不会陷入麻烦的细节,这使得编写和理解代码变得更加简单。
+你可以在此处看到有关**CompletableFutures**的重要信息:它们会在你执行操作时自动解包并重新包装它们所携带的对象。这样你就不会陷入麻烦的细节,这使得编写和理解代码变得更加简单。
我们可以消除中间变量并将操作链接在一起,就像我们使用Streams一样:
@@ -1417,18 +1481,20 @@ public class CompletableApplyChained {
System.out.println(timer.duration());
}
}
-/* Output:
+```
+
+输出结果:
+
+```
Machina0: ONE
Machina0: TWO
Machina0: THREE
Machina0: complete
514
-*/
-
```
在这里,我们还添加了一个**Timer**,它向我们展示每一步增加100毫秒,还有一些额外的开销。
-**CompletableFutures**的一个重要好处是它们鼓励使用私有子类原则(不分享任何东西)。默认情况下,使用**thenApply()**来应用一个不与任何人通信的函数 - 它只需要一个参数并返回一个结果。这是函数式编程的基础,并且它在并发性方面非常有效[^5]。并行流和ComplempleFutures旨在支持这些原则。只要您不决定共享数据(共享非常容易,甚至意外)您可以编写相对安全的并发程序。
+**CompletableFutures**的一个重要好处是它们鼓励使用私有子类原则(不分享任何东西)。默认情况下,使用**thenApply()**来应用一个不与任何人通信的函数 - 它只需要一个参数并返回一个结果。这是函数式编程的基础,并且它在并发性方面非常有效[^5]。并行流和ComplempleFutures旨在支持这些原则。只要你不决定共享数据(共享非常容易,甚至意外)你可以编写相对安全的并发程序。
回调**thenApply()**开始一个操作,在这种情况下,在完成所有任务之前,不会完成**e CompletableFuture**的创建。虽然这有时很有用,但是启动所有任务通常更有价值,这样就可以运行时继续前进并执行其他操作。我们通过在操作结束时添加Async来实现此目的:
@@ -1451,7 +1517,11 @@ public class CompletableApplyAsync {
System.out.println(timer.duration());
}
}
-/* Output:
+```
+
+输出结果:
+
+```
116
Machina0: ONE
Machina0: TWO
@@ -1459,19 +1529,18 @@ Machina0:THREE
Machina0: complete
Machina0: complete
552
-*/
```
同步调用(我们通常使用得那种)意味着“当你完成工作时,返回”,而异步调用以意味着“立刻返回但是继续后台工作。”正如你所看到的,**cf**的创建现在发生得跟快。每次调用 **thenApplyAsync()** 都会立刻返回,因此可以进行下一次调用,整个链接序列的完成速度比以前快得快。
事实上,如果没有回调**cf.join() t**方法,程序会在完成其工作之前退出(尝试取出该行)对**join()**阻止了**main()**进程的进行,直到cf操作完成,我们可以看到大部分时间的确在哪里度过。
-这种“立即返回”的异步能力需要**CompletableFuture**库进行一些秘密工作。特别是,它必须将您需要的操作链存储为一组回调。当第一个后台操作完成并返回时,第二个后台操作必须获取生成的**Machina**并开始工作,当完成后,下一个操作将接管,等等。但是没有我们普通的函数调用序列,通过程序调用栈控制,这个顺序会丢失,所以它使用回调 - 一个函数地址表来存储。
+这种“立即返回”的异步能力需要**CompletableFuture**库进行一些秘密工作。特别是,它必须将你需要的操作链存储为一组回调。当第一个后台操作完成并返回时,第二个后台操作必须获取生成的**Machina**并开始工作,当完成后,下一个操作将接管,等等。但是没有我们普通的函数调用序列,通过程序调用栈控制,这个顺序会丢失,所以它使用回调 - 一个函数地址表来存储。
-幸运的是,您需要了解有关回调的所有信息。程序员将你手工造成的混乱称为“回调地狱”。通过异步调用,**CompletableFuture**为您管理所有回调。除非你知道关于你的系统有什么特定的改变,否则你可能想要使用异步调用。
+幸运的是,你需要了解有关回调的所有信息。程序员将你手工造成的混乱称为“回调地狱”。通过异步调用,**CompletableFuture**为你管理所有回调。除非你知道关于你的系统有什么特定的改变,否则你可能想要使用异步调用。
- 其他操作
-当您查看**CompletableFuture**的Javadoc时,您会看到它有很多方法,但这个方法的大部分来自不同操作的变体。例如,有**thenApply()**,**thenApplyAsync()**和**thenApplyAsync()**的第二种形式,它接受运行任务的**Executor**(在本书中我们忽略了**Executor**选项)。
+当你查看**CompletableFuture**的Javadoc时,你会看到它有很多方法,但这个方法的大部分来自不同操作的变体。例如,有**thenApply()**,**thenApplyAsync()**和**thenApplyAsync()**的第二种形式,它接受运行任务的**Executor**(在本书中我们忽略了**Executor**选项)。
这是一个显示所有“基本”操作的示例,它们不涉及组合两个CompletableFutures或异常(我们将在稍后查看)。首先,我们将重复使用两个实用程序以提供简洁和方便:
@@ -1549,7 +1618,11 @@ public class CompletableOperations {
System.out.println("dependents: " + c.getNumberOfDependents());
}
}
-/* Output:
+```
+
+输出结果:
+
+```
1
runAsync
thenRunAsync
@@ -1568,21 +1641,20 @@ java.util.concurrent.CompletableFuture@6d311334[Complet ed exceptionally]
777
dependents: 1
dependents: 2
-*/
```
**main()**包含一系列可由其**int**值引用的测试。**cfi(1)**演示了**showr()**正常工作。**cfi(2)**是调用**runAsync()**的示例。由于**Runnable**不产生返回值,因此结果是**CompletableFuture **,因此使用**voidr()**。
注意使用**cfi(3)**,**thenRunAsync()**似乎与**runAsync()**一致,差异显示在后续的测试中:
**runAsync()**是一个静态方法,所以你不会像**cfi(2)**一样调用它。相反你可以在**QuittingCompletable.java**中使用它。后续测试中**supplyAsync()**也是静态方法,但是需要一个**Supplier**而不是**Runnable**并产生一个**CompletableFuture**来代替**CompletableFuture**。
-含有“then”的方法将进一步的操作应用于现有的**CompletableFuture **。与**thenRunAsync()**不同的是,将**cfi(4)**,**cfi(5)**和**cfi(6)**的“ then”方法作为未包装的**Integer**的参数。如您通过使用**voidr()**所见,然后**AcceptAsync()**接受了一个**Consumer**,因此不会产生结果。**thenApplyAsync()**接受一个**Function**并因此产生一个结果(该结果的类型可以不同于其参数)。**thenComposeAsync()**与**thenApplyAsync()**非常相似,不同之处在于其Function必须产生已经包装在**CompletableFuture**中的结果。
+含有“then”的方法将进一步的操作应用于现有的**CompletableFuture **。与**thenRunAsync()**不同的是,将**cfi(4)**,**cfi(5)**和**cfi(6)**的“ then”方法作为未包装的**Integer**的参数。如你通过使用**voidr()**所见,然后**AcceptAsync()**接受了一个**Consumer**,因此不会产生结果。**thenApplyAsync()**接受一个**Function**并因此产生一个结果(该结果的类型可以不同于其参数)。**thenComposeAsync()**与**thenApplyAsync()**非常相似,不同之处在于其Function必须产生已经包装在**CompletableFuture**中的结果。
**cfi(7)**示例演示了**obtrudeValue()**,它强制将值作为结果。**cfi(8)**使用**toCompletableFuture()**从**CompletionStage**生成**CompletableFuture**。**c.complete(9)**显示了如何通过给它一个结果来完成一个任务(**future**)(与**obtrudeValue()**相对,后者可能会迫使其结果替换该结果)。
如果你调用**CompletableFuture**中的**cancel()**方法,它也会完成并且是非常好的完成。
如果任务(**future**)未完成,则**getNow()**方法返回**CompletableFuture**的完成值,或者返回**getNow()**的替换参数。
-最后,我们看一下依赖(dependents)的概念。如果我们将两个**thenApplyAsync()**调用链接到**CompletableFuture**上,则依赖项的数量仍为1。但是,如果我们将另一个**thenApplyAsync()**直接附加到**c**,则现在有两个依赖项:两个链和另一个链。这表明您可以拥有一个**CompletionStage**,当它完成时,可以根据其结果派生多个新任务。
+最后,我们看一下依赖(dependents)的概念。如果我们将两个**thenApplyAsync()**调用链接到**CompletableFuture**上,则依赖项的数量仍为1。但是,如果我们将另一个**thenApplyAsync()**直接附加到**c**,则现在有两个依赖项:两个链和另一个链。这表明你可以拥有一个**CompletionStage**,当它完成时,可以根据其结果派生多个新任务。
### 结合CompletableFutures
-第二类**CompletableFuture**方法采用两个**CompletableFuture**并以各种方式将它们组合在一起。一个**CompletableFuture**通常会先于另一个完成,就好像两者都在比赛中一样。这些方法使您可以以不同的方式处理结果。
+第二类**CompletableFuture**方法采用两个**CompletableFuture**并以各种方式将它们组合在一起。一个**CompletableFuture**通常会先于另一个完成,就好像两者都在比赛中一样。这些方法使你可以以不同的方式处理结果。
为了对此进行测试,我们将创建一个任务,该任务将完成的时间作为其参数之一,因此我们可以控制。
**CompletableFuture**首先完成:
@@ -1674,7 +1746,11 @@ public class DualCompletableOperations {
join();
}
}
-/* Output:
+```
+
+输出结果:
+
+```
Workable[BW]
runAfterEither
Workable[AW]
@@ -1713,14 +1789,13 @@ thenAcceptBoth: Workable[AW], Workable[BW]
Workable[AW]
*****************
allOf
- */
```
为了便于访问,**cfA**和**cfB**是静态的。**init()**总是使用较短的延迟(因此总是“获胜”)使用“ B”初始化两者。**join()**是在这两种方法上调用**join()**并显示边框的另一种便捷方法。
所有这些“双重”方法都以一个**CompletableFuture**作为调用该方法的对象,第二个**CompletableFuture**作为第一个参数,然后是要执行的操作。
-通过使用**Shower()**和**void()**,您可以看到“运行”和“接受”是终端操作,而“应用”和“组合”产生了新的承载载荷的**CompletableFutures**。
+通过使用**Shower()**和**void()**,你可以看到“运行”和“接受”是终端操作,而“应用”和“组合”产生了新的承载载荷的**CompletableFutures**。
-方法的名称是不言自明的,您可以通过查看输出来验证这一点。一个特别有趣的方法是CombineAsync(),它等待两个**CompletableFuture**完成,然后将它们都交给BiFunction,然后BiFunction可以将结果加入到所得**CompletableFuture**的有效负载中。
+方法的名称是不言自明的,你可以通过查看输出来验证这一点。一个特别有趣的方法是CombineAsync(),它等待两个**CompletableFuture**完成,然后将它们都交给BiFunction,然后BiFunction可以将结果加入到所得**CompletableFuture**的有效负载中。
### 模拟
@@ -1814,11 +1889,11 @@ public class FrostedCake {
}
```
-一旦您对背后的想法感到满意。**CompletableFutures**它们相对易于使用。
+一旦你对背后的想法感到满意。**CompletableFutures**它们相对易于使用。
### 例外情况
-与**CompletableFutur**e在处理链中包装对象的方式相同,它还可以缓冲异常。这些不会在处理过程中显示给调用者,而只会在您尝试提取结果时显示。为了展示它们是如何工作的,我们将从创建一个在某些情况下引发异常的类开始:
+与**CompletableFutur**e在处理链中包装对象的方式相同,它还可以缓冲异常。这些不会在处理过程中显示给调用者,而只会在你尝试提取结果时显示。为了展示它们是如何工作的,我们将从创建一个在某些情况下引发异常的类开始:
```java
// concurrent/Breakable.java
@@ -1843,12 +1918,11 @@ public class Breakable {
return b;
}
}
-
```
-**failcount**为正时,每次将对象传递给**work()**方法可减少**failcount**。当它为零时,**work()**会引发异常。如果您给它的**failcount**为零,则它永远不会引发异常。
+**failcount**为正时,每次将对象传递给**work()**方法可减少**failcount**。当它为零时,**work()**会引发异常。如果你给它的**failcount**为零,则它永远不会引发异常。
请注意,它报告在抛出异常时抛出异常。
-在下面的**test()**方法中,**work()**多次应用于**Breakable**,因此,如果**failcount**在范围内,则会引发异常。但是,在测试**A**到**E**中,您可以从输出中看到抛出了异常,但是它们从未出现:
+在下面的**test()**方法中,**work()**多次应用于**Breakable**,因此,如果**failcount**在范围内,则会引发异常。但是,在测试**A**到**E**中,你可以从输出中看到抛出了异常,但是它们从未出现:
```java
// concurrent/CompletableExceptions.java
@@ -1893,8 +1967,11 @@ public class CompletableExceptions {
}
}
}
+```
-/* Output:
+输出结果:
+
+```
Throwing Exception for A
Breakable_B [1]
Throwing Exception for B
@@ -1920,13 +1997,12 @@ Throwing Exception for H
true
done? false
java.lang.RuntimeException: forced
-*/
```
测试**A**到**E**运行到抛出异常的地步,然后……什么都没有。只有在测试**F**中调用**get()**时,我们才能看到抛出的异常。
-测试**G**显示,您可以首先检查在处理过程中是否引发了异常,而没有引发该异常。但是,测试H告诉我们,无论异常成功与否,异常仍然可以被视为“完成”
+测试**G**显示,你可以首先检查在处理过程中是否引发了异常,而没有引发该异常。但是,测试H告诉我们,无论异常成功与否,异常仍然可以被视为“完成”
代码的最后一部分显示了如何在**CompletableFuture**中插入异常,而不管是否存在任何故障。
-加入或获取结果时,我们不会使用粗略的try-catch,而是使用**CompletableFuture**提供的更复杂的机制来自动响应异常。您可以使用与所有**CompletableFuture**相同的表格来执行此操作:在链中插入**CompletableFuture**调用。有三个选项:**exclusively(**),**handle()**和**whenComplete()**:
+加入或获取结果时,我们不会使用粗略的try-catch,而是使用**CompletableFuture**提供的更复杂的机制来自动响应异常。你可以使用与所有**CompletableFuture**相同的表格来执行此操作:在链中插入**CompletableFuture**调用。有三个选项:**exclusively(**),**handle()**和**whenComplete()**:
```java
// concurrent/CatchCompletableExceptions.java
@@ -1973,7 +2049,11 @@ public class CatchCompletableExceptions {
handleException(0);
}
}
-/* Output:
+```
+
+输出结果:
+
+```
**** Failure Mode ****
Breakable_exceptionally [1]
Throwing Exception for exceptionally
@@ -2002,12 +2082,11 @@ Breakable_whenComplete [-3]
Breakable_whenComplete [-4]
Breakable_whenComplete [-4] OK
result: Breakable_whenComplete [-4]
-*/
```
只有在有异常的情况下,**exclusively()**参数才会运行。**Exclusively()**的局限性在于,该函数只能返回输入的相同类型的值。**exclusively()**通过将一个好的对象重新插入流中而恢复到可行状态。
-**handle()**始终被调用,您必须检查一下**fail**是否为**true**才能查看是否发生了异常。但是**handle()**可以产生任何新类型,因此它使您可以执行处理,而不仅可以像**exception()**那样进行恢复。
-**whenComplete()**就像**handle()**一样,您必须测试是否失败,但是该参数是使用者,并且不会修改正在传递的结果对象。
+**handle()**始终被调用,你必须检查一下**fail**是否为**true**才能查看是否发生了异常。但是**handle()**可以产生任何新类型,因此它使你可以执行处理,而不仅可以像**exception()**那样进行恢复。
+**whenComplete()**就像**handle()**一样,你必须测试是否失败,但是该参数是使用者,并且不会修改正在传递的结果对象。
### 流异常
@@ -2041,20 +2120,23 @@ public class StreamExceptions {
}
}
}
-/* Output:
+```
+
+输出结果:
+
+```
Entering try
Breakable_C [2]
Breakable_C [1]
Throwing Exception for C
Breakable_C failed
-*/
```
-使用**CompletableFutures**,我们看到了测试**A**到**E**的进展,但是使用**Streams**,直到您应用了终端操作(如[1]的**forEach()**),一切都没有开始。**CompletableFuture**执行工作并捕获任何异常以供以后检索。比较这两者并不是一件容易的事,因为**Stream**没有终端操作根本无法执行任何操作,但是**Stream**绝对不会存储其异常。
+使用**CompletableFutures**,我们看到了测试**A**到**E**的进展,但是使用**Streams**,直到你应用了终端操作(如[1]的**forEach()**),一切都没有开始。**CompletableFuture**执行工作并捕获任何异常以供以后检索。比较这两者并不是一件容易的事,因为**Stream**没有终端操作根本无法执行任何操作,但是**Stream**绝对不会存储其异常。
### 检查异常
-CompletableFutures和并行Streams都不支持包含已检查异常的操作。相反,您必须在调用操作时处理检查到的异常,这会产生不太优雅的代码:
+CompletableFutures和并行Streams都不支持包含已检查异常的操作。相反,你必须在调用操作时处理检查到的异常,这会产生不太优雅的代码:
```java
// concurrent/ThrowsChecked.java
@@ -2095,15 +2177,15 @@ public class ThrowsChecked {
}
```
-如果您尝试像对 **nochecked()** 一样对 **withchecked()** 使用方法引用,则编译器会抱怨[1]和[2]。相反,您必须写出lambda表达式(或编写一个不会引发异常的包装器方法)。
+如果你尝试像对 **nochecked()** 一样对 **withchecked()** 使用方法引用,则编译器会抱怨[1]和[2]。相反,你必须写出lambda表达式(或编写一个不会引发异常的包装器方法)。
## 死锁
-由于任务可能会被阻塞,因此一个任务有可能卡在等待另一个任务上,而任务又在等待另一个任务,依此类推,直到链回到第一个任务上。您会遇到一个不断循环的任务,彼此等待,没有人能动。这称为死锁[^6]
-如果您尝试运行某个程序并立即陷入死锁,则可以立即查找该错误。真正的问题是,当您的程序看起来运行良好,但具有隐藏潜力死锁。在这里,您可能没有任何迹象表明可能发生死锁,因此该缺陷在您的程序中是潜在的,直到它意外发生为止(通常是对客户而言(几乎肯定很难复制))。因此,通过仔细的程序设计防止死锁是开发并发系统的关键部分。
+由于任务可能会被阻塞,因此一个任务有可能卡在等待另一个任务上,而任务又在等待另一个任务,依此类推,直到链回到第一个任务上。你会遇到一个不断循环的任务,彼此等待,没有人能动。这称为死锁[^6]
+如果你尝试运行某个程序并立即陷入死锁,则可以立即查找该错误。真正的问题是,当你的程序看起来运行良好,但具有隐藏潜力死锁。在这里,你可能没有任何迹象表明可能发生死锁,因此该缺陷在你的程序中是潜在的,直到它意外发生为止(通常是对客户而言(几乎肯定很难复制))。因此,通过仔细的程序设计防止死锁是开发并发系统的关键部分。
埃德斯·迪克斯特拉(Essger Dijkstra)发明的"哲学家进餐"问题是经典的死锁例证。基本描述指定了五位哲学家(此处显示的示例允许任何数字)。这些哲学家将一部分时间花在思考上,一部分时间在吃饭上。他们在思考的时候并不需要任何共享资源,但是他们使用的餐具数量有限。在最初的问题描述中,器物是叉子,需要两个叉子才能从桌子中间的碗里取出意大利面。常见的版本是使用筷子。显然,每个哲学家都需要两个筷子才能吃饭。
引入了一个困难:作为哲学家,他们的钱很少,所以他们只能买五根筷子(更普遍地说,筷子的数量与哲学家相同)。它们之间围绕桌子隔开。当一个哲学家想要吃饭时,该哲学家必须拿起左边和右边的筷子。如果任一侧的哲学家都在使用所需的筷子,则我们的哲学家必须等待,直到必要的筷子可用为止。
-**StickHolder**类通过将单个筷子保持在大小为1的**BlockingQueue**中来管理它。**BlockingQueue**是一个设计用于在并发程序中安全使用的集合,如果您调用take()并且队列为空,则它将阻塞(等待)。将新元素放入队列后,将释放该块并返回该值:
+**StickHolder**类通过将单个筷子保持在大小为1的**BlockingQueue**中来管理它。**BlockingQueue**是一个设计用于在并发程序中安全使用的集合,如果你调用take()并且队列为空,则它将阻塞(等待)。将新元素放入队列后,将释放该块并返回该值:
```java
// concurrent/StickHolder.java
@@ -2198,17 +2280,17 @@ public class DiningPhilosophers {
}
```
-当您停止查看输出时,该程序将死锁。但是,根据您的计算机配置,您可能不会看到死锁。看来这取决于计算机上的内核数7。两个核心似乎不会产生死锁,但似乎有两个以上的核心很容易产生死锁。此行为使该示例更好地说明了死锁,因为您可能正在具有两个内核的计算机上编写程序(如果确实是导致问题的原因),并且确信该程序可以正常工作,只能启动它将其安装在另一台计算机上时出现死锁。请注意,仅仅因为您不容易看到死锁,并不意味着该程序就不会在两核计算机上死锁。该程序仍然容易死锁,很少发生-可以说是最坏的情况,因为问题不容易解决。
+当你停止查看输出时,该程序将死锁。但是,根据你的计算机配置,你可能不会看到死锁。看来这取决于计算机上的内核数7。两个核心似乎不会产生死锁,但似乎有两个以上的核心很容易产生死锁。此行为使该示例更好地说明了死锁,因为你可能正在具有两个内核的计算机上编写程序(如果确实是导致问题的原因),并且确信该程序可以正常工作,只能启动它将其安装在另一台计算机上时出现死锁。请注意,仅仅因为你不容易看到死锁,并不意味着该程序就不会在两核计算机上死锁。该程序仍然容易死锁,很少发生-可以说是最坏的情况,因为问题不容易解决。
在DiningPhilosophers构造函数中,每个哲学家都获得一个左右StickHolder的引用。除最后一个哲学家外,每个哲学家都通过以下方式初始化:
哲学家之间的下一双筷子。最后一位哲学家右手的筷子为零,因此圆桌会议完成了。那是因为最后一位哲学家正坐在第一个哲学家的旁边,而且他们俩都共用零筷子。[1]显示了以n为模数选择的右摇杆,将最后一个哲学家缠绕在第一个哲学家的旁边。
现在,所有哲学家都可以尝试吃饭,每个哲学家都在旁边等待哲学家放下筷子。
要开始在[3]上运行的每个Philosopher,我调用runAsync(),这意味着DiningPhilosophers构造函数立即在[4]处返回。没有任何东西可以阻止main()完成,该程序只是退出而无济于事。Nap对象阻止main()退出,然后在三秒钟后强制退出(可能是)死锁的程序。
-在给定的配置中,哲学家几乎没有时间思考。因此,他们都在尝试吃饭时争夺筷子,而且僵局往往很快发生。您可以更改此:
+在给定的配置中,哲学家几乎没有时间思考。因此,他们都在尝试吃饭时争夺筷子,而且僵局往往很快发生。你可以更改此:
1. 通过增加[4]的值来添加更多哲学家。
2. 在Philosopher.java中取消注释行[1]。
-任一种方法都会减少死锁的可能性,这表明编写并发程序并认为它是安全的危险,因为它似乎“在我的机器上运行正常”。您可以轻松地说服自己该程序没有死锁,即使它不是。这个例子很有趣,因为它演示了程序似乎可以正确运行,同时仍然容易出现死锁。
+任一种方法都会减少死锁的可能性,这表明编写并发程序并认为它是安全的危险,因为它似乎“在我的机器上运行正常”。你可以轻松地说服自己该程序没有死锁,即使它不是。这个例子很有趣,因为它演示了程序似乎可以正确运行,同时仍然容易出现死锁。
为了解决该问题,我们观察到当四个同时满足条件:
1. 互斥。任务使用的至少一种资源必须不可共享。在这里,筷子一次只能由一位哲学家使用。
@@ -2216,19 +2298,20 @@ public class DiningPhilosophers {
3. 不能抢先从任务中夺走资源。任务仅作为正常事件释放资源。我们的哲学家很有礼貌,他们不会抓住其他哲学家的筷子。
4. 可能发生循环等待,即一个任务等待另一个任务持有的资源,而该任务又等待另一个任务持有的资源,依此类推,直到一个任务正在等待另一个任务持有的资源。第一项任务,从而使一切陷入僵局。在**DiningPhilosophers.java**中,发生循环等待是因为每个哲学家都先尝试获取右筷子,然后再获取左筷子。
-因为必须满足所有这些条件才能导致死锁,所以您只能阻止其中一个解除死锁。在此程序中,防止死锁的一种简单方法是打破第四个条件。之所以会发生这种情况,是因为每个哲学家都尝试按照特定的顺序拾起自己的筷子:先右后左。因此,每个哲学家都有可能在等待左手的同时握住右手的筷子,从而导致循环等待状态。但是,如果其中一位哲学家尝试首先拿起左筷子,则该哲学家决不会阻止紧邻右方的哲学家拿起筷子,从而排除了循环等待。
+因为必须满足所有这些条件才能导致死锁,所以你只能阻止其中一个解除死锁。在此程序中,防止死锁的一种简单方法是打破第四个条件。之所以会发生这种情况,是因为每个哲学家都尝试按照特定的顺序拾起自己的筷子:先右后左。因此,每个哲学家都有可能在等待左手的同时握住右手的筷子,从而导致循环等待状态。但是,如果其中一位哲学家尝试首先拿起左筷子,则该哲学家决不会阻止紧邻右方的哲学家拿起筷子,从而排除了循环等待。
在**DiningPhilosophers.java**中,取消注释[1]和其后的一行。这将原来的哲学家[1]替换为筷子颠倒的哲学家。通过确保第二位哲学家拾起并在右手之前放下左筷子,我们消除了死锁的可能性。
-这只是解决问题的一种方法。您也可以通过防止其他情况之一来解决它。
-没有语言支持可以帮助防止死锁;您有责任通过精心设计来避免这种情况。对于试图调试死锁程序的人来说,这些都不是安慰。当然,避免并发问题的最简单,最好的方法是永远不要共享资源-不幸的是,这并不总是可能的。
+这只是解决问题的一种方法。你也可以通过防止其他情况之一来解决它。
+没有语言支持可以帮助防止死锁;你有责任通过精心设计来避免这种情况。对于试图调试死锁程序的人来说,这些都不是安慰。当然,避免并发问题的最简单,最好的方法是永远不要共享资源-不幸的是,这并不总是可能的。
## 构造函数非线程安全
-当你在脑子里想象一个对象构造的过程,你会很容易认为这个过程是线程安全的。毕竟,在对象初始化完成前没人能见到这个对象,所以又怎么会产生对于这个对象的争议呢?确实,java语言规范(JLS)自信满满地陈述道:“没有必要使构造器同步,因为它会锁定正在构造的对象,而这通常会使得该对象直到其所有构造器完成所有工作后,才对其他线程可见。”
-不幸的是,对象构造过程像其他任何事物一样容易受到共享内存并发问题的影响,只是作用机制可能更微妙罢了。
-考虑使用静态字段为每个对象自动创建唯一标识符的过程。为了测试其不同的实现过程,我们从一个接口开始:
+当你在脑子里想象一个对象构造的过程,你会很容易认为这个过程是线程安全的。毕竟,在对象初始化完成前对外不可见,所以又怎会对此产生争议呢?确实,Java 语言规范(JLS)自信满满地陈述道:“没必要使构造器的线程同步,因为它会锁定正在构造的对象,直到构造器完成初始化后才对其他线程可见。”
+不幸的是,对象的构造过程如其他操作一样,也会受到共享内存并发问题的影响,只是作用机制可能更微妙罢了。
+
+设想下使用一个静态字段为每个对象自动创建唯一标识符的过程。为了测试其不同的实现过程,我们从一个接口开始。代码示例:
```java
//concurrent/HasID.java
@@ -2237,7 +2320,7 @@ public interface HasID {
}
```
-然后StaticIDField类以显式方式实现该接口:
+然后 **StaticIDField** 类显式地实现该接口。代码示例:
```java
// concurrent/StaticIDField.java
@@ -2248,7 +2331,7 @@ public class StaticIDField implements HasID {
}
```
-正如您所想的,此类是一个简单无害的类,它甚至没有一个显式的构造器来引发问题。当我们运行多个用于创建此类对象的线程时,究竟会发生什么,为了搞清楚这点,我们做了以下测试:
+如你所想,该类是个简单无害的类,它甚至都没一个显式的构造器来引发问题。当我们运行多个用于创建此类对象的线程时,究竟会发生什么?为了搞清楚这点,我们做了以下测试。代码示例:
```java
// concurrent/IDChecker.java
@@ -2293,10 +2376,10 @@ public class IDChecker {
}
```
-MakeObjects类是一个供应者类,包含一个能够产生List\类型的列表对象的get()方法。通过从每个HasID对象提取ID并放入列表中来生成这个列表对象,而test()方法则创建了两个并行的CompletableFuture对象,用于运行MakeObjects供应者类,然后获取运行结果。
-使用Guava库中的Sets.intersection()方法,计算出这两个返回的List\对象中有多少相同的ID(使用谷歌Guava库里的方法比使用官方的retainAll()方法速度快得多)。
+**MakeObjects** 类是一个生产者类,包含一个能够产生 List\ 类型的列表对象的 `get()` 方法。通过从每个 `HasID` 对象提取 `ID` 并放入列表中来生成这个列表对象,而 `test()` 方法则创建了两个并行的 **CompletableFuture** 对象,用于运行 **MakeObjects** 生产者类,然后获取运行结果。
+使用 Guava 库中的 **Sets.**`intersection()` 方法,计算出这两个返回的 List\ 对象中有多少相同的 `ID`(使用谷歌 Guava 库里的方法比使用官方的 `retainAll()` 方法速度快得多)。
-现在我们可以测试上面的StaticIDField类了:
+现在我们可以测试上面的 **StaticIDField** 类了。代码示例:
```java
// concurrent/TestStaticIDField.java
@@ -2306,12 +2389,15 @@ public class TestStaticIDField {
IDChecker.test(StaticIDField::new);
}
}
-/* Output:
+```
+
+输出结果:
+
+```
13287
-*/
```
-结果中出现了很多重复项。很显然,纯静态int用于构造过程并不是线程安全的。让我们使用AtomicInteger来使其变为线程安全的:
+结果中出现了很多重复项。很显然,纯静态 `int` 用于构造过程并不是线程安全的。让我们使用 **AtomicInteger** 来使其变为线程安全的。代码示例:
```java
// concurrent/GuardedIDField.java
@@ -2327,9 +2413,12 @@ public class GuardedIDField implements HasID {
public static void main(String[] args) { IDChecker.test(GuardedIDField::new);
}
}
-/* Output:
+```
+
+输出结果:
+
+```
0
-*/
```
构造器有一种更微妙的状态共享方式:通过构造器参数:
@@ -2379,14 +2468,17 @@ public class SharedConstructorArgument{
IDChecker.test(() -> new SharedUser(safe));
}
}
-/* Output:
+```
+
+输出结果:
+
+```
24838
0
-*/
```
-在这里,SharedUser构造器实际上共享了相同的参数。即使SharedUser以完全无害且合理的方式使用其自己的参数,其构造器的调用方式也会引起冲突。SharedUser甚至不知道它是以这种方式调用的,更不必说控制它了。
-同步构造器并不被java语言所支持,但是通过使用同步语块来创建你自己的同步构造器是可能的(请参阅附录:Low-Level Concurrency,来进一步了解同步关键字——synchronized)。尽管JLS(java语言规范)这样陈述道:“……它会锁定正在构造的对象”,但这并不是真的——构造器实际上只是一个静态方法,因此同步构造器实际上会锁定该类的Class对象。我们可以通过创建自己的静态对象并锁定它,来达到与同步构造器相同的效果:
+在这里,**SharedUser** 构造器实际上共享了相同的参数。即使 **SharedUser** 以完全无害且合理的方式使用其自己的参数,其构造器的调用方式也会引起冲突。**SharedUser** 甚至不知道它是以这种方式调用的,更不必说控制它了。
+同步构造器并不被java语言所支持,但是通过使用同步语块来创建你自己的同步构造器是可能的(请参阅附录:[并发底层原理](./Appendix-Low-Level-Concurrency.md),来进一步了解同步关键字—— `synchronized`)。尽管JLS(java语言规范)这样陈述道:“……它会锁定正在构造的对象”,但这并不是真的——构造器实际上只是一个静态方法,因此同步构造器实际上会锁定该类的Class对象。我们可以通过创建自己的静态对象并锁定它,来达到与同步构造器相同的效果:
```java
// concurrent/SynchronizedConstructor.java
@@ -2416,10 +2508,12 @@ public class SynchronizedConstructor{
IDChecker.test(() -> new SyncConstructor(unsafe));
}
}
-/* Output:
- 0
-*/
+```
+
+输出结果:
+```
+ 0
```
Unsafe类的共享使用现在就变得安全了。另一种方法是将构造器设为私有(因此可以防止继承),并提供一个静态Factory方法来生成新对象:
@@ -2451,17 +2545,20 @@ public class SynchronizedFactory{
IDChecker.test(() -> SyncFactory.factory(unsafe));
}
}
-/* Output:
+```
+
+输出结果:
+
+```
0
-*/
```
-通过同步静态工厂方法,可以在构造过程中锁定Class对象。
-这些示例充分表明了在并发Java程序中检测和管理共享状态有多困难。即使您采取“不共享任何内容”的策略,也很容易产生意外的共享事件。
+通过同步静态工厂方法,可以在构造过程中锁定 **Class** 对象。
+这些示例充分表明了在并发Java程序中检测和管理共享状态有多困难。即使你采取“不共享任何内容”的策略,也很容易产生意外的共享事件。
## 复杂性和代价
-假设您正在做披萨,我们把从整个流程的当前步骤到下一个步骤所需的工作量,在这里一一表示为枚举变量的一部分:
+假设你正在做披萨,我们把从整个流程的当前步骤到下一个步骤所需的工作量,在这里一一表示为枚举变量的一部分:
@@ -2562,7 +2659,11 @@ public class OnePizza{
}));
}
}
-/* Output:
+```
+
+输出结果:
+
+```
Pizza 0: ROLLED
Pizza 0: SAUCED
Pizza 0: CHEESED
@@ -2571,10 +2672,9 @@ Pizza 0: BAKED
Pizza 0: SLICED
Pizza 0: BOXED
1622
-*/
```
-时间以毫秒为单位,加总所有步骤的工作量,会得出与我们的期望值相符的数字。 如果您以这种方式制作了五个披萨,那么您会认为它花费的时间是原来的五倍。 但是,如果这还不够快怎么办? 我们可以从尝试并行流方法开始:
+时间以毫秒为单位,加总所有步骤的工作量,会得出与我们的期望值相符的数字。 如果你以这种方式制作了五个披萨,那么你会认为它花费的时间是原来的五倍。 但是,如果这还不够快怎么办? 我们可以从尝试并行流方法开始:
```java
// concurrent/PizzaStreams.java
@@ -2593,7 +2693,11 @@ public class PizzaStreams{
.forEach(za -> { while(!za.complete()) za.next(); }); System.out.println(timer.duration());
}
}
-/* Output:
+```
+
+输出结果:
+
+```
Pizza 2: ROLLED
Pizza 0: ROLLED
Pizza 1: ROLLED
@@ -2630,12 +2734,11 @@ Pizza 0:BOXED
Pizza 4:BOXED
Pizza 3:BOXED
1739
-*/
```
现在,我们制作五个披萨的时间与制作单个披萨的时间就差不多了。 尝试删除标记为[1]的行后,你会发现它花费的时间是原来的五倍。 你还可以尝试将QUANTITY更改为4、8、10、16和17,看看会有什么不同,并猜猜看为什么会这样。
-PizzaStreams类产生的每个并行流在它的forEach()内完成所有工作,如果我们将其各个步骤用映射的方式一步一步处理,情况会有所不同吗?
+**PizzaStreams** 类产生的每个并行流在它的forEach()内完成所有工作,如果我们将其各个步骤用映射的方式一步一步处理,情况会有所不同吗?
```java
// concurrent/PizzaParallelSteps.java
@@ -2663,7 +2766,11 @@ public class PizzaParallelSteps{
System.out.println(timer.duration());
}
}
-/* Output:
+```
+
+输出结果:
+
+```
Pizza 2: ROLLED
Pizza 0: ROLLED
Pizza 1: ROLLED
@@ -2705,12 +2812,11 @@ Pizza 4: BOXED
Pizza4: complete
Pizza3: complete
1738
-*/
```
-答案是“否”,事后看来这并不奇怪,因为每个披萨都需要按顺序执行步骤。因此,没法通过分步执行操作来进一步提高速度,就像上文的PizzaParallelSteps.java里面展示的一样。
+答案是“否”,事后看来这并不奇怪,因为每个披萨都需要按顺序执行步骤。因此,没法通过分步执行操作来进一步提高速度,就像上文的 `PizzaParallelSteps.java` 里面展示的一样。
-我们可以使用CompletableFutures重写这个例子:
+我们可以使用 **CompletableFutures** 重写这个例子:
```java
// concurrent/CompletablePizza.java
@@ -2755,7 +2861,11 @@ public class CompletablePizza{
System.out.println(timer.duration());
}
}
-/* Output:
+```
+
+输出结果:
+
+```
169
Pizza 0: ROLLED
Pizza 1: ROLLED
@@ -2798,18 +2908,17 @@ Pizza2: complete
Pizza3: complete
Pizza4: complete
1797
-*/
```
-并行流和CompletableFutures是Java并发工具箱中最先进发达的技术。 您应该始终首先选择其中之一。 当一个问题很容易并行处理时,或者说,很容易把数据分解成相同的、易于处理的各个部分时,使用并行流方法处理最为合适(而如果您决定不借助它而由自己完成,您就必须撸起袖子,深入研究Spliterator的文档)。
+并行流和 **CompletableFutures** 是 Java 并发工具箱中最先进发达的技术。 你应该始终首先选择其中之一。 当一个问题很容易并行处理时,或者说,很容易把数据分解成相同的、易于处理的各个部分时,使用并行流方法处理最为合适(而如果你决定不借助它而由自己完成,你就必须撸起袖子,深入研究Spliterator的文档)。
-而当工作的各个部分内容各不相同时,使用CompletableFutures是最好的选择。比起面向数据,CompletableFutures更像是面向任务的。
+而当工作的各个部分内容各不相同时,使用 **CompletableFutures** 是最好的选择。比起面向数据,CompletableFutures** 更像是面向任务的。
对于披萨问题,结果似乎也没有什么不同。实际上,并行流方法看起来更简洁,仅出于这个原因,我认为并行流作为解决问题的首次尝试方法更具吸引力。
-由于制作披萨总需要一定的时间,无论您使用哪种并发方法,你能做到的最好情况,是在制作一个披萨的相同时间内制作n个披萨。 在这里当然很容易看出来,但是当您处理更复杂的问题时,您就可能忘记这一点。 通常,在项目开始时进行粗略的计算,就能很快弄清楚最大可能的并行吞吐量,这可以防止您因为采取无用的加快运行速度的举措而忙得团团转。
+由于制作披萨总需要一定的时间,无论你使用哪种并发方法,你能做到的最好情况,是在制作一个披萨的相同时间内制作n个披萨。 在这里当然很容易看出来,但是当你处理更复杂的问题时,你就可能忘记这一点。 通常,在项目开始时进行粗略的计算,就能很快弄清楚最大可能的并行吞吐量,这可以防止你因为采取无用的加快运行速度的举措而忙得团团转。
-使用CompletableFutures或许可以轻易地带来重大收益,但是在尝试更进一步时需要倍加小心,因为额外增加的成本和工作量会非常容易远远超出你之前拼命挤出的那一点点收益。
+使用 **CompletableFutures** 或许可以轻易地带来重大收益,但是在尝试更进一步时需要倍加小心,因为额外增加的成本和工作量会非常容易远远超出你之前拼命挤出的那一点点收益。
@@ -2820,7 +2929,7 @@ Pizza4: complete
[^3]:有人谈论在Java——10中围绕泛型做一些类似的基本改进,这将是非常令人难以置信的。
[^4]:这是一种有趣的,虽然不一致的方法。通常,我们期望在公共接口上使用显式类表示不同的行为
[^5]:不,永远不会有纯粹的功能性Java。我们所能期望的最好的是一种在JVM上运行的全新语言。
-[^6]:当两个任务能够更改其状态以使它们不会被阻止但它们从未取得任何有用的进展时,您也可以使用活动锁。
+[^6]:当两个任务能够更改其状态以使它们不会被阻止但它们从未取得任何有用的进展时,你也可以使用活动锁。
From 3e9902373c973a06e4cbb0744568cfa811db2691 Mon Sep 17 00:00:00 2001
From: LeonTheProfessional
Date: Sun, 19 Jan 2020 19:40:03 +0800
Subject: [PATCH 010/224] =?UTF-8?q?=E4=B8=80=E4=BA=9B=E5=8B=98=E8=AF=AF=20?=
=?UTF-8?q?(#358)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
* 更改了移位运算符中较有误导性的描述
“仅使用右侧的5 个低阶位” 中的“右侧”之前主语不明确,容易引起歧义。改为“仅使用右值的5 个低阶位” 则明确表示为等号右侧值的5个低阶位,使语义更清晰。
* 翻译勘误
”数字的二进制表示称为有符号的两个补数。“这句的原文为“The binary representation of the numbers is referred to as signed twos complement”。这里的“2's complement” 为计算机术语“补码”,所以这句话应翻译为“数字的二进制表示形式是带符号的补码”。
* 内部类勘误
row:913 原文为Calleel,这里可能将1写成了字母l
row:1429 原文为LocalInnerClass$1LocalCounter.class,根据我的理解应该为LocalInnerClass$LocalCounter.class,请主编校对
---
docs/book/11-Inner-Classes.md | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/docs/book/11-Inner-Classes.md b/docs/book/11-Inner-Classes.md
index b4157f75..ce3e0043 100644
--- a/docs/book/11-Inner-Classes.md
+++ b/docs/book/11-Inner-Classes.md
@@ -910,7 +910,7 @@ Other operation
3
```
-这个例子进一步展示了外围类实现一个接口与内部类实现此接口之间的区别。就代码而言,**Calleel** 是更简单的解决方式。**Callee2** 继承自 **MyIncrement**,后者已经有了一个不同的 `increment()` 方法,并且与 **Incrementable** 接口期望的 `increment()` 方法完全不相关。所以如果 **Callee2** 继承了 **MyIncrement**,就不能为了 **Incrementable** 的用途而覆盖 `increment()` 方法,于是只能使用内部类独立地实现 **Incrementable**,还要注意,当创建了一个内部类时,并没有在外围类的接口中添加东西,也没有修改外围类的接口。
+这个例子进一步展示了外围类实现一个接口与内部类实现此接口之间的区别。就代码而言,**Callee1** 是更简单的解决方式。**Callee2** 继承自 **MyIncrement**,后者已经有了一个不同的 `increment()` 方法,并且与 **Incrementable** 接口期望的 `increment()` 方法完全不相关。所以如果 **Callee2** 继承了 **MyIncrement**,就不能为了 **Incrementable** 的用途而覆盖 `increment()` 方法,于是只能使用内部类独立地实现 **Incrementable**,还要注意,当创建了一个内部类时,并没有在外围类的接口中添加东西,也没有修改外围类的接口。
注意,在 **Callee2** 中除了 `getCallbackReference()` 以外,其他成员都是 **private** 的。要想建立与外部世界的任何连接,接口 **Incrementable** 都是必需的。在这里可以看到,**interface** 是如何允许接口与接口的实现完全独立的。
内部类 **Closure** 实现了 **Incrementable**,以提供一个返回 **Callee2** 的“钩子”(hook)-而且是一个安全的钩子。无论谁获得此 **Incrementable** 的引用,都只能调用 `increment()`,除此之外没有其他功能(不像指针那样,允许你做很多事情)。
@@ -1426,7 +1426,7 @@ Anonymous inner 9
```java
Counter.class
LocalInnerClass$1.class
-LocalInnerClass$1LocalCounter.class
+LocalInnerClass$LocalCounter.class
LocalInnerClass.class
```
From 74fd7f7104b00d502c953fce69da62d9fcee7cdb Mon Sep 17 00:00:00 2001
From: Qimiao Chen
Date: Mon, 20 Jan 2020 16:13:34 +0800
Subject: [PATCH 011/224] Fix typo in Streams (#359)
---
docs/book/14-Streams.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/docs/book/14-Streams.md b/docs/book/14-Streams.md
index 764d3ac9..3633550e 100644
--- a/docs/book/14-Streams.md
+++ b/docs/book/14-Streams.md
@@ -42,7 +42,7 @@ public class Randoms {
首先,我们给 **Random** 对象一个种子(以便程序再次运行时产生相同的输出)。`ints()` 方法产生一个流并且 `ints()` 方法有多种方式的重载 — 两个参数限定了数值产生的边界。这将生成一个整数流。我们可以使用中间流操作(intermediate stream operation) `distinct()` 来获取它们的非重复值,然后使用 `limit()` 方法获取前 7 个元素。接下来,我们使用 `sorted()` 方法排序。最终使用 `forEach()` 方法遍历输出,它根据传递给它的函数对每个流对象执行操作。在这里,我们传递了一个可以在控制台显示每个元素的方法引用。`System.out::println` 。
-注意 `Randoms.java` 中没有声明任何变量。流流可以在不使用赋值或可变数据的情况下对有状态的系统建模,这非常有用。
+注意 `Randoms.java` 中没有声明任何变量。流可以在不使用赋值或可变数据的情况下对有状态的系统建模,这非常有用。
声明式编程(Declarative programming)是一种:声明要做什么,而非怎么做的编程风格。正如我们在函数式编程中所看到的。**注意**,命令式编程的形式更难以理解。代码示例:
From ab08db16aa9152d412a57fd653c3ca7ecb927380 Mon Sep 17 00:00:00 2001
From: Crimson_Loves_Code <39024757+OrientationJump@users.noreply.github.com>
Date: Tue, 21 Jan 2020 14:05:37 +0800
Subject: [PATCH 012/224] =?UTF-8?q?Fix=20issue:#112=20=E7=BF=BB=E8=AF=91Su?=
=?UTF-8?q?mmary=20(#360)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Signed-off-by: crimson <1291463831@qq.com>
---
docs/book/24-Concurrent-Programming.md | 97 +++++++++++++++++++++++---
1 file changed, 86 insertions(+), 11 deletions(-)
diff --git a/docs/book/24-Concurrent-Programming.md b/docs/book/24-Concurrent-Programming.md
index 5a82b1e6..9ecea280 100644
--- a/docs/book/24-Concurrent-Programming.md
+++ b/docs/book/24-Concurrent-Programming.md
@@ -247,7 +247,7 @@ Java实验告诉我们,结果是悄然灾难性的。程序员很容易陷入
这是我们将在本章的其余部分介绍的内容。请记住,本章的重点是使用最新的高级Java并发结构。使用这些使得你的生活比旧的替代品更加轻松。但是,你仍会在遗留代码中遇到一些低级工具。有时,你可能会被迫自己使用其中的一些。附录:[并发底层原理](./Appendix-Low-Level-Concurrency.md)包含一些更原始的Java并发元素的介绍。
-- Parallel Streams(并发流)
+- Parallel Streams(并行流)
到目前为止,我已经强调了Java 8 Streams提供的改进语法。现在你对该语法(作为一个粉丝,我希望)感到满意,你可以获得额外的好处:你可以通过简单地将parallel()添加到表达式来并行化流。这是一种简单,强大,坦率地说是利用多处理器的惊人方式
添加parallel()来提高速度似乎是微不足道的,但是,唉,它就像你刚刚在[残酷的真相](#The-Brutal-Truth)中学到的那样简单。我将演示并解释一些盲目添加parallel()到Stream表达式的缺陷。
@@ -2280,7 +2280,7 @@ public class DiningPhilosophers {
}
```
-当你停止查看输出时,该程序将死锁。但是,根据你的计算机配置,你可能不会看到死锁。看来这取决于计算机上的内核数7。两个核心似乎不会产生死锁,但似乎有两个以上的核心很容易产生死锁。此行为使该示例更好地说明了死锁,因为你可能正在具有两个内核的计算机上编写程序(如果确实是导致问题的原因),并且确信该程序可以正常工作,只能启动它将其安装在另一台计算机上时出现死锁。请注意,仅仅因为你不容易看到死锁,并不意味着该程序就不会在两核计算机上死锁。该程序仍然容易死锁,很少发生-可以说是最坏的情况,因为问题不容易解决。
+当你停止查看输出时,该程序将死锁。但是,根据你的计算机配置,你可能不会看到死锁。看来这取决于计算机上的内核数[^7]。两个核心似乎不会产生死锁,但似乎有两个以上的核心很容易产生死锁。此行为使该示例更好地说明了死锁,因为你可能正在具有两个内核的计算机上编写程序(如果确实是导致问题的原因),并且确信该程序可以正常工作,只能启动它将其安装在另一台计算机上时出现死锁。请注意,仅仅因为你不容易看到死锁,并不意味着该程序就不会在两核计算机上死锁。该程序仍然容易死锁,很少发生-可以说是最坏的情况,因为问题不容易解决。
在DiningPhilosophers构造函数中,每个哲学家都获得一个左右StickHolder的引用。除最后一个哲学家外,每个哲学家都通过以下方式初始化:
哲学家之间的下一双筷子。最后一位哲学家右手的筷子为零,因此圆桌会议完成了。那是因为最后一位哲学家正坐在第一个哲学家的旁边,而且他们俩都共用零筷子。[1]显示了以n为模数选择的右摇杆,将最后一个哲学家缠绕在第一个哲学家的旁边。
现在,所有哲学家都可以尝试吃饭,每个哲学家都在旁边等待哲学家放下筷子。
@@ -2308,10 +2308,10 @@ public class DiningPhilosophers {
## 构造函数非线程安全
-当你在脑子里想象一个对象构造的过程,你会很容易认为这个过程是线程安全的。毕竟,在对象初始化完成前对外不可见,所以又怎会对此产生争议呢?确实,Java 语言规范(JLS)自信满满地陈述道:“没必要使构造器的线程同步,因为它会锁定正在构造的对象,直到构造器完成初始化后才对其他线程可见。”
+当你在脑子里想象一个对象构造的过程,你会很容易认为这个过程是线程安全的。毕竟,在对象初始化完成前对外不可见,所以又怎会对此产生争议呢?确实,[Java 语言规范](https://docs.oracle.com/javase/specs/jls/se8/html/jls-8.html#jls-8.8.3) (JLS)自信满满地陈述道:“*没必要使构造器的线程同步,因为它会锁定正在构造的对象,直到构造器完成初始化后才对其他线程可见。*”
不幸的是,对象的构造过程如其他操作一样,也会受到共享内存并发问题的影响,只是作用机制可能更微妙罢了。
-设想下使用一个静态字段为每个对象自动创建唯一标识符的过程。为了测试其不同的实现过程,我们从一个接口开始。代码示例:
+设想下使用一个**静态**字段为每个对象自动创建唯一标识符的过程。为了测试其不同的实现过程,我们从一个接口开始。代码示例:
```java
//concurrent/HasID.java
@@ -2516,7 +2516,7 @@ public class SynchronizedConstructor{
0
```
-Unsafe类的共享使用现在就变得安全了。另一种方法是将构造器设为私有(因此可以防止继承),并提供一个静态Factory方法来生成新对象:
+**Unsafe**类的共享使用现在就变得安全了。另一种方法是将构造器设为私有(因此可以防止继承),并提供一个静态Factory方法来生成新对象:
```java
// concurrent/SynchronizedFactory.java
@@ -2642,7 +2642,7 @@ public class Pizza{
}
```
-这只算得上是一个简单的状态机,就像Machina类一样。
+这只算得上是一个平凡的状态机,就像**Machina**类一样。
制作一个披萨,当披萨饼最终被放在盒子中时,就算完成最终任务了。 如果一个人在做一个披萨饼,那么所有步骤都是线性进行的,即一个接一个地进行:
@@ -2736,9 +2736,9 @@ Pizza 3:BOXED
1739
```
-现在,我们制作五个披萨的时间与制作单个披萨的时间就差不多了。 尝试删除标记为[1]的行后,你会发现它花费的时间是原来的五倍。 你还可以尝试将QUANTITY更改为4、8、10、16和17,看看会有什么不同,并猜猜看为什么会这样。
+现在,我们制作五个披萨的时间与制作单个披萨的时间就差不多了。 尝试删除标记为[1]的行后,你会发现它花费的时间是原来的五倍。 你还可以尝试将**QUANTITY**更改为4、8、10、16和17,看看会有什么不同,并猜猜看为什么会这样。
-**PizzaStreams** 类产生的每个并行流在它的forEach()内完成所有工作,如果我们将其各个步骤用映射的方式一步一步处理,情况会有所不同吗?
+**PizzaStreams** 类产生的每个并行流在它的`forEach()`内完成所有工作,如果我们将其各个步骤用映射的方式一步一步处理,情况会有所不同吗?
```java
// concurrent/PizzaParallelSteps.java
@@ -2910,9 +2910,9 @@ Pizza4: complete
1797
```
-并行流和 **CompletableFutures** 是 Java 并发工具箱中最先进发达的技术。 你应该始终首先选择其中之一。 当一个问题很容易并行处理时,或者说,很容易把数据分解成相同的、易于处理的各个部分时,使用并行流方法处理最为合适(而如果你决定不借助它而由自己完成,你就必须撸起袖子,深入研究Spliterator的文档)。
+并行流和 **CompletableFutures** 是 Java 并发工具箱中最先进发达的技术。 你应该始终首先选择其中之一。 当一个问题很容易并行处理时,或者说,很容易把数据分解成相同的、易于处理的各个部分时,使用并行流方法处理最为合适(而如果你决定不借助它而由自己完成,你就必须撸起袖子,深入研究**Spliterator**的文档)。
-而当工作的各个部分内容各不相同时,使用 **CompletableFutures** 是最好的选择。比起面向数据,CompletableFutures** 更像是面向任务的。
+而当工作的各个部分内容各不相同时,使用 **CompletableFutures** 是最好的选择。比起面向数据,**CompletableFutures** 更像是面向任务的。
对于披萨问题,结果似乎也没有什么不同。实际上,并行流方法看起来更简洁,仅出于这个原因,我认为并行流作为解决问题的首次尝试方法更具吸引力。
@@ -2924,12 +2924,87 @@ Pizza4: complete
## 本章小结
-[^1]:例如,Eric-Raymond在“VIIX编程艺术”(Addison-Wesley,2004)中提出了一个很好的案例。
+需要并发的唯一理由是“等待太多”。这也可以包括用户界面的响应速度,但是由于Java用于构建用户界面时并不高效,因此[^8]这仅仅意味着“您的程序运行速度还不够快”。
+
+如果并发很容易,则没有理由拒绝并发。 正因为并发实际上很难,所以您应该仔细考虑是否值得为此付出努力,并考虑您能否以其他方式提升速度。
+
+例如,迁移到更快的硬件(这可能比消耗程序员的时间要便宜得多)或者将程序分解成多个部分,然后在不同的机器上运行这些部分。
+
+奥卡姆剃刀是一个经常被误解的原则。 我看过至少一部电影,他们将其定义为”最简单的解决方案是正确的解决方案“,就好像这是某种毋庸置疑的法律。实际上,这是一个准则:面对多种方法时,请先尝试需要最少假设的方法。 在编程世界中,这已演变为“尝试可能可行的最简单的方法”。当您了解了特定工具的知识时——就像你现在了解了有关并发性的知识一样,你可能会很想使用它,或者提前规定你的解决方案必须能够“速度飞快”,从而来证明从一开始就进行并发设计是合理的。但是,我们的奥卡姆剃刀编程版本表示您应该首先尝试最简单的方法(这种方法开发起来也更便宜),然后看看它是否足够好。
+
+由于我出身于底层学术背景(物理学和计算机工程),所以我很容易想到所有小轮子转动的成本。我确定使用最简单的方法不够快的场景出现的次数已经数不过来了,但是尝试后却发现它实际上绰绰有余。
+
+### 缺点
+
+并发编程的主要缺点是:
+
+1. 在线程等待共享资源时会降低速度。
+
+2. 线程管理产生额外CPU开销。
+
+3. 糟糕的设计决策带来无法弥补的复杂性。
+
+4. 诸如饥饿,竞速,死锁和活锁(多线程各自处理单个任务而整体却无法完成)之类的问题。
+
+5. 跨平台的不一致。 通过一些示例,我发现了某些计算机上很快出现的竞争状况,而在其他计算机上却没有。 如果您在后者上开发程序,则在分发程序时可能会感到非常惊讶。
+
+
+
+另外,并发的应用是一门艺术。 Java旨在允许您创建尽可能多的所需要的对象来解决问题——至少在理论上是这样。[^9]但是,线程不是典型的对象:每个线程都有其自己的执行环境,包括堆栈和其他必要的元素,使其比普通对象大得多。 在大多数环境中,只能在内存用光之前创建数千个**Thread**对象。通常,您只需要几个线程即可解决问题,因此一般来说创建线程没有什么限制,但是对于某些设计而言,它会成为一种约束,可能迫使您使用完全不同的方案。
+
+### 共享内存陷阱
+
+并发性的主要困难之一是因为可能有多个任务共享一个资源(例如对象中的内存),并且您必须确保多个任务不会同时读取和更改该资源。
+
+我花了多年的时间研究并发并发。 我了解到您永远无法相信使用共享内存并发的程序可以正常工作。 您可以轻易发现它是错误的,但永远无法证明它是正确的。 这是众所周知的并发原则之一。[^10]
+
+我遇到了许多人,他们对编写正确的线程程序的能力充满信心。 我偶尔开始认为我也可以做好。 对于一个特定的程序,我最初是在只有单个CPU的机器上编写的。 那时我能够说服自己该程序是正确的,因为我以为我对Java工具很了解。 而且在我的单CPU计算机上也没有失败。而到了具有多个CPU的计算机,程序出现问题不能运行后,我感到很惊讶,但这还只是众多问题中的一个而已。 这不是Java的错; “写一次,到处运行”,在单核与多核计算机间无法扩展到并发编程领域。这是并发编程的基本问题。 实际上您可以在单CPU机器上发现一些并发问题,但是在多线程实际上真的在并行运行的多CPU机器上,就会出现一些其他问题。
+
+再举一个例子,哲学家就餐的问题可以很容易地进行调整,因此几乎不会产生死锁,这会给您一种一切都棒极了的印象。当涉及到共享内存并发编程时,您永远不应该对自己的编程能力变得过于自信。
+
+
+
+### This Albatross is Big
+
+如果您对Java并发感到不知所措,那说明您身处在一家出色的公司里。您 可以访问**Thread**类的[Javadoc](https://docs.oracle.com/javase/8/docs/api/java/lang/Thread.html)页面, 看一下哪些方法现在是**Deprecated**(废弃的)。这些是Java语言设计者犯过错的地方,因为他们在设计语言时对并发性了解不足。
+
+事实证明,在Java的后续版本中添加的许多库解决方案都是无效的,甚至是无用的。 幸运的是,Java 8中的并行**Streams**和**CompletableFutures**都非常有价值。但是当您使用旧代码时,仍然会遇到旧的解决方案。
+
+在本书的其他地方,我谈到了Java的一个基本问题:每个失败的实验都永远嵌入在语言或库中。 Java并发强调了这个问题。尽管有不少错误,但错误并不是那么多,因为有很多不同的尝试方法来解决问题。 好的方面是,这些尝试产生了更好,更简单的设计。 不利之处在于,在找到好的方法之前,您很容易迷失于旧的设计中。
+
+### 其他类库
+
+本章重点介绍了相对安全易用的并行工具流和**CompletableFutures**,并且仅涉及Java标准库中一些更细粒度的工具。 为避免您不知所措,我没有介绍您可能实际在实践中使用的某些库。我们使用了几个**Atomic**(原子)类,**ConcurrentLinkedDeque**,**ExecutorService**和**ArrayBlockingQueue**。附录:[并发底层原理](./Appendix-Low-Level-Concurrency.md)涵盖了其他一些内容,但是您还想探索**java.util.concurrent**的Javadocs。 但是要小心,因为某些库组件已被新的更好的组件所取代。
+
+### 考虑为并发设计的语言
+
+通常,请谨慎地使用并发。 如果需要使用它,请尝试使用最现代的方法:并行流或**CompletableFutures**。 这些功能旨在(假设您不尝试共享内存)使您摆脱麻烦(在Java的世界范围内)。
+
+如果您的并发问题变得比高级Java构造所支持的问题更大且更复杂,请考虑使用专为并发设计的语言,仅在需要并发的程序部分中使用这种语言是有可能的。 在撰写本文时,JVM上最纯粹的功能语言是Clojure(Lisp的一种版本)和Frege(Haskell的一种实现)。这些使您可以在其中编写应用程序的并发部分语言,并通过JVM轻松地与您的主要Java代码进行交互。 或者,您可以选择更复杂的方法,即通过外部功能接口(FFI)将JVM之外的语言与另一种为并发设计的语言进行通信。[^11]
+
+你很容易被一种语言绑定,迫使自己尝试使用该语言来做所有事情。 一个常见的示例是构建HTML / JavaScript用户界面。 这些工具确实很难使用,令人讨厌,并且有许多库允许您通过使用自己喜欢的语言编写代码来生成这些工具(例如,**Scala.js**允许您在Scala中完成代码)。
+
+心理上的便利是一个合理的考虑因素。 但是,我希望我在本章(以及附录:[并发底层原理](./Appendix-Low-Level-Concurrency.md))中已经表明Java并发是一个你可能无法逃离很深的洞。 与Java语言的任何其他部分相比,在视觉上检查代码同时记住所有陷阱所需要的的知识要困难得多。
+
+无论使用特定的语言、库使得并发看起来多么简单,都要将其视为一种妖术,因为总是有东西会在您最不期望出现的时候咬您。
+
+### 拓展阅读
+
+《Java Concurrency in Practice》,出自Brian Goetz,Tim Peierls, Joshua Bloch,Joseph Bowbeer,David Holmes和 Doug Lea (Addison Wesley,2006年)——这些基本上就是Java并发世界中的名人名单了《Java Concurrency in Practice》第二版,出自 Doug Lea (Addison-Wesley,2000年)。尽管这本书出版时间远远早于Java 5发布,但Doug的大部分工作都写入了**java.util.concurrent**库。因此,这本书对于全面理解并发问题至关重要。 它超越了Java,讨论了跨语言和技术的并发编程。 尽管它在某些地方可能很钝,但值得多次重读(最好是在两个月之间进行消化)。 道格(Doug)是世界上为数不多的真正了解并发编程的人之一,因此这是值得的。
+
+
+
+[^1]:例如,Eric-Raymond在“Unix编程艺术”(Addison-Wesley,2004)中提出了一个很好的案例。
[^2]:可以说,试图将并发性用于后续语言是一种注定要失败的方法,但你必须得出自己的结论
[^3]:有人谈论在Java——10中围绕泛型做一些类似的基本改进,这将是非常令人难以置信的。
[^4]:这是一种有趣的,虽然不一致的方法。通常,我们期望在公共接口上使用显式类表示不同的行为
[^5]:不,永远不会有纯粹的功能性Java。我们所能期望的最好的是一种在JVM上运行的全新语言。
[^6]:当两个任务能够更改其状态以使它们不会被阻止但它们从未取得任何有用的进展时,你也可以使用活动锁。
+[^7]: 而不是超线程;通常每个内核有两个超线程,并且在询问内核数量时,本书所使用的Java版本会报告超线程的数量。超线程产生了更快的上下文切换,但是只有实际的内核才真的工作,而不是超线程。 ↩
+[^8]: 库就在那里用于调用,而语言本身就被设计用于此目的,但实际上它很少发生,以至于可以说”没有“。↩
+[^9]: 举例来说,如果没有Flyweight设计模式,在工程中创建数百万个对象用于有限元分析可能在Java中不可行。↩
+[^10]: 在科学中,虽然从来没有一种理论被证实过,但是一种理论必须是可证伪的才有意义。而对于并发性,我们大部分时间甚至都无法得到这种可证伪性。↩
+[^11]: 尽管**Go**语言显示了FFI的前景,但在撰写本文时,它并未提供跨所有平台的解决方案。
From d6e00fcbb41b30c9574911d95150fb2e0ab55543 Mon Sep 17 00:00:00 2001
From: Joe <736777445@qq.com>
Date: Tue, 21 Jan 2020 16:41:33 +0800
Subject: [PATCH 013/224] =?UTF-8?q?chapter=2024=20=E5=B9=B6=E5=8F=91?=
=?UTF-8?q?=E7=BC=96=E7=A8=8B=E7=BF=BB=E8=AF=91=E5=AE=8C=E6=88=90?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
README.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/README.md b/README.md
index 6a247bf8..7d5c6774 100644
--- a/README.md
+++ b/README.md
@@ -43,7 +43,7 @@
- [x] [第二十一章 数组](docs/book/21-Arrays.md)
- [x] [第二十二章 枚举](docs/book/22-Enumerations.md)
- [x] [第二十三章 注解](docs/book/23-Annotations.md)
-- [ ] [第二十四章 并发编程](docs/book/24-Concurrent-Programming.md)
+- [x] [第二十四章 并发编程](docs/book/24-Concurrent-Programming.md)
- [ ] [第二十五章 设计模式](docs/book/25-Patterns.md)
- [x] [附录:补充](docs/book/Appendix-Supplements.md)
- [x] [附录:编程指南](docs/book/Appendix-Programming-Guidelines.md)
From ee50c09b55b7ead39cb0809f68e21e1dab3e7415 Mon Sep 17 00:00:00 2001
From: dellenovo
Date: Tue, 21 Jan 2020 18:04:01 +0800
Subject: [PATCH 014/224] =?UTF-8?q?Fix=20issue=20#362:=E9=87=8D=E6=96=B0?=
=?UTF-8?q?=E7=BF=BB=E8=AF=91What=E2=80=99s=20impressive=20is=20it=20is=20?=
=?UTF-8?q?not=20intellectually=20prohibitive=20to=20assemble=20such=20a?=
=?UTF-8?q?=20model=20(#363)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Signed-off-by: lifei.zhang
---
docs/book/20-Generics.md | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/docs/book/20-Generics.md b/docs/book/20-Generics.md
index 2c02e10a..09b0364c 100644
--- a/docs/book/20-Generics.md
+++ b/docs/book/20-Generics.md
@@ -1230,7 +1230,7 @@ public class Store extends ArrayList {
*/
```
-`Store.toString()` 显示了结果:尽管有复杂的层次结构,但多层的集合仍然是类型安全的和可管理的。令人印象深刻的是,组装这样的模型在理论上并不是禁止的。
+`Store.toString()` 显示了结果:尽管有复杂的层次结构,但多层的集合仍然是类型安全的和可管理的。令人印象深刻的是,组装这样的模型并不需要耗费过多精力。
**Shelf** 使用 `Suppliers.fill()` 这个实用程序,该实用程序接受 **Collection** (第一个参数),并使用 **Supplier** (第二个参数),以元素的数量为 **n** (第三个参数)来填充它。 **Suppliers** 类将会在本章末尾定义,其中的方法都是在执行某种填充操作,并在本章的其他示例中使用。
@@ -5185,4 +5185,4 @@ Neal After 对于 Java 问题(尤其是擦除)的看法可以从这里找到
-
\ No newline at end of file
+
From 2010769b2403cd49223d888e8e1f4674318cc7b1 Mon Sep 17 00:00:00 2001
From: Crimson_Loves_Code <39024757+OrientationJump@users.noreply.github.com>
Date: Wed, 22 Jan 2020 09:31:31 +0800
Subject: [PATCH 015/224] fix issue:#161 (#364)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
* Fix issue:#112 翻译Summary
Signed-off-by: crimson <1291463831@qq.com>
* fix issue:#161 翻译工厂模式
Signed-off-by: crimson <1291463831@qq.com>
* fix issue:#161
Signed-off-by: crimson <1291463831@qq.com>
---
docs/book/25-Patterns.md | 360 +++++++++++++++++++++++++++++++++++++++
1 file changed, 360 insertions(+)
diff --git a/docs/book/25-Patterns.md b/docs/book/25-Patterns.md
index 9bd8e81c..22ce3cfb 100644
--- a/docs/book/25-Patterns.md
+++ b/docs/book/25-Patterns.md
@@ -425,8 +425,368 @@ Spinning
## 工厂模式
+当你发现必须将新类型添加到系统中时,合理的第一步是使用多态性为这些新类型创建一个通用接口。这会将你系统中的其余代码与要添加的特定类型的信息分开,使得可以在不改变现有代码的情况下添加新类型……或者看起来如此。起初,在这种设计中,似乎你必须更改代码的唯一地方就是你继承新类型的地方,但这并不是完全正确的。 你仍然必须创建新类型的对象,并且在创建时必须指定要使用的确切构造器。因此,如果创建对象的代码分布在整个应用程序中,那么在添加新类型时,你将遇到相同的问题——你仍然必须追查你代码中新类型碍事的所有地方。恰好是类型的创建碍事,而不是类型的使用(通过多态处理),但是效果是一样的:添加新类型可能会引起问题。
+
+解决方案是强制对象的创建都通过通用工厂进行,而不是允许创建代码在整个系统中传播。 如果你程序中的所有代码都必须执行通过该工厂创建你的一个对象,那么在添加新类时只需要修改工厂即可。
+
+由于每个面向对象的程序都会创建对象,并且很可能会通过添加新类型来扩展程序,因此工厂是最通用的设计模式之一。
+
+举例来说,让我们重新看一下**Shape**系统。 首先,我们需要一个用于所有示例的基本框架。 如果无法创建**Shape**对象,则需要抛出一个合适的异常:
+
+```java
+// patterns/shapes/BadShapeCreation.java package patterns.shapes;
+public class BadShapeCreation extends RuntimeException {
+ public BadShapeCreation(String msg) {
+ super(msg);
+ }
+}
+```
+
+接下来,是一个**Shape**基类:
+
+```java
+// patterns/shapes/Shape.java
+package patterns.shapes;
+public class Shape {
+ private static int counter = 0;
+ private int id = counter++;
+ @Override
+ public String toString(){
+ return getClass().getSimpleName() + "[" + id + "]";
+ }
+ public void draw() {
+ System.out.println(this + " draw");
+ }
+ public void erase() {
+ System.out.println(this + " erase");
+ }
+}
+```
+
+该类自动为每一个**Shape**对象创建一个唯一的`id`。
+
+`toString()`使用运行期信息来发现特定的**Shape**子类的名字。
+
+现在我们能很快创建一些**Shape**子类了:
+
+```java
+// patterns/shapes/Circle.java
+package patterns.shapes;
+public class Circle extends Shape {}
+```
+
+```java
+// patterns/shapes/Square.java
+package patterns.shapes;
+public class Square extends Shape {}
+```
+
+```java
+// patterns/shapes/Triangle.java
+package patterns.shapes;
+public class Triangle extends Shape {}
+```
+
+工厂是具有能够创建对象的方法的类。 我们有几个示例版本,因此我们将定义一个接口:
+
+```java
+// patterns/shapes/FactoryMethod.java
+package patterns.shapes;
+public interface FactoryMethod {
+ Shape create(String type);
+}
+```
+
+`create()`接收一个参数,这个参数使其决定要创建哪一种**Shape**对象,这里是`String`,但是它其实可以是任何数据集合。对象的初始化数据(这里是字符串)可能来自系统外部。 这个例子将测试工厂:
+
+```java
+// patterns/shapes/FactoryTest.java
+package patterns.shapes;
+import java.util.stream.*;
+public class FactoryTest {
+ public static void test(FactoryMethod factory) {
+ Stream.of("Circle", "Square", "Triangle",
+ "Square", "Circle", "Circle", "Triangle")
+ .map(factory::create)
+ .peek(Shape::draw)
+ .peek(Shape::erase)
+ .count(); // Terminal operation
+ }
+}
+```
+
+在主函数`main()`里,要记住除非你在最后使用了一个终结操作,否则**Stream**不会做任何事情。在这里,`count()`的值被丢弃了。
+
+创建工厂的一种方法是显式创建每种类型:
+
+```java
+// patterns/ShapeFactory1.java
+// A simple static factory method
+import java.util.*;
+import java.util.stream.*;
+import patterns.shapes.*;
+public class ShapeFactory1 implements FactoryMethod {
+ public Shape create(String type) {
+ switch(type) {
+ case "Circle": return new Circle();
+ case "Square": return new Square();
+ case "Triangle": return new Triangle();
+ default: throw new BadShapeCreation(type);
+ }
+ }
+ public static void main(String[] args) {
+ FactoryTest.test(new ShapeFactory1());
+ }
+}
+```
+
+输出结果:
+
+```java
+Circle[0] draw
+Circle[0] erase
+Square[1] draw
+Square[1] erase
+Triangle[2] draw
+Triangle[2] erase
+Square[3] draw
+Square[3] erase
+Circle[4] draw
+Circle[4] erase
+Circle[5] draw
+Circle[5] erase
+Triangle[6] draw
+Triangle[6] erase
+```
+
+`create()`现在是添加新类型的Shape时系统中唯一需要更改的其他代码。
+
+### 动态工厂
+
+前面例子中的**静态**`create()`方法强制所有创建操作都集中在一个位置,因此这是添加新类型的**Shape**时唯一必须更改代码的地方。这当然是一个合理的解决方案,因为它把创建对象的过程限制在一个框内。但是,如果你在添加新类时无需修改任何内容,那就太好了。 以下版本使用反射在首次需要时将**Shape**的构造器动态加载到工厂列表中:
+
+```java
+// patterns/ShapeFactory2.java
+import java.util.*;
+import java.lang.reflect.*;
+import java.util.stream.*;
+import patterns.shapes.*;
+public class ShapeFactory2 implements FactoryMethod {
+ Map factories = new HashMap<>();
+ static Constructor load(String id) {
+ System.out.println("loading " + id);
+ try {
+ return Class.forName("patterns.shapes." + id)
+ .getConstructor();
+ } catch(ClassNotFoundException |
+ NoSuchMethodException e) {
+ throw new BadShapeCreation(id);
+ }
+ }
+ public Shape create(String id) {
+ try {
+ return (Shape)factories
+ .computeIfAbsent(id, ShapeFactory2::load)
+ .newInstance();
+ } catch(InstantiationException |
+ IllegalAccessException |
+ InvocationTargetException e) {
+ throw new BadShapeCreation(id);
+ }
+ }
+ public static void main(String[] args) {
+ FactoryTest.test(new ShapeFactory2());
+ }
+}
+```
+
+输出结果:
+
+```java
+loading Circle
+Circle[0] draw
+Circle[0] erase
+loading Square
+Square[1] draw
+Square[1] erase
+loading Triangle
+Triangle[2] draw
+Triangle[2] erase
+Square[3] draw
+Square[3] erase
+Circle[4] draw
+Circle[4] erase
+Circle[5] draw
+Circle[5] erase
+Triangle[6] draw
+Triangle[6] erase
+```
+
+和之前一样,`create()`方法基于你传递给它的**String**参数生成新的**Shape**s,但是在这里,它是通过在**HashMap**中查找作为键的**String**来实现的。 返回的值是一个构造器,该构造器用于通过调用`newInstance()`创建新的**Shape**对象。
+
+然而,当你开始运行程序时,工厂的`map`为空。`create()`使用`map`的`computeIfAbsent()`方法来查找构造器(如果该构造器已存在于`map`中)。如果不存在则使用`load()`计算出该构造器,并将其插入到`map`中。 从输出中可以看到,每种特定类型的**Shape**都是在第一次请求时才加载的,然后只需要从`map`中检索它。
+
+### 多态工厂
+
+《设计模式》这本书强调指出,采用“工厂方法”模式的原因是可以从基本工厂中继承出不同类型的工厂。 再次修改示例,使工厂方法位于单独的类中:
+
+```java
+// patterns/ShapeFactory3.java
+// Polymorphic factory methods
+import java.util.*;
+import java.util.function.*;
+import java.util.stream.*;
+import patterns.shapes.*;
+interface PolymorphicFactory {
+ Shape create();
+}
+class RandomShapes implements Supplier {
+ private final PolymorphicFactory[] factories;
+ private Random rand = new Random(42);
+ RandomShapes(PolymorphicFactory... factories){
+ this.factories = factories;
+ }
+ public Shape get() {
+ return factories[ rand.nextInt(factories.length)].create();
+ }
+}
+public class ShapeFactory3 {
+ public static void main(String[] args) {
+ RandomShapes rs = new RandomShapes(
+ Circle::new,
+ Square::new,
+ Triangle::new);
+ Stream.generate(rs)
+ .limit(6)
+ .peek(Shape::draw)
+ .peek(Shape::erase)
+ .count();
+ }
+}
+```
+
+输出结果:
+
+```java
+Triangle[0] draw
+Triangle[0] erase
+Circle[1] draw
+Circle[1] erase
+Circle[2] draw
+Circle[2] erase
+Triangle[3] draw
+Triangle[3] erase
+Circle[4] draw
+Circle[4] erase
+Square[5] draw
+Square[5] erase
+```
+
+**RandomShapes**实现了**Supplier \**,因此可用于通过`Stream.generate()`创建**Stream**。 它的构造器采用**PolymorphicFactory**对象的可变参数列表。 变量参数列表以数组形式出现,因此列表是以数组形式在内部存储的。`get()`方法随机获取此数组中一个对象的索引,并在结果上调用`create()`以产生新的**Shape**对象。 添加新类型的**Shape**时,**RandomShapes**构造器是唯一需要更改的地方。 请注意,此构造器需要**Supplier \**。 我们传递给其**Shape**构造器的方法引用,该引用可满足**Supplier \**约定,因为Java 8支持结构一致性。
+
+鉴于**ShapeFactory2.java**可能会抛出异常,使用此方法则没有任何异常——它在编译时完全确定。
+
+### 抽象工厂
+
+抽象工厂模式看起来像我们之前所见的工厂对象,但拥有不是一个工厂方法而是几个工厂方法, 每个工厂方法都会创建不同种类的对象。 这个想法是在创建工厂对象时,你决定如何使用该工厂创建的所有对象。 《设计模式》中提供的示例实现了跨各种图形用户界面(GUI)的可移植性:你创建一个适合你正在使用的GUI的工厂对象,然后从中请求菜单,按钮,滑块等等,它将自动为GUI创建适合该项目版本的组件。 因此,你可以将从一个GUI更改为另一个所产生的影响隔离限制在一处。 作为另一个示例,假设你正在创建一个通用游戏环境来支持不同类型的游戏。 使用抽象工厂看起来就像下文那样:
+
+```java
+// patterns/abstractfactory/GameEnvironment.java
+// An example of the Abstract Factory pattern
+// {java patterns.abstractfactory.GameEnvironment}
+package patterns.abstractfactory;
+import java.util.function.*;
+interface Obstacle {
+ void action();
+}
+
+interface Player {
+ void interactWith(Obstacle o);
+}
+
+class Kitty implements Player {
+ @Override
+ public void interactWith(Obstacle ob) {
+ System.out.print("Kitty has encountered a ");
+ ob.action();
+ }
+}
+
+class KungFuGuy implements Player {
+ @Override
+ public void interactWith(Obstacle ob) {
+ System.out.print("KungFuGuy now battles a ");
+ ob.action();
+ }
+}
+
+class Puzzle implements Obstacle {
+ @Override
+ public void action() {
+ System.out.println("Puzzle");
+ }
+}
+
+class NastyWeapon implements Obstacle {
+ @Override
+ public void action() {
+ System.out.println("NastyWeapon");
+ }
+}
+
+// The Abstract Factory:
+class GameElementFactory {
+ Supplier player;
+ Supplier obstacle;
+}
+
+// Concrete factories:
+class KittiesAndPuzzles extends GameElementFactory {
+ KittiesAndPuzzles() {
+ player = Kitty::new;
+ obstacle = Puzzle::new;
+ }
+}
+
+class KillAndDismember extends GameElementFactory {
+ KillAndDismember() {
+ player = KungFuGuy::new;
+ obstacle = NastyWeapon::new;
+ }
+}
+
+public class GameEnvironment {
+ private Player p;
+ private Obstacle ob;
+
+ public GameEnvironment(GameElementFactory factory) {
+ p = factory.player.get();
+ ob = factory.obstacle.get();
+ }
+ public void play() {
+ p.interactWith(ob);
+ }
+ public static void main(String[] args) {
+ GameElementFactory kp = new KittiesAndPuzzles(), kd = new KillAndDismember();
+ GameEnvironment g1 = new GameEnvironment(kp), g2 = new GameEnvironment(kd);
+ g1.play();
+ g2.play();
+ }
+}
+
+```
+
+输出结果:
+
+```java
+Kitty has encountered a Puzzle
+KungFuGuy now battles a NastyWeapon
+```
+
+在这种环境中,**Player**对象与**Obstacle**对象进行交互,但是根据你所玩游戏的类型,存在不同类型的玩家和障碍物。 你可以通过选择特定的**GameElementFactory**来确定游戏的类型,然后**GameEnvironment**控制游戏的设置和玩法。 在此示例中,设置和玩法非常简单,但是这些活动(初始条件和状态变化)可以决定游戏的大部分结果。 这里,**GameEnvironment**不是为继承而设计的,尽管这样做很有意义。 它还包含“双重调度”和“工厂方法”的示例,稍后将对这两个示例进行说明。
+
## 函数对象
From 38fa2d87cc2e8d8511edd9985c7a46f56e2ddd86 Mon Sep 17 00:00:00 2001
From: dellenovo
Date: Wed, 22 Jan 2020 20:29:05 +0800
Subject: [PATCH 016/224] =?UTF-8?q?Fix=20issue=20#365:=E6=94=B9=E4=B8=A4?=
=?UTF-8?q?=E4=B8=AA=E5=AD=97=20(#366)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
* Fix issue #362:重新翻译What’s impressive is it is not intellectually prohibitive to assemble such a model
Signed-off-by: lifei.zhang
* Fix issue #362:改两个字
Signed-off-by: lifei.zhang
---
docs/book/20-Generics.md | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/docs/book/20-Generics.md b/docs/book/20-Generics.md
index 09b0364c..b03c5e64 100644
--- a/docs/book/20-Generics.md
+++ b/docs/book/20-Generics.md
@@ -1454,13 +1454,13 @@ public class ReturnGenericType {
例如,假设一个应用使用了两个类库 **X** 和 **Y**,**Y** 使用了类库 **Z**。随着 Java 5 的出现,这个应用和这些类库的创建者最终可能希望迁移到泛型上。但是当进行迁移时,它们有着不同的动机和限制。为了实现迁移兼容性,每个类库与应用必须与其他所有的部分是否使用泛型无关。因此,它们不能探测其他类库是否使用了泛型。因此,某个特定的类库使用了泛型这样的证据必须被”擦除“。
-如果没有某种类型的迁移途径,所有已经构建了很长时间的类库就需要与希望迁移到 Java 泛型上的开发者们说再见了。类库毫无争议是编程语言的一部分,对生产效率有着极大的影响,所以这种代码无法接受。擦除是否是最佳的活唯一的迁移途径,还待时间来证明。
+如果没有某种类型的迁移途径,所有已经构建了很长时间的类库就需要与希望迁移到 Java 泛型上的开发者们说再见了。类库毫无争议是编程语言的一部分,对生产效率有着极大的影响,所以这种代码无法接受。擦除是否是最佳的或唯一的迁移途径,还待时间来证明。
### 擦除的问题
因此,擦除主要的正当理由是从非泛化代码到泛化代码的转变过程,以及在不破坏现有类库的情况下将泛型融入到语言中。擦除允许你继续使用现有的非泛型客户端代码,直至客户端准备好用泛型重写这些代码。这是一个崇高的动机,因为它不会骤然破坏所有现有的代码。
-擦除的代码是显著的。泛型不能用于显式地引用运行时类型的操作中,例如转型、**instanceof** 操作和 **new** 表达式。因为所有关于参数的类型信息都丢失了,当你在编写泛型代码时,必须时刻提醒自己,你只是看起来拥有有关参数的类型信息而已。
+擦除的代价是显著的。泛型不能用于显式地引用运行时类型的操作中,例如转型、**instanceof** 操作和 **new** 表达式。因为所有关于参数的类型信息都丢失了,当你在编写泛型代码时,必须时刻提醒自己,你只是看起来拥有有关参数的类型信息而已。
考虑如下的代码段:
From 058720f8c4a23d985b01f5d0a7ae37ec39110bb9 Mon Sep 17 00:00:00 2001
From: Qimiao Chen
Date: Thu, 30 Jan 2020 18:15:09 +0800
Subject: [PATCH 017/224] =?UTF-8?q?=E8=AF=AD=E5=8F=A5=E9=94=99=E8=AF=AF=20?=
=?UTF-8?q?(#368)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
docs/book/23-Annotations.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/docs/book/23-Annotations.md b/docs/book/23-Annotations.md
index 2e38bd9e..cbca57ff 100644
--- a/docs/book/23-Annotations.md
+++ b/docs/book/23-Annotations.md
@@ -8,7 +8,7 @@
注解在一定程度上是把元数据和源代码文件结合在一起的趋势所激发的,而不是保存在外部文档。这同样是对像 C# 语言对于 Java 语言特性压力的一种回应。
-注解是 Java 5 所引入的众多语言变化之一。它们提供了 Java 无法表达的但是你需要完整表述程序所需的信息。因此,注解使得我们可以以编译器验证的格式存储程序的额外信息。注解可以生成描述符文件,甚至是新的类定义,并且有助于减轻编写“样板”代码的负担。通过使用注解,你可以将元数据保存在 Java 源代码中。并拥有如下有下优势:简单易读的代码,编译器类型检查,使用 annotation API 为自己的注解构造处理工具。即使 Java 定义了一些类型的元数据,但是一般来说注解类型的添加和如何使用完全取决于你。
+注解是 Java 5 所引入的众多语言变化之一。它们提供了 Java 无法表达的但是你需要完整表述程序所需的信息。因此,注解使得我们可以以编译器验证的格式存储程序的额外信息。注解可以生成描述符文件,甚至是新的类定义,并且有助于减轻编写“样板”代码的负担。通过使用注解,你可以将元数据保存在 Java 源代码中。并拥有如下优势:简单易读的代码,编译器类型检查,使用 annotation API 为自己的注解构造处理工具。即使 Java 定义了一些类型的元数据,但是一般来说注解类型的添加和如何使用完全取决于你。
注解的语法十分简单,主要是在现有语法中添加 @ 符号。Java 5 引入了前三种定义在 **java.lang** 包中的注解:
From 953a122b5f11bcc9eed71f0b4dad941aea837266 Mon Sep 17 00:00:00 2001
From: Qimiao Chen
Date: Fri, 31 Jan 2020 10:01:05 +0800
Subject: [PATCH 018/224] Polishing (#369)
---
docs/book/23-Annotations.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/docs/book/23-Annotations.md b/docs/book/23-Annotations.md
index cbca57ff..070e4ae2 100644
--- a/docs/book/23-Annotations.md
+++ b/docs/book/23-Annotations.md
@@ -107,7 +107,7 @@ public class PasswordUtils {
}
```
-注解的元素在使用时表现为 名-值 对的形式,并且需要放置在 `@UseCase` 声明之后的括号内。在 `encryptPassword()` 方法的注解中,并没有给出 **description** 的默认值,所以在 **@interface UseCase** 的注解处理器分析处理这个类的时候会使用该元素的默认值。
+注解的元素在使用时表现为 名-值 对的形式,并且需要放置在 `@UseCase` 声明之后的括号内。在 `encryptPassword()` 方法的注解中,并没有给出 **description** 的值,所以在 **@interface UseCase** 的注解处理器分析处理这个类的时候会使用该元素的默认值。
你应该能够想象到如何使用这套工具来“勾勒”出将要建造的系统,然后在建造的过程中逐渐实现系统的各项功能。
From 5f24880e33dc6628cdb7cd5887494c9cd91933fb Mon Sep 17 00:00:00 2001
From: Qimiao Chen
Date: Fri, 31 Jan 2020 14:50:24 +0800
Subject: [PATCH 019/224] Fix typo (#371)
---
docs/book/23-Annotations.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/docs/book/23-Annotations.md b/docs/book/23-Annotations.md
index 070e4ae2..785676c5 100644
--- a/docs/book/23-Annotations.md
+++ b/docs/book/23-Annotations.md
@@ -580,7 +580,7 @@ public class SimpleProcessor
你唯一需要实现的方法就是 `process()`,这里是所有行为发生的地方。第一个参数告诉你哪个注解是存在的,第二个参数保留了剩余信息。我们所做的事情只是打印了注解(这里只存在一个),可以看 **TypeElement** 文档中的其他行为。通过使用 `process()` 的第二个操作,我们循环所有被 **@Simple** 注解的元素,并且针对每一个元素调用我们的 `display()` 方法。所有 **Element** 展示了本身的基本信息;例如,`getModifiers()` 告诉你它是否为 **public** 和 **static** 的。
-**Element** 只能执行那些编译器解析的所有基本对象共有的操作,而类和方法之类的东西有额外的信息需要提取。所以(如果你阅读了正确的文档,但是我没有在任何文档中找到——我不得不通过 StackOverflow 寻找线索)你检查它是哪种 **ElementKind**,让后将其向下转换为更具体的元素类型,注入针对 CLASS 的 TypeElement 和 针对 METHOD 的ExecutableElement。此时,可以为这些元素调用其他方法。
+**Element** 只能执行那些编译器解析的所有基本对象共有的操作,而类和方法之类的东西有额外的信息需要提取。所以(如果你阅读了正确的文档,但是我没有在任何文档中找到——我不得不通过 StackOverflow 寻找线索)你检查它是哪种 **ElementKind**,然后将其向下转换为更具体的元素类型,注入针对 CLASS 的 TypeElement 和 针对 METHOD 的ExecutableElement。此时,可以为这些元素调用其他方法。
动态向下转型(在编译期不进行检查)并不像是 Java 的做事方式,这非常不直观这也是为什么我从未想过要这样做事。相反,我花了好几天的时间,试图发现你应该如何访问这些信息,而这些信息至少在某种程度上是用不起作用的恰当方法简单明了的。我还没有遇到任何东西说上面是规范的形式,但在我看来是。
From de0e00f1de88e9e01707de585dfbf5378d006471 Mon Sep 17 00:00:00 2001
From: Qimiao Chen
Date: Sat, 1 Feb 2020 14:21:13 +0800
Subject: [PATCH 020/224] Polising (#370)
* Polishing
* Polishing
* Polishing
* Update 23-Annotations.md
---
docs/book/23-Annotations.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/docs/book/23-Annotations.md b/docs/book/23-Annotations.md
index 785676c5..8448423f 100644
--- a/docs/book/23-Annotations.md
+++ b/docs/book/23-Annotations.md
@@ -208,7 +208,7 @@ public @interface SimulatingNull {
当有些框架需要一些额外的信息才能与你的源代码协同工作,这种情况下注解就会变得十分有用。像 Enterprise JavaBeans (EJB3 之前)这样的技术,每一个 Bean 都需要需要大量的接口和部署描述文件,而这些就是“样板”文件。Web Service,自定义标签库以及对象/关系映射工具(例如 Toplink 和 Hibernate)通常都需要 XML 描述文件,而这些文件脱离于代码之外。除了定义 Java 类,程序员还必须忍受沉闷,重复的提供某些信息,例如类名和包名等已经在原始类中已经提供的信息。每当你使用外部描述文件时,他就拥有了一个类的两个独立信息源,这经常导致代码的同步问题。同时这也要求了为项目工作的程序员在知道如何编写 Java 程序的同时,也必须知道如何编辑描述文件。
-假设你想提供一些基本的对象/关系映射功能,能够自动生成数据库表。你可以使用 XML 描述文件来指明类的名字、每个成员以及数据库映射的相关信息。但是,通过使用注解,你可以把所有信息都保存在 **JavaBean** 源文件中。为此你需要一些用于定义数据库名称、数据库列以及将 SQL 类型映射到属性的注解。
+假设你想提供一些基本的对象/关系映射功能,能够自动生成数据库表。你可以使用 XML 描述文件来指明类的名字、每个成员以及数据库映射的相关信息。但是,通过使用注解,你可以把所有信息都保存在 **JavaBean** 源文件中。为此你需要一些用于定义数据库表名称、数据库列以及将 SQL 类型映射到属性的注解。
以下是一个注解的定义,它告诉注解处理器应该创建一个数据库表:
From 4b2d43edf4549fec1d265f0ae4787b7efde03722 Mon Sep 17 00:00:00 2001
From: Joe <736777445@qq.com>
Date: Sun, 2 Feb 2020 19:54:24 +0800
Subject: [PATCH 021/224] Update 03-Objects-Everywhere.md
#367
---
docs/book/03-Objects-Everywhere.md | 9 ++++++++-
1 file changed, 8 insertions(+), 1 deletion(-)
diff --git a/docs/book/03-Objects-Everywhere.md b/docs/book/03-Objects-Everywhere.md
index 95cf3898..e1ab1282 100644
--- a/docs/book/03-Objects-Everywhere.md
+++ b/docs/book/03-Objects-Everywhere.md
@@ -82,11 +82,18 @@ Java 确定了每种基本类型的内存占用大小。 这些大小不会像
char c = 'x';
Character ch = new Character(c);
```
-或者你也可以使用下面的形式,基本类型自动转换成包装类型(自动装箱):
+或者你也可以使用下面的形式:
```java
Character ch = new Character('x');
```
+
+基本类型自动转换成包装类型(自动装箱)
+
+```java
+Character ch = 'x';
+```
+
相对的,包装类型转化为基本类型(自动拆箱):
```java
From edd8100cdfdacca9b4e7a6addbff8f621e93a767 Mon Sep 17 00:00:00 2001
From: Joe <736777445@qq.com>
Date: Sun, 2 Feb 2020 19:56:09 +0800
Subject: [PATCH 022/224] Update 03-Objects-Everywhere.md
close #367
---
docs/book/03-Objects-Everywhere.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/docs/book/03-Objects-Everywhere.md b/docs/book/03-Objects-Everywhere.md
index e1ab1282..e6ebe9d9 100644
--- a/docs/book/03-Objects-Everywhere.md
+++ b/docs/book/03-Objects-Everywhere.md
@@ -370,7 +370,7 @@ void nothing2() {
Java 采取了一种新的方法避免了以上这些问题:为一个类库生成一个明确的名称,Java 创建者希望我们反向使用自己的网络域名,因为域名通常是唯一的。因此我的域名是 MindviewInc.com,所以我将我的 foibles 类库命名为 com.mindviewinc.utility.foibles。反转域名后,`.` 用来代表子目录的划分。
-在 Java 1.0 和 Java 1.1 中,域扩展名 com、 edu、 org 和 net 等按惯例大写,因此类库中会出现这样类似的名称:com.mindviewinc.utility.foibles。然而,在 Java 2 的开发过程中,他们发现这会导致问题,所以现在整个包名都是小写的。此机制意味着所有文件都自动存在于自己的命名空间中,文件中的每个类都具有唯一标识符。这样,Java 语言可以防止名称冲突。
+在 Java 1.0 和 Java 1.1 中,域扩展名 com、 edu、 org 和 net 等按惯例大写,因此类库中会出现这样类似的名称:Com.mindviewinc.utility.foibles。然而,在 Java 2 的开发过程中,他们发现这会导致问题,所以现在整个包名都是小写的。此机制意味着所有文件都自动存在于自己的命名空间中,文件中的每个类都具有唯一标识符。这样,Java 语言可以防止名称冲突。
使用反向 URL 是一种新的命名空间方法,在此之前尚未有其他语言这么做过。Java 中有许多这些“创造性”地解决问题的方法。正如你想象,如果我们未经测试就添加一个功能并用于生产,那么在将来发现该功能的问题再想纠正,通常为时已晚(有些错误太严重了就得从语言中删除新功能。)
From 9f9e25172324fcff14eba01d020699a3580a9df7 Mon Sep 17 00:00:00 2001
From: eliondog <1258468356@qq.com>
Date: Thu, 6 Feb 2020 10:07:27 +0800
Subject: [PATCH 023/224] =?UTF-8?q?=E4=BF=AE=E6=94=B9=E4=BA=86=E9=94=99?=
=?UTF-8?q?=E5=88=AB=E5=AD=97=EF=BC=8C[=E5=95=86=E5=8A=A1=E8=99=9A?=
=?UTF-8?q?=E4=BC=9A]=E6=94=B9=E4=B8=BA[=E5=95=86=E5=8A=A1=E8=81=9A?=
=?UTF-8?q?=E4=BC=9A]=20(#374)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
docs/book/00-Preface.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/docs/book/00-Preface.md b/docs/book/00-Preface.md
index 93871fca..f1843204 100644
--- a/docs/book/00-Preface.md
+++ b/docs/book/00-Preface.md
@@ -87,7 +87,7 @@ Java 的普及性对于其受欢迎程度有重要意义。学习 Java 会让你
感谢 *Ben Muschko* 在整理构建文件方面的工作,还有感谢 *Hans Dockter* 给 *Ben* 提供了时间。
-感谢 *Jeremy Cerise* 和 *Bill Frasure* 来到开发商务虚会预订,并随后提供了宝贵的帮助。
+感谢 *Jeremy Cerise* 和 *Bill Frasure* 来到开发商务聚会预订,并随后提供了宝贵的帮助。
感谢所有花时间和精力来科罗拉多州克雷斯特德比特(Crested Butte, Colorado)镇参加我的研讨会,开发商务聚会和其他活动的人!你们的贡献可能不容易看到,但却非常重要!
From 9ad128cb71f7f5dd3316348c0ac533cd871389ed Mon Sep 17 00:00:00 2001
From: eliondog <1258468356@qq.com>
Date: Thu, 6 Feb 2020 13:43:25 +0800
Subject: [PATCH 024/224] =?UTF-8?q?Fix=EF=BC=9A=E4=BF=AE=E6=AD=A3=E8=AF=AD?=
=?UTF-8?q?=E5=8F=A5=E9=97=AE=E9=A2=98=E5=8F=8A=E9=94=99=E5=AD=97=20(#375)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
* 修改了错别字,[商务虚会]改为[商务聚会]
* Fix:修正语句问题
* Fix:修正语句问题
* Fix:修正语句问题
* Fix:修正语句问题
* Fix:修正语句问题
* Fix:修正语句问题
---
docs/book/00-Introduction.md | 2 +-
docs/book/01-What-is-an-Object.md | 6 +++---
2 files changed, 4 insertions(+), 4 deletions(-)
diff --git a/docs/book/00-Introduction.md b/docs/book/00-Introduction.md
index ed3cf59a..50749578 100644
--- a/docs/book/00-Introduction.md
+++ b/docs/book/00-Introduction.md
@@ -93,7 +93,7 @@ Sun 在图形界面的最后一次尝试,称为 JavaFX。当 Oracle 收购 Sun
现今 Swing 依然是 Java 发行版的一部分(只接受维护,不再有新功能开发)。而 Java 现在是一个开源项目,它应该始终可用。此外,Swing 和 JavaFX 有一些有限的交互性。这些可能是为了帮助开发者过渡到 JavaFX。
-桌面程序领域似乎从未尝勾起 Java 设计师的野心。Java 没有在图形界面取得该有的一席之地。另外,曾被大肆吹嘘的 JavaBeans 也没有获得任何影响力。(许多不幸的作者花了很多精力在 Swing 上编写书籍,甚至只用 JavaBeans 编写书籍)。Java 图形界面程序大多数情况下仅用于 IDE(集成开发环境)和一些企业内部应用程序。你可以采用 Java 开发图形界面,但这并非 Java 最擅长的领域。如果你必须学习 Swing,可以参考 *Thinking in Java* 第4版(可从 www.OnJava8.com 获得)或者通过其他专门的书籍中学习。。
+桌面程序领域似乎从未尝勾起 Java 设计师的野心。Java 没有在图形界面取得该有的一席之地。另外,曾被大肆吹嘘的 JavaBeans 也没有获得任何影响力。(许多不幸的作者花了很多精力在 Swing 上编写书籍,甚至只用 JavaBeans 编写书籍)。Java 图形界面程序大多数情况下仅用于 IDE(集成开发环境)和一些企业内部应用程序。你可以采用 Java 开发图形界面,但这并非 Java 最擅长的领域。如果你必须学习 Swing,可以参考 *Thinking in Java* 第4版(可从 www.OnJava8.com 获得)或者通过其他专门的书籍学习。
diff --git a/docs/book/01-What-is-an-Object.md b/docs/book/01-What-is-an-Object.md
index 83afe39b..5b0d6ca5 100644
--- a/docs/book/01-What-is-an-Object.md
+++ b/docs/book/01-What-is-an-Object.md
@@ -11,7 +11,7 @@
所有编程语言都提供抽象机制。从某种程度上来说,问题的复杂度直接取决于抽象的类型和质量。这里的“类型”意思是:抽象的内容是什么?汇编语言是对底层机器的轻微抽象。接着出现的“命令式”语言(如 FORTRAN,BASIC 和 C)是对汇编语言的抽象。与汇编相比,这类语言已有了长足的改进,但它们的抽象原理依然要求我们着重考虑计算机的结构,而非问题本身的结构。
-程序员必须要在机器模型(“解决方案空间”)和实际解决的问题模型(“问题空间”)之间建立起一种关联。这个过程既费精力,又脱离编程语言本身的范畴。这使得程序代码很难编写,维护代价高昂。同时还造就了一门副产业的“编程方法”学科。
+程序员必须要在机器模型(“解决方案空间”)和实际解决的问题模型(“问题空间”)之间建立起一种关联。这个过程既费精力,又脱离编程语言本身的范畴。这使得程序代码很难编写,维护代价高昂。同时还造就了一个副产业“编程方法”学科。
为机器建模的另一个方法是为要解决的问题制作模型。对一些早期语言来说,如 LISP 和 APL,它们的做法是“从不同的角度观察世界”——“所有问题都归纳为列表”或“所有问题都归纳为算法”。PROLOG 则将所有
问题都归纳为决策链。对于这些语言,我们认为它们一部分是“基于约束”的编程,另一部分则是专为
@@ -37,7 +37,7 @@ Simula 是一个很好的例子。正如这个名字所暗示的,它的作用
因此,在面向对象的程序设计中,尽管我们真正要做的是新建各种各样的数据“类型”(Type),但几乎所有面向对象的程序设计语言都采用了 `class` 关键字。当你看到 “type” 这个词的时候,请同时想到 `class`;反之亦然。
-创建好一个类后,可根据情况生成许多对象。随后,可将那些对象作为要解决问题中存在的元素进行处理。事实上,当我们进行面向对象的程序设计时,面临的最大一项挑战性就是:如何在“问题空间”(问题实际存在的地方)的元素与“方案空间”(对实际问题进行建模的地方,如计算机)的元素之间建立理想的“一对一”的映射关系。
+创建好一个类后,可根据情况生成许多对象。随后,可将那些对象作为要解决问题中存在的元素进行处理。事实上,当我们进行面向对象的程序设计时,面临的最大一项挑战是:如何在“问题空间”(问题实际存在的地方)的元素与“方案空间”(对实际问题进行建模的地方,如计算机)的元素之间建立理想的“一对一”的映射关系。
那么如何利用对象完成真正有用的工作呢?必须有一种办法能向对象发出请求,令其解决一些实际的问题,比如完成一次交易、在屏幕上画一些东西或者打开一个开关等等。每个对象仅能接受特定的请求。我们向对象发出的请求是通过它的“接口”(Interface)定义的,对象的“类型”或“类”则规定了它的接口形式。“类型”与“接口”的对应关系是面向对象程序设计的基础。
@@ -50,7 +50,7 @@ Light lt = new Light();
lt.on();
```
-在这个例子中,类型/类的名称是 **Light**,可向 **Light** 对象发出的请求包括包括打开 `on`、关闭 `off`、变得更明亮 `brighten` 或者变得更暗淡 `dim`。通过声明一个引用,如 `lt` 和 `new` 关键字,我们创建了一个 **Light** 类型的对象,再用等号将其赋给引用。
+在这个例子中,类型/类的名称是 **Light**,可向 **Light** 对象发出的请求包括打开 `on`、关闭 `off`、变得更明亮 `brighten` 或者变得更暗淡 `dim`。通过声明一个引用,如 `lt` 和 `new` 关键字,我们创建了一个 **Light** 类型的对象,再用等号将其赋给引用。
为了向对象发送消息,我们使用句点符号 `.` 将 `lt` 和消息名称 `on` 连接起来。可以看出,使用一些预先定义好的类时,我们在程序里采用的代码是非常简单直观的。
From 350881d519780e8aeadaab86578079b2de22f4dc Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E5=B7=A6=E5=85=83?= <275395953@qq.com>
Date: Sat, 8 Feb 2020 22:48:38 +0800
Subject: [PATCH 025/224] =?UTF-8?q?=E7=AC=AC=E4=BA=8C=E5=8D=81=E4=BA=94?=
=?UTF-8?q?=E7=AB=A0=20=E8=AE=BE=E8=AE=A1=E6=A8=A1=E5=BC=8F=20=E7=BF=BB?=
=?UTF-8?q?=E8=AF=91=E6=9B=B4=E6=96=B0=EF=BC=88=E2=80=9C=E5=87=BD=E6=95=B0?=
=?UTF-8?q?=E5=AF=B9=E8=B1=A1=E2=80=9D=E5=B0=8F=E8=8A=82=E7=BF=BB=E8=AF=91?=
=?UTF-8?q?=E5=AE=8C=E6=88=90=EF=BC=89=20(#377)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
docs/book/25-Patterns.md | 252 +++++++++++++++++++++++++++++++++++++++
1 file changed, 252 insertions(+)
diff --git a/docs/book/25-Patterns.md b/docs/book/25-Patterns.md
index 22ce3cfb..65b09573 100644
--- a/docs/book/25-Patterns.md
+++ b/docs/book/25-Patterns.md
@@ -789,6 +789,258 @@ KungFuGuy now battles a NastyWeapon
## 函数对象
+一个 *函数对象* 封装了一个函数。其特点就是将被调用函数的选择与那个函数被调用的位置进行解耦。
+
+*《设计模式》* 中也提到了这个术语,但是没有使用。然而,*函数对象* 的话题却在那本书的很多模式中被反复论及。
+
+### 命令模式
+
+从最直观的角度来看,*命令模式* 就是一个函数对象:一个作为对象的函数。我们可以将 *函数对象* 作为参数传递给其他方法或者对象,来执行特定的操作。
+
+在Java 8之前,想要产生单个函数的效果,我们必须明确将方法包含在对象中,而这需要太多的仪式了。而利用Java 8的lambda特性, *命令模式* 的实现将是微不足道的。
+
+```java
+// patterns/CommandPattern.java
+import java.util.*;
+
+public class CommandPattern {
+ public static void main(String[] args) {
+ List macro = Arrays.asList(
+ () -> System.out.print("Hello "),
+ () -> System.out.print("World! "),
+ () -> System.out.print("I'm the command pattern!")
+ );
+ macro.forEach(Runnable::run);
+ }
+}
+/* Output:
+Hello World! I'm the command pattern!
+*/
+```
+
+*命令模式* 的主要特点是允许向一个方法或者对象传递一个想要的动作。在上面的例子中,这个对象就是 **macro** ,而 *命令模式* 提供了将一系列需要一起执行的动作集进行排队的方法。在这里,*命令模式* 允许我们动态的创建新的行为,通常情况下我们需要编写新的代码才能完成这个功能,而在上面的例子中,我们可以通过解释运行一个脚本来完成这个功能(如果需要实现的东西很复杂请参考解释器模式)。
+
+*《设计模式》* 认为“命令模式是回调的面向对象的替代品”。尽管如此,我认为"back"(回来)这个词是callback(回调)这一概念的基本要素。也就是说,我认为回调(callback)实际上是返回到回调的创建者所在的位置。另一方面,对于 *命令* 对象,通常只需创建它并将其交给某种方法或对象,而不是自始至终以其他方式联系命令对象。不管怎样,这就是我对它的看法。在本章的后面内容中,我将会把一组设计模式放在“回调”的标题下面。
+
+### 策略模式
+
+*策略模式* 看起来像是从同一个基类继承而来的一系列 *命令* 类。但是仔细查看 *命令模式*,你就会发现它也具有同样的结构:一系列分层次的 *函数对象*。不同之处在于,这些函数对象的用法和策略模式不同。就像前面的 `io/DirList.java` 那个例子,使用 *命令* 是为了解决特定问题 -- 从一个列表中选择文件。“不变的部分”是被调用的那个方法,而变化的部分被分离出来放到 *函数对象* 中。我认为 *命令模式* 在编码阶段提供了灵活性,而 *策略模式* 的灵活性在运行时才会体现出来。尽管如此,这种区别却是非常模糊的。
+
+另外,*策略模式* 还可以添加一个“上下文(context)”,这个上下文(context)可以是一个代理类(surrogate class),用来控制对某个特定 *策略* 对象的选择和使用。就像 *桥接模式* 一样!下面我们来一探究竟:
+
+```java
+// patterns/strategy/StrategyPattern.java
+// {java patterns.strategy.StrategyPattern}
+package patterns.strategy;
+import java.util.function.*;
+import java.util.*;
+
+// The common strategy base type:
+class FindMinima {
+ Function, List> algorithm;
+}
+
+// The various strategies:
+class LeastSquares extends FindMinima {
+ LeastSquares() {
+ // Line is a sequence of points (Dummy data):
+ algorithm = (line) -> Arrays.asList(1.1, 2.2);
+ }
+}
+
+class Perturbation extends FindMinima {
+ Perturbation() {
+ algorithm = (line) -> Arrays.asList(3.3, 4.4);
+ }
+}
+
+class Bisection extends FindMinima {
+ Bisection() {
+ algorithm = (line) -> Arrays.asList(5.5, 6.6);
+ }
+}
+
+// The "Context" controls the strategy:
+class MinimaSolver {
+ private FindMinima strategy;
+ MinimaSolver(FindMinima strat) {
+ strategy = strat;
+ }
+ List minima(List line) {
+ return strategy.algorithm.apply(line);
+ }
+ void changeAlgorithm(FindMinima newAlgorithm) {
+ strategy = newAlgorithm;
+ }
+}
+
+public class StrategyPattern {
+ public static void main(String[] args) {
+ MinimaSolver solver =
+ new MinimaSolver(new LeastSquares());
+ List line = Arrays.asList(
+ 1.0, 2.0, 1.0, 2.0, -1.0,
+ 3.0, 4.0, 5.0, 4.0 );
+ System.out.println(solver.minima(line));
+ solver.changeAlgorithm(new Bisection());
+ System.out.println(solver.minima(line));
+ }
+}
+/* Output:
+[1.1, 2.2]
+[5.5, 6.6]
+*/
+```
+
+`MinimaSolver` 中的 `changeAlgorithm()` 方法将一个不同的策略插入到了 `私有` 域 `strategy` 中,这使得在调用 `minima()` 方法时,可以使用新的策略。
+
+我们可以通过将上下文注入到 `FindMinima` 中来简化我们的解决方法。
+
+```java
+// patterns/strategy/StrategyPattern2.java // {java patterns.strategy.StrategyPattern2}
+package patterns.strategy;
+import java.util.function.*;
+import java.util.*;
+
+// "Context" is now incorporated:
+class FindMinima2 {
+ Function, List> algorithm;
+ FindMinima2() { leastSquares(); } // default
+ // The various strategies:
+ void leastSquares() {
+ algorithm = (line) -> Arrays.asList(1.1, 2.2);
+ }
+ void perturbation() {
+ algorithm = (line) -> Arrays.asList(3.3, 4.4);
+ }
+ void bisection() {
+ algorithm = (line) -> Arrays.asList(5.5, 6.6);
+ }
+ List minima(List line) {
+ return algorithm.apply(line);
+ }
+}
+
+public class StrategyPattern2 {
+ public static void main(String[] args) {
+ FindMinima2 solver = new FindMinima2();
+ List line = Arrays.asList(
+ 1.0, 2.0, 1.0, 2.0, -1.0,
+ 3.0, 4.0, 5.0, 4.0 );
+ System.out.println(solver.minima(line));
+ solver.bisection();
+ System.out.println(solver.minima(line));
+ }
+}
+/* Output:
+[1.1, 2.2]
+[5.5, 6.6]
+*/
+```
+
+`FindMinima2` 封装了不同的算法,也包含了“上下文”(Context),所以它便可以在一个单独的类中控制算法的选择了。
+
+### 责任链模式
+
+*责任链模式* 也许可以被看作一个使用了 *策略* 对象的“递归的动态一般化”。此时我们进行一次调用,在一个链序列中的每个策略都试图满足这个调用。这个过程直到有一个策略成功满足该调用或者到达链序列的末尾才结束。在递归方法中,一个方法将反复调用它自身直至达到某个终止条件;使用责任链,一个方法会调用相同的基类方法(拥有不同的实现),这个基类方法将会调用基类方法的其他实现,如此反复直至达到某个终止条件。
+
+除了调用某个方法来满足某个请求以外,链中的多个方法都有机会满足这个请求,因此它有点专家系统的意味。由于责任链实际上就是一个链表,它能够动态创建,因此它可以看作是一个更一般的动态构建的 `switch` 语句。
+
+在上面的 `StrategyPattern.java` 例子中,我们可能想自动发现一个解决方法。而 *责任链* 就可以达到这个目的:
+
+```java
+// patterns/chain/ChainOfResponsibility.java
+// Using the Functional interface
+// {java patterns.chain.ChainOfResponsibility}
+package patterns.chain;
+import java.util.*;
+import java.util.function.*;
+
+class Result {
+ boolean success;
+ List line;
+ Result(List data) {
+ success = true;
+ line = data;
+ }
+ Result() {
+ success = false;
+ line = Collections.emptyList();
+ }
+}
+
+class Fail extends Result {}
+
+interface Algorithm {
+ Result algorithm(List line);
+}
+
+class FindMinima {
+ public static Result leastSquares(List line) {
+ System.out.println("LeastSquares.algorithm");
+ boolean weSucceed = false;
+ if(weSucceed) // Actual test/calculation here
+ return new Result(Arrays.asList(1.1, 2.2));
+ else // Try the next one in the chain:
+ return new Fail();
+ }
+ public static Result perturbation(List line) {
+ System.out.println("Perturbation.algorithm");
+ boolean weSucceed = false;
+ if(weSucceed) // Actual test/calculation here
+ return new Result(Arrays.asList(3.3, 4.4));
+ else
+ return new Fail();
+ }
+ public static Result bisection(List line) {
+ System.out.println("Bisection.algorithm");
+ boolean weSucceed = true;
+ if(weSucceed) // Actual test/calculation here
+ return new Result(Arrays.asList(5.5, 6.6));
+ else
+ return new Fail();
+ }
+ static List, Result>>
+ algorithms = Arrays.asList(
+ FindMinima::leastSquares,
+ FindMinima::perturbation,
+ FindMinima::bisection
+ );
+ public static Result minima(List line) {
+ for(Function, Result> alg :
+ algorithms) {
+ Result result = alg.apply(line);
+ if(result.success)
+ return result;
+ }
+ return new Fail();
+ }
+}
+
+public class ChainOfResponsibility {
+ public static void main(String[] args) {
+ FindMinima solver = new FindMinima();
+ List line = Arrays.asList(
+ 1.0, 2.0, 1.0, 2.0, -1.0,
+ 3.0, 4.0, 5.0, 4.0);
+ Result result = solver.minima(line);
+ if(result.success)
+ System.out.println(result.line);
+ else
+ System.out.println("No algorithm found");
+ }
+}
+/* Output:
+LeastSquares.algorithm
+Perturbation.algorithm
+Bisection.algorithm
+[5.5, 6.6]
+*/
+```
+
+我们从定义一个 `Result` 类开始,这个类包含一个 `success` 标志,因此接收者就可以知道算法是否成功执行,而 `line` 变量保存了真实的数据。当算法执行失败时, `Fail` 类可以作为返回值。要注意的是,当算法执行失败时,返回一个 `Result` 对象要比抛出一个异常更加合适,因为我们有时可能并不打算解决这个问题,而是希望程序继续执行下去。
+
+每一个 `Algorithm` 接口的实现,都实现了不同的 `algorithm()` 方法。在 `FindMinama` 中,将会创建一个算法的列表(这就是所谓的“链”),而 `minima()` 方法只是遍历这个列表,然后找到能够成功执行的算法而已。
## 接口改变
From 10422d7e0f2fa0ac6d15ffbc76e3f901d8c57882 Mon Sep 17 00:00:00 2001
From: ddtyjmyjm
Date: Sun, 9 Feb 2020 15:02:01 +0800
Subject: [PATCH 026/224] =?UTF-8?q?=20Fix:=E4=BF=AE=E6=AD=A3=20Chap.7=20?=
=?UTF-8?q?=E8=AF=AD=E5=8F=A5=E9=97=AE=E9=A2=98=20(#378)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
* Fix:修正 Chap.6 语句问题
* 修改一些错误
* 修改格式的使用
* 修改格式的使用
---
docs/book/07-Implementation-Hiding.md | 56 +++++++++++++--------------
1 file changed, 28 insertions(+), 28 deletions(-)
diff --git a/docs/book/07-Implementation-Hiding.md b/docs/book/07-Implementation-Hiding.md
index a331bd5b..884466c1 100644
--- a/docs/book/07-Implementation-Hiding.md
+++ b/docs/book/07-Implementation-Hiding.md
@@ -3,25 +3,25 @@
# 第七章 封装
-访问控制(或者隐藏实现)与"最初的实现不恰当"有关。
+> *访问控制(Access control)*(或者*隐藏实现(implementation hiding)*)与“最初的实现不恰当”有关。
-所有优秀的作者——包括这些编写软件的人——都知道一件好的作品都是经过反复打磨才变得优秀的。如果你把一段代码置于某个位置一段时间,过一会重新来看,你可能发现更好的实现方式。这是重构的原动力之一,重构就是重写可工作的代码,使之更加可读,易懂,因而更易维护。
+所有优秀的作者——包括那些编写软件的人——都知道一件好的作品都是经过反复打磨才变得优秀的。如果你把一段代码置于某个位置一段时间,过一会重新来看,你可能发现更好的实现方式。这是*重构*(refactoring)的原动力之一,重构就是重写可工作的代码,使之更加可读,易懂,因而更易维护。
-但是,在修改和完善代码的愿望下,也存在巨大的压力。通常,客户端程序员希望你的代码在某些方面保持不变。所以你想修改代码,但他们希望代码保持不变。由此引出了面向对象设计中的一个基本问题:"如何区分变动的事物和不变的事物"。
+但是,在修改和完善代码的愿望下,也存在巨大的压力。通常,一些用户(*客户端程序员(client programmers)*)希望你的代码在某些方面保持不变。所以你想修改代码,但他们希望代码保持不变。由此引出了面向对象设计中的一个基本问题:“如何区分变动的事物和不变的事物”。
-这个问题对于类库而言尤其重要。类库的使用者必须依赖他们所使用的那部分类库,并且知道如果使用了类库的新版本,不需要改写代码。另一方面,类库的开发者必须有修改和改进类库的自由,并保证客户代码不会受这些改动影响。
+这个问题对于类库(library)而言尤其重要。类库的使用者必须依赖他们所使用的那部分类库,并且知道如果使用了类库的新版本,不需要改写代码。另一方面,类库的开发者必须有修改和改进类库的自由,并保证客户代码不会受这些改动影响。
这可以通过约定解决。例如,类库开发者必须同意在修改类库中的一个类时,不会移除已有的方法,因为那样将会破坏客户端程序员的代码。与之相反的情况更加复杂。在有成员属性的情况下,类库开发者如何知道哪些属性被客户端程序员使用?这同样会发生在那些只为实现类库类而创建的方法上,它们也不是设计成可供客户端程序员调用的。如果类库开发者想删除旧的实现,添加新的实现,结果会怎样呢?任何这些成员的改动都可能破环客户端程序员的代码。因此类库开发者会被束缚,不能修改任何事物。
-为了解决这一问题,Java 提供了访问修饰符供类库开发者指明哪些对于客户端程序员是可用的,哪些是不可用的。访问控制权限的等级,从"最大权限"到"最小权限"依次是:**public**,**protected**,包访问权限(没有关键字)和 **private**。根据上一段的内容,你可能会想,作为一名类库设计者,你会尽可能将一切都设为 **private**,仅向客户端程序员暴露你愿意他们使用的方法。这就是你通常所做的,尽管这与使用其他语言(尤其是 C)编程和访问不受任何限制的人们的直觉相违背。
+为了解决这一问题,Java 提供了*访问修饰符*(access specifier)供类库开发者指明哪些对于客户端程序员是可用的,哪些是不可用的。访问控制权限的等级,从“最大权限”到“最小权限”依次是:**public**,**protected**,*包访问权限(package access)*(没有关键字)和 **private**。根据上一段的内容,你可能会想,作为一名类库设计者,你会尽可能将一切都设为 **private**,仅向客户端程序员暴露你愿意他们使用的方法。这就是你通常所做的,尽管这与那些使用其他语言(尤其是 C)编程以及习惯了不受限制地访问任何东西的人们的直觉相违背。
-然而,构建类库的概念和对类库组件的访问控制仍然不完善。其中仍然存在问题就是如何将类库组件捆绑到一个内聚到类库单元中。Java 中通过 package 关键字加以控制,类是在相同包下还是不同包下会影响访问修饰符。所以在这章开始,你将会学习如何将类库组件置于同一个包下,之后你就能明白访问修饰符的全部含义。
+然而,类库组件的概念和对类库组件访问的控制仍然不完善。其中仍然存在问题就是如何将类库组件捆绑到一个内聚的类库单元中。Java 中通过 **package** 关键字加以控制,类在相同包下还是在不同包下,会影响访问修饰符。所以在这章开始,你将会学习如何将类库组件置于同一个包下,之后你就能明白访问修饰符的全部含义。
## 包的概念
-包内包含一组类,它们被组织在一个单独的命名空间下。
+包内包含一组类,它们被组织在一个单独的*命名空间*(namespace)下。
例如,标准 Java 发布中有一个工具库,它被组织在 **java.util** 命名空间下。**java.util** 中含有一个类,叫做 **ArrayList**。使用 **ArrayList** 的一种方式是用其全名 **java.util.ArrayList**。
@@ -55,15 +55,15 @@ import java.util.*
之所以使用导入,是为了提供一种管理命名空间的机制。所有类名之间都是相互隔离的。类 **A** 中的方法 `f()` 不会与类 **B** 中具有相同签名的方法 `f()` 冲突。但是如果类名冲突呢?假设你创建了一个 **Stack** 类,打算安装在一台已经有别人所写的 **Stack** 类的机器上,该怎么办呢?这种类名的潜在冲突,正是我们需要在 Java 中对命名空间进行完全控制的原因。为了解决冲突,我们为每个类创建一个唯一标识符组合。
-到目前为止的大部分示例都只存在单个文件,并为本地使用的,所以尚未受到包名的干扰。但是,这些示例其实已经位于包中了,叫做"未命名"包或默认包。这当然是一种选择,为了简单起见,本书其余部分会尽可能采用这种方式。但是,如果你打算为相同机器上的其他 Java 程序创建友好的类库或程序时,就必须仔细考虑以防类名冲突。
+到目前为止的大部分示例都只存在单个文件,并为本地使用的,所以尚未受到包名的干扰。但是,这些示例其实已经位于包中了,叫做“未命名”包或*默认包*(default package)。这当然是一种选择,为了简单起见,本书其余部分会尽可能采用这种方式。但是,如果你打算为相同机器上的其他 Java 程序创建友好的类库或程序时,就必须仔细考虑以防类名冲突。
-一个 Java 源代码文件称为一个*编译单元*(有时也称*翻译单元*)。每个编译单元的文件名后缀必须是 **.java**。在编译单元中可以有一个 **public** 类,它的类名必须与文件名相同(包括大小写,但不包括后缀名 **.java**)。每个编译单元中只能有一个 **public** 类,否则编译器不接受。如果这个编译单元中还有其他类,那么在包之外是无法访问到这些类的,因为它们不是 **public** 类,此时它们支持主 **public** 类。
+一个 Java 源代码文件称为一个*编译单元(compilation unit)*(有时也称*翻译单元(translation unit)*)。每个编译单元的文件名后缀必须是 **.java**。在编译单元中可以有一个 **public** 类,它的类名必须与文件名相同(包括大小写,但不包括后缀名 **.java**)。每个编译单元中只能有一个 **public** 类,否则编译器不接受。如果这个编译单元中还有其他类,那么在包之外是无法访问到这些类的,因为它们不是 **public** 类,此时它们为主 **public** 类提供“支持”类 。
### 代码组织
-当编译一个 **.java** 文件时,**.java** 文件的每个类都会有一个输出文件。每个输出的文件名和 **.java** 文件中每个类的类名相同,只是后缀名是 **.class**。因此,在编译少量的 **.java** 文件后,会得到大量的 **.class** 文件。如果你使用过编译型语言,那么你可能习惯编译后产生一个中间文件(通常称为"obj"文件),然后与使用链接器(创建可执行文件)或类库生成器(创建类库)产生的其他同类文件打包到一起的情况。这不是 Java 工作的方式。在 Java 中,可运行程序是一组 **.class** 文件,它们可以打包压缩成一个 Java 文档文件(JAR,使用 **jar** 文档生成器)。Java 解释器负责查找、加载和解释这些文件。
+当编译一个 **.java** 文件时,**.java** 文件的每个类都会有一个输出文件。每个输出的文件名和 **.java** 文件中每个类的类名相同,只是后缀名是 **.class**。因此,在编译少量的 **.java** 文件后,会得到大量的 **.class** 文件。如果你使用过编译型语言,那么你可能习惯编译后产生一个中间文件(通常称为“obj”文件),然后与使用链接器(创建可执行文件)或类库生成器(创建类库)产生的其他同类文件打包到一起的情况。这不是 Java 工作的方式。在 Java 中,可运行程序是一组 **.class** 文件,它们可以打包压缩成一个 Java 文档文件(JAR,使用 **jar** 文档生成器)。Java 解释器负责查找、加载和解释这些文件。
-类库是一组类文件。每个源文件通常都含有一个 **public** 类和任意数量的非 **public** 类,因此每个文件都有一个构件。如果把这些组件集中在一起,就需要使用关键字 **package**。
+类库是一组类文件。每个源文件通常都含有一个 **public** 类和任意数量的非 **public** 类,因此每个文件都有一个 **public** 组件。如果把这些组件集中在一起,就需要使用关键字 **package**。
如果你使用了 **package** 语句,它必须是文件中除了注释之外的第一行代码。当你如下这样写:
@@ -71,9 +71,9 @@ import java.util.*
package hiding;
```
-意味着这个编译单元是一个名为 **hiding** 类库的一部分。换句话说,你正在声明的编译单元中的 **public** 类名称位于名为 **hiding** 的保护伞下。任何人想要使用该名称,必须指明完整的类名或者使用 **import** 关键字导入 **hiding**。(注意,Java 包名按惯例一律小写,即使中间的单词也需要小写,与驼峰命名不同)
+意味着这个编译单元是一个名为 **hiding** 类库的一部分。换句话说,你正在声明的编译单元中的 **public** 类名称位于名为 **hiding** 的保护伞下。任何人想要使用该名称,必须指明完整的类名或者使用 **import** 关键字导入 **hiding** 。(注意,Java 包名按惯例一律小写,即使中间的单词也需要小写,与驼峰命名不同)
-例如,假设文件名是 **MyClass.java**,这意味着文件中只能有一个 **public** 类,且类名必须是 MyClass(大小写也与文件名相同):
+例如,假设文件名是 **MyClass.java** ,这意味着文件中只能有一个 **public** 类,且类名必须是 **MyClass**(大小写也与文件名相同):
```java
// hiding/mypackage/MyClass.java
@@ -117,7 +117,7 @@ public class ImportedMyClass {
将所有的文件放在一个子目录还解决了其他的两个问题:创建独一无二的包名和查找可能隐藏于目录结构某处的类。这是通过将 **.class** 文件所在的路径位置编码成 **package** 名称来实现的。按照惯例,**package** 名称是类的创建者的反顺序的 Internet 域名。如果你遵循惯例,因为 Internet 域名是独一无二的,所以你的 **package** 名称也应该是独一无二的,不会发生名称冲突。如果你没有自己的域名,你就得构造一组不大可能与他人重复的组合(比如你的姓名),来创建独一无二的 package 名称。如果你打算发布 Java 程序代码,那么花些力气去获取一个域名是值得的。
-此技巧的第二部分是把 **package** 名称分解成你机器上的一个目录,所以当 Java 解释器必须要加载一个 .class 文件时,它能定位到 **.class** 文件所在的位置。首先,它找出环境变量 **CLASSPATH**(通过操作系统设置,有时也能通过 Java 的安装程序或基于 Java 的工具设置)。**CLASSPATH** 包含一个或多个目录,用作查找 .**class** 文件的根目录。从根目录开始,Java 解释器获取包名并将每个句点替换成反斜杠,生成一个基于根目录的路径名(包名 foo.bar.baz 变成 foo\bar\baz 或 foo/bar/baz 或其它,取决于你的操作系统)。然后这个路径与 **CLASSPATH** 的不同项连接,解释器就在这些目录中查找与你所创建的类名称相关的 **.class** 文件(解释器还会查找某些涉及 Java 解释器所在位置的标准目录)。
+此技巧的第二部分是把 **package** 名称分解成你机器上的一个目录,所以当 Java 解释器必须要加载一个 .class 文件时,它能定位到 **.class** 文件所在的位置。首先,它找出环境变量 **CLASSPATH**(通过操作系统设置,有时也能通过 Java 的安装程序或基于 Java 的工具设置)。**CLASSPATH** 包含一个或多个目录,用作查找 .**class** 文件的根目录。从根目录开始,Java 解释器获取包名并将每个句点替换成反斜杠,生成一个基于根目录的路径名(取决于你的操作系统,包名 **foo.bar.baz** 变成 **foo\bar\baz** 或 **foo/bar/baz** 或其它)。然后这个路径与 **CLASSPATH** 的不同项连接,解释器就在这些目录中查找与你所创建的类名称相关的 **.class** 文件(解释器还会查找某些涉及 Java 解释器所在位置的标准目录)。
为了理解这点,比如说我的域名 **MindviewInc.com**,将之反转并全部改为小写后就是 **com.mindviewinc**,这将作为我创建的类的独一无二的全局名称。(com、edu、org等扩展名之前在 Java 包中都是大写,但是 Java 2 之后都统一用小写。)我决定再创建一个名为 **simple** 的类库,从而细分名称:
@@ -197,7 +197,7 @@ com.mindviewinc.simple.List
当编译器遇到导入 **simple** 库的 **import** 语句时,它首先会在 CLASSPATH 指定的目录中查找子目录 **com/mindviewinc/simple**,然后从已编译的文件中找出名称相符者(对 **Vector** 而言是 **Vector.class**,对 **List** 而言是 **List.class**)。注意,这两个类和其中要访问的方法都必须是 **public** 修饰的。
-对于 Java 新手而言,设置 CLASSPATH 是一件麻烦的事(我最初使用时这么觉得),后面版本的 JDK 更加智能。你会发现当你安装好 JDK 时,即使不设置 CLASSPATH,也能够编译和运行基本的 Java 程序。但是,为了编译和运行本书的代码示例(从[https://github.com/BruceEckel/OnJava8-examples](https://github.com/BruceEckel/OnJava8-examples) 取得),你必须将本书程序代码树的基本目录加入到 CLASSPATH 中( gradlew 命令管理自身的 CLASSPATH,所以如果你想直接使用 javac 和 java,不用 Gradle 的话,就需要设置 CLASSPATH)。
+对于 Java 新手而言,设置 CLASSPATH 是一件麻烦的事(我最初使用时是这么觉得的),后面版本的 JDK 更加智能。你会发现当你安装好 JDK 时,即使不设置 CLASSPATH,也能够编译和运行基本的 Java 程序。但是,为了编译和运行本书的代码示例(从[https://github.com/BruceEckel/OnJava8-examples](https://github.com/BruceEckel/OnJava8-examples) 取得),你必须将本书程序代码树的基本目录加入到 CLASSPATH 中( gradlew 命令管理自身的 CLASSPATH,所以如果你想直接使用 javac 和 java,不用 Gradle 的话,就需要设置 CLASSPATH)。
### 冲突
@@ -230,9 +230,9 @@ java.util.Vector v = new java.util.Vector();
具备了以上知识,现在就可以创建自己的工具库来减少重复的程序代码了。
-一般来说,我会使用反转后的域名来命名要创建的工具包,比如 **com.mindviewinc.util**,但为了简化,这里我把工具包命名为 **onjava**。
+一般来说,我会使用反转后的域名来命名要创建的工具包,比如 **com.mindviewinc.util** ,但为了简化,这里我把工具包命名为 **onjava**。
-比如,下面是"控制流"一章中使用到的 `range()` 方法,采用了 for-in 语法进行简单的遍历:
+比如,下面是“控制流”一章中使用到的 `range()` 方法,采用了 for-in 语法进行简单的遍历:
```java
// onjava/Range.java
@@ -276,13 +276,13 @@ public class Range {
### 使用 import 改变行为
-Java 没有 C 的*条件编译*功能,该功能使你不必更改任何程序代码而能够切换开关产生不同的行为。Java 之所以去掉此功能,可能是因为 C 在绝大多数情况下使用该功能解决跨平台问题:程序代码的不同部分要根据不同的平台来编译。而 Java 自身就是跨平台设计的,这个功能就没有必要了。
+Java 没有 C 的*条件编译*(conditional compilation)功能,该功能使你不必更改任何程序代码而能够切换开关产生不同的行为。Java 之所以去掉此功能,可能是因为 C 在绝大多数情况下使用该功能解决跨平台问题:程序代码的不同部分要根据不同的平台来编译。而 Java 自身就是跨平台设计的,这个功能就没有必要了。
但是,条件编译还有其他的用途。调试是一个很常见的用途,调试功能在开发过程中是开启的,在发布的产品中是禁用的。可以通过改变导入的 **package** 来实现这一目的,修改的方法是将程序中的代码从调试版改为发布版。这个技术可用于任何种类的条件代码。
### 使用包的忠告
-当创建一个包时,包名就隐含了目录结构。这个包必须位于包名指定的目录中,该目录必须在以 CLASSPATH 开始的目录中可以查询到。 最初使用关键字 **package** 可能会有点不顺,因为除非遵守"包名对应目录路径"的规则,否则会收到很多意外的运行时错误信息如找不到特定的类,即使这个类就位于同一目录中。如果你收到类似信息,尝试把 **package** 语句注释掉,如果程序能运行的话,你就知道问题出现在哪里了。
+当创建一个包时,包名就隐含了目录结构。这个包必须位于包名指定的目录中,该目录必须在以 CLASSPATH 开始的目录中可以查询到。 最初使用关键字 **package** 可能会有点不顺,因为除非遵守“包名对应目录路径”的规则,否则会收到很多意外的运行时错误信息如找不到特定的类,即使这个类就位于同一目录中。如果你收到类似信息,尝试把 **package** 语句注释掉,如果程序能运行的话,你就知道问题出现在哪里了。
注意,编译过的代码通常位于与源代码的不同目录中。这是很多工程的标准,而且集成开发环境(IDE)通常会自动为我们做这些。必须保证 JVM 通过 CLASSPATH 能找到编译后的代码。
@@ -296,13 +296,13 @@ Java 访问权限修饰符 **public**,**protected** 和 **private** 位于定
### 包访问权限
-本章之前的所有示例要么使用 **public** 访问修饰符,要么就没使用修饰符(默认访问)。默认访问权限没有关键字,通常被称为包访问权限(有时也称为 friendly)。这意味着当前包中的所有其他类都可以访问那个成员。对于这个包之外的类,这个成员看上去是 **private** 的。由于一个编译单元(即一个文件)只能隶属于一个包,所以通过包访问权限,位于同一编译单元中的所有类彼此之间都是可访问的。
+本章之前的所有示例要么使用 **public** 访问修饰符,要么就没使用修饰符(*默认访问权限(default access)*)。默认访问权限没有关键字,通常被称为*包访问权限(package access)*(有时也称为 **friendly**)。这意味着当前包中的所有其他类都可以访问那个成员。对于这个包之外的类,这个成员看上去是 **private** 的。由于一个编译单元(即一个文件)只能隶属于一个包,所以通过包访问权限,位于同一编译单元中的所有类彼此之间都是可访问的。
-包访问权限可以把相关类聚到一个包下,以便它们能轻易地相互访问。包里的类给它们的包访问权限的成员赋予了相互访问的权限,所以你"拥有”了包内的程序代码。只能通过你所拥有的代码去访问你所拥有的其他代码,这样规定很有意义。构建包访问权限机制是将类聚集在包中的重要原因之一。在许多语言中,在文件中组织定义的方式是任意的,但是在 Java 中你被强制以一种合理的方式组织它们。另外,你可能会将不应该对当前包中的类具有访问权限的类排除在包外。
+包访问权限可以把相关类聚到一个包下,以便它们能轻易地相互访问。包里的类赋予了它们包访问权限的成员相互访问的权限,所以你"拥有”了包内的程序代码。只能通过你所拥有的代码去访问你所拥有的其他代码,这样规定很有意义。构建包访问权限机制是将类聚集在包中的重要原因之一。在许多语言中,在文件中组织定义的方式是任意的,但是在 Java 中你被强制以一种合理的方式组织它们。另外,你可能会将不应该对当前包中的类具有访问权限的类排除在包外。
-类控制着哪些代码有权访问自己的成员。其他包中的代码不能一上来就说"嗨,我是 **Bob** 的朋友!",然后想看到 **Bob** 的 **protected**,包访问权限和 **private** 成员。取得对成员的访问权的唯一方式是:
+类控制着哪些代码有权访问自己的成员。其他包中的代码不能一上来就说"嗨,我是 **Bob** 的朋友!",然后想看到 **Bob** 的 **protected**、包访问权限和 **private** 成员。取得对成员的访问权的唯一方式是:
-1. 使成员成为 public。那么无论是谁,无论在哪,都可以访问它。
+1. 使成员成为 **public**。那么无论是谁,无论在哪,都可以访问它。
2. 赋予成员默认包访问权限,不用加任何访问修饰符,然后将其他类放在相同的包内。这样,其他类就可以访问该成员。
3. 在"复用"这一章你将看到,继承的类既可以访问 **public** 成员,也可以访问 **protected** 成员(但不能访问 **private** 成员)。只有当两个类处于同一个包内,它才可以访问包访问权限的成员。但现在不用担心继承和 **protected**。
4. 提供访问器(accessor)和修改器(mutator)方法(有时也称为"get/set" 方法),从而读取和改变值。
@@ -327,7 +327,7 @@ public class Cookie {
}
```
-记住,**Cookie.java** 文件产生的类文件必须位于名为 **dessert** 的子目录中,该子目录在 **hiding** (表明本书的"封装"章节)下,它必须在 CLASSPATH 的几个目录之下。不要错误地认为 Java 总是会将当前目录视作查找行为的起点之一。如果你的 CLASSPATH 中没有 `.`,Java 就不会查找单独当前目录。
+记住,**Cookie.java** 文件产生的类文件必须位于名为 **dessert** 的子目录中,该子目录在 **hiding** (表明本书的"封装"章节)下,它必须在 CLASSPATH 的几个目录之下。不要错误地认为 Java 总是会将当前目录视作查找行为的起点之一。如果你的 CLASSPATH 中没有 **.**,Java 就不会查找单独当前目录。
现在,使用 **Cookie** 创建一个程序:
```java
@@ -384,7 +384,7 @@ class Pie {
}
```
-最初看上去这两个文件毫不相关,但在 **Cake** 中可以创建一个 **Pie** 对象并调用它的 `f()` 方法。(注意,你的 CLASSPATH 中一定得有 `.`,这样文件才能编译)通常会认为 **Pie** 和 `f()` 具有包访问权限,因此不能被 **Cake** 访问。它们的确具有包访问权限,这是部分正确。**Cake.java** 可以访问它们是因为它们在相同的目录中且没有给自己设定明确的包名。Java 把这样的文件看作是隶属于该目录的默认包中,因此它们为该目录中所有的其他文件都提供了包访问权限。
+最初看上去这两个文件毫不相关,但在 **Cake** 中可以创建一个 **Pie** 对象并调用它的 `f()` 方法。(注意,你的 CLASSPATH 中一定得有 **.**,这样文件才能编译)通常会认为 **Pie** 和 `f()` 具有包访问权限,因此不能被 **Cake** 访问。它们的确具有包访问权限,这是部分正确。**Cake.java** 可以访问它们是因为它们在相同的目录中且没有给自己设定明确的包名。Java 把这样的文件看作是隶属于该目录的默认包中,因此它们为该目录中所有的其他文件都提供了包访问权限。
### private: 你无法访问
@@ -556,7 +556,7 @@ new PublicConstructor();
## 接口和实现
-访问控制通常被称为实现的隐藏。将数据和方法包装进类中并把具体实现隐藏被称作是封装。其结果就是一个同时带有特征和行为的数据类型。
+访问控制通常被称为*隐藏实现*(implementation hiding)。将数据和方法包装进类中并把具体实现隐藏被称作是*封装*(encapsulation)。其结果就是一个同时带有特征和行为的数据类型。
出于两个重要的原因,访问控制在数据类型内部划定了边界。第一个原因是确立客户端程序员可以使用和不能使用的边界。可以在结构中建立自己的内部机制而不必担心客户端程序员偶尔将内部实现作为他们可以使用的接口的一部分。
@@ -613,7 +613,7 @@ import hiding.*;
如果获取了一个在 **hiding** 包中的类,只用来完成 **Widget** 或 **hiding** 包下一些其他 **public** 类所要执行的任务,怎么办呢? 你不想自找麻烦为客户端程序员创建说明文档,并且你认为不久后会完全改变原有方案并将旧版本删除,替换成新版本。为了保留此灵活性,需要确保客户端程序员不依赖隐藏在 **hiding** 中的任何特定细节,那么把 **public** 关键字从类中去掉,给予它包访问权限,就可以了。
-当你创建了一个包访问权限的类,把类中的属性声明为 **private** 仍然是有意义的——应该尽可能将所有属性都声明为 **private**,但是通常把方法声明成与类(包访问权限)相同的访问权限也是合理的。由于一个包访问权限的类只能被用于包内,除非你被强制将某些方法声明为 **public**,这种情况下,编译器会告诉你。
+当你创建了一个包访问权限的类,把类中的属性声明为 **private** 仍然是有意义的——应该尽可能将所有属性都声明为 **private**,但是通常把方法声明成与类(包访问权限)相同的访问权限也是合理的。一个包访问权限的类只能被用于包内,除非强制将某些方法声明为 **public**,这种情况下,编译器会告诉你。
注意,类既不能是 **private** 的(这样除了该类自身,任何类都不能访问它),也不能是 **protected** 的。所以对于类的访问权限只有两种选择:包访问权限或者 **public**。为了防止类被外界访问,可以将所有的构造器声明为 **private**,这样只有你自己能创建对象(在类的 static 成员中):
@@ -664,7 +664,7 @@ public class Lunch {
**Soup1** 和 **Soup2** 展示了如何通过将你所有的构造器声明为 **private** 的方式防止直接创建某个类的对象。记住,如果你不显式地创建构造器,编译器会自动为你创建一个无参构造器(没有参数的构造器)。如果我们编写了无参构造器,那么编译器就不会自动创建构造器了。将构造器声明为 **private**,那么谁也无法创建该类的对象了。但是现在别人该怎么使用这个类呢?上述例子给出了两个选择。在 **Soup1** 中,有一个 **static** 方法,它的作用是创建一个新的 **Soup1** 对象并返回对象的引用。如果想要在返回引用之前在 **Soup1** 上做一些额外操作,或是记录创建了多少个 **Soup1** 对象(可以用来限制数量),这种做法是有用的。
-**Soup2** 用到了所谓的*设计模式*。这种模式叫做*单例模式*,因为它只允许创建类的一个对象。**Soup2** 类的对象是作为 **Soup2** 的 **static** **private** 成员而创建的,所以有且只有一个,你只能通过 **public** 修饰的 `access()` 方法访问到这个对象。
+**Soup2** 用到了所谓的*设计模式*(design pattern)。这种模式叫做*单例模式*(singleton),因为它只允许创建类的一个对象。**Soup2** 类的对象是作为 **Soup2** 的 **static** **private** 成员而创建的,所以有且只有一个,你只能通过 **public** 修饰的 `access()` 方法访问到这个对象。
From acca795d5ff23560d1d6c004864befb56edfe36f Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E5=B7=A6=E5=85=83?=
Date: Wed, 12 Feb 2020 20:15:18 +0800
Subject: [PATCH 027/224] =?UTF-8?q?=E7=AC=AC=E4=BA=8C=E5=8D=81=E4=BA=94?=
=?UTF-8?q?=E7=AB=A0=20=E8=AE=BE=E8=AE=A1=E6=A8=A1=E5=BC=8F=20=E7=BF=BB?=
=?UTF-8?q?=E8=AF=91=E6=9B=B4=E6=96=B0=EF=BC=88=E2=80=9C=E6=94=B9=E5=8F=98?=
=?UTF-8?q?=E6=8E=A5=E5=8F=A3=E2=80=9D=E5=B0=8F=E8=8A=82=E7=BF=BB=E8=AF=91?=
=?UTF-8?q?=E5=AE=8C=E6=88=90=EF=BC=89=20(#379)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
* 第二十五章 设计模式 翻译更新(“函数对象”小节翻译完成)
* 第二十五章 设计模式 翻译更新(“改变接口”小节翻译完成)
---
docs/book/25-Patterns.md | 138 ++++++++++++++++++++++++++++++++++++++-
1 file changed, 137 insertions(+), 1 deletion(-)
diff --git a/docs/book/25-Patterns.md b/docs/book/25-Patterns.md
index 65b09573..d8a8bf87 100644
--- a/docs/book/25-Patterns.md
+++ b/docs/book/25-Patterns.md
@@ -1043,8 +1043,144 @@ Bisection.algorithm
每一个 `Algorithm` 接口的实现,都实现了不同的 `algorithm()` 方法。在 `FindMinama` 中,将会创建一个算法的列表(这就是所谓的“链”),而 `minima()` 方法只是遍历这个列表,然后找到能够成功执行的算法而已。
-## 接口改变
+## 改变接口
+有时候我们需要解决的问题很简单,仅仅是“我没有需要的接口”而已。有两种设计模式用来解决这个问题:*适配器模式* 接受一种类型并且提供一个对其他类型的接口。*外观模式* 为一组类创建了一个接口,这样做只是为了提供一种更方便的方法来处理库或资源。
+
+### 适配器模式(Adapter)
+
+当我们手头有某个类,而我们需要的却是另外一个类,我们就可以通过 *适配器模式* 来解决问题。唯一需要做的就是产生出我们需要的那个类,有许多种方法可以完成这种适配。
+
+```java
+// patterns/adapt/Adapter.java
+// Variations on the Adapter pattern
+// {java patterns.adapt.Adapter}
+package patterns.adapt;
+
+class WhatIHave {
+ public void g() {}
+ public void h() {}
+}
+
+interface WhatIWant {
+ void f();
+}
+
+class ProxyAdapter implements WhatIWant {
+ WhatIHave whatIHave;
+ ProxyAdapter(WhatIHave wih) {
+ whatIHave = wih;
+ }
+ @Override
+ public void f() {
+ // Implement behavior using
+ // methods in WhatIHave:
+ whatIHave.g();
+ whatIHave.h();
+ }
+}
+
+class WhatIUse {
+ public void op(WhatIWant wiw) {
+ wiw.f();
+ }
+}
+
+// Approach 2: build adapter use into op():
+class WhatIUse2 extends WhatIUse {
+ public void op(WhatIHave wih) {
+ new ProxyAdapter(wih).f();
+ }
+}
+
+// Approach 3: build adapter into WhatIHave:
+class WhatIHave2 extends WhatIHave implements WhatIWant {
+ @Override
+ public void f() {
+ g();
+ h();
+ }
+}
+
+// Approach 4: use an inner class:
+class WhatIHave3 extends WhatIHave {
+ private class InnerAdapter implements WhatIWant {
+ @Override
+ public void f() {
+ g();
+ h();
+ }
+ }
+ public WhatIWant whatIWant() {
+ return new InnerAdapter();
+ }
+}
+
+public class Adapter {
+ public static void main(String[] args) {
+ WhatIUse whatIUse = new WhatIUse();
+ WhatIHave whatIHave = new WhatIHave();
+ WhatIWant adapt= new ProxyAdapter(whatIHave);
+ whatIUse.op(adapt);
+ // Approach 2:
+ WhatIUse2 whatIUse2 = new WhatIUse2();
+ whatIUse2.op(whatIHave);
+ // Approach 3:
+ WhatIHave2 whatIHave2 = new WhatIHave2();
+ whatIUse.op(whatIHave2);
+ // Approach 4:
+ WhatIHave3 whatIHave3 = new WhatIHave3();
+ whatIUse.op(whatIHave3.whatIWant());
+ }
+}
+```
+
+我想冒昧的借用一下术语“proxy”(代理),因为在 *《设计模式》* 里,他们坚持认为一个代理(proxy)必须拥有和它所代理的对象一模一样的接口。但是,如果把这两个词一起使用,叫做“代理适配器(proxy adapter)”,似乎更合理一些。
+
+### 外观模式(Façade)
+
+当我想方设法试图将需求初步(first-cut)转化成对象的时候,通常我使用的原则是:
+
+>“把所有丑陋的东西都隐藏到对象里去”。
+
+基本上说,*外观模式* 干的就是这个事情。如果我们有一堆让人头晕的类以及交互(Interactions),而它们又不是客户端程序员必须了解的,那我们就可以为客户端程序员创建一个接口只提供那些必要的功能。
+
+外观模式经常被实现为一个符合单例模式(Singleton)的抽象工厂(abstract factory)。当然,你可以通过创建包含 **静态** 工厂方法(static factory methods)的类来达到上述效果。
+
+```java
+// patterns/Facade.java
+
+class A { A(int x) {} }
+
+class B { B(long x) {} }
+
+class C { C(double x) {} }
+
+// Other classes that aren't exposed by the
+// facade go here ...
+public class Facade {
+ static A makeA(int x) { return new A(x); }
+ static B makeB(long x) { return new B(x); }
+ static C makeC(double x) { return new C(x); }
+ public static void main(String[] args) {
+ // The client programmer gets the objects
+ // by calling the static methods:
+ A a = Facade.makeA(1);
+ B b = Facade.makeB(1);
+ C c = Facade.makeC(1.0);
+ }
+}
+```
+
+《设计模式》给出的例子并不是真正的 *外观模式* ,而仅仅是一个类使用了其他的类而已。
+
+#### 包(Package)作为外观模式的变体
+
+我感觉,*外观模式* 更倾向于“过程式的(procedural)”,也就是非面向对象的(non-object-oriented):我们是通过调用某些函数才得到对象。它和抽象工厂(Abstract factory)到底有多大差别呢?*外观模式* 关键的一点是隐藏某个库的一部分类(以及它们的交互),使它们对于客户端程序员不可见,这样那些类的接口就更加简练和易于理解了。
+
+其实,这也正是 Java 的 packaging(包)的功能所完成的事情:在库以外,我们只能创建和使用被声明为公共(public)的那些类;所有非公共(non-public)的类只能被同一 package 的类使用。看起来,*外观模式* 似乎是 Java 内嵌的一个功能。
+
+公平起见,*《设计模式》* 主要是写给 C++ 读者的。尽管 C++ 有命名空间(namespaces)机制来防止全局变量和类名称之间的冲突,但它并没有提供类隐藏的机制,而在 Java 里我们可以通过声明 non-public 类来实现这一点。我认为,大多数情况下 Java 的 package 功能就足以解决针对 *外观模式* 的问题了。
## 解释器
From 2834f9093c6b14a6af940cfd432e0178d5509c4a Mon Sep 17 00:00:00 2001
From: wsb200514
Date: Sun, 23 Feb 2020 14:14:32 +0800
Subject: [PATCH 028/224] =?UTF-8?q?=E4=BD=86=E6=98=AF=E6=88=91=E4=B8=8D?=
=?UTF-8?q?=E6=83=B3=E8=BF=9B=E5=85=A5=E7=96=AF=E7=8B=82=E7=9A=84=E4=BA=BA?=
=?UTF-8?q?=E7=BE=A4=E4=BC=97->=E4=B8=AD=20(#385)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
docs/book/24-Concurrent-Programming.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/docs/book/24-Concurrent-Programming.md b/docs/book/24-Concurrent-Programming.md
index 9ecea280..0f4d8d74 100644
--- a/docs/book/24-Concurrent-Programming.md
+++ b/docs/book/24-Concurrent-Programming.md
@@ -3,7 +3,7 @@
# 第二十四章 并发编程
->爱丽丝:“但是我不想进入疯狂的人群众”
+>爱丽丝:“但是我不想进入疯狂的人群中”
>
>猫咪:“oh,你无能为力,我们都疯了,我疯了,你也疯了”
>
From 400e96d81dbbfd0efcc0228ae3c43b64ef5e9b2b Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E5=BE=90=E9=9B=81=E9=91=AB?=
Date: Thu, 27 Feb 2020 19:03:19 +0800
Subject: [PATCH 029/224] =?UTF-8?q?=E5=BF=BD=E8=B7=AF->=E5=BF=BD=E7=95=A5?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
docs/book/15-Exceptions.md | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/docs/book/15-Exceptions.md b/docs/book/15-Exceptions.md
index 653b548d..5ee290bb 100644
--- a/docs/book/15-Exceptions.md
+++ b/docs/book/15-Exceptions.md
@@ -206,7 +206,7 @@ MyException: Originated in g()
FullConstructors.main(FullConstructors.java:24)
```
-新增的代码非常简短:两个构造器定义了 MyException 类型对象的创建方式。对于第二个构造器,使用 super 关键宇明确调用了其基类构造器,它接受一个字符串作为参数。
+新增的代码非常简短:两个构造器定义了 MyException 类型对象的创建方式。对于第二个构造器,使用 super 关键字明确调用了其基类构造器,它接受一个字符串作为参数。
在异常处理程序中,调用了在 Throwable 类声明(Exception 即从此类继承)的 printStackTrace() 方法。就像从输出中看到的,它将打印“从方法调用处直到异常抛出处”的方法调用序列。这里,信息被发送到了 System.out,并自动地被捕获和显示在输出中。但是,如果调用默认版本:
@@ -2068,7 +2068,7 @@ try {
如果想把“被检查的异常”这种功能“屏蔽”掉的话,这看上去像是一个好办法。不用“吞下”异常,也不必把它放到方法的异常说明里面,而异常链还能保证你不会丢失任何原始异常的信息。
-这种技巧给了你一种选择,你可以不写 try-catch 子句和/或异常说明,直接忽路异常,让它自己沿着调用栈往上“冒泡”,同时,还可以用 getCause() 捕获并处理特定的异常,就像这样:
+这种技巧给了你一种选择,你可以不写 try-catch 子句和/或异常说明,直接忽略异常,让它自己沿着调用栈往上“冒泡”,同时,还可以用 getCause() 捕获并处理特定的异常,就像这样:
```java
// exceptions/TurnOffChecking.java
From a653a6383a340c05afd963a376e4d61cb895db6a Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E5=BE=90=E9=9B=81=E9=91=AB?=
Date: Fri, 28 Feb 2020 22:05:39 +0800
Subject: [PATCH 030/224] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E8=B7=B3=E8=BD=AC?=
=?UTF-8?q?=E9=93=BE=E6=8E=A5=E9=94=99=E8=AF=AF?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
docs/book/19-Type-Information.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/docs/book/19-Type-Information.md b/docs/book/19-Type-Information.md
index dd6514ce..773d88ae 100644
--- a/docs/book/19-Type-Information.md
+++ b/docs/book/19-Type-Information.md
@@ -1348,7 +1348,7 @@ x.getClass().equals(Derived.class)) true
### 类方法提取器
-通常,你不会直接使用反射工具,但它们可以帮助你创建更多的动态代码。反射是用来支持其他 Java 特性的,例如对象序列化(参见[附录:对象序列化](#ch040.xhtml#appendix-object-serialization))。但是,有时动态提取有关类的信息很有用。
+通常,你不会直接使用反射工具,但它们可以帮助你创建更多的动态代码。反射是用来支持其他 Java 特性的,例如对象序列化(参见[附录:对象序列化](https://lingcoder.github.io/OnJava8/#/book/Appendix-Object-Serialization))。但是,有时动态提取有关类的信息很有用。
考虑一个类方法提取器。查看类定义的源代码或 JDK 文档,只显示*在该类定义中*定义或重写的方法。但是,可能还有几十个来自基类的可用方法。找到它们既单调又费时[^1]。幸运的是,反射提供了一种方法,可以简单地编写一个工具类自动地向你展示所有的接口:
From 80bf77452addba0c6bcf55b0d279604745b2c876 Mon Sep 17 00:00:00 2001
From: XuYanxin
Date: Sat, 29 Feb 2020 12:16:28 +0800
Subject: [PATCH 031/224] =?UTF-8?q?=E5=BF=BD=E8=B7=AF->=E5=BF=BD=E7=95=A5?=
=?UTF-8?q?=20(#388)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
* 忽路->忽略
* 修复跳转链接错误
---
docs/book/15-Exceptions.md | 4 ++--
docs/book/19-Type-Information.md | 2 +-
2 files changed, 3 insertions(+), 3 deletions(-)
diff --git a/docs/book/15-Exceptions.md b/docs/book/15-Exceptions.md
index 653b548d..5ee290bb 100644
--- a/docs/book/15-Exceptions.md
+++ b/docs/book/15-Exceptions.md
@@ -206,7 +206,7 @@ MyException: Originated in g()
FullConstructors.main(FullConstructors.java:24)
```
-新增的代码非常简短:两个构造器定义了 MyException 类型对象的创建方式。对于第二个构造器,使用 super 关键宇明确调用了其基类构造器,它接受一个字符串作为参数。
+新增的代码非常简短:两个构造器定义了 MyException 类型对象的创建方式。对于第二个构造器,使用 super 关键字明确调用了其基类构造器,它接受一个字符串作为参数。
在异常处理程序中,调用了在 Throwable 类声明(Exception 即从此类继承)的 printStackTrace() 方法。就像从输出中看到的,它将打印“从方法调用处直到异常抛出处”的方法调用序列。这里,信息被发送到了 System.out,并自动地被捕获和显示在输出中。但是,如果调用默认版本:
@@ -2068,7 +2068,7 @@ try {
如果想把“被检查的异常”这种功能“屏蔽”掉的话,这看上去像是一个好办法。不用“吞下”异常,也不必把它放到方法的异常说明里面,而异常链还能保证你不会丢失任何原始异常的信息。
-这种技巧给了你一种选择,你可以不写 try-catch 子句和/或异常说明,直接忽路异常,让它自己沿着调用栈往上“冒泡”,同时,还可以用 getCause() 捕获并处理特定的异常,就像这样:
+这种技巧给了你一种选择,你可以不写 try-catch 子句和/或异常说明,直接忽略异常,让它自己沿着调用栈往上“冒泡”,同时,还可以用 getCause() 捕获并处理特定的异常,就像这样:
```java
// exceptions/TurnOffChecking.java
diff --git a/docs/book/19-Type-Information.md b/docs/book/19-Type-Information.md
index dd6514ce..773d88ae 100644
--- a/docs/book/19-Type-Information.md
+++ b/docs/book/19-Type-Information.md
@@ -1348,7 +1348,7 @@ x.getClass().equals(Derived.class)) true
### 类方法提取器
-通常,你不会直接使用反射工具,但它们可以帮助你创建更多的动态代码。反射是用来支持其他 Java 特性的,例如对象序列化(参见[附录:对象序列化](#ch040.xhtml#appendix-object-serialization))。但是,有时动态提取有关类的信息很有用。
+通常,你不会直接使用反射工具,但它们可以帮助你创建更多的动态代码。反射是用来支持其他 Java 特性的,例如对象序列化(参见[附录:对象序列化](https://lingcoder.github.io/OnJava8/#/book/Appendix-Object-Serialization))。但是,有时动态提取有关类的信息很有用。
考虑一个类方法提取器。查看类定义的源代码或 JDK 文档,只显示*在该类定义中*定义或重写的方法。但是,可能还有几十个来自基类的可用方法。找到它们既单调又费时[^1]。幸运的是,反射提供了一种方法,可以简单地编写一个工具类自动地向你展示所有的接口:
From aa75297ba6d759bfd3f9f3dda8b416d44dcf1bcf Mon Sep 17 00:00:00 2001
From: grisse
Date: Sat, 29 Feb 2020 12:17:12 +0800
Subject: [PATCH 032/224] =?UTF-8?q?Fix=20#380=20=E5=92=8C=E4=BF=AE?=
=?UTF-8?q?=E8=AE=A2=E9=97=AD=E5=8C=85=E6=AE=B5=E8=90=BD=20(#387)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
docs/book/11-Inner-Classes.md | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/docs/book/11-Inner-Classes.md b/docs/book/11-Inner-Classes.md
index ce3e0043..f7862a5c 100644
--- a/docs/book/11-Inner-Classes.md
+++ b/docs/book/11-Inner-Classes.md
@@ -368,7 +368,7 @@ public class Parcel5 {
}
```
-**PDestination** 类是 `destination()` 方法的一部分,而不是 **Parcel5** 的一部分。所以,在 `destination()` 之外不能访问 **PDestination**,注意出现在 **return** 语句中的向上转型-返回的是 **Destination** 的引用,它是 **PDestination** 的基类。当然,在 `destination()` 中定义了内部类 **PDestination**,并不意味着一旦 `dest()` 方法执行完毕,**PDestination** 就不可用了。
+**PDestination** 类是 `destination()` 方法的一部分,而不是 **Parcel5** 的一部分。所以,在 `destination()` 之外不能访问 **PDestination**,注意出现在 **return** 语句中的向上转型-返回的是 **Destination** 的引用,它是 **PDestination** 的基类。当然,在 `destination()` 中定义了内部类 **PDestination**,并不意味着一旦 `destination()` 方法执行完毕,**PDestination** 就不可用了。
你可以在同一个子目录下的任意类中对某个内部类使用类标识符 **PDestination**,这并不会有命名冲突。
@@ -429,7 +429,7 @@ public class Parcel7 {
}
```
-`contents()` 方法将返回值的生成与表示这个返回值的类的定义结合在一起!另外,这个类是匿名的,它没有名字。更糟的是,看起来似乎是你正要创建一个 **Contents** 对象。但是然后(在到达语句结束的分号之前)你却说:“等一等,我想在这里插入一个类的定义。
+`contents()` 方法将返回值的生成与表示这个返回值的类的定义结合在一起!另外,这个类是匿名的,它没有名字。更糟的是,看起来似乎是你正要创建一个 **Contents** 对象。但是然后(在到达语句结束的分号之前)你却说:“等一等,我想在这里插入一个类的定义。”
这种奇怪的语法指的是:“创建一个继承自 **Contents** 的匿名类的对象。”通过 **new** 表达式返回的引用被自动向上转型为对 **Contents** 的引用。上述匿名内部类的语法是下述形式的简化形式:
@@ -823,7 +823,7 @@ public class MultiImplementation {
闭包(**closure**)是一个可调用的对象,它记录了一些信息,这些信息来自于创建它的作用域。通过这个定义,可以看出内部类是面向对象的闭包,因为它不仅包含外围类对象(创建内部类的作用域)的信息,还自动拥有一个指向此外围类对象的引用,在此作用域内,内部类有权操作所有的成员,包括 **private** 成员。
-在 Java 8 之前,生成闭包行为的唯一方式就是内部类。在 Java 8 之后,我们可以使用 lambda 表达式来生成闭包行为,并且语法更加精细和简洁;你将会在 [函数式编程 ]() 这一章节中学习相关细节。即使应该优先使用 lambda 表达式用于内部类闭包,你依旧会看到那些 Java 8 以前的代码,即使用内部类来表示闭包的方式,所以非常有必要来理解这种形式。
+在 Java 8 之前,内部类是实现闭包的唯一方式。在 Java 8 中,我们可以使用 lambda 表达式来实现闭包行为,并且语法更加优雅和简洁,你将会在 [函数式编程 ]() 这一章节中学习相关细节。尽管相对于内部类,你可能更喜欢使用 lambda 表达式实现闭包,但是你会看到并需要理解那些在 Java 8 之前通过内部类方式实现闭包的代码,因此仍然有必要来理解这种方式。
Java 最引人争议的问题之一就是,人们认为 Java 应该包含某种类似指针的机制,以允许回调(callback)。通过回调,对象能够携带一些信息,这些信息允许它在稍后的某个时刻调用初始的对象。稍后将会看到这是一个非常有用的概念。如果回调是通过指针实现的,那么就只能寄希望于程序员不会误用该指针。然而,读者应该已经了解到,Java 更小心仔细,所以没有在语言中包括指针。
From 08235e78dad5e075a412f74f4e8c65fcfe5cbe9c Mon Sep 17 00:00:00 2001
From: grisse
Date: Sat, 29 Feb 2020 12:17:39 +0800
Subject: [PATCH 033/224] fix #384 (#386)
---
docs/book/14-Streams.md | 1 +
1 file changed, 1 insertion(+)
diff --git a/docs/book/14-Streams.md b/docs/book/14-Streams.md
index 3633550e..3950ebf7 100644
--- a/docs/book/14-Streams.md
+++ b/docs/book/14-Streams.md
@@ -1128,6 +1128,7 @@ public class FileToWordsTest {
```
Not much of a cheese shop really
+is it
```
在 `System.out.format()` 中的 `%s` 表明参数为 **String** 类型。
From 8729f5c259d2e8ee350cd382306837e30add079d Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E5=BE=90=E9=9B=81=E9=91=AB?=
Date: Tue, 3 Mar 2020 13:19:18 +0800
Subject: [PATCH 034/224] fix Class.getInterface() -> Class.getInterfaces()
---
docs/book/19-Type-Information.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/docs/book/19-Type-Information.md b/docs/book/19-Type-Information.md
index 773d88ae..98af551f 100644
--- a/docs/book/19-Type-Information.md
+++ b/docs/book/19-Type-Information.md
@@ -252,7 +252,7 @@ Canonical name : typeinfo.toys.Toy
`printInfo()` 函数使用 `getName()` 来产生完整类名,使用 `getSimpleName()` 产生不带包名的类名,`getCanonicalName()` 也是产生完整类名(除内部类和数组外,对大部分类产生的结果与 `getName()` 相同)。`isInterface()` 用于判断某个 `Class` 对象代表的是否为一个接口。因此,通过 `Class` 对象,你可以得到关于该类型的所有信息。
-在主方法中调用的 `Class.getInterface()` 方法返回的是存放 `Class` 对象的数组,里面的 `Class` 对象表示的是那个类实现的接口。
+在主方法中调用的 `Class.getInterfaces()` 方法返回的是存放 `Class` 对象的数组,里面的 `Class` 对象表示的是那个类实现的接口。
另外,你还可以调用 `getSuperclass()` 方法来得到父类的 `Class` 对象,再用父类的 `Class` 对象调用该方法,重复多次,你就可以得到一个对象完整的类继承结构。
From cfa16bee91618b7a4980f4d4be57fb45ed83c418 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E5=BE=90=E9=9B=81=E9=91=AB?=
Date: Tue, 3 Mar 2020 14:53:16 +0800
Subject: [PATCH 035/224] =?UTF-8?q?=E5=90=A6=E8=80=85=20->=20=E5=90=A6?=
=?UTF-8?q?=E5=88=99?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
docs/book/19-Type-Information.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/docs/book/19-Type-Information.md b/docs/book/19-Type-Information.md
index 98af551f..580a0a3a 100644
--- a/docs/book/19-Type-Information.md
+++ b/docs/book/19-Type-Information.md
@@ -501,7 +501,7 @@ public class DynamicSupplier implements Supplier {
14
```
-注意,这个类必须假设与它一起工作的任何类型都有一个无参构造器,否者运行时会抛出异常。编译期对该程序不会产生任何警告信息。
+注意,这个类必须假设与它一起工作的任何类型都有一个无参构造器,否则运行时会抛出异常。编译期对该程序不会产生任何警告信息。
当你将泛型语法用于 `Class` 对象时,`newInstance()` 将返回该对象的确切类型,而不仅仅只是在 `ToyTest.java` 中看到的基类 `Object`。然而,这在某种程度上有些受限:
From 32b5603531247a202ddc3b07c7548c018924cf27 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E5=BE=90=E9=9B=81=E9=91=AB?=
Date: Tue, 3 Mar 2020 15:02:34 +0800
Subject: [PATCH 036/224] =?UTF-8?q?=E7=BC=96=E8=AF=91=E6=9C=9F=20->=20?=
=?UTF-8?q?=E7=BC=96=E8=AF=91=E5=99=A8=20=EF=BC=8C=E5=8E=9F=E6=96=87"the?=
=?UTF-8?q?=20compiler=20will=20only=20allow=20you=20to=20say=20that=20the?=
=?UTF-8?q?=20superclass=20reference=20is=20=E2=80=9Csome=20class=20that?=
=?UTF-8?q?=20is=20a=20superclass=20of=20FancyToy=E2=80=9D=20a"?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
docs/book/19-Type-Information.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/docs/book/19-Type-Information.md b/docs/book/19-Type-Information.md
index 580a0a3a..6fbc8795 100644
--- a/docs/book/19-Type-Information.md
+++ b/docs/book/19-Type-Information.md
@@ -527,7 +527,7 @@ public class GenericToyTest {
}
```
-如果你手头的是超类,那编译期将只允许你声明超类引用为“某个类,它是 `FancyToy` 的超类”,就像在表达式 `Class super FancyToy>` 中所看到的那样。而不会接收 `Class` 这样的声明。这看上去显得有些怪,因为 `getSuperClass()` 方法返回的是基类(不是接口),并且编译器在编译期就知道它是什么类型了(在本例中就是 `Toy.class`),而不仅仅只是"某个类"。不管怎样,正是由于这种含糊性,`up.newInstance` 的返回值不是精确类型,而只是 `Object`。
+如果你手头的是超类,那编译器将只允许你声明超类引用为“某个类,它是 `FancyToy` 的超类”,就像在表达式 `Class super FancyToy>` 中所看到的那样。而不会接收 `Class` 这样的声明。这看上去显得有些怪,因为 `getSuperClass()` 方法返回的是基类(不是接口),并且编译器在编译期就知道它是什么类型了(在本例中就是 `Toy.class`),而不仅仅只是"某个类"。不管怎样,正是由于这种含糊性,`up.newInstance` 的返回值不是精确类型,而只是 `Object`。
### `cast()` 方法
From 849890da3b64a42b412c878d90907743078cb197 Mon Sep 17 00:00:00 2001
From: XuYanxin
Date: Wed, 4 Mar 2020 10:01:48 +0800
Subject: [PATCH 037/224] fix Class.getInterface() -> Class.getInterfaces()
(#392)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
* 忽路->忽略
* 修复跳转链接错误
* fix Class.getInterface() -> Class.getInterfaces()
* 否者 -> 否则
* 编译期 -> 编译器 ,原文"the compiler will only allow you to say that the superclass reference is “some class that is a superclass of FancyToy” a"
---
docs/book/19-Type-Information.md | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/docs/book/19-Type-Information.md b/docs/book/19-Type-Information.md
index 773d88ae..6fbc8795 100644
--- a/docs/book/19-Type-Information.md
+++ b/docs/book/19-Type-Information.md
@@ -252,7 +252,7 @@ Canonical name : typeinfo.toys.Toy
`printInfo()` 函数使用 `getName()` 来产生完整类名,使用 `getSimpleName()` 产生不带包名的类名,`getCanonicalName()` 也是产生完整类名(除内部类和数组外,对大部分类产生的结果与 `getName()` 相同)。`isInterface()` 用于判断某个 `Class` 对象代表的是否为一个接口。因此,通过 `Class` 对象,你可以得到关于该类型的所有信息。
-在主方法中调用的 `Class.getInterface()` 方法返回的是存放 `Class` 对象的数组,里面的 `Class` 对象表示的是那个类实现的接口。
+在主方法中调用的 `Class.getInterfaces()` 方法返回的是存放 `Class` 对象的数组,里面的 `Class` 对象表示的是那个类实现的接口。
另外,你还可以调用 `getSuperclass()` 方法来得到父类的 `Class` 对象,再用父类的 `Class` 对象调用该方法,重复多次,你就可以得到一个对象完整的类继承结构。
@@ -501,7 +501,7 @@ public class DynamicSupplier implements Supplier {
14
```
-注意,这个类必须假设与它一起工作的任何类型都有一个无参构造器,否者运行时会抛出异常。编译期对该程序不会产生任何警告信息。
+注意,这个类必须假设与它一起工作的任何类型都有一个无参构造器,否则运行时会抛出异常。编译期对该程序不会产生任何警告信息。
当你将泛型语法用于 `Class` 对象时,`newInstance()` 将返回该对象的确切类型,而不仅仅只是在 `ToyTest.java` 中看到的基类 `Object`。然而,这在某种程度上有些受限:
@@ -527,7 +527,7 @@ public class GenericToyTest {
}
```
-如果你手头的是超类,那编译期将只允许你声明超类引用为“某个类,它是 `FancyToy` 的超类”,就像在表达式 `Class super FancyToy>` 中所看到的那样。而不会接收 `Class` 这样的声明。这看上去显得有些怪,因为 `getSuperClass()` 方法返回的是基类(不是接口),并且编译器在编译期就知道它是什么类型了(在本例中就是 `Toy.class`),而不仅仅只是"某个类"。不管怎样,正是由于这种含糊性,`up.newInstance` 的返回值不是精确类型,而只是 `Object`。
+如果你手头的是超类,那编译器将只允许你声明超类引用为“某个类,它是 `FancyToy` 的超类”,就像在表达式 `Class super FancyToy>` 中所看到的那样。而不会接收 `Class` 这样的声明。这看上去显得有些怪,因为 `getSuperClass()` 方法返回的是基类(不是接口),并且编译器在编译期就知道它是什么类型了(在本例中就是 `Toy.class`),而不仅仅只是"某个类"。不管怎样,正是由于这种含糊性,`up.newInstance` 的返回值不是精确类型,而只是 `Object`。
### `cast()` 方法
From 93189c9d33ef8af795b32f6e71b7ab29c4637888 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E5=BE=90=E9=9B=81=E9=91=AB?=
Date: Wed, 4 Mar 2020 14:27:08 +0800
Subject: [PATCH 038/224] ierator -> iterator
---
docs/book/22-Enumerations.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/docs/book/22-Enumerations.md b/docs/book/22-Enumerations.md
index 309d9cfe..a8c86bc7 100644
--- a/docs/book/22-Enumerations.md
+++ b/docs/book/22-Enumerations.md
@@ -2102,7 +2102,7 @@ table 与前一个例子中 initRow() 方法的调用次序完全相同。
虽然 Java 中的枚举比 C 或 C++中的 enum 更成熟,但它仍然是一个“小”功能,Java 没有它也已经(虽然有点笨拙)存在很多年了。而本章正好说明了一个“小”功能所能带来的价值。有时恰恰因为它,你才能够优雅而干净地解决问题。正如我在本书中一再强调的那样,优雅与清晰很重要,正是它们区别了成功的解决方案与失败的解决方案。而失败的解决方案就是因为其他人无法理解它。
-关于清晰的话题,Java 1.0 对术语 enumeration 的选择正是一个不幸的反例。对于一个专门用于从序列中选择每一个元素的对象而言,Java 竟然没有使用更通用、更普遍接受的术语 ierator 来表示它(参见[集合 ]() 章节),有些语言甚至将枚举的数据类型称为 “enumerators”!Java 修正了这个错误,但是 Enumeration 接口已经无法轻易地抹去了,因此它将一直存在于旧的(甚至有些新的)代码、类库以及文档中。
+关于清晰的话题,Java 1.0 对术语 enumeration 的选择正是一个不幸的反例。对于一个专门用于从序列中选择每一个元素的对象而言,Java 竟然没有使用更通用、更普遍接受的术语 iterator 来表示它(参见[集合 ]() 章节),有些语言甚至将枚举的数据类型称为 “enumerators”!Java 修正了这个错误,但是 Enumeration 接口已经无法轻易地抹去了,因此它将一直存在于旧的(甚至有些新的)代码、类库以及文档中。
From 661d8cca576013b95ed4ffacd6fed42076adf8c2 Mon Sep 17 00:00:00 2001
From: XuYanxin
Date: Wed, 4 Mar 2020 15:12:46 +0800
Subject: [PATCH 039/224] fix ierator -> iterator (#393)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
* 忽路->忽略
* 修复跳转链接错误
* fix Class.getInterface() -> Class.getInterfaces()
* 否者 -> 否则
* 编译期 -> 编译器 ,原文"the compiler will only allow you to say that the superclass reference is “some class that is a superclass of FancyToy” a"
* ierator -> iterator
---
docs/book/22-Enumerations.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/docs/book/22-Enumerations.md b/docs/book/22-Enumerations.md
index 309d9cfe..a8c86bc7 100644
--- a/docs/book/22-Enumerations.md
+++ b/docs/book/22-Enumerations.md
@@ -2102,7 +2102,7 @@ table 与前一个例子中 initRow() 方法的调用次序完全相同。
虽然 Java 中的枚举比 C 或 C++中的 enum 更成熟,但它仍然是一个“小”功能,Java 没有它也已经(虽然有点笨拙)存在很多年了。而本章正好说明了一个“小”功能所能带来的价值。有时恰恰因为它,你才能够优雅而干净地解决问题。正如我在本书中一再强调的那样,优雅与清晰很重要,正是它们区别了成功的解决方案与失败的解决方案。而失败的解决方案就是因为其他人无法理解它。
-关于清晰的话题,Java 1.0 对术语 enumeration 的选择正是一个不幸的反例。对于一个专门用于从序列中选择每一个元素的对象而言,Java 竟然没有使用更通用、更普遍接受的术语 ierator 来表示它(参见[集合 ]() 章节),有些语言甚至将枚举的数据类型称为 “enumerators”!Java 修正了这个错误,但是 Enumeration 接口已经无法轻易地抹去了,因此它将一直存在于旧的(甚至有些新的)代码、类库以及文档中。
+关于清晰的话题,Java 1.0 对术语 enumeration 的选择正是一个不幸的反例。对于一个专门用于从序列中选择每一个元素的对象而言,Java 竟然没有使用更通用、更普遍接受的术语 iterator 来表示它(参见[集合 ]() 章节),有些语言甚至将枚举的数据类型称为 “enumerators”!Java 修正了这个错误,但是 Enumeration 接口已经无法轻易地抹去了,因此它将一直存在于旧的(甚至有些新的)代码、类库以及文档中。
From ece6f5d401b92dc6fd70f7c1837b642286d83df1 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E5=BE=90=E9=9B=81=E9=91=AB?=
Date: Wed, 4 Mar 2020 16:20:10 +0800
Subject: [PATCH 040/224] nextint -> nextInt
---
docs/book/19-Type-Information.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/docs/book/19-Type-Information.md b/docs/book/19-Type-Information.md
index 6fbc8795..3c70fe47 100644
--- a/docs/book/19-Type-Information.md
+++ b/docs/book/19-Type-Information.md
@@ -1171,7 +1171,7 @@ class Part implements Supplier {
private static Random rand = new Random(47);
public Part get() {
- int n = rand.nextint(prototypes.size());
+ int n = rand.nextInt(prototypes.size());
return prototypes.get(n).get();
}
}
From da39cb240cafa21da5c0d6f8ba6538e98da47f43 Mon Sep 17 00:00:00 2001
From: Joe <736777445@qq.com>
Date: Wed, 4 Mar 2020 19:45:29 -0600
Subject: [PATCH 041/224] nextint -> nextInt (#394)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
* 忽路->忽略
* 修复跳转链接错误
* fix Class.getInterface() -> Class.getInterfaces()
* 否者 -> 否则
* 编译期 -> 编译器 ,原文"the compiler will only allow you to say that the superclass reference is “some class that is a superclass of FancyToy” a"
* ierator -> iterator
* nextint -> nextInt
---
docs/book/19-Type-Information.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/docs/book/19-Type-Information.md b/docs/book/19-Type-Information.md
index 6fbc8795..3c70fe47 100644
--- a/docs/book/19-Type-Information.md
+++ b/docs/book/19-Type-Information.md
@@ -1171,7 +1171,7 @@ class Part implements Supplier {
private static Random rand = new Random(47);
public Part get() {
- int n = rand.nextint(prototypes.size());
+ int n = rand.nextInt(prototypes.size());
return prototypes.get(n).get();
}
}
From 75b665ecac2cc1a091f002d8e92ffd5b9489cb42 Mon Sep 17 00:00:00 2001
From: xgsteins <33283328+xgsteins@users.noreply.github.com>
Date: Mon, 9 Mar 2020 10:25:21 +0800
Subject: [PATCH 042/224] Fix typo (#398)
---
docs/book/22-Enumerations.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/docs/book/22-Enumerations.md b/docs/book/22-Enumerations.md
index a8c86bc7..0259ec56 100644
--- a/docs/book/22-Enumerations.md
+++ b/docs/book/22-Enumerations.md
@@ -817,7 +817,7 @@ UTILITY]
KITCHEN]
```
-使用 static import 可以简化 enum 常量的使用。EnumSet 的方法的名字都相当直观,你可以查阅 JDK 文档找到其完整详细的描述。如果仔细研究了 EnunSet 的文档,你还会发现 of() 方法被重载了很多次,不但为可变数量参数进行了重载,而且为接收 2 至 5 个显式的参数的情况都进行了重载。这也从侧面表现了 EnumSet 对性能的关注。因为,其实只使用单独的 of() 方法解决可变参数已经可以解决整个问题了,但是对比显式的参数,会有一点性能损失。采用现在这种设计,当你只使用 2 到 5 个参数调用 of() 方法时,你可以调用对应的重载过的方法(速度稍快一点),而当你使用一个参数或多过 5 个参数时,你调用的将是使用可变参数的 of() 方法。注意,如果你只使用一个参数,编译器并不会构造可变参数的数组,所以与调用只有一个参数的方法相比,也就不会有额外的性能损耗。
+使用 static import 可以简化 enum 常量的使用。EnumSet 的方法的名字都相当直观,你可以查阅 JDK 文档找到其完整详细的描述。如果仔细研究了 EnumSet 的文档,你还会发现 of() 方法被重载了很多次,不但为可变数量参数进行了重载,而且为接收 2 至 5 个显式的参数的情况都进行了重载。这也从侧面表现了 EnumSet 对性能的关注。因为,其实只使用单独的 of() 方法解决可变参数已经可以解决整个问题了,但是对比显式的参数,会有一点性能损失。采用现在这种设计,当你只使用 2 到 5 个参数调用 of() 方法时,你可以调用对应的重载过的方法(速度稍快一点),而当你使用一个参数或多过 5 个参数时,你调用的将是使用可变参数的 of() 方法。注意,如果你只使用一个参数,编译器并不会构造可变参数的数组,所以与调用只有一个参数的方法相比,也就不会有额外的性能损耗。
EnumSet 的基础是 long,一个 long 值有 64 位,而一个 enum 实例只需一位 bit 表示其是否存在。
也就是说,在不超过一个 long 的表达能力的情况下,你的 EnumSet 可以应用于最多不超过 64 个元素的 enum。如果 enum 超过了 64 个元素会发生什么呢?
From beeb36ad3e7f518bd730260a177fa910005b1f5c Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E5=BE=90=E9=9B=81=E9=91=AB?=
Date: Mon, 9 Mar 2020 15:27:12 +0800
Subject: [PATCH 043/224] =?UTF-8?q?fix=20=E8=BF=99=E5=8F=A5=E7=BF=BB?=
=?UTF-8?q?=E8=AF=91=E7=AC=AC=E4=B8=80=E5=8F=8D=E5=BA=94=E6=B2=A1=E5=A4=AA?=
=?UTF-8?q?=E7=9C=8B=E6=87=82=EF=BC=8C=E7=BB=93=E5=90=88=E5=8E=9F=E6=96=87?=
=?UTF-8?q?=E5=92=8C=E4=B8=8B=E9=9D=A2=E7=9A=84=E4=BE=8B=E5=AD=90=E4=BC=98?=
=?UTF-8?q?=E5=8C=96=E4=BA=86=E4=B8=80=E4=B8=8B=E7=BF=BB=E8=AF=91?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
docs/book/21-Arrays.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/docs/book/21-Arrays.md b/docs/book/21-Arrays.md
index bda4af74..0ff7dad6 100644
--- a/docs/book/21-Arrays.md
+++ b/docs/book/21-Arrays.md
@@ -2045,7 +2045,7 @@ public class ParallelSetAll {
与使用for循环手工执行复制相比,**copyOf()** 和 **copyOfRange()** 复制数组要快得多。这些方法被重载以处理所有类型。
-我们开始复制数组的整数和整数:
+我们从复制 **int** 和 **Integer** 数组开始:
```JAVA
// arrays/ArrayCopying.java
// Demonstrate Arrays.copy() and Arrays.copyOf()
From 3c08903cf0e58229fe8ae1360869bc55d6dd7024 Mon Sep 17 00:00:00 2001
From: XuYanxin
Date: Tue, 10 Mar 2020 08:43:21 +0800
Subject: [PATCH 044/224] =?UTF-8?q?=E4=BC=98=E5=8C=96=E4=BA=86=E4=B8=80?=
=?UTF-8?q?=E4=B8=8B=E7=BF=BB=E8=AF=91=20(#399)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
* 忽路->忽略
* 修复跳转链接错误
* fix Class.getInterface() -> Class.getInterfaces()
* 否者 -> 否则
* 编译期 -> 编译器 ,原文"the compiler will only allow you to say that the superclass reference is “some class that is a superclass of FancyToy” a"
* ierator -> iterator
* nextint -> nextInt
* fix 这句翻译第一反应没太看懂,结合原文和下面的例子优化了一下翻译
---
docs/book/21-Arrays.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/docs/book/21-Arrays.md b/docs/book/21-Arrays.md
index bda4af74..0ff7dad6 100644
--- a/docs/book/21-Arrays.md
+++ b/docs/book/21-Arrays.md
@@ -2045,7 +2045,7 @@ public class ParallelSetAll {
与使用for循环手工执行复制相比,**copyOf()** 和 **copyOfRange()** 复制数组要快得多。这些方法被重载以处理所有类型。
-我们开始复制数组的整数和整数:
+我们从复制 **int** 和 **Integer** 数组开始:
```JAVA
// arrays/ArrayCopying.java
// Demonstrate Arrays.copy() and Arrays.copyOf()
From b84fd371c67351e9f7eee8bb17f4fc413543a64d Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E9=A9=AC=E7=87=95=E9=BE=99?=
Date: Tue, 10 Mar 2020 13:59:12 +0800
Subject: [PATCH 045/224] =?UTF-8?q?=E9=9D=A2=E5=90=91=E5=AF=B9=E8=B1=A1=20?=
=?UTF-8?q?(#400)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
docs/book/00-Introduction.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/docs/book/00-Introduction.md b/docs/book/00-Introduction.md
index 50749578..ad07f27a 100644
--- a/docs/book/00-Introduction.md
+++ b/docs/book/00-Introduction.md
@@ -35,7 +35,7 @@ Java 语言曾规划设计的许多功能并未按照承诺兑现。本书中,
可能你已在学校、书籍或网络上了学过这些。只要你觉得对上述的编程基本概念熟悉,你就可以完成本书的学习。
-你可以通过在 On Java 8 的网站上免费下载 《Think in C》来补充学习 Java 所需要的前置知识。本书介绍了 Java 语言的基本控制机制以及面对对象编程(OOP)的概念。在本书中我引述了一些 C/C++ 语言中的一些特性来帮助读者更好的理解 Java。毕竟 Java 是在它们的基础之上发明的,理解他们之间的区别,有助于读者更好地学习 Java。我会试图简化这些引述,尽量让没有 C/C++ 基础的读者也能很好地理解。
+你可以通过在 On Java 8 的网站上免费下载 《Think in C》来补充学习 Java 所需要的前置知识。本书介绍了 Java 语言的基本控制机制以及面向对象编程(OOP)的概念。在本书中我引述了一些 C/C++ 语言中的一些特性来帮助读者更好的理解 Java。毕竟 Java 是在它们的基础之上发明的,理解他们之间的区别,有助于读者更好地学习 Java。我会试图简化这些引述,尽量让没有 C/C++ 基础的读者也能很好地理解。
## JDK文档
From 7b8094287411818b64e35a75055c9f7272733dea Mon Sep 17 00:00:00 2001
From: bigpengry <32670376+bigpengry@users.noreply.github.com>
Date: Tue, 10 Mar 2020 16:17:03 +0800
Subject: [PATCH 046/224] fix #381 (#401)
---
docs/book/09-Polymorphism.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/docs/book/09-Polymorphism.md b/docs/book/09-Polymorphism.md
index 351a9ea9..31ffde5e 100644
--- a/docs/book/09-Polymorphism.md
+++ b/docs/book/09-Polymorphism.md
@@ -646,7 +646,7 @@ Derived dynamicGet()
在“初始化和清理”和“复用”两章中已经简单地介绍过构造器的调用顺序,但那时还没有介绍多态。
-在派生类的构造过程中总会调用基类的构造器。初始化会自动按继承层次结构上移,因此每个基类的构造器都会被调用到。这么做是有意义的,因为构造器有着特殊的任务:检查对象是否被正确地构造。由于属性通常声明为 **private**,你必须假定派生类只能访问自己的成员而不能访问基类的成员。只有基类的构造器拥有恰当的知识和权限来初始化自身的元素。因此,必须得调用所有构造器;否则就不能构造完整的对象。这就是编译器强制每个派生类部分必须调用构造器的原因。如果在派生类的构造器主体中没有显式地调用基类构造器,编译器就会默默地调用无参构造器。如果没有无参构造器,编译器就会报错(当类中不含构造器时,编译器会自动合成一个无参构造器)。
+在派生类的构造过程中总会调用基类的构造器。初始化会自动按继承层次结构上移,因此每个基类的构造器都会被调用到。这么做是有意义的,因为构造器有着特殊的任务:检查对象是否被正确地构造。由于属性通常声明为 **private**,你必须假定派生类只能访问自己的成员而不能访问基类的成员。只有基类的构造器拥有恰当的知识和权限来初始化自身的元素。因此,必须得调用所有构造器;否则就不能构造完整的对象。这就是为什么编译器会强制调用每个派生类中的构造器的原因。如果在派生类的构造器主体中没有显式地调用基类构造器,编译器就会默默地调用无参构造器。如果没有无参构造器,编译器就会报错(当类中不含构造器时,编译器会自动合成一个无参构造器)。
下面的例子展示了组合、继承和多态在构建顺序上的作用:
From e2b674da283423c794e72c0132ace072bd41e3cc Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E5=BE=90=E9=9B=81=E9=91=AB?=
Date: Fri, 13 Mar 2020 11:57:16 +0800
Subject: [PATCH 047/224] fix ojb -> obj
---
docs/book/20-Generics.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/docs/book/20-Generics.md b/docs/book/20-Generics.md
index b03c5e64..6b0c0517 100644
--- a/docs/book/20-Generics.md
+++ b/docs/book/20-Generics.md
@@ -1409,7 +1409,7 @@ class Manipulator3 {
private HasF obj;
Manipulator3(HasF x) {
- ojb = x;
+ obj = x;
}
public void manipulate() {
From 8df287501de675e800911b9fa1d2031099b1df9f Mon Sep 17 00:00:00 2001
From: XuYanxin
Date: Fri, 13 Mar 2020 13:58:23 +0800
Subject: [PATCH 048/224] fix ojb -> obj (#402)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
* 忽路->忽略
* 修复跳转链接错误
* fix Class.getInterface() -> Class.getInterfaces()
* 否者 -> 否则
* 编译期 -> 编译器 ,原文"the compiler will only allow you to say that the superclass reference is “some class that is a superclass of FancyToy” a"
* ierator -> iterator
* nextint -> nextInt
* fix 这句翻译第一反应没太看懂,结合原文和下面的例子优化了一下翻译
* fix ojb -> obj
---
docs/book/20-Generics.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/docs/book/20-Generics.md b/docs/book/20-Generics.md
index b03c5e64..6b0c0517 100644
--- a/docs/book/20-Generics.md
+++ b/docs/book/20-Generics.md
@@ -1409,7 +1409,7 @@ class Manipulator3 {
private HasF obj;
Manipulator3(HasF x) {
- ojb = x;
+ obj = x;
}
public void manipulate() {
From 8ae607d7d254c10b5d0feba0123d9960046e19d0 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E5=BE=90=E9=9B=81=E9=91=AB?=
Date: Fri, 13 Mar 2020 15:01:25 +0800
Subject: [PATCH 049/224] fix example code
---
docs/book/20-Generics.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/docs/book/20-Generics.md b/docs/book/20-Generics.md
index 6b0c0517..e78ed133 100644
--- a/docs/book/20-Generics.md
+++ b/docs/book/20-Generics.md
@@ -1595,7 +1595,7 @@ import java.util.function.*;
import onjava.*;
public class FilledList extends ArrayList {
- FilledList gen, int size) {
+ FilledList(Supplier gen, int size) {
Suppliers.fill(this, gen, size);
}
From 03afe1ec52e6795ce1efd3d3c36adb221a6cbdfa Mon Sep 17 00:00:00 2001
From: XuYanxin
Date: Fri, 13 Mar 2020 16:19:45 +0800
Subject: [PATCH 050/224] fix example code (#403)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
* 忽路->忽略
* 修复跳转链接错误
* fix Class.getInterface() -> Class.getInterfaces()
* 否者 -> 否则
* 编译期 -> 编译器 ,原文"the compiler will only allow you to say that the superclass reference is “some class that is a superclass of FancyToy” a"
* ierator -> iterator
* nextint -> nextInt
* fix 这句翻译第一反应没太看懂,结合原文和下面的例子优化了一下翻译
* fix ojb -> obj
* fix example code
---
docs/book/20-Generics.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/docs/book/20-Generics.md b/docs/book/20-Generics.md
index 6b0c0517..e78ed133 100644
--- a/docs/book/20-Generics.md
+++ b/docs/book/20-Generics.md
@@ -1595,7 +1595,7 @@ import java.util.function.*;
import onjava.*;
public class FilledList extends ArrayList {
- FilledList gen, int size) {
+ FilledList(Supplier gen, int size) {
Suppliers.fill(this, gen, size);
}
From 3b2c9dace55b80d5ef3beea1e24c10209e48c94f Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E5=BE=90=E9=9B=81=E9=91=AB?=
Date: Sat, 14 Mar 2020 23:46:35 +0800
Subject: [PATCH 051/224] add package&class name
---
docs/book/20-Generics.md | 2 ++
1 file changed, 2 insertions(+)
diff --git a/docs/book/20-Generics.md b/docs/book/20-Generics.md
index e78ed133..7e0d32bc 100644
--- a/docs/book/20-Generics.md
+++ b/docs/book/20-Generics.md
@@ -2702,6 +2702,8 @@ public class CompilerIntelligence {
下面展示一个简单的 **Holder** 类:
```java
+// generics/Holder.java
+
public class Holder {
private T value;
From 4285663233016fe41f2f135a9790d4f4d7536d4f Mon Sep 17 00:00:00 2001
From: XuYanxin
Date: Sat, 14 Mar 2020 23:55:09 +0800
Subject: [PATCH 052/224] add package&class name (#404)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
* 忽路->忽略
* 修复跳转链接错误
* fix Class.getInterface() -> Class.getInterfaces()
* 否者 -> 否则
* 编译期 -> 编译器 ,原文"the compiler will only allow you to say that the superclass reference is “some class that is a superclass of FancyToy” a"
* ierator -> iterator
* nextint -> nextInt
* fix 这句翻译第一反应没太看懂,结合原文和下面的例子优化了一下翻译
* fix ojb -> obj
* fix example code
* add package&class name
---
docs/book/20-Generics.md | 2 ++
1 file changed, 2 insertions(+)
diff --git a/docs/book/20-Generics.md b/docs/book/20-Generics.md
index e78ed133..7e0d32bc 100644
--- a/docs/book/20-Generics.md
+++ b/docs/book/20-Generics.md
@@ -2702,6 +2702,8 @@ public class CompilerIntelligence {
下面展示一个简单的 **Holder** 类:
```java
+// generics/Holder.java
+
public class Holder {
private T value;
From 31b40249c3fcb273a6d7020d0bff84762d121c46 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E5=BE=90=E9=9B=81=E9=91=AB?=
Date: Sun, 15 Mar 2020 00:18:10 +0800
Subject: [PATCH 053/224] =?UTF-8?q?fix=20=E9=94=99=E5=88=AB=E5=AD=97?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
docs/book/20-Generics.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/docs/book/20-Generics.md b/docs/book/20-Generics.md
index 7e0d32bc..a88918a5 100644
--- a/docs/book/20-Generics.md
+++ b/docs/book/20-Generics.md
@@ -2756,7 +2756,7 @@ false
*/
```
-**Holder** 有一个接受 **T** 类型对象的 `set()` 方法,一个返回 T 对象的 `get()` 方法和一个接受 Object 对象的 `equals()` 方法。正如你所见,如果创建了一个 `Holder`,就不能将其向上转型为 `Holder`,但是可以向上转型为 `Holder extends Fruit>`。如果调用 `get()`,只能返回一个 **Fruit**——这就是在给定“任何;额扩展自 **Fruit** 的对象”这一边界后,它所能知道的一切了。如果你知道更多的信息,就可以将其转型到某种具体的 **Fruit** 而不会导致任何警告,但是存在得到 **ClassCastException** 的风险。`set()` 方法不能工作在 **Apple** 和 **Fruit** 上,因为 `set()` 的参数也是"**? extends Fruit**",意味着它可以是任何事物,编译器无法验证“任何事物”的类型安全性。
+**Holder** 有一个接受 **T** 类型对象的 `set()` 方法,一个返回 T 对象的 `get()` 方法和一个接受 Object 对象的 `equals()` 方法。正如你所见,如果创建了一个 `Holder`,就不能将其向上转型为 `Holder`,但是可以向上转型为 `Holder extends Fruit>`。如果调用 `get()`,只能返回一个 **Fruit**——这就是在给定“任何扩展自 **Fruit** 的对象”这一边界后,它所能知道的一切了。如果你知道更多的信息,就可以将其转型到某种具体的 **Fruit** 而不会导致任何警告,但是存在得到 **ClassCastException** 的风险。`set()` 方法不能工作在 **Apple** 和 **Fruit** 上,因为 `set()` 的参数也是"**? extends Fruit**",意味着它可以是任何事物,编译器无法验证“任何事物”的类型安全性。
但是,`equals()` 方法可以正常工作,因为它接受的参数是 **Object** 而不是 **T** 类型。因此,编译器只关注传递进来和要返回的对象类型。它不会分析代码,以查看是否执行了任何实际的写入和读取操作。
From 2153062b0e46806e4c97aa72ad2bbbc7962fd7a8 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E9=A9=AC=E7=87=95=E9=BE=99?=
Date: Sun, 15 Mar 2020 12:21:44 +0800
Subject: [PATCH 054/224] =?UTF-8?q?=E2=80=9CJava=20=E5=B0=B1=E4=B8=8D?=
=?UTF-8?q?=E4=BC=9A=E6=9F=A5=E6=89=BE=E5=8D=95=E7=8B=AC=E5=BD=93=E5=89=8D?=
=?UTF-8?q?=E7=9B=AE=E5=BD=95=E2=80=9D=E6=94=B9=E4=B8=BA=E2=80=9DJava=20?=
=?UTF-8?q?=E5=B0=B1=E4=B8=8D=E4=BC=9A=E6=9F=A5=E6=89=BE=E5=BD=93=E5=89=8D?=
=?UTF-8?q?=E7=9B=AE=E5=BD=95=E2=80=9C=20(#406)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
* 面向对象
* Update 07-Implementation-Hiding.md
“Java 就不会查找单独当前目录”改为”Java 就不会查找当前目录“
---
docs/book/07-Implementation-Hiding.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/docs/book/07-Implementation-Hiding.md b/docs/book/07-Implementation-Hiding.md
index 884466c1..02737cbf 100644
--- a/docs/book/07-Implementation-Hiding.md
+++ b/docs/book/07-Implementation-Hiding.md
@@ -327,7 +327,7 @@ public class Cookie {
}
```
-记住,**Cookie.java** 文件产生的类文件必须位于名为 **dessert** 的子目录中,该子目录在 **hiding** (表明本书的"封装"章节)下,它必须在 CLASSPATH 的几个目录之下。不要错误地认为 Java 总是会将当前目录视作查找行为的起点之一。如果你的 CLASSPATH 中没有 **.**,Java 就不会查找单独当前目录。
+记住,**Cookie.java** 文件产生的类文件必须位于名为 **dessert** 的子目录中,该子目录在 **hiding** (表明本书的"封装"章节)下,它必须在 CLASSPATH 的几个目录之下。不要错误地认为 Java 总是会将当前目录视作查找行为的起点之一。如果你的 CLASSPATH 中没有 **.**,Java 就不会查找当前目录。
现在,使用 **Cookie** 创建一个程序:
```java
From 62dcb5efabeab21a5836311ba8dc26cc51d4b9a3 Mon Sep 17 00:00:00 2001
From: XuYanxin
Date: Sun, 15 Mar 2020 12:22:14 +0800
Subject: [PATCH 055/224] =?UTF-8?q?fix=20=E9=94=99=E5=88=AB=E5=AD=97?=
=?UTF-8?q?=EF=BC=8C=E4=BB=BB=E4=BD=95=E9=A2=9D=EF=BC=9B=20=20->=20?=
=?UTF-8?q?=E4=BB=BB=E4=BD=95=20(#405)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
* 忽路->忽略
* 修复跳转链接错误
* fix Class.getInterface() -> Class.getInterfaces()
* 否者 -> 否则
* 编译期 -> 编译器 ,原文"the compiler will only allow you to say that the superclass reference is “some class that is a superclass of FancyToy” a"
* ierator -> iterator
* nextint -> nextInt
* fix 这句翻译第一反应没太看懂,结合原文和下面的例子优化了一下翻译
* fix ojb -> obj
* fix example code
* add package&class name
* fix 错别字
---
docs/book/20-Generics.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/docs/book/20-Generics.md b/docs/book/20-Generics.md
index 7e0d32bc..a88918a5 100644
--- a/docs/book/20-Generics.md
+++ b/docs/book/20-Generics.md
@@ -2756,7 +2756,7 @@ false
*/
```
-**Holder** 有一个接受 **T** 类型对象的 `set()` 方法,一个返回 T 对象的 `get()` 方法和一个接受 Object 对象的 `equals()` 方法。正如你所见,如果创建了一个 `Holder`,就不能将其向上转型为 `Holder`,但是可以向上转型为 `Holder extends Fruit>`。如果调用 `get()`,只能返回一个 **Fruit**——这就是在给定“任何;额扩展自 **Fruit** 的对象”这一边界后,它所能知道的一切了。如果你知道更多的信息,就可以将其转型到某种具体的 **Fruit** 而不会导致任何警告,但是存在得到 **ClassCastException** 的风险。`set()` 方法不能工作在 **Apple** 和 **Fruit** 上,因为 `set()` 的参数也是"**? extends Fruit**",意味着它可以是任何事物,编译器无法验证“任何事物”的类型安全性。
+**Holder** 有一个接受 **T** 类型对象的 `set()` 方法,一个返回 T 对象的 `get()` 方法和一个接受 Object 对象的 `equals()` 方法。正如你所见,如果创建了一个 `Holder`,就不能将其向上转型为 `Holder`,但是可以向上转型为 `Holder extends Fruit>`。如果调用 `get()`,只能返回一个 **Fruit**——这就是在给定“任何扩展自 **Fruit** 的对象”这一边界后,它所能知道的一切了。如果你知道更多的信息,就可以将其转型到某种具体的 **Fruit** 而不会导致任何警告,但是存在得到 **ClassCastException** 的风险。`set()` 方法不能工作在 **Apple** 和 **Fruit** 上,因为 `set()` 的参数也是"**? extends Fruit**",意味着它可以是任何事物,编译器无法验证“任何事物”的类型安全性。
但是,`equals()` 方法可以正常工作,因为它接受的参数是 **Object** 而不是 **T** 类型。因此,编译器只关注传递进来和要返回的对象类型。它不会分析代码,以查看是否执行了任何实际的写入和读取操作。
From b193b9b4808ce51d32c7eb27cab6b7cdc88050dd Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E5=BE=90=E9=9B=81=E9=91=AB?=
Date: Sun, 15 Mar 2020 22:30:14 +0800
Subject: [PATCH 056/224] =?UTF-8?q?fix=20=E9=94=99=E5=88=AB=E5=AD=97?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
docs/book/20-Generics.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/docs/book/20-Generics.md b/docs/book/20-Generics.md
index a88918a5..6c5aab81 100644
--- a/docs/book/20-Generics.md
+++ b/docs/book/20-Generics.md
@@ -3584,7 +3584,7 @@ public class UseList {
}
```
-因为擦除,所以重载方法产生了的类型签名。
+因为擦除,所以重载方法产生了相同的类型签名。
因而,当擦除后的参数不能产生唯一的参数列表时,你必须提供不同的方法名:
From 6096fafbea4d5dfaed2c6d10caa03085a52d4fe2 Mon Sep 17 00:00:00 2001
From: XuYanxin
Date: Thu, 19 Mar 2020 20:56:00 +0800
Subject: [PATCH 057/224] =?UTF-8?q?=E5=AF=B9=E7=85=A7=E5=8E=9F=E6=96=87?=
=?UTF-8?q?=E5=92=8C=E8=AF=AD=E5=A2=83=EF=BC=8C=E4=BF=AE=E5=A4=8D=E4=BA=86?=
=?UTF-8?q?=E6=95=B4=E6=AE=B5=E7=BF=BB=E8=AF=91=20(#410)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
docs/book/24-Concurrent-Programming.md | 10 +++++-----
1 file changed, 5 insertions(+), 5 deletions(-)
diff --git a/docs/book/24-Concurrent-Programming.md b/docs/book/24-Concurrent-Programming.md
index 0f4d8d74..6f19e1e3 100644
--- a/docs/book/24-Concurrent-Programming.md
+++ b/docs/book/24-Concurrent-Programming.md
@@ -82,17 +82,17 @@ _并行_
## 并发的超能力
-想象一下,你是一部科幻电影。你必须在高层建筑中搜索一个精心巧妙地隐藏在建筑物的一千万个房间之一中的单个物品。你进入建筑物并移动走廊。走廊分开了。
+想象一下,你置身于一部科幻电影。你必须在高层建筑中搜索一个精心巧妙地隐藏在建筑物的一千万个房间之一中的单个物品。你进入建筑物并沿着走廊向下移动。走廊分开了。
你自己完成这项任务需要一百个生命周期。
-现在假设你有一个奇怪的超级大国。你可以将自己分开,然后在继续前进的同时将另一半送到另一个走廊。每当你在走廊或楼梯上遇到分隔到下一层时,你都会重复这个分裂的技巧。最后,你会有一个人在整个建筑物的每个终点走廊。
+现在假设你有一个奇怪的超能力。你可以将自己一分为二,然后在继续前进的同时将另一半送到另一个走廊。每当你在走廊或楼梯上遇到分隔到下一层时,你都会重复这个分裂的技巧。最终,整个建筑中的每个走廊的终点都有一个你。
-每个走廊都有一千个房间。你的超级大国正在变得有点瘦,所以你只能让自己50个人同时搜索房间。
+每个走廊都有一千个房间。你的超能力变得有点弱,所以你只能分裂出50个自己来搜索这间房间。
-一旦克隆体进入房间,它必须搜索房间的所有裂缝和隐藏的口袋。它切换到第二个超级大国。它分成了一百万个纳米机器人,每个机器人都会飞到或爬到房间里一些看不见的地方。你不明白这种力量 - 一旦你启动它就会起作用。在他们自己的控制下,纳米机器人开始行动,搜索房间然后回来重新组装成你,突然,不知何故,你只知道物品是否在房间里
+一旦克隆体进入房间,它必须搜索房间的每个角落。这时它切换到了第二种超能力。它分裂成了一百万个纳米机器人,每个机器人都会飞到或爬到房间里一些看不见的地方。你不需要了解这种功能 - 一旦你开启它就会自动工作。在他们自己的控制下,纳米机器人开始行动,搜索房间然后回来重新组装成你,突然间,你获得了寻找的物品是否在房间内的消息。
-我很想能够说,“你在科幻小说中的超级大国?这就是并发性。“每当你有更多的任务要解决时,它就像分裂两个一样简单。问题是我们用来描述这种现象的任何模型最终都是抽象的
+我很想说,“并发就是刚才描述的置身于科幻电影中的超能力“就像你自己可以一分为二然后解决更多的问题一样简单。但是问题在于,我们来描述这种现象的任何模型最终都是泄漏抽象的(leaky abstraction)。
以下是其中一个漏洞:在理想的世界中,每次克隆自己时,你还会复制硬件处理器来运行该克隆。但当然不会发生这种情况 - 你的机器上可能有四个或八个处理器(通常在写入时)。你可能还有更多,并且仍有许多情况只有一个处理器。在抽象的讨论中,物理处理器的分配方式不仅可以泄漏,甚至可以支配你的决策
From 91c0847608c83f0bfe4be22d1b0cb995d83380e8 Mon Sep 17 00:00:00 2001
From: XuYanxin
Date: Thu, 19 Mar 2020 23:42:55 +0800
Subject: [PATCH 058/224] =?UTF-8?q?=E4=BC=98=E5=8C=96=E7=BF=BB=E8=AF=91=20?=
=?UTF-8?q?(#411)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
* 对照原文和语境,修复了整段翻译
* 优化翻译
* fix 翻译错误
* fix 单词少了一截
---
docs/book/24-Concurrent-Programming.md | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/docs/book/24-Concurrent-Programming.md b/docs/book/24-Concurrent-Programming.md
index 6f19e1e3..09f88c73 100644
--- a/docs/book/24-Concurrent-Programming.md
+++ b/docs/book/24-Concurrent-Programming.md
@@ -117,7 +117,7 @@ _并行_
当“同时”执行的任务相互干扰时,会出现问题。他可以以如此微妙和偶然的方式发生,可能公平地说,并发性“可以说是确定性的,但实际上是非确定性的。”也就是说,你可以假设编写通过维护和代码检查正常工作的并发程序。然而,在实践中,编写仅看起来可行的并发程序更为常见,但是在适当的条件下,将会失败。这些情况可能会发生,或者很少发生,你在测试期间从未看到它们。实际上,编写测试代码通常无法为并发程序生成故障条件。由此产生的失败只会偶尔发生,因此它们以客户投诉的形式出现。
这是推动并发的最强有力的论据之一:如果你忽略它,你可能会被咬。
-因此,并发似乎充满了危险,如果这让你有点害怕,这可能是一件好事。尽管Java 8在并发性方面做出了很大改进,但仍然没有像编译时验证或检查异常那样的安全网来告诉你何时出现错误。通过并发,你可以自己动手,只有知识渊博,可疑和积极,才能用Java编写可靠的并发代码。
+因此,并发似乎充满了危险,如果这让你有点害怕,这可能是一件好事。尽管Java 8在并发性方面做出了很大改进,但仍然没有像编译时验证(compile-time verification)或受检查的异常(checked exceptions)那样的安全网来告诉你何时出现错误。通过并发,你只能依靠自己,只有知识渊博,保持怀疑和积极进取的人,才能用Java编写可靠的并发代码。
@@ -129,7 +129,7 @@ _并行_
使用多处理器机器,可以在这些处理器之间分配多个任务,这可以显着提高吞吐量。强大的多处理器Web服务器通常就是这种情况,它可以在程序中为CPU分配大量用户请求,每个请求分配一个线程。
-但是,并发性通常可以提高在单个处理器上运行的程序的性能。这听起来像是一个双向的。如果考虑一下,由于上下文切换的成本增加(从一个任务更改为另一个任务),在单个处理器上运行的并发程序实际上应该比程序的所有部分顺序运行具有更多的开销。在表面上,将程序的所有部分作为单个任务运行并节省上下文切换的成本似乎更便宜。
+但是,并发性通常可以提高在单个处理器上运行的程序的性能。这听起来有点违反直觉。如果考虑一下,由于上下文切换的成本增加(从一个任务更改为另一个任务),在单个处理器上运行的并发程序实际上应该比程序的所有部分顺序运行具有更多的开销。在表面上,将程序的所有部分作为单个任务运行并节省上下文切换的成本似乎更便宜。
可以产生影响的问题是阻塞。如果你的程序中的一个任务由于程序控制之外的某些条件(通常是I/O)而无法继续,我们会说任务或线程阻塞(在我们的科幻故事中,克隆体已敲门而且是等待它打开)。如果没有并发性,整个程序就会停止,直到外部条件发生变化。但是,如果使用并发编写程序,则当一个任务被阻止时,程序中的其他任务可以继续执行,因此程序继续向前移动。实际上,从性能的角度来看,在单处理器机器上使用并发是没有意义的,除非其中一个任务可能阻塞。
From 814e5b01683d4490737fcde5c44b3904c7418697 Mon Sep 17 00:00:00 2001
From: legendyql
Date: Thu, 26 Mar 2020 12:47:11 +0800
Subject: [PATCH 059/224] Update 12-Collections.md (#414)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
优化翻译:
英文原文:“Here you see both methods, as well as the more conventional addAll() method that’s part of all Collection types:”
原翻译:下边的示例展示了这两个方法,以及更通用的 addAll() 方法,所有 Collection 类型都包含该方法:
应改为:下边的示例展示了这两个方法,以及更通用的 、所有 Collection类型都包含的addAll()`方法:
原翻译中说的“addAll() 方法”,容易误解为上文所说的Collections.addAll()方法,作者此处指的是Collection类型的addAll()方法,改后的翻译意思更明确,更易理解。
---
docs/book/12-Collections.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/docs/book/12-Collections.md b/docs/book/12-Collections.md
index fa296446..bc0569b8 100644
--- a/docs/book/12-Collections.md
+++ b/docs/book/12-Collections.md
@@ -194,7 +194,7 @@ public class SimpleCollection {
在 **java.util** 包中的 **Arrays** 和 **Collections** 类中都有很多实用的方法,可以在一个 **Collection** 中添加一组元素。
-`Arrays.asList()` 方法接受一个数组或是逗号分隔的元素列表(使用可变参数),并将其转换为 **List** 对象。 `Collections.addAll()` 方法接受一个 **Collection** 对象,以及一个数组或是一个逗号分隔的列表,将其中元素添加到 **Collection** 中。下边的示例展示了这两个方法,以及更通用的 `addAll()` 方法,所有 **Collection** 类型都包含该方法:
+`Arrays.asList()` 方法接受一个数组或是逗号分隔的元素列表(使用可变参数),并将其转换为 **List** 对象。 `Collections.addAll()` 方法接受一个 **Collection** 对象,以及一个数组或是一个逗号分隔的列表,将其中元素添加到 **Collection** 中。下边的示例展示了这两个方法,以及更通用的 、所有 **Collection** 类型都包含的`addAll()` 方法:
```java
// collections/AddingGroups.java
From 4fb0202a445860cff40c81cff296b66f3cb77f09 Mon Sep 17 00:00:00 2001
From: xianwdong <408050009@qq.com>
Date: Thu, 26 Mar 2020 21:55:55 +0800
Subject: [PATCH 060/224] fix typo (#419)
---
docs/book/20-Generics.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/docs/book/20-Generics.md b/docs/book/20-Generics.md
index 6c5aab81..c2175405 100644
--- a/docs/book/20-Generics.md
+++ b/docs/book/20-Generics.md
@@ -349,7 +349,7 @@ public class RandomList extends ArrayList {
public static void main(String[] args) {
RandomList rs = new RandomList<>();
- Array.stream("The quick brown fox jumped over the lazy brown dog".split(" ")).forEach(rs::add);
+ Arrays.stream("The quick brown fox jumped over the lazy brown dog".split(" ")).forEach(rs::add);
IntStream.range(0, 11).forEach(i ->
System.out.print(rs.select() + " "));
}
From 30bc85130160d830d32e8cb0b85ac6c643ef6ec9 Mon Sep 17 00:00:00 2001
From: LingCoder
Date: Thu, 2 Apr 2020 00:54:09 +0800
Subject: [PATCH 061/224] fix #421
---
docs/book/07-Implementation-Hiding.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/docs/book/07-Implementation-Hiding.md b/docs/book/07-Implementation-Hiding.md
index 02737cbf..8db30838 100644
--- a/docs/book/07-Implementation-Hiding.md
+++ b/docs/book/07-Implementation-Hiding.md
@@ -560,7 +560,7 @@ new PublicConstructor();
出于两个重要的原因,访问控制在数据类型内部划定了边界。第一个原因是确立客户端程序员可以使用和不能使用的边界。可以在结构中建立自己的内部机制而不必担心客户端程序员偶尔将内部实现作为他们可以使用的接口的一部分。
-这直接引出了第二个原因:将接口与实现分离。如果在一组程序中使用结构,而客户端程序员只能向 **public** 接口发送消息的话,那么就可以自由地修改任何不是 **public** 的事物(例如包访问权限,protected,或 private 修饰的事物),却不会破坏客户端代码。
+这直接引出了第二个原因:将接口与实现分离。如果在一组程序中使用接口,而客户端程序员只能向 **public** 接口发送消息的话,那么就可以自由地修改任何不是 **public** 的事物(例如包访问权限,protected,或 private 修饰的事物),却不会破坏客户端代码。
为了清晰起见,你可以采用一种创建类的风格:**public** 成员放在类的开头,接着是 **protected** 成员,包访问权限成员,最后是 **private** 成员。这么做的好处是类的使用者可以从头读起,首先会看到对他们而言最重要的部分(public 成员,因为可以从文件外访问它们),直到遇到非 **public** 成员时停止阅读,下面就是内部实现了:
From cbefc1d39fdb4919394e03ed94dbf95f38c834a7 Mon Sep 17 00:00:00 2001
From: LingCoder
Date: Thu, 2 Apr 2020 08:39:20 +0800
Subject: [PATCH 062/224] fix issue#415
---
docs/book/00-Preface.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/docs/book/00-Preface.md b/docs/book/00-Preface.md
index f1843204..daea9e85 100644
--- a/docs/book/00-Preface.md
+++ b/docs/book/00-Preface.md
@@ -25,7 +25,7 @@
## 语言设计错误
-每种语言都有设计错误。当新手程序员涉足语言特性并猜测应用场景和使用方式时,他们体验到极大的不确定性和挫折感。承认错误令人尴尬,但这种糟糕的初学者经历比认识到你错了什么还要糟糕。哎,每一种语言/库的设计错误都会永久地嵌入在 Java 的发行版中。
+每种语言都有设计错误。当新手程序员涉足语言特性并猜测应用场景和使用方式时,他们体验到极大的不确定性和挫折感。承认错误令人尴尬,但这种糟糕的初学者经历比认识到你错在哪里还要糟糕。唉,每一种语言/库的设计错误都会永久地嵌入在 Java 的发行版中。
诺贝尔经济学奖得主约瑟夫·斯蒂格利茨(*Joseph Stiglitz*)有一套适用于这里的人生哲学,叫做“承诺升级理论”:继续犯错误的成本由别人承担,而承认错误的成本由自己承担。
From 4db224983e0706cb73a57d5c4ddd39cfaaf8c02a Mon Sep 17 00:00:00 2001
From: john-h3 <369425422@qq.com>
Date: Thu, 2 Apr 2020 14:51:55 +0800
Subject: [PATCH 063/224] =?UTF-8?q?=E7=BF=BB=E8=AF=91=E4=BA=86=E4=BA=8C?=
=?UTF-8?q?=E5=8D=81=E4=BA=94=E7=AB=A0=E7=9A=84=20Interpreter:=20Run-Time?=
=?UTF-8?q?=20Flexibility=20(#422)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
docs/book/25-Patterns.md | 5 ++++-
1 file changed, 4 insertions(+), 1 deletion(-)
diff --git a/docs/book/25-Patterns.md b/docs/book/25-Patterns.md
index d8a8bf87..f3d37647 100644
--- a/docs/book/25-Patterns.md
+++ b/docs/book/25-Patterns.md
@@ -1183,8 +1183,11 @@ public class Facade {
公平起见,*《设计模式》* 主要是写给 C++ 读者的。尽管 C++ 有命名空间(namespaces)机制来防止全局变量和类名称之间的冲突,但它并没有提供类隐藏的机制,而在 Java 里我们可以通过声明 non-public 类来实现这一点。我认为,大多数情况下 Java 的 package 功能就足以解决针对 *外观模式* 的问题了。
-## 解释器
+## 解释器:运行时的弹性
+如果程序的用户需要更好的运行时弹性,例如创建脚本来增加需要的系统功能,你就能使用解释器设计模式。这个模式下,你可以创建一个语言解释器并将它嵌入你的程序内。
+
+在开发程序的过程中,设计自己的语言并为它构建一个解释器是一件让人分心且耗时的事。最好的解决方案就是复用代码:使用一个已经构建好并被调试过的解释器。Python 语言可以免费地嵌入营利性的应用中而不需要任何的协议许可、授权费或者是任何的声明。此外,有一个完全使用 Java 字节码实现的 Python 版本(叫做 Jython), 能够轻易地合并到 Java 程序中。Python 是一门非常易学习的脚本语言,代码的读写很有逻辑性。它支持函数与对象,有大量的可用库,并且可运行在所有的平台上。你可以在 [www.Python.org](https://www.python.org/) 上下载 Python 并了解更多信息。
## 回调
From 552e0dd66ad847f9479c8c95cf674108fcb81690 Mon Sep 17 00:00:00 2001
From: FengBaoheng
Date: Fri, 3 Apr 2020 10:01:40 +0800
Subject: [PATCH 064/224] =?UTF-8?q?fix=20typo=20=E6=9C=AC=E7=AB=A0?=
=?UTF-8?q?=E5=B0=8F=E8=8A=82->=E6=9C=AC=E7=AB=A0=E5=B0=8F=E7=BB=93=20(#42?=
=?UTF-8?q?3)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
docs/book/20-Generics.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/docs/book/20-Generics.md b/docs/book/20-Generics.md
index c2175405..4a0a0add 100644
--- a/docs/book/20-Generics.md
+++ b/docs/book/20-Generics.md
@@ -1090,7 +1090,7 @@ Serializable]
*/
```
-在第十二章 [集合的本章小节](book/12-Collections.md#本章小结) 部分将会用到这里的输出结果。
+在第十二章 [集合的本章小结](book/12-Collections.md#本章小结) 部分将会用到这里的输出结果。
From a1903a20e1634a9631716993035871d9db3a90d7 Mon Sep 17 00:00:00 2001
From: LingCoder
Date: Mon, 6 Apr 2020 19:48:51 +0800
Subject: [PATCH 065/224] fix issue #426
---
docs/book/09-Polymorphism.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/docs/book/09-Polymorphism.md b/docs/book/09-Polymorphism.md
index 31ffde5e..0e4eac6e 100644
--- a/docs/book/09-Polymorphism.md
+++ b/docs/book/09-Polymorphism.md
@@ -1156,7 +1156,7 @@ SadActor
**Stage** 对象中包含了 **Actor** 引用,该引用被初始化为指向一个 **HappyActor** 对象,这意味着 `performPlay()` 会产生一个特殊行为。但是既然引用可以在运行时与其他不同的对象绑定,那么它就可以被替换成对 **SadActor** 的引用,`performPlay()` 的行为随之改变。这样你就获得了运行时的动态灵活性(这被称为状态模式)。与之相反,我们不能在运行时决定继承不同的对象,那在编译时就完全确定下来了。
-有一条通用准则:使用继承表达行为的差异,使用属性表达状态的变化。在上个例子中,两者都用到了。通过继承的到的两个不同类在 `act()` 方法中表达了不同的行为,**Stage** 通过组合使自己的状态发生变化。这里状态的改变产生了行为的改变。
+有一条通用准则:使用继承表达行为的差异,使用属性表达状态的变化。在上个例子中,两者都用到了。通过继承得到的两个不同类在 `act()` 方法中表达了不同的行为,**Stage** 通过组合使自己的状态发生变化。这里状态的改变产生了行为的改变。
### 替代 vs 扩展
From 7653db6417eadbc461c3b652b1b23ec3f65a93d8 Mon Sep 17 00:00:00 2001
From: alton zheng <53368134+alton-zheng@users.noreply.github.com>
Date: Wed, 8 Apr 2020 10:16:37 +0800
Subject: [PATCH 066/224] alton#417 (#418)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
* alton#417
* fix issue#417
* fix issue#417 20200328
* fix issue#417 20180328 15:16
* fix issue#417 异常检查 -> 检查型异常
* fix issue#417 20180328 16:00
* fix issue#417
* fix issue#417
---
docs/book/24-Concurrent-Programming.md | 672 +++++++++++++++----------
1 file changed, 408 insertions(+), 264 deletions(-)
diff --git a/docs/book/24-Concurrent-Programming.md b/docs/book/24-Concurrent-Programming.md
index 09f88c73..f863dc8b 100644
--- a/docs/book/24-Concurrent-Programming.md
+++ b/docs/book/24-Concurrent-Programming.md
@@ -26,6 +26,7 @@
对于更多凌乱,低级别的细节,请参阅附录:[并发底层原理](./Appendix-Low-Level-Concurrency.md)。要进一步深入这个领域,你还必须阅读Brian Goetz等人的Java Concurrency in Practice。虽然在写作时,这本书已有十多年的历史,但它仍然包含你必须了解和理解的必需品。理想情况下,本章和附录是该书的精心准备。另一个有价值的资源是**Bill Venner**的Inside the Java Virtual Machine,它详细描述了JVM的最内部工作方式,包括线程。
+
## 术语问题
在编程文献中并发、并行、多任务、多处理、多线程、分布式系统(以及可能的其他)使用了许多相互冲突的方式,并且经常被混淆。Brian Goetz在2016年的演讲中指出了这一点[From Concurrent to Parallel](https://www.youtube.com/watch?v=NsDE7E8sIdQ),他提出了一个合理的解释:
@@ -739,7 +740,6 @@ public class ParallelStreamPuzzle3 {
实际上,在许多情况下,并行流确实可以毫不费力地更快地产生结果。但正如你所见,只需将**parallel()**打到你的Stream操作上并不一定是安全的事情。在使用**parallel()**之前,你必须了解并行性如何帮助或损害你的操作。有个错误认识是认为并行性总是一个好主意。事实上并不是。Stream意味着你不需要重写所有代码以便并行运行它。流什么都不做的是取代理解并行性如何工作的需要,以及它是否有助于实现你的目标。
-
## 创建和运行任务
如果无法通过并行流实现并发,则必须创建并运行自己的任务。稍后你将看到运行任务的理想Java 8方法是CompletableFuture,但我们将使用更基本的工具介绍概念。
@@ -1200,9 +1200,7 @@ public class CountingStream {
- Lambda和方法引用作为任务
-使用lambdas和方法引用,你不仅限于使用**Runnables**和**Callables**。因为Java 8通过匹配签名来支持lambda和方法引用(即,它支持结构一致性),所以我们可以将notRunnables或Callables的参数传递给ExecutorService:
-
-使用lambdas和方法引用,你不仅限于使用**Runnables**和**Callables**。因为Java 8通过匹配签名来支持lambda和方法引用(即,它支持结构一致性),所以我们可以将不是**Runnables**或**Callables**的参数传递给**ExecutorService**:
+在 `java8` , 你不需要受限于在 `Runnables ` 和 `Callables` 时,使用`lambdas` 和方法引用, 同样也可以通过匹配签名来引用(即,它支持结构一致性)。 所以我们可以将 `notRunnables` 或 `Callables` 的参数传递给`ExecutorService` :
```java
// concurrent/LambdasAndMethodReferences.java
@@ -1223,12 +1221,12 @@ public class LambdasAndMethodReferences {
ExecutorService exec =
Executors.newCachedThreadPool();
exec.submit(() -> System.out.println("Lambda1"));
- exec.submit(newNotRunnable()::go);
+ exec.submit(new NotRunnable()::go);
exec.submit(() -> {
System.out.println("Lambda2");
return 1;
});
- exec.submit(newNotCallable()::get);
+ exec.submit(new NotCallable()::get);
exec.shutdown();
}
}
@@ -1326,7 +1324,6 @@ public class QuittingTasks {
我使用**peek()**将**QuittableTasks**传递给**ExecutorService**,然后将这些任务收集到**List.main()**中,只要任何任务仍在运行,就会阻止程序退出。即使为每个任务按顺序调用quit()方法,任务也不会按照它们创建的顺序关闭。独立运行的任务不会确定性地响应信号。
-
## CompletableFuture类
作为介绍,这里是使用CompletableFutures在QuittingTasks.java中:
@@ -1360,11 +1357,11 @@ public class QuittingCompletable {
7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 2526 27 28 29 30 31 32 33 34 6 35 4 38 39 40 41 42 43 4445 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 6263 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 8081 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 9899 100 101 102 103 104 105 106 107 108 109 110 111 1121 113 114 116 117 118 119 120 121 122 123 124 125 126127 128 129 130 131 132 133 134 135 136 137 138 139 140141 142 143 144 145 146 147 148 149 5 115 37 36 2 3
```
-任务是一个**List **,就像在**QuittingTasks.java**中一样,但是在这个例子中,没有**peek()**将每个**QuittableTask**提交给**ExecutorService**。相反,在创建cfutures期间,每个任务都交给**CompletableFuture::runAsync**。这执行**VerifyTask.run(**)并返回**CompletableFuture **。因为**run()**不返回任何内容,所以在这种情况下我只使用**CompletableFuture**调用**join()**来等待它完成。
+任务是一个 `List`,就像在 `QuittingTasks.java` 中一样,但是在这个例子中,没有 `peek()` 将每个 `QuittableTask` 提交给 `ExecutorService`。相反,在创建 `cfutures` 期间,每个任务都交给 `CompletableFuture::runAsync`。这执行 `VerifyTask.run()` 并返回 `CompletableFuture` 。因为 `run()` 不返回任何内容,所以在这种情况下我只使用 `CompletableFuture` 调用 `join()` 来等待它完成。
-在此示例中需要注意的重要事项是,运行任务不需要**ExecutorService**。这由**CompletableFuture**管理(尽管有提供自己的**ExecutorService**的选项)。你也不需要调用**shutdown()**;事实上,除非你像我这样明确地调用**join()**,程序将尽快退出,而不必等待任务完成。
+在本例中需要注意的重要一点是,运行任务不需要使用 `ExecutorService`。而是直接交给 `CompletableFuture` 管理 (不过你可以向它提供自己定义的 `ExectorService`)。您也不需要调用 `shutdown()`;事实上,除非你像我在这里所做的那样显式地调用 `join()`,否则程序将尽快退出,而不必等待任务完成。
-这个例子只是一个起点。你很快就会看到ComplempleFutures能够做得更多。
+这个例子只是一个起点。你很快就会看到 `ComplempleFuture` 能够做得更多。
### 基本用法
@@ -1379,7 +1376,7 @@ public class Machina {
State step() {
if(equals(END))
return END;
- return values()[ordinal() + 1];
+ return values()[ordinal() + 1];
}
}
private State state = State.START;
@@ -1392,11 +1389,12 @@ public class Machina {
new Nap(0.1);
m.state = m.state.step();
}
- System.out.println(m);return m;
+ System.out.println(m);
+ return m;
}
@Override
- public StringtoString() {
- return"Machina" + id + ": " + (state.equals(State.END)? "complete" : state);
+ public String toString() {
+ return"Machina" + id + ": " + (state.equals(State.END)? "complete" : state);
}
}
@@ -1404,7 +1402,7 @@ public class Machina {
这是一个有限状态机,一个微不足道的机器,因为它没有分支......它只是从头到尾遍历一条路径。**work()**方法将机器从一个状态移动到下一个状态,并且需要100毫秒才能完成“工作”。
-我们可以用**CompletableFuture**做的一件事是使用**completedFuture()**将它包装在感兴趣的对象中
+**CompletableFuture**可以被用来做的一件事是, 使用**completedFuture()**将它感兴趣的对象进行包装。
```java
// concurrent/CompletedMachina.java
@@ -1428,7 +1426,7 @@ public class CompletedMachina {
通常,**get()**在等待结果时阻塞调用线程。此块可以通过**InterruptedException**或**ExecutionException**中断。在这种情况下,阻止永远不会发生,因为CompletableFutureis已经完成,所以答案立即可用。
-当我们将**handle()**包装在**CompletableFuture**中时,我们发现我们可以在**CompletableFuture**上添加操作来处理所包含的对象,事情变得更加有趣:
+当我们将**handle()**包装在**CompletableFuture**中时,发现我们可以在**CompletableFuture**上添加操作来处理所包含的对象,使得事情变得更加有趣:
```java
// concurrent/CompletableApply.java
@@ -1450,7 +1448,7 @@ public class CompletableApply {
}
```
-输出结果:
+**输出结果**:
```
Machina0: ONE
@@ -1459,9 +1457,9 @@ Machina0: THREE
Machina0: complete
```
-**thenApply()**应用一个接受输入并产生输出的函数。在这种情况下,**work()**函数产生与它相同的类型,因此每个得到的**CompletableFuture**仍然被输入为**Machina**,但是(类似于**Streams**中的**map()**)**Function**也可以返回不同的类型,这将反映在返回类型
+`thenApply()` 应用一个接收输入并产生输出的函数。在本例中,`work()` 函数产生的类型与它所接收的类型相同 (`Machina`),因此每个 `CompletableFuture`添加的操作的返回类型都为 `Machina`,但是(类似于流中的 `map()` )函数也可以返回不同的类型,这将体现在返回类型上。
-你可以在此处看到有关**CompletableFutures**的重要信息:它们会在你执行操作时自动解包并重新包装它们所携带的对象。这样你就不会陷入麻烦的细节,这使得编写和理解代码变得更加简单。
+你可以在此处看到有关**CompletableFutures**的重要信息:它们会在你执行操作时自动解包并重新包装它们所携带的对象。这使得编写和理解代码变得更加简单, 而不会在陷入在麻烦的细节中。
我们可以消除中间变量并将操作链接在一起,就像我们使用Streams一样:
@@ -1493,10 +1491,10 @@ Machina0: complete
514
```
-在这里,我们还添加了一个**Timer**,它向我们展示每一步增加100毫秒,还有一些额外的开销。
-**CompletableFutures**的一个重要好处是它们鼓励使用私有子类原则(不分享任何东西)。默认情况下,使用**thenApply()**来应用一个不与任何人通信的函数 - 它只需要一个参数并返回一个结果。这是函数式编程的基础,并且它在并发性方面非常有效[^5]。并行流和ComplempleFutures旨在支持这些原则。只要你不决定共享数据(共享非常容易,甚至意外)你可以编写相对安全的并发程序。
+这里我们还添加了一个 `Timer`,它的功能在每一步都显性地增加 100ms 等待时间之外,还将 `CompletableFuture` 内部 `thenApply` 带来的额外开销给体现出来了。
+**CompletableFutures** 的一个重要好处是它们鼓励使用私有子类原则(不共享任何东西)。默认情况下,使用 **thenApply()** 来应用一个不对外通信的函数 - 它只需要一个参数并返回一个结果。这是函数式编程的基础,并且它在并发特性方面非常有效[^5]。并行流和 `ComplempleFutures` 旨在支持这些原则。只要你不决定共享数据(共享非常容易导致意外发生)你就可以编写出相对安全的并发程序。
-回调**thenApply()**开始一个操作,在这种情况下,在完成所有任务之前,不会完成**e CompletableFuture**的创建。虽然这有时很有用,但是启动所有任务通常更有价值,这样就可以运行时继续前进并执行其他操作。我们通过在操作结束时添加Async来实现此目的:
+回调 `thenApply()` 一旦开始一个操作,在完成所有任务之前,不会完成 **CompletableFuture** 的构建。虽然这有时很有用,但是开始所有任务通常更有价值,这样就可以运行继续前进并执行其他操作。我们可通过`thenApplyAsync()` 来实现此目的:
```java
// concurrent/CompletableApplyAsync.java
@@ -1531,67 +1529,75 @@ Machina0: complete
552
```
-同步调用(我们通常使用得那种)意味着“当你完成工作时,返回”,而异步调用以意味着“立刻返回但是继续后台工作。”正如你所看到的,**cf**的创建现在发生得跟快。每次调用 **thenApplyAsync()** 都会立刻返回,因此可以进行下一次调用,整个链接序列的完成速度比以前快得快。
+同步调用(我们通常使用的那种)意味着:“当你完成工作时,才返回”,而异步调用以意味着: “立刻返回并继续后续工作”。 正如你所看到的,`cf` 的创建现在发生的更快。每次调用 `thenApplyAsync()` 都会立刻返回,因此可以进行下一次调用,整个调用链路完成速度比以前快得多。
-事实上,如果没有回调**cf.join() t**方法,程序会在完成其工作之前退出(尝试取出该行)对**join()**阻止了**main()**进程的进行,直到cf操作完成,我们可以看到大部分时间的确在哪里度过。
+事实上,如果没有回调 `cf.join()` 方法,程序会在完成其工作之前退出。而 `cf.join()` 直到cf操作完成之前,阻止 `main()` 进程结束。我们还可以看出本示例大部分时间消耗在 `cf.join()` 这。
-这种“立即返回”的异步能力需要**CompletableFuture**库进行一些秘密工作。特别是,它必须将你需要的操作链存储为一组回调。当第一个后台操作完成并返回时,第二个后台操作必须获取生成的**Machina**并开始工作,当完成后,下一个操作将接管,等等。但是没有我们普通的函数调用序列,通过程序调用栈控制,这个顺序会丢失,所以它使用回调 - 一个函数地址表来存储。
+这种“立即返回”的异步能力需要 `CompletableFuture` 库进行一些秘密(`client` 无感)工作。特别是,它将你需要的操作链存储为一组回调。当操作的第一个链路(后台操作)完成并返回时,第二个链路(后台操作)必须获取生成的 `Machina` 并开始工作,以此类推! 但这种异步机制没有我们可以通过程序调用栈控制的普通函数调用序列,它的调用链路顺序会丢失,因此它使用一个函数地址来存储的回调来解决这个问题。
-幸运的是,你需要了解有关回调的所有信息。程序员将你手工造成的混乱称为“回调地狱”。通过异步调用,**CompletableFuture**为你管理所有回调。除非你知道关于你的系统有什么特定的改变,否则你可能想要使用异步调用。
+幸运的是,这就是你需要了解的有关回调的全部信息。程序员将这种人为制造的混乱称为 callback hell(回调地狱)。通过异步调用,`CompletableFuture` 帮你管理所有回调。 除非你知道系统的一些具体的变化,否则你更想使用异步调用来实现程序。
- 其他操作
-当你查看**CompletableFuture**的Javadoc时,你会看到它有很多方法,但这个方法的大部分来自不同操作的变体。例如,有**thenApply()**,**thenApplyAsync()**和**thenApplyAsync()**的第二种形式,它接受运行任务的**Executor**(在本书中我们忽略了**Executor**选项)。
-这是一个显示所有“基本”操作的示例,它们不涉及组合两个CompletableFutures或异常(我们将在稍后查看)。首先,我们将重复使用两个实用程序以提供简洁和方便:
+当你查看`CompletableFuture`的 `Javadoc` 时,你会看到它有很多方法,但这个方法的大部分来自不同操作的变体。例如,有 `thenApply()`,`thenApplyAsync()` 和第二种形式的 `thenApplyAsync()`,它们使用 `Executor` 来运行任务(在本书中,我们忽略了 `Executor` 选项)。
+
+下面的示例展示了所有"基本"操作,这些操作既不涉及组合两个 `CompletableFuture`,也不涉及异常(我们将在后面介绍)。首先,为了提供简洁性和方便性,我们应该重用以下两个实用程序:
```java
-// concurrent/CompletableUtilities.java
-package onjava; import java.util.concurrent.*;
+package onjava;
+import java.util.concurrent.*;
+
public class CompletableUtilities {
- // Get and show value stored in a CF:
- public static void showr(CompletableFuture> c) {
- try {
- System.out.println(c.get());
- } catch(InterruptedException
- | ExecutionException e) {
- throw new RuntimeException(e);
- }
+ // Get and show value stored in a CF:
+ public static void showr(CompletableFuture> c) {
+ try {
+ System.out.println(c.get());
+ } catch(InterruptedException
+ | ExecutionException e) {
+ throw new RuntimeException(e);
}
- // For CF operations that have no value:
- public static void voidr(CompletableFuture c) {
- try {
- c.get(); // Returns void
- } catch(InterruptedException
- | ExecutionException e) {
- throw new RuntimeException(e);
- }
+ }
+ // For CF operations that have no value:
+ public static void voidr(CompletableFuture c) {
+ try {
+ c.get(); // Returns void
+ } catch(InterruptedException
+ | ExecutionException e) {
+ throw new RuntimeException(e);
}
+ }
}
```
-**showr()**在**CompletableFuture **上调用**get()**并显示结果,捕获两个可能的异常。**voidr()**是**CompletableFuture **的**showr()**版本,即**CompletableFutures**,仅在任务完成或失败时显示。
+`showr()` 在 `CompletableFuture` 上调用 `get()`,并显示结果,`try/catch` 两个可能会出现的异常。
-为简单起见,以下**CompletableFutures**只包装整数。**cfi()**是一个方便的方法,它在完成的**CompletableFuture **中包装一个**int**:
+`voidr()` 是 `CompletableFuture` 的 `showr()` 版本,也就是说,`CompletableFutures` 只为任务完成或失败时显示信息。
+
+为简单起见,下面的 `CompletableFutures` 只包装整数。`cfi()` 是一个便利的方法,它把一个整数包装在一个完整的 `CompletableFuture` :
```java
// concurrent/CompletableOperations.java
import java.util.concurrent.*;
import static onjava.CompletableUtilities.*;
+
public class CompletableOperations {
static CompletableFuture cfi(int i) {
- return CompletableFuture.completedFuture( Integer.valueOf(i));
+ return
+ CompletableFuture.completedFuture(
+ Integer.valueOf(i));
}
+
public static void main(String[] args) {
showr(cfi(1)); // Basic test
voidr(cfi(2).runAsync(() ->
- System.out.println("runAsync")));
+ System.out.println("runAsync")));
voidr(cfi(3).thenRunAsync(() ->
- System.out.println("thenRunAsync")));
+ System.out.println("thenRunAsync")));
voidr(CompletableFuture.runAsync(() ->
- System.out.println("runAsync is static")));
+ System.out.println("runAsync is static")));
showr(CompletableFuture.supplyAsync(() -> 99));
voidr(cfi(4).thenAcceptAsync(i ->
- System.out.println("thenAcceptAsync: " + i)));
+ System.out.println("thenAcceptAsync: " + i)));
showr(cfi(5).thenApplyAsync(i -> i + 42));
showr(cfi(6).thenComposeAsync(i -> cfi(i + 99)));
CompletableFuture c = cfi(7);
@@ -1603,24 +1609,27 @@ public class CompletableOperations {
showr(c);
c = new CompletableFuture<>();
c.cancel(true);
- System.out.println("cancelled: " + c.isCancelled());
+ System.out.println("cancelled: " +
+ c.isCancelled());
System.out.println("completed exceptionally: " +
- c.isCompletedExceptionally());
+ c.isCompletedExceptionally());
System.out.println("done: " + c.isDone());
System.out.println(c);
c = new CompletableFuture<>();
System.out.println(c.getNow(777));
c = new CompletableFuture<>();
c.thenApplyAsync(i -> i + 42)
- .thenApplyAsync(i -> i * 12);
- System.out.println("dependents: " + c.getNumberOfDependents());
+ .thenApplyAsync(i -> i * 12);
+ System.out.println("dependents: " +
+ c.getNumberOfDependents());
c.thenApplyAsync(i -> i / 2);
- System.out.println("dependents: " + c.getNumberOfDependents());
+ System.out.println("dependents: " +
+ c.getNumberOfDependents());
}
}
```
-输出结果:
+**输出结果** :
```
1
@@ -1643,112 +1652,152 @@ dependents: 1
dependents: 2
```
-**main()**包含一系列可由其**int**值引用的测试。**cfi(1)**演示了**showr()**正常工作。**cfi(2)**是调用**runAsync()**的示例。由于**Runnable**不产生返回值,因此结果是**CompletableFuture **,因此使用**voidr()**。
-注意使用**cfi(3)**,**thenRunAsync()**似乎与**runAsync()**一致,差异显示在后续的测试中:
-**runAsync()**是一个静态方法,所以你不会像**cfi(2)**一样调用它。相反你可以在**QuittingCompletable.java**中使用它。后续测试中**supplyAsync()**也是静态方法,但是需要一个**Supplier**而不是**Runnable**并产生一个**CompletableFuture**来代替**CompletableFuture**。
-含有“then”的方法将进一步的操作应用于现有的**CompletableFuture **。与**thenRunAsync()**不同的是,将**cfi(4)**,**cfi(5)**和**cfi(6)**的“ then”方法作为未包装的**Integer**的参数。如你通过使用**voidr()**所见,然后**AcceptAsync()**接受了一个**Consumer**,因此不会产生结果。**thenApplyAsync()**接受一个**Function**并因此产生一个结果(该结果的类型可以不同于其参数)。**thenComposeAsync()**与**thenApplyAsync()**非常相似,不同之处在于其Function必须产生已经包装在**CompletableFuture**中的结果。
-**cfi(7)**示例演示了**obtrudeValue()**,它强制将值作为结果。**cfi(8)**使用**toCompletableFuture()**从**CompletionStage**生成**CompletableFuture**。**c.complete(9)**显示了如何通过给它一个结果来完成一个任务(**future**)(与**obtrudeValue()**相对,后者可能会迫使其结果替换该结果)。
-如果你调用**CompletableFuture**中的**cancel()**方法,它也会完成并且是非常好的完成。
-如果任务(**future**)未完成,则**getNow()**方法返回**CompletableFuture**的完成值,或者返回**getNow()**的替换参数。
-最后,我们看一下依赖(dependents)的概念。如果我们将两个**thenApplyAsync()**调用链接到**CompletableFuture**上,则依赖项的数量仍为1。但是,如果我们将另一个**thenApplyAsync()**直接附加到**c**,则现在有两个依赖项:两个链和另一个链。这表明你可以拥有一个**CompletionStage**,当它完成时,可以根据其结果派生多个新任务。
-
-### 结合CompletableFutures
-
-第二类**CompletableFuture**方法采用两个**CompletableFuture**并以各种方式将它们组合在一起。一个**CompletableFuture**通常会先于另一个完成,就好像两者都在比赛中一样。这些方法使你可以以不同的方式处理结果。
-为了对此进行测试,我们将创建一个任务,该任务将完成的时间作为其参数之一,因此我们可以控制。
-**CompletableFuture**首先完成:
-
+- `main()` 包含一系列可由其 `int` 值引用的测试。
+ - `cfi(1)` 演示了 `showr()` 正常工作。
+ - `cfi(2)` 是调用 `runAsync()` 的示例。由于 `Runnable` 不产生返回值,因此使用了返回 `CompletableFuture ` 的`voidr()` 方法。
+ - 注意使用 `cfi(3)`,`thenRunAsync()` 效果似乎与 上例 `cfi(2)` 使用的 `runAsync()`相同,差异在后续的测试中体现:
+ - `runAsync()` 是一个 `static` 方法,所以你通常不会像`cfi(2)`一样调用它。相反你可以在 `QuittingCompletable.java` 中使用它。
+ - 后续测试中表明 `supplyAsync()` 也是静态方法,区别在于它需要一个 `Supplier` 而不是`Runnable`, 并产生一个`CompletableFuture` 而不是 `CompletableFuture`。
+ - `then` 系列方法将对现有的 `CompletableFuture` 进一步操作。
+ - 与 `thenRunAsync()` 不同,`cfi(4)`,`cfi(5)` 和`cfi(6)` "then" 方法的参数是未包装的 `Integer`。
+ - 通过使用 `voidr()`方法可以看到:
+ - `AcceptAsync()`接收了一个 `Consumer`,因此不会产生结果。
+ - `thenApplyAsync()` 接收一个`Function`, 并生成一个结果(该结果的类型可以不同于其输入类型)。
+ - `thenComposeAsync()` 与 `thenApplyAsync()`非常相似,唯一区别在于其 `Function` 必须产生已经包装在`CompletableFuture`中的结果。
+ - `cfi(7)` 示例演示了 `obtrudeValue()`,它强制将值作为结果。
+ - `cfi(8)` 使用 `toCompletableFuture()` 从 `CompletionStage` 生成一个`CompletableFuture`。
+ - `c.complete(9)` 显示了如何通过给它一个结果来完成一个`task`(`future`)(与 `obtrudeValue()` 相对,后者可能会迫使其结果替换该结果)。
+ - 如果你调用 `CompletableFuture`中的 `cancel()`方法,如果已经完成此任务,则正常结束。 如果尚未完成,则使用 `CancellationException` 完成此 `CompletableFuture`。
+ - 如果任务(`future`)完成,则**getNow()**方法返回`CompletableFuture`的完成值,否则返回`getNow()`的替换参数。
+ - 最后,我们看一下依赖(`dependents`)的概念。如果我们将两个`thenApplyAsync()`调用链路到`CompletableFuture`上,则依赖项的数量不会增加,保持为1。但是,如果我们另外将另一个`thenApplyAsync()`直接附加到`c`,则现在有两个依赖项:两个一起的链路和另一个单独附加的链路。
+ - 这表明你可以使用一个`CompletionStage`,当它完成时,可以根据其结果派生多个新任务。
+
+
+
+### 结合 CompletableFuture
+
+第二种类型的 `CompletableFuture` 方法采用两种 `CompletableFuture` 并以各异方式将它们组合在一起。就像两个人在比赛一样, 一个`CompletableFuture`通常比另一个更早地到达终点。这些方法允许你以不同的方式处理结果。
+为了测试这一点,我们将创建一个任务,它有一个我们可以控制的定义了完成任务所需要的时间量的参数。
+CompletableFuture 先完成:
```java
// concurrent/Workable.java
import java.util.concurrent.*;
import onjava.Nap;
+
public class Workable {
String id;
final double duration;
+
public Workable(String id, double duration) {
this.id = id;
this.duration = duration;
}
+
@Override
public String toString() {
return "Workable[" + id + "]";
}
+
public static Workable work(Workable tt) {
new Nap(tt.duration); // Seconds
tt.id = tt.id + "W";
System.out.println(tt);
return tt;
}
+
public static CompletableFuture make(String id, double duration) {
- return CompletableFuture.completedFuture( new Workable(id, duration)) .thenApplyAsync(Workable::work);
+ return CompletableFuture
+ .completedFuture(
+ new Workable(id, duration)
+ )
+ .thenApplyAsync(Workable::work);
}
}
```
-在**make()**中,**work()**方法应用于**CompletableFuture.work()**需要持续时间才能完成,然后将字母W附加到id上以指示工作已完成。
-现在,我们可以创建多个竞争的**CompletableFuture**,并使用**CompletableFuture**库:
+在 `make()`中,`work()`方法应用于`CompletableFuture`。`work()`需要一定的时间才能完成,然后它将字母W附加到id上,表示工作已经完成。
+
+现在我们可以创建多个竞争的 `CompletableFuture`,并使用 `CompletableFuture` 库中的各种方法来进行操作:
```java
// concurrent/DualCompletableOperations.java
import java.util.concurrent.*;
import static onjava.CompletableUtilities.*;
+
public class DualCompletableOperations {
static CompletableFuture cfA, cfB;
+
static void init() {
cfA = Workable.make("A", 0.15);
- cfB = Workable.make("B", 0.10);// Always wins
+ cfB = Workable.make("B", 0.10); // Always wins
}
+
static void join() {
cfA.join();
cfB.join();
System.out.println("*****************");
}
+
public static void main(String[] args) {
init();
- voidr(cfA.runAfterEitherAsync(cfB, () -> System.out.println("runAfterEither")));
+ voidr(cfA.runAfterEitherAsync(cfB, () ->
+ System.out.println("runAfterEither")));
join();
+
init();
- voidr(cfA.runAfterBothAsync(cfB, () -> System.out.println("runAfterBoth")));
+ voidr(cfA.runAfterBothAsync(cfB, () ->
+ System.out.println("runAfterBoth")));
join();
+
init();
showr(cfA.applyToEitherAsync(cfB, w -> {
System.out.println("applyToEither: " + w);
return w;
}));
join();
+
init();
voidr(cfA.acceptEitherAsync(cfB, w -> {
System.out.println("acceptEither: " + w);
}));
join();
+
init();
- voidr(cfA.thenAcceptBothAsync(cfB, (w1, w2) -> { System.out.println("thenAcceptBoth: " + w1 + ", " + w2);
+ voidr(cfA.thenAcceptBothAsync(cfB, (w1, w2) -> {
+ System.out.println("thenAcceptBoth: "
+ + w1 + ", " + w2);
}));
join();
+
init();
showr(cfA.thenCombineAsync(cfB, (w1, w2) -> {
- System.out.println("thenCombine: " + w1 + ", " + w2);
+ System.out.println("thenCombine: "
+ + w1 + ", " + w2);
return w1;
}));
join();
+
init();
CompletableFuture
- cfC = Workable.make("C", 0.08),
- cfD = Workable.make("D", 0.09);
+ cfC = Workable.make("C", 0.08),
+ cfD = Workable.make("D", 0.09);
CompletableFuture.anyOf(cfA, cfB, cfC, cfD)
- .thenRunAsync(() -> System.out.println("anyOf"));
+ .thenRunAsync(() ->
+ System.out.println("anyOf"));
join();
+
init();
cfC = Workable.make("C", 0.08);
cfD = Workable.make("D", 0.09);
CompletableFuture.allOf(cfA, cfB, cfC, cfD)
- .thenRunAsync(() -> System.out.println("allOf"));
+ .thenRunAsync(() ->
+ System.out.println("allOf"));
join();
}
}
```
-输出结果:
+**输出结果**:
```
Workable[BW]
@@ -1791,109 +1840,149 @@ thenAcceptBoth: Workable[AW], Workable[BW]
allOf
```
-为了便于访问,**cfA**和**cfB**是静态的。**init()**总是使用较短的延迟(因此总是“获胜”)使用“ B”初始化两者。**join()**是在这两种方法上调用**join()**并显示边框的另一种便捷方法。
-所有这些“双重”方法都以一个**CompletableFuture**作为调用该方法的对象,第二个**CompletableFuture**作为第一个参数,然后是要执行的操作。
-通过使用**Shower()**和**void()**,你可以看到“运行”和“接受”是终端操作,而“应用”和“组合”产生了新的承载载荷的**CompletableFutures**。
+- 为了方便访问, 将 `cfA` 和 `cfB` 定义为 `static`的。
+ - `init()`方法用于 `A`, `B` 初始化这两个变量,因为 `B` 总是给出比`A`较短的延迟,所以总是 `win` 的一方。
+ - `join()` 是在两个方法上调用 `join()` 并显示边框的另一个便利方法。
+- 所有这些 “`dual`” 方法都以一个 `CompletableFuture` 作为调用该方法的对象,第二个 `CompletableFuture` 作为第一个参数,然后是要执行的操作。
+- 通过使用 `showr()` 和 `voidr()` 可以看到,“`run`”和“`accept`”是终端操作,而“`apply`”和“`combine`”则生成新的 `payload-bearing` (承载负载)的 `CompletableFuture`。
+- 方法的名称不言自明,你可以通过查看输出来验证这一点。一个特别有趣的方法是 `combineAsync()`,它等待两个 `CompletableFuture` 完成,然后将它们都交给一个 `BiFunction`,这个 `BiFunction` 可以将结果加入到最终的 `CompletableFuture` 的有效负载中。
-方法的名称是不言自明的,你可以通过查看输出来验证这一点。一个特别有趣的方法是CombineAsync(),它等待两个**CompletableFuture**完成,然后将它们都交给BiFunction,然后BiFunction可以将结果加入到所得**CompletableFuture**的有效负载中。
### 模拟
-作为一个示例,说明如何使用**CompletableFutures**将一系列操作组合在一起,让我们模拟制作蛋糕的过程。在第一个阶段中,我们准备并将成分混合成面糊:
+作为使用 `CompletableFuture` 将一系列操作组合的示例,让我们模拟一下制作蛋糕的过程。在第一阶段,我们准备并将原料混合成面糊:
```java
// concurrent/Batter.java
import java.util.concurrent.*;
import onjava.Nap;
+
public class Batter {
- static class Eggs {}
- static class Milk {}
- static class Sugar {}
- static class Flour {}
+ static class Eggs {
+ }
+
+ static class Milk {
+ }
+
+ static class Sugar {
+ }
+
+ static class Flour {
+ }
+
static T prepare(T ingredient) {
new Nap(0.1);
return ingredient;
}
+
static CompletableFuture prep(T ingredient) {
return CompletableFuture
.completedFuture(ingredient)
.thenApplyAsync(Batter::prepare);
}
+
public static CompletableFuture mix() {
- CompletableFuture eggs = prep(new Eggs()); CompletableFuture milk = prep(new Milk()); CompletableFuture sugar = prep(new Sugar()); CompletableFuture flour = prep(new Flour()); CompletableFuture.allOf(eggs, milk, sugar, flour)
- .join();
+ CompletableFuture eggs = prep(new Eggs());
+ CompletableFuture milk = prep(new Milk());
+ CompletableFuture sugar = prep(new Sugar());
+ CompletableFuture flour = prep(new Flour());
+ CompletableFuture
+ .allOf(eggs, milk, sugar, flour)
+ .join();
new Nap(0.1); // Mixing time
return CompletableFuture.completedFuture(new Batter());
}
}
-
```
-每种成分都需要一些时间来准备。**allOf()**等待所有配料准备就绪,然后需要更多时间将其混合到面糊中。
-
-接下来,我们将单批面糊放入四个锅中进行烘烤。产品作为**CompletableFutures**流返回:
+每种原料都需要一些时间来准备。`allOf()` 等待所有的配料都准备好,然后使用更多些的时间将其混合成面糊。接下来,我们把单批面糊放入四个平底锅中烘烤。产品作为 `CompletableFutures` 流返回:
```java
// concurrent/Baked.java
+
import java.util.concurrent.*;
import java.util.stream.*;
import onjava.Nap;
+
public class Baked {
- static class Pan {}
+ static class Pan {
+ }
+
static Pan pan(Batter b) {
new Nap(0.1);
return new Pan();
}
+
static Baked heat(Pan p) {
new Nap(0.1);
return new Baked();
}
- static CompletableFuture bake(CompletableFuture cfb){
- return cfb.thenApplyAsync(Baked::pan)
- .thenApplyAsync(Baked::heat);
+
+ static CompletableFuture bake(CompletableFuture cfb) {
+ return cfb
+ .thenApplyAsync(Baked::pan)
+ .thenApplyAsync(Baked::heat);
}
+
public static Stream> batch() {
CompletableFuture batter = Batter.mix();
- return Stream.of(bake(batter), bake(batter), bake(batter), bake(batter));
+ return Stream.of(
+ bake(batter),
+ bake(batter),
+ bake(batter),
+ bake(batter)
+ );
}
}
```
-最后,我们创建了一批糖,并用它对蛋糕进行糖化:
+最后,我们制作了一批糖,并用它对蛋糕进行糖化:
```java
// concurrent/FrostedCake.java
+
import java.util.concurrent.*;
import java.util.stream.*;
import onjava.Nap;
+
final class Frosting {
- private Frosting() {}
+ private Frosting() {
+ }
+
static CompletableFuture make() {
new Nap(0.1);
- return CompletableFuture.completedFuture(new Frosting());
+ return CompletableFuture
+ .completedFuture(new Frosting());
}
}
+
public class FrostedCake {
public FrostedCake(Baked baked, Frosting frosting) {
new Nap(0.1);
}
+
@Override
public String toString() {
return "FrostedCake";
}
+
public static void main(String[] args) {
- Baked.batch()
- .forEach(baked -> baked.thenCombineAsync(Frosting.make(), (cake, frosting) -> new FrostedCake(cake, frosting)) .thenAcceptAsync(System.out::println)
- .join());
+ Baked.batch().forEach(
+ baked -> baked
+ .thenCombineAsync(Frosting.make(),
+ (cake, frosting) ->
+ new FrostedCake(cake, frosting))
+ .thenAcceptAsync(System.out::println)
+ .join());
}
}
```
-一旦你对背后的想法感到满意。**CompletableFutures**它们相对易于使用。
+一旦你习惯了这种背后的想法, `CompletableFuture` 它们相对易于使用。
-### 例外情况
+### 异常
-与**CompletableFutur**e在处理链中包装对象的方式相同,它还可以缓冲异常。这些不会在处理过程中显示给调用者,而只会在你尝试提取结果时显示。为了展示它们是如何工作的,我们将从创建一个在某些情况下引发异常的类开始:
+与 `CompletableFuture` 在处理链中包装对象的方式相同,它也会缓冲异常。这些在处理时调用者是无感的,但仅当你尝试提取结果时才会被告知。为了说明它们是如何工作的,我们首先创建一个类,它在特定的条件下抛出一个异常:
```java
// concurrent/Breakable.java
@@ -1901,18 +1990,25 @@ import java.util.concurrent.*;
public class Breakable {
String id;
private int failcount;
+
public Breakable(String id, int failcount) {
this.id = id;
this.failcount = failcount;
}
+
@Override
public String toString() {
return "Breakable_" + id + " [" + failcount + "]";
}
+
public static Breakable work(Breakable b) {
- if(--b.failcount == 0) {
- System.out.println( "Throwing Exception for " + b.id + "");
- throw new RuntimeException( "Breakable_" + b.id + " failed");
+ if (--b.failcount == 0) {
+ System.out.println(
+ "Throwing Exception for " + b.id + ""
+ );
+ throw new RuntimeException(
+ "Breakable_" + b.id + " failed"
+ );
}
System.out.println(b);
return b;
@@ -1920,23 +2016,25 @@ public class Breakable {
}
```
-**failcount**为正时,每次将对象传递给**work()**方法可减少**failcount**。当它为零时,**work()**会引发异常。如果你给它的**failcount**为零,则它永远不会引发异常。
-请注意,它报告在抛出异常时抛出异常。
-在下面的**test()**方法中,**work()**多次应用于**Breakable**,因此,如果**failcount**在范围内,则会引发异常。但是,在测试**A**到**E**中,你可以从输出中看到抛出了异常,但是它们从未出现:
+当`failcount` > 0,且每次将对象传递给 `work()` 方法时, `failcount - 1` 。当`failcount - 1 = 0` 时,`work()` 将抛出一个异常。如果传给 `work()` 的 `failcount = 0` ,`work()` 永远不会抛出异常。
+
+注意,异常信息此示例中被抛出( `RuntimeException` )
+
+在下面示例 `test()` 方法中,`work()` 多次应用于 `Breakable`,因此如果 `failcount` 在范围内,就会抛出异常。然而,在测试`A`到`E`中,你可以从输出中看到抛出了异常,但它们从未出现:
```java
// concurrent/CompletableExceptions.java
import java.util.concurrent.*;
public class CompletableExceptions {
static CompletableFuture test(String id, int failcount) {
- return
- CompletableFuture.completedFuture(
+ return CompletableFuture.completedFuture(
new Breakable(id, failcount))
.thenApply(Breakable::work)
.thenApply(Breakable::work)
.thenApply(Breakable::work)
.thenApply(Breakable::work);
}
+
public static void main(String[] args) {
// Exceptions don't appear ...
test("A", 1);
@@ -1947,22 +2045,24 @@ public class CompletableExceptions {
// ... until you try to fetch the value:
try {
test("F", 2).get(); // or join()
- } catch(Exception e) {
+ } catch (Exception e) {
System.out.println(e.getMessage());
}
// Test for exceptions:
System.out.println(
- test("G", 2).isCompletedExceptionally());
+ test("G", 2).isCompletedExceptionally()
+ );
// Counts as "done":
System.out.println(test("H", 2).isDone());
// Force an exception:
CompletableFuture cfi =
- new CompletableFuture<>();
+ new CompletableFuture<>();
System.out.println("done? " + cfi.isDone());
- cfi.completeExceptionally( new RuntimeException("forced"));
+ cfi.completeExceptionally(
+ new RuntimeException("forced"));
try {
cfi.get();
- } catch(Exception e) {
+ } catch (Exception e) {
System.out.println(e.getMessage());
}
}
@@ -1999,10 +2099,11 @@ done? false
java.lang.RuntimeException: forced
```
-测试**A**到**E**运行到抛出异常的地步,然后……什么都没有。只有在测试**F**中调用**get()**时,我们才能看到抛出的异常。
-测试**G**显示,你可以首先检查在处理过程中是否引发了异常,而没有引发该异常。但是,测试H告诉我们,无论异常成功与否,异常仍然可以被视为“完成”
-代码的最后一部分显示了如何在**CompletableFuture**中插入异常,而不管是否存在任何故障。
-加入或获取结果时,我们不会使用粗略的try-catch,而是使用**CompletableFuture**提供的更复杂的机制来自动响应异常。你可以使用与所有**CompletableFuture**相同的表格来执行此操作:在链中插入**CompletableFuture**调用。有三个选项:**exclusively(**),**handle()**和**whenComplete()**:
+测试 `A` 到 `E` 运行到抛出异常,然后…并没有将抛出的异常暴露给调用方。只有在测试F中调用 `get()` 时,我们才会看到抛出的异常。
+测试 `G` 表明,你可以首先检查在处理期间是否抛出异常,而不抛出该异常。然而,test `H` 告诉我们,不管异常是否成功,它仍然被视为已“完成”。
+代码的最后一部分展示了如何将异常插入到 `CompletableFuture` 中,而不管是否存在任何失败。
+在连接或获取结果时,我们使用 `CompletableFuture` 提供的更复杂的机制来自动响应异常,而不是使用粗糙的 `try-catch`。
+你可以使用与我们看到的所有 `CompletableFuture` 相同的表单来完成此操作:在链中插入一个 `CompletableFuture` 调用。有三个选项 `exceptionally()`,`handle()`, `whenComplete()`:
```java
// concurrent/CatchCompletableExceptions.java
@@ -2010,38 +2111,42 @@ import java.util.concurrent.*;
public class CatchCompletableExceptions {
static void handleException(int failcount) {
// Call the Function only if there's an
- // exception, must produce same type as came in:
+ // exception, must produce same type as came in:
CompletableExceptions
- .test("exceptionally", failcount)
- .exceptionally((ex) -> { // Function
- if(ex == null)
- System.out.println("I don't get it yet");
- return new Breakable(ex.getMessage(), 0);
- })
- .thenAccept(str ->
- System.out.println("result: " + str));
+ .test("exceptionally", failcount)
+ .exceptionally((ex) -> { // Function
+ if (ex == null)
+ System.out.println("I don't get it yet");
+ return new Breakable(ex.getMessage(), 0);
+ })
+ .thenAccept(str ->
+ System.out.println("result: " + str));
+
// Create a new result (recover):
CompletableExceptions
- .test("handle", failcount)
- .handle((result, fail) -> { // BiFunction
- if(fail != null)
- return "Failure recovery object";
- else
- return result + " is good"; })
- .thenAccept(str ->
- System.out.println("result: " + str));
- // Do something but pass the same result through:
+ .test("handle", failcount)
+ .handle((result, fail) -> { // BiFunction
+ if (fail != null)
+ return "Failure recovery object";
+ else
+ return result + " is good";
+ })
+ .thenAccept(str ->
+ System.out.println("result: " + str));
+
+ // Do something but pass the same result through:
CompletableExceptions
- .test("whenComplete", failcount)
- .whenComplete((result, fail) -> {// BiConsumer
- if(fail != null)
- System.out.println("It failed");
- else
- System.out.println(result + " OK");
- })
- .thenAccept(r ->
- System.out.println("result: " + r));
+ .test("whenComplete", failcount)
+ .whenComplete((result, fail) -> { // BiConsumer
+ if (fail != null)
+ System.out.println("It failed");
+ else
+ System.out.println(result + " OK");
+ })
+ .thenAccept(r ->
+ System.out.println("result: " + r));
}
+
public static void main(String[] args) {
System.out.println("**** Failure Mode ****");
handleException(2);
@@ -2084,26 +2189,35 @@ Breakable_whenComplete [-4] OK
result: Breakable_whenComplete [-4]
```
-只有在有异常的情况下,**exclusively()**参数才会运行。**Exclusively()**的局限性在于,该函数只能返回输入的相同类型的值。**exclusively()**通过将一个好的对象重新插入流中而恢复到可行状态。
-**handle()**始终被调用,你必须检查一下**fail**是否为**true**才能查看是否发生了异常。但是**handle()**可以产生任何新类型,因此它使你可以执行处理,而不仅可以像**exception()**那样进行恢复。
-**whenComplete()**就像**handle()**一样,你必须测试是否失败,但是该参数是使用者,并且不会修改正在传递的结果对象。
+- `exceptionally()` 参数仅在出现异常时才运行。`exceptionally()` 局限性在于,该函数只能返回输入类型相同的值。
-### 流异常
+- `exceptionally()` 通过将一个好的对象插入到流中来恢复到一个可行的状态。
-通过修改**CompletableExceptions.java**,看看**CompletableFuture**异常与**Streams**异常有何不同:
+- `handle()` 一致被调用来查看是否发生异常(必须检查fail是否为true)。
+
+ - 但是 `handle()` 可以生成任何新类型,所以它允许执行处理,而不是像使用 `exceptionally()`那样简单地恢复。
+
+ - `whenComplete()` 类似于handle(),同样必须测试它是否失败,但是参数是一个消费者,并且不修改传递给它的结果对象。
+
+
+### 流异常(Stream Exception)
+
+通过修改**CompletableExceptions.java**,看看 **CompletableFuture**异常与流异常有何不同:
```java
// concurrent/StreamExceptions.java
import java.util.concurrent.*;
import java.util.stream.*;
public class StreamExceptions {
+
static Stream test(String id, int failcount) {
- return Stream.of(new Breakable(id, failcount)).
- map(Breakable::work)
- .map(Breakable::work
- .map(Breakable::work)
- .map(Breakable::work);
+ return Stream.of(new Breakable(id, failcount))
+ .map(Breakable::work)
+ .map(Breakable::work)
+ .map(Breakable::work)
+ .map(Breakable::work);
}
+
public static void main(String[] args) {
// No operations are even applied ...
test("A", 1);
@@ -2114,8 +2228,8 @@ public class StreamExceptions {
// ... until there's a terminal operation:
System.out.println("Entering try");
try {
- c.forEach(System.out::println);// [1]
- } catch(Exception e) {
+ c.forEach(System.out::println); // [1]
+ } catch (Exception e) {
System.out.println(e.getMessage());
}
}
@@ -2132,111 +2246,135 @@ Throwing Exception for C
Breakable_C failed
```
-使用**CompletableFutures**,我们看到了测试**A**到**E**的进展,但是使用**Streams**,直到你应用了终端操作(如[1]的**forEach()**),一切都没有开始。**CompletableFuture**执行工作并捕获任何异常以供以后检索。比较这两者并不是一件容易的事,因为**Stream**没有终端操作根本无法执行任何操作,但是**Stream**绝对不会存储其异常。
+使用 `CompletableFuture`,我们可以看到测试A到E的进展,但是使用流,在你应用一个终端操作之前(e.g. `forEach()`),什么都不会暴露给 Client
+
+`CompletableFuture` 执行工作并捕获任何异常供以后检索。比较这两者并不容易,因为 `Stream` 在没有终端操作的情况下根本不做任何事情——但是流绝对不会存储它的异常。
-### 检查异常
+### 检查性异常
-CompletableFutures和并行Streams都不支持包含已检查异常的操作。相反,你必须在调用操作时处理检查到的异常,这会产生不太优雅的代码:
+`CompletableFuture` 和 `parallel Stream` 都不支持包含检查性异常的操作。相反,你必须在调用操作时处理检查到的异常,这会产生不太优雅的代码:
```java
// concurrent/ThrowsChecked.java
import java.util.stream.*;
import java.util.concurrent.*;
+
public class ThrowsChecked {
class Checked extends Exception {}
+
static ThrowsChecked nochecked(ThrowsChecked tc) {
return tc;
}
+
static ThrowsChecked withchecked(ThrowsChecked tc) throws Checked {
return tc;
}
+
static void testStream() {
Stream.of(new ThrowsChecked())
- .map(ThrowsChecked::nochecked)
- // .map(ThrowsChecked::withchecked); // [1]
- .map(tc -> {
- try {
- return withchecked(tc);
- } catch(Checked e) {
- throw new RuntimeException(e);
- }
- });
+ .map(ThrowsChecked::nochecked)
+ // .map(ThrowsChecked::withchecked); // [1]
+ .map(
+ tc -> {
+ try {
+ return withchecked(tc);
+ } catch (Checked e) {
+ throw new RuntimeException(e);
+ }
+ });
}
+
static void testCompletableFuture() {
- CompletableFuture .completedFuture(new ThrowsChecked())
- .thenApply(ThrowsChecked::nochecked)
- // .thenApply(ThrowsChecked::withchecked); // [2]
- .thenApply(tc -> {
- try {
- return withchecked(tc);
- } catch(Checked e) {
- throw new RuntimeException(e);
- }
- });
+ CompletableFuture
+ .completedFuture(new ThrowsChecked())
+ .thenApply(ThrowsChecked::nochecked)
+ // .thenApply(ThrowsChecked::withchecked); // [2]
+ .thenApply(
+ tc -> {
+ try {
+ return withchecked(tc);
+ } catch (Checked e) {
+ throw new RuntimeException(e);
+ }
+ });
}
}
```
-如果你尝试像对 **nochecked()** 一样对 **withchecked()** 使用方法引用,则编译器会抱怨[1]和[2]。相反,你必须写出lambda表达式(或编写一个不会引发异常的包装器方法)。
-
+如果你试图像使用 `nochecked()` 那样使用` withchecked()` 的方法引用,编译器会在 `[1]` 和 `[2]` 中报错。相反,你必须写出lambda表达式(或者编写一个不会抛出异常的包装器方法)。
+
## 死锁
-由于任务可能会被阻塞,因此一个任务有可能卡在等待另一个任务上,而任务又在等待另一个任务,依此类推,直到链回到第一个任务上。你会遇到一个不断循环的任务,彼此等待,没有人能动。这称为死锁[^6]
-如果你尝试运行某个程序并立即陷入死锁,则可以立即查找该错误。真正的问题是,当你的程序看起来运行良好,但具有隐藏潜力死锁。在这里,你可能没有任何迹象表明可能发生死锁,因此该缺陷在你的程序中是潜在的,直到它意外发生为止(通常是对客户而言(几乎肯定很难复制))。因此,通过仔细的程序设计防止死锁是开发并发系统的关键部分。
-埃德斯·迪克斯特拉(Essger Dijkstra)发明的"哲学家进餐"问题是经典的死锁例证。基本描述指定了五位哲学家(此处显示的示例允许任何数字)。这些哲学家将一部分时间花在思考上,一部分时间在吃饭上。他们在思考的时候并不需要任何共享资源,但是他们使用的餐具数量有限。在最初的问题描述中,器物是叉子,需要两个叉子才能从桌子中间的碗里取出意大利面。常见的版本是使用筷子。显然,每个哲学家都需要两个筷子才能吃饭。
-引入了一个困难:作为哲学家,他们的钱很少,所以他们只能买五根筷子(更普遍地说,筷子的数量与哲学家相同)。它们之间围绕桌子隔开。当一个哲学家想要吃饭时,该哲学家必须拿起左边和右边的筷子。如果任一侧的哲学家都在使用所需的筷子,则我们的哲学家必须等待,直到必要的筷子可用为止。
-**StickHolder**类通过将单个筷子保持在大小为1的**BlockingQueue**中来管理它。**BlockingQueue**是一个设计用于在并发程序中安全使用的集合,如果你调用take()并且队列为空,则它将阻塞(等待)。将新元素放入队列后,将释放该块并返回该值:
+由于任务可以被阻塞,因此一个任务有可能卡在等待另一个任务上,而后者又在等待别的任务,这样一直下去,知道这个链条上的任务又在等待第一个任务释放锁。这得到了一个任务之间相互等待的连续循环, 没有哪个线程能继续, 这称之为死锁[^6]
+如果你运行一个程序,而它马上就死锁了, 你可以立即跟踪下去。真正的问题在于,程序看起来工作良好, 但是具有潜在的死锁危险。这时, 死锁可能发生,而事先却没有任何征兆, 所以 `bug` 会潜伏在你的程序例,直到客户发现它出乎意料的发生(以一种几乎肯定是很难重现的方式发生)。因此在编写并发程序的时候,进行仔细的程序设计以防止死锁是关键部分。
+埃德斯·迪克斯特拉(`Essger Dijkstra`)发明的“哲学家进餐"问题是经典的死锁例证。基本描述指定了五位哲学家(此处显示的示例允许任何数目)。这些哲学家将花部分时间思考,花部分时间就餐。他们在思考的时候并不需要任何共享资源;但是他们使用的餐具数量有限。在最初的问题描述中,餐具是叉子,需要两个叉子才能从桌子中间的碗里取出意大利面。常见的版本是使用筷子, 显然,每个哲学家都需要两根筷子才能吃饭。
+引入了一个困难:作为哲学家,他们的钱很少,所以他们只能买五根筷子(更一般地讲,筷子的数量与哲学家相同)。他们围在桌子周围,每人之间放一根筷子。 当一个哲学家要就餐时,该哲学家必须同时持有左边和右边的筷子。如果任一侧的哲学家都在使用所需的筷子,则我们的哲学家必须等待,直到可得到必须的筷子。
+
+**StickHolder** 类通过将单根筷子保持在大小为1的**BlockingQueue**中来管理它。**BlockingQueue**是一个设计用于在并发程序中安全使用的集合,如果你调用take()并且队列为空,则它将阻塞(等待)。将新元素放入队列后,将释放该块并返回该值:
```java
// concurrent/StickHolder.java
import java.util.concurrent.*;
public class StickHolder {
- private static class Chopstick {}
+ private static class Chopstick {
+ }
+
private Chopstick stick = new Chopstick();
- private BlockingQueue holder = new ArrayBlockingQueue<>(1);
+ private BlockingQueue holder =
+ new ArrayBlockingQueue<>(1);
+
public StickHolder() {
putDown();
}
+
public void pickUp() {
try {
- holder.take();// Blocks if unavailable
- } catch(InterruptedException e) {
+ holder.take(); // Blocks if unavailable
+ } catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
+
public void putDown() {
try {
holder.put(stick);
- } catch(InterruptedException e) {
+ } catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
```
-为简单起见,**StickHolder**从未真正制作过**Chopstick**,而是在类中将其保密。如果调用**pickUp()**而该筷子不可用,则**pickUp()**会阻塞,直到另一位调用**putDown()**的哲学家返回了该摇杆。请注意,此类中的所有线程安全性都是通过**BlockingQueue**实现的。
+为简单起见,`Chopstick`(`static`) 实际上不是由 `StickHolder` 生产的,而是在其类中保持私有的。
+
+如果您调用了`pickUp()`,而 `stick` 不可用,那么`pickUp()`将阻塞该 `stick`,直到另一个哲学家调用`putDown()` 将 `stick` 返回。
+
+注意,该类中的所有线程安全都是通过 `BlockingQueue` 实现的。
-每个哲学家都是一个任务,尝试将左右两把筷子都拿起,使其可以进食,然后使用**putDown()**释放这些筷子:
+每个哲学家都是一项任务,他们试图把筷子分别 `pickUp()` 在左手和右手上,这样筷子才能吃东西,然后通过 `putDown()` 放下 `stick`。
```java
// concurrent/Philosopher.java
public class Philosopher implements Runnable {
private final int seat;
private final StickHolder left, right;
+
public Philosopher(int seat, StickHolder left, StickHolder right) {
this.seat = seat;
this.left = left;
this.right = right;
}
+
@Override
public String toString() {
return "P" + seat;
}
+
@Override
public void run() {
- while(true) {
- // System.out.println("Thinking");
- // [1] right.pickUp();
+ while (true) {
+ // System.out.println("Thinking"); // [1]
+ right.pickUp();
left.pickUp();
System.out.println(this + " eating");
right.putDown();
@@ -2247,7 +2385,8 @@ public class Philosopher implements Runnable {
```
没有两个哲学家可以同时成功调用take()同一只筷子。另外,如果一个哲学家已经拿过筷子,那么下一个试图拿起同一根筷子的哲学家将阻塞,等待其被释放。
-结果是一个看似无辜的程序陷入了死锁。我在这里使用数组而不是集合,只是因为结果语法更简洁:
+
+结果是一个看似无辜的程序陷入了死锁。我在这里使用数组而不是集合,只是因为这种语法更简洁:
```java
// concurrent/DiningPhilosophers.java
@@ -2256,62 +2395,73 @@ public class Philosopher implements Runnable {
import java.util.*;
import java.util.concurrent.*;
import onjava.Nap;
+
public class DiningPhilosophers {
private StickHolder[] sticks;
private Philosopher[] philosophers;
+
public DiningPhilosophers(int n) {
sticks = new StickHolder[n];
Arrays.setAll(sticks, i -> new StickHolder());
philosophers = new Philosopher[n];
- Arrays.setAll(philosophers,
- i -> new Philosopher(i, sticks[i], sticks[(i + 1) % n]));// [1]
+ Arrays.setAll(philosophers, i ->
+ new Philosopher(i,
+ sticks[i], sticks[(i + 1) % n])); // [1]
// Fix by reversing stick order for this one:
- // philosophers[1] = // [2]
- // new Philosopher(0, sticks[0], sticks[1]);
+ // philosophers[1] = // [2]
+ // new Philosopher(0, sticks[0], sticks[1]);
Arrays.stream(philosophers)
- .forEach(CompletableFuture::runAsync);// [3]
+ .forEach(CompletableFuture::runAsync); // [3]
}
+
public static void main(String[] args) {
// Returns right away:
- new DiningPhilosophers(5);// [4]
+ new DiningPhilosophers(5); // [4]
// Keeps main() from exiting:
new Nap(3, "Shutdown");
}
}
```
-当你停止查看输出时,该程序将死锁。但是,根据你的计算机配置,你可能不会看到死锁。看来这取决于计算机上的内核数[^7]。两个核心似乎不会产生死锁,但似乎有两个以上的核心很容易产生死锁。此行为使该示例更好地说明了死锁,因为你可能正在具有两个内核的计算机上编写程序(如果确实是导致问题的原因),并且确信该程序可以正常工作,只能启动它将其安装在另一台计算机上时出现死锁。请注意,仅仅因为你不容易看到死锁,并不意味着该程序就不会在两核计算机上死锁。该程序仍然容易死锁,很少发生-可以说是最坏的情况,因为问题不容易解决。
-在DiningPhilosophers构造函数中,每个哲学家都获得一个左右StickHolder的引用。除最后一个哲学家外,每个哲学家都通过以下方式初始化:
-哲学家之间的下一双筷子。最后一位哲学家右手的筷子为零,因此圆桌会议完成了。那是因为最后一位哲学家正坐在第一个哲学家的旁边,而且他们俩都共用零筷子。[1]显示了以n为模数选择的右摇杆,将最后一个哲学家缠绕在第一个哲学家的旁边。
-现在,所有哲学家都可以尝试吃饭,每个哲学家都在旁边等待哲学家放下筷子。
-要开始在[3]上运行的每个Philosopher,我调用runAsync(),这意味着DiningPhilosophers构造函数立即在[4]处返回。没有任何东西可以阻止main()完成,该程序只是退出而无济于事。Nap对象阻止main()退出,然后在三秒钟后强制退出(可能是)死锁的程序。
-在给定的配置中,哲学家几乎没有时间思考。因此,他们都在尝试吃饭时争夺筷子,而且僵局往往很快发生。你可以更改此:
+- 当你停止查看输出时,该程序将死锁。但是,根据你的计算机配置,你可能不会看到死锁。看来这取决于计算机上的内核数[^7]。两个核心不会产生死锁,但两核以上却很容易产生死锁。
+- 此行为使该示例更好地说明了死锁,因为你可能正在具有2核的计算机上编写程序(如果确实是导致问题的原因),并且确信该程序可以正常工作,只能启动它将其安装在另一台计算机上时出现死锁。请注意,不能因为你没或不容易看到死锁,这并不意味着此程序不会在2核机器上发生死锁。 该程序仍然有死锁倾向,只是很少发生——可以说是最糟糕的情况,因为问题不容易出现。
+- 在 `DiningPhilosophers` 的构造方法中,每个哲学家都获得一个左右筷子的引用。除最后一个哲学家外,都是通过把哲学家放在下一双空闲筷子之间来初始化:
+ - 最后一位哲学家得到了第0根筷子作为他的右筷子,所以圆桌就完成。
+ - 那是因为最后一位哲学家正坐在第一个哲学家的旁边,而且他们俩都共用零筷子。[1]显示了以n为模数选择的右筷子,将最后一个哲学家绕到第一个哲学家的旁边。
+- 现在,所有哲学家都可以尝试吃饭,每个哲学家都在旁边等待哲学家放下筷子。
+ - 为了让每个哲学家在[3]上运行,调用 `runAsync()`,这意味着DiningPhilosophers的构造函数立即返回到[4]。
+ - 如果没有任何东西阻止 `main()` 完成,程序就会退出,不会做太多事情。
+ - `Nap` 对象阻止 `main()` 退出,然后在三秒后强制退出(假设/可能是)死锁程序。
+ - 在给定的配置中,哲学家几乎不花时间思考。因此,他们在吃东西的时候都争着用筷子,而且往往很快就会陷入僵局。你可以改变这个:
1. 通过增加[4]的值来添加更多哲学家。
+
2. 在Philosopher.java中取消注释行[1]。
-任一种方法都会减少死锁的可能性,这表明编写并发程序并认为它是安全的危险,因为它似乎“在我的机器上运行正常”。你可以轻松地说服自己该程序没有死锁,即使它不是。这个例子很有趣,因为它演示了程序似乎可以正确运行,同时仍然容易出现死锁。
-为了解决该问题,我们观察到当四个同时满足条件:
+任一种方法都会减少死锁的可能性,这表明编写并发程序并认为它是安全的危险,因为它似乎“在我的机器上运行正常”。你可以轻松地说服自己该程序没有死锁,即使它不是。这个示例相当有趣,因为它演示了看起来可以正确运行,但实际上会可能发生死锁的程序。
-1. 互斥。任务使用的至少一种资源必须不可共享。在这里,筷子一次只能由一位哲学家使用。
-2. 至少一个任务必须拥有资源,并等待获取当前由另一任务拥有的资源。也就是说,要使僵局发生,哲学家必须握住一根筷子,等待另一根筷子。
-3. 不能抢先从任务中夺走资源。任务仅作为正常事件释放资源。我们的哲学家很有礼貌,他们不会抓住其他哲学家的筷子。
-4. 可能发生循环等待,即一个任务等待另一个任务持有的资源,而该任务又等待另一个任务持有的资源,依此类推,直到一个任务正在等待另一个任务持有的资源。第一项任务,从而使一切陷入僵局。在**DiningPhilosophers.java**中,发生循环等待是因为每个哲学家都先尝试获取右筷子,然后再获取左筷子。
+要修正死锁问题,你必须明白,当以下四个条件同时满足时,就会发生死锁:
+
+1) 互斥条件。任务使用的资源中至少有一个不能共享的。 这里,一根筷子一次就只能被一个哲学家使用。
+2) 至少有一个任务它必须持有一个资源且正在等待获取一个被当前别的任务持有的资源。也就是说,要发生死锁,哲学家必须拿着一根筷子并且等待另一根。
+3) 资源不能被任务抢占, 任务必须把资源释放当作普通事件。哲学家很有礼貌,他们不会从其它哲学家那里抢筷子。
+4) 必须有循环等待, 这时,一个任务等待其它任务所持有的资源, 后者又在等待另一个任务所持有的资源, 这样一直下去,知道有一个任务在等待第一个任务所持有的资源, 使得大家都被锁住。 在 `DiningPhilosophers.java` 中, 因为每个哲学家都试图先得到右边的 筷子, 然后得到左边的 筷子, 所以发生了循环等待。
+
+因为必须满足所有条件才能导致死锁,所以要阻止死锁的话,只需要破坏其中一个即可。在此程序中,防止死锁的一种简单方法是打破第四个条件。之所以会发生这种情况,是因为每个哲学家都尝试按照特定的顺序拾起自己的筷子:先右后左。因此,每个哲学家都有可能在等待左手的同时握住右手的筷子,从而导致循环等待状态。但是,如果其中一位哲学家尝试首先拿起左筷子,则该哲学家决不会阻止紧邻右方的哲学家拿起筷子,从而排除了循环等待。
-因为必须满足所有这些条件才能导致死锁,所以你只能阻止其中一个解除死锁。在此程序中,防止死锁的一种简单方法是打破第四个条件。之所以会发生这种情况,是因为每个哲学家都尝试按照特定的顺序拾起自己的筷子:先右后左。因此,每个哲学家都有可能在等待左手的同时握住右手的筷子,从而导致循环等待状态。但是,如果其中一位哲学家尝试首先拿起左筷子,则该哲学家决不会阻止紧邻右方的哲学家拿起筷子,从而排除了循环等待。
在**DiningPhilosophers.java**中,取消注释[1]和其后的一行。这将原来的哲学家[1]替换为筷子颠倒的哲学家。通过确保第二位哲学家拾起并在右手之前放下左筷子,我们消除了死锁的可能性。
这只是解决问题的一种方法。你也可以通过防止其他情况之一来解决它。
没有语言支持可以帮助防止死锁;你有责任通过精心设计来避免这种情况。对于试图调试死锁程序的人来说,这些都不是安慰。当然,避免并发问题的最简单,最好的方法是永远不要共享资源-不幸的是,这并不总是可能的。
-
-## 构造函数非线程安全
+## 构造方法非线程安全
当你在脑子里想象一个对象构造的过程,你会很容易认为这个过程是线程安全的。毕竟,在对象初始化完成前对外不可见,所以又怎会对此产生争议呢?确实,[Java 语言规范](https://docs.oracle.com/javase/specs/jls/se8/html/jls-8.html#jls-8.8.3) (JLS)自信满满地陈述道:“*没必要使构造器的线程同步,因为它会锁定正在构造的对象,直到构造器完成初始化后才对其他线程可见。*”
+
不幸的是,对象的构造过程如其他操作一样,也会受到共享内存并发问题的影响,只是作用机制可能更微妙罢了。
-设想下使用一个**静态**字段为每个对象自动创建唯一标识符的过程。为了测试其不同的实现过程,我们从一个接口开始。代码示例:
+设想下使用一个 **static** 字段为每个对象自动创建唯一标识符的过程。为了测试其不同的实现过程,我们从一个接口开始。代码示例:
```java
//concurrent/HasID.java
@@ -2377,7 +2527,8 @@ public class IDChecker {
```
**MakeObjects** 类是一个生产者类,包含一个能够产生 List\ 类型的列表对象的 `get()` 方法。通过从每个 `HasID` 对象提取 `ID` 并放入列表中来生成这个列表对象,而 `test()` 方法则创建了两个并行的 **CompletableFuture** 对象,用于运行 **MakeObjects** 生产者类,然后获取运行结果。
-使用 Guava 库中的 **Sets.**`intersection()` 方法,计算出这两个返回的 List\ 对象中有多少相同的 `ID`(使用谷歌 Guava 库里的方法比使用官方的 `retainAll()` 方法速度快得多)。
+
+使用 Guava 库中的 **Sets.`intersection()` 方法,计算出这两个返回的 List\ 对象中有多少相同的 `ID`(使用谷歌 Guava 库里的方法比使用官方的 `retainAll()` 方法速度快得多)。
现在我们可以测试上面的 **StaticIDField** 类了。代码示例:
@@ -2478,6 +2629,7 @@ public class SharedConstructorArgument{
```
在这里,**SharedUser** 构造器实际上共享了相同的参数。即使 **SharedUser** 以完全无害且合理的方式使用其自己的参数,其构造器的调用方式也会引起冲突。**SharedUser** 甚至不知道它是以这种方式调用的,更不必说控制它了。
+
同步构造器并不被java语言所支持,但是通过使用同步语块来创建你自己的同步构造器是可能的(请参阅附录:[并发底层原理](./Appendix-Low-Level-Concurrency.md),来进一步了解同步关键字—— `synchronized`)。尽管JLS(java语言规范)这样陈述道:“……它会锁定正在构造的对象”,但这并不是真的——构造器实际上只是一个静态方法,因此同步构造器实际上会锁定该类的Class对象。我们可以通过创建自己的静态对象并锁定它,来达到与同步构造器相同的效果:
```java
@@ -2554,14 +2706,13 @@ public class SynchronizedFactory{
```
通过同步静态工厂方法,可以在构造过程中锁定 **Class** 对象。
+
这些示例充分表明了在并发Java程序中检测和管理共享状态有多困难。即使你采取“不共享任何内容”的策略,也很容易产生意外的共享事件。
-
+
## 复杂性和代价
假设你正在做披萨,我们把从整个流程的当前步骤到下一个步骤所需的工作量,在这里一一表示为枚举变量的一部分:
-
-
```java
// concurrent/Pizza.java import java.util.function.*;
@@ -2920,17 +3071,15 @@ Pizza4: complete
使用 **CompletableFutures** 或许可以轻易地带来重大收益,但是在尝试更进一步时需要倍加小心,因为额外增加的成本和工作量会非常容易远远超出你之前拼命挤出的那一点点收益。
-
-
## 本章小结
-需要并发的唯一理由是“等待太多”。这也可以包括用户界面的响应速度,但是由于Java用于构建用户界面时并不高效,因此[^8]这仅仅意味着“您的程序运行速度还不够快”。
+需要并发的唯一理由是“等待太多”。这也可以包括用户界面的响应速度,但是由于Java用于构建用户界面时并不高效,因此[^8]这仅仅意味着“你的程序运行速度还不够快”。
-如果并发很容易,则没有理由拒绝并发。 正因为并发实际上很难,所以您应该仔细考虑是否值得为此付出努力,并考虑您能否以其他方式提升速度。
+如果并发很容易,则没有理由拒绝并发。 正因为并发实际上很难,所以你应该仔细考虑是否值得为此付出努力,并考虑你能否以其他方式提升速度。
例如,迁移到更快的硬件(这可能比消耗程序员的时间要便宜得多)或者将程序分解成多个部分,然后在不同的机器上运行这些部分。
-奥卡姆剃刀是一个经常被误解的原则。 我看过至少一部电影,他们将其定义为”最简单的解决方案是正确的解决方案“,就好像这是某种毋庸置疑的法律。实际上,这是一个准则:面对多种方法时,请先尝试需要最少假设的方法。 在编程世界中,这已演变为“尝试可能可行的最简单的方法”。当您了解了特定工具的知识时——就像你现在了解了有关并发性的知识一样,你可能会很想使用它,或者提前规定你的解决方案必须能够“速度飞快”,从而来证明从一开始就进行并发设计是合理的。但是,我们的奥卡姆剃刀编程版本表示您应该首先尝试最简单的方法(这种方法开发起来也更便宜),然后看看它是否足够好。
+奥卡姆剃刀是一个经常被误解的原则。 我看过至少一部电影,他们将其定义为”最简单的解决方案是正确的解决方案“,就好像这是某种毋庸置疑的法律。实际上,这是一个准则:面对多种方法时,请先尝试需要最少假设的方法。 在编程世界中,这已演变为“尝试可能可行的最简单的方法”。当你了解了特定工具的知识时——就像你现在了解了有关并发性的知识一样,你可能会很想使用它,或者提前规定你的解决方案必须能够“速度飞快”,从而来证明从一开始就进行并发设计是合理的。但是,我们的奥卡姆剃刀编程版本表示你应该首先尝试最简单的方法(这种方法开发起来也更便宜),然后看看它是否足够好。
由于我出身于底层学术背景(物理学和计算机工程),所以我很容易想到所有小轮子转动的成本。我确定使用最简单的方法不够快的场景出现的次数已经数不过来了,但是尝试后却发现它实际上绰绰有余。
@@ -2946,54 +3095,48 @@ Pizza4: complete
4. 诸如饥饿,竞速,死锁和活锁(多线程各自处理单个任务而整体却无法完成)之类的问题。
-5. 跨平台的不一致。 通过一些示例,我发现了某些计算机上很快出现的竞争状况,而在其他计算机上却没有。 如果您在后者上开发程序,则在分发程序时可能会感到非常惊讶。
+5. 跨平台的不一致。 通过一些示例,我发现了某些计算机上很快出现的竞争状况,而在其他计算机上却没有。 如果你在后者上开发程序,则在分发程序时可能会感到非常惊讶。
-
-
-另外,并发的应用是一门艺术。 Java旨在允许您创建尽可能多的所需要的对象来解决问题——至少在理论上是这样。[^9]但是,线程不是典型的对象:每个线程都有其自己的执行环境,包括堆栈和其他必要的元素,使其比普通对象大得多。 在大多数环境中,只能在内存用光之前创建数千个**Thread**对象。通常,您只需要几个线程即可解决问题,因此一般来说创建线程没有什么限制,但是对于某些设计而言,它会成为一种约束,可能迫使您使用完全不同的方案。
+另外,并发的应用是一门艺术。 Java旨在允许你创建尽可能多的所需要的对象来解决问题——至少在理论上是这样。[^9]但是,线程不是典型的对象:每个线程都有其自己的执行环境,包括堆栈和其他必要的元素,使其比普通对象大得多。 在大多数环境中,只能在内存用光之前创建数千个**Thread**对象。通常,你只需要几个线程即可解决问题,因此一般来说创建线程没有什么限制,但是对于某些设计而言,它会成为一种约束,可能迫使你使用完全不同的方案。
### 共享内存陷阱
-并发性的主要困难之一是因为可能有多个任务共享一个资源(例如对象中的内存),并且您必须确保多个任务不会同时读取和更改该资源。
-
-我花了多年的时间研究并发并发。 我了解到您永远无法相信使用共享内存并发的程序可以正常工作。 您可以轻易发现它是错误的,但永远无法证明它是正确的。 这是众所周知的并发原则之一。[^10]
+并发性的主要困难之一是因为可能有多个任务共享一个资源(例如对象中的内存),并且你必须确保多个任务不会同时读取和更改该资源。
-我遇到了许多人,他们对编写正确的线程程序的能力充满信心。 我偶尔开始认为我也可以做好。 对于一个特定的程序,我最初是在只有单个CPU的机器上编写的。 那时我能够说服自己该程序是正确的,因为我以为我对Java工具很了解。 而且在我的单CPU计算机上也没有失败。而到了具有多个CPU的计算机,程序出现问题不能运行后,我感到很惊讶,但这还只是众多问题中的一个而已。 这不是Java的错; “写一次,到处运行”,在单核与多核计算机间无法扩展到并发编程领域。这是并发编程的基本问题。 实际上您可以在单CPU机器上发现一些并发问题,但是在多线程实际上真的在并行运行的多CPU机器上,就会出现一些其他问题。
-
-再举一个例子,哲学家就餐的问题可以很容易地进行调整,因此几乎不会产生死锁,这会给您一种一切都棒极了的印象。当涉及到共享内存并发编程时,您永远不应该对自己的编程能力变得过于自信。
+我花了多年的时间研究并发并发。 我了解到你永远无法相信使用共享内存并发的程序可以正常工作。 你可以轻易发现它是错误的,但永远无法证明它是正确的。 这是众所周知的并发原则之一。[^10]
+我遇到了许多人,他们对编写正确的线程程序的能力充满信心。 我偶尔开始认为我也可以做好。 对于一个特定的程序,我最初是在只有单个CPU的机器上编写的。 那时我能够说服自己该程序是正确的,因为我以为我对Java工具很了解。 而且在我的单CPU计算机上也没有失败。而到了具有多个CPU的计算机,程序出现问题不能运行后,我感到很惊讶,但这还只是众多问题中的一个而已。 这不是Java的错; “写一次,到处运行”,在单核与多核计算机间无法扩展到并发编程领域。这是并发编程的基本问题。 实际上你可以在单CPU机器上发现一些并发问题,但是在多线程实际上真的在并行运行的多CPU机器上,就会出现一些其他问题。
+再举一个例子,哲学家就餐的问题可以很容易地进行调整,因此几乎不会产生死锁,这会给你一种一切都棒极了的印象。当涉及到共享内存并发编程时,你永远不应该对自己的编程能力变得过于自信。
### This Albatross is Big
-如果您对Java并发感到不知所措,那说明您身处在一家出色的公司里。您 可以访问**Thread**类的[Javadoc](https://docs.oracle.com/javase/8/docs/api/java/lang/Thread.html)页面, 看一下哪些方法现在是**Deprecated**(废弃的)。这些是Java语言设计者犯过错的地方,因为他们在设计语言时对并发性了解不足。
+如果你对Java并发感到不知所措,那说明你身处在一家出色的公司里。你可以访问**Thread**类的[Javadoc](https://docs.oracle.com/javase/8/docs/api/java/lang/Thread.html)页面, 看一下哪些方法现在是**Deprecated**(废弃的)。这些是Java语言设计者犯过错的地方,因为他们在设计语言时对并发性了解不足。
-事实证明,在Java的后续版本中添加的许多库解决方案都是无效的,甚至是无用的。 幸运的是,Java 8中的并行**Streams**和**CompletableFutures**都非常有价值。但是当您使用旧代码时,仍然会遇到旧的解决方案。
+事实证明,在Java的后续版本中添加的许多库解决方案都是无效的,甚至是无用的。 幸运的是,Java 8中的并行**Streams**和**CompletableFutures**都非常有价值。但是当你使用旧代码时,仍然会遇到旧的解决方案。
-在本书的其他地方,我谈到了Java的一个基本问题:每个失败的实验都永远嵌入在语言或库中。 Java并发强调了这个问题。尽管有不少错误,但错误并不是那么多,因为有很多不同的尝试方法来解决问题。 好的方面是,这些尝试产生了更好,更简单的设计。 不利之处在于,在找到好的方法之前,您很容易迷失于旧的设计中。
+在本书的其他地方,我谈到了Java的一个基本问题:每个失败的实验都永远嵌入在语言或库中。 Java并发强调了这个问题。尽管有不少错误,但错误并不是那么多,因为有很多不同的尝试方法来解决问题。 好的方面是,这些尝试产生了更好,更简单的设计。 不利之处在于,在找到好的方法之前,你很容易迷失于旧的设计中。
### 其他类库
-本章重点介绍了相对安全易用的并行工具流和**CompletableFutures**,并且仅涉及Java标准库中一些更细粒度的工具。 为避免您不知所措,我没有介绍您可能实际在实践中使用的某些库。我们使用了几个**Atomic**(原子)类,**ConcurrentLinkedDeque**,**ExecutorService**和**ArrayBlockingQueue**。附录:[并发底层原理](./Appendix-Low-Level-Concurrency.md)涵盖了其他一些内容,但是您还想探索**java.util.concurrent**的Javadocs。 但是要小心,因为某些库组件已被新的更好的组件所取代。
+本章重点介绍了相对安全易用的并行工具流和**CompletableFutures**,并且仅涉及Java标准库中一些更细粒度的工具。 为避免你不知所措,我没有介绍你可能实际在实践中使用的某些库。我们使用了几个**Atomic**(原子)类,**ConcurrentLinkedDeque**,**ExecutorService**和**ArrayBlockingQueue**。附录:[并发底层原理](./Appendix-Low-Level-Concurrency.md)涵盖了其他一些内容,但是你还想探索**java.util.concurrent**的Javadocs。 但是要小心,因为某些库组件已被新的更好的组件所取代。
### 考虑为并发设计的语言
-通常,请谨慎地使用并发。 如果需要使用它,请尝试使用最现代的方法:并行流或**CompletableFutures**。 这些功能旨在(假设您不尝试共享内存)使您摆脱麻烦(在Java的世界范围内)。
+通常,请谨慎地使用并发。 如果需要使用它,请尝试使用最现代的方法:并行流或**CompletableFutures**。 这些功能旨在(假设你不尝试共享内存)使你摆脱麻烦(在Java的世界范围内)。
-如果您的并发问题变得比高级Java构造所支持的问题更大且更复杂,请考虑使用专为并发设计的语言,仅在需要并发的程序部分中使用这种语言是有可能的。 在撰写本文时,JVM上最纯粹的功能语言是Clojure(Lisp的一种版本)和Frege(Haskell的一种实现)。这些使您可以在其中编写应用程序的并发部分语言,并通过JVM轻松地与您的主要Java代码进行交互。 或者,您可以选择更复杂的方法,即通过外部功能接口(FFI)将JVM之外的语言与另一种为并发设计的语言进行通信。[^11]
+如果你的并发问题变得比高级Java构造所支持的问题更大且更复杂,请考虑使用专为并发设计的语言,仅在需要并发的程序部分中使用这种语言是有可能的。 在撰写本文时,JVM上最纯粹的功能语言是Clojure(Lisp的一种版本)和Frege(Haskell的一种实现)。这些使你可以在其中编写应用程序的并发部分语言,并通过JVM轻松地与你的主要Java代码进行交互。 或者,你可以选择更复杂的方法,即通过外部功能接口(FFI)将JVM之外的语言与另一种为并发设计的语言进行通信。[^11]
-你很容易被一种语言绑定,迫使自己尝试使用该语言来做所有事情。 一个常见的示例是构建HTML / JavaScript用户界面。 这些工具确实很难使用,令人讨厌,并且有许多库允许您通过使用自己喜欢的语言编写代码来生成这些工具(例如,**Scala.js**允许您在Scala中完成代码)。
+你很容易被一种语言绑定,迫使自己尝试使用该语言来做所有事情。 一个常见的示例是构建HTML / JavaScript用户界面。 这些工具确实很难使用,令人讨厌,并且有许多库允许你通过使用自己喜欢的语言编写代码来生成这些工具(例如,**Scala.js**允许你在Scala中完成代码)。
心理上的便利是一个合理的考虑因素。 但是,我希望我在本章(以及附录:[并发底层原理](./Appendix-Low-Level-Concurrency.md))中已经表明Java并发是一个你可能无法逃离很深的洞。 与Java语言的任何其他部分相比,在视觉上检查代码同时记住所有陷阱所需要的的知识要困难得多。
-无论使用特定的语言、库使得并发看起来多么简单,都要将其视为一种妖术,因为总是有东西会在您最不期望出现的时候咬您。
+无论使用特定的语言、库使得并发看起来多么简单,都要将其视为一种妖术,因为总是有东西会在你最不期望出现的时候咬你。
### 拓展阅读
《Java Concurrency in Practice》,出自Brian Goetz,Tim Peierls, Joshua Bloch,Joseph Bowbeer,David Holmes和 Doug Lea (Addison Wesley,2006年)——这些基本上就是Java并发世界中的名人名单了《Java Concurrency in Practice》第二版,出自 Doug Lea (Addison-Wesley,2000年)。尽管这本书出版时间远远早于Java 5发布,但Doug的大部分工作都写入了**java.util.concurrent**库。因此,这本书对于全面理解并发问题至关重要。 它超越了Java,讨论了跨语言和技术的并发编程。 尽管它在某些地方可能很钝,但值得多次重读(最好是在两个月之间进行消化)。 道格(Doug)是世界上为数不多的真正了解并发编程的人之一,因此这是值得的。
-
-
[^1]:例如,Eric-Raymond在“Unix编程艺术”(Addison-Wesley,2004)中提出了一个很好的案例。
[^2]:可以说,试图将并发性用于后续语言是一种注定要失败的方法,但你必须得出自己的结论
[^3]:有人谈论在Java——10中围绕泛型做一些类似的基本改进,这将是非常令人难以置信的。
@@ -3007,4 +3150,5 @@ Pizza4: complete
[^11]: 尽管**Go**语言显示了FFI的前景,但在撰写本文时,它并未提供跨所有平台的解决方案。
-
+
+
\ No newline at end of file
From d426b5557485c66c45744b28cb35db53ac647124 Mon Sep 17 00:00:00 2001
From: unclesesame
Date: Sat, 11 Apr 2020 11:34:43 +0800
Subject: [PATCH 067/224] =?UTF-8?q?=E7=AC=AC=E5=8D=81=E4=B8=80=E5=92=8C?=
=?UTF-8?q?=E5=8D=81=E4=BA=8C=E7=AB=A0=E7=9A=84=E8=AF=AD=E6=B3=95=E9=94=99?=
=?UTF-8?q?=E8=AF=AF=20(#428)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
* Update 12-Collections.md
删除多余的 “的”
* Update 11-Inner-Classes.md
“获联”改为“获取”
* Update 12-Collections.md
---
docs/book/11-Inner-Classes.md | 2 +-
docs/book/12-Collections.md | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/docs/book/11-Inner-Classes.md b/docs/book/11-Inner-Classes.md
index f7862a5c..ca4abfc2 100644
--- a/docs/book/11-Inner-Classes.md
+++ b/docs/book/11-Inner-Classes.md
@@ -957,7 +957,7 @@ public abstract class Event {
`ready()` 告诉你何时可以运行 `action()` 方法了。当然,可以在派生类中覆盖 `ready()` 方法,使得 **Event** 能够基于时间以外的其他因素而触发。
-下面的文件包含了一个用来管理并触发事件的实际控制框架。**Event** 对象被保存在 **List**\<**Event**\> 类型(读作“Event 的列表”)的容器对象中,容器会在 [集合 ]() 中详细介绍。目前读者只需要知道 `add()` 方法用来将一个 **Event** 添加到 **List** 的尾端,`size()` 方法用来得到 **List** 中元素的个数,foreach 语法用来连续获联 **List** 中的 **Event**,`remove()` 方法用来从 **List** 中移除指定的 **Event**。
+下面的文件包含了一个用来管理并触发事件的实际控制框架。**Event** 对象被保存在 **List**\<**Event**\> 类型(读作“Event 的列表”)的容器对象中,容器会在 [集合 ]() 中详细介绍。目前读者只需要知道 `add()` 方法用来将一个 **Event** 添加到 **List** 的尾端,`size()` 方法用来得到 **List** 中元素的个数,foreach 语法用来连续获取 **List** 中的 **Event**,`remove()` 方法用来从 **List** 中移除指定的 **Event**。
```java
// innerclasses/controller/Controller.java
diff --git a/docs/book/12-Collections.md b/docs/book/12-Collections.md
index bc0569b8..8c9c88a5 100644
--- a/docs/book/12-Collections.md
+++ b/docs/book/12-Collections.md
@@ -1276,7 +1276,7 @@ C B A A
**Collection** 是所有序列集合共有的根接口。它可能会被认为是一种“附属接口”(incidental interface),即因为要表示其他若干个接口的共性而出现的接口。此外,**java.util.AbstractCollection** 类提供了 **Collection** 的默认实现,使得你可以创建 **AbstractCollection** 的子类型,而其中没有不必要的代码重复。
-使用接口描述的一个理由是它可以使我们创建更通用的代码。通过针对接口而非具体实现来编写代码,我们的代码可以应用于更多类型的对象。[^6]因此,如果所编写的方法接受一个 **Collection** ,那么该方法可以应用于任何实现了 **Collection** 的类——这也就使得一个新类可以选择去实现 **Collection** 接口,以便该方法可以使用它。标准 C++ 类库中的的集合并没有共同的基类——集合之间的所有共性都是通过迭代器实现的。在 Java 中,遵循 C++ 的方式看起来似乎很明智,即用迭代器而不是 **Collection** 来表示集合之间的共性。但是,这两种方法绑定在了一起,因为实现 **Collection** 就意味着需要提供 `iterator()` 方法:
+使用接口描述的一个理由是它可以使我们创建更通用的代码。通过针对接口而非具体实现来编写代码,我们的代码可以应用于更多类型的对象。[^6]因此,如果所编写的方法接受一个 **Collection** ,那么该方法可以应用于任何实现了 **Collection** 的类——这也就使得一个新类可以选择去实现 **Collection** 接口,以便该方法可以使用它。标准 C++ 类库中的集合并没有共同的基类——集合之间的所有共性都是通过迭代器实现的。在 Java 中,遵循 C++ 的方式看起来似乎很明智,即用迭代器而不是 **Collection** 来表示集合之间的共性。但是,这两种方法绑定在了一起,因为实现 **Collection** 就意味着需要提供 `iterator()` 方法:
```java
// collections/InterfaceVsIterator.java
From 19760c32c1568ec88b9b17c18be94d91cf171e08 Mon Sep 17 00:00:00 2001
From: FengBaoheng <344092466@qq.com>
Date: Mon, 13 Apr 2020 10:18:58 +0800
Subject: [PATCH 068/224] fix typos on 20-Generics (#430)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
* fix typo 本章小节->本章小结
* fix typos on 20-Generics
修正20章的打字错误
---
docs/book/20-Generics.md | 12 +++++++-----
1 file changed, 7 insertions(+), 5 deletions(-)
diff --git a/docs/book/20-Generics.md b/docs/book/20-Generics.md
index 4a0a0add..e207114d 100644
--- a/docs/book/20-Generics.md
+++ b/docs/book/20-Generics.md
@@ -1292,6 +1292,7 @@ public class LostInformation {
[K,V]
[Q]
[POSITION,MOMENTUM]
+*/
```
根据 JDK 文档,**Class.getTypeParameters()** “返回一个 **TypeVariable** 对象数组,表示泛型声明中声明的类型参数...” 这暗示你可以发现这些参数类型。但是正如上例中输出所示,你只能看到用作参数占位符的标识符,这并非有用的信息。
@@ -1616,6 +1617,7 @@ public class FilledList extends ArrayList {
/* Output:
[Hello,Hello,Hello,Hello]
[47,47,47,47]
+*/
```
即使编译器无法得知 `add()` 中的 **T** 的任何信息,但它仍可以在编译期确保你放入 **FilledList** 中的对象是 **T** 类型。因此,即使擦除移除了方法或类中的实际类型的信息,编译器仍可以确保方法或类中使用的类型的内部一致性。
@@ -2636,7 +2638,7 @@ public class NonCovariantGenerics {
}
```
-尽管你在首次阅读这段代码时会认为“不能将一个 **Apple** 集合赋值给一个 **Fruit** 集合”。记住,泛型不仅仅是关于集合,它真正要表达的是“不能把一个涉及 **Apple** 的泛型赋值给一个涉及 **Fruit** 的泛型”。如果像在数组中的情况一样,编译器对代码的了解足够多,可以确定所涉及到的集合,那么它可能会留下一些余地。但是它不知道任何有关这方面的信息,因此它拒绝向上转型。然而实际上这也不是向上转型—— **Apple** 的 **List** 不是 **Fruit** 的 **List**。**Apple** 的 **List** 将持有 **Apple** 和 **Apple** 的子类型,**Fruit** 的 **List** 将持有任何类型的 **Fruit**。是的,这包括 **Apple**,但是它不是一个 **Apple** 的 **List**,它仍然是 **Fruit** 的 **List**。**Apple** 的 **List** 在类型上不等价于 **Fruit** 的 **List**,即使 **Apple** 是一种 **Fruit** 类型。
+尽管你在首次阅读这段代码时会认为“不能将一个 **Apple** 集合赋值给一个 **Fruit** 集合”。记住,泛型不仅仅是关于集合,它真正要表达的是“不能把一个涉及 **Apple** 的泛型赋值给一个涉及 **Fruit** 的泛型”。如果像在数组中的情况一样,编译器对代码的了解足够多,可以确定所涉及到的集合,那么它可能会留下一些余地。但是它不知道任何有关这方面的信息,因此它拒绝向上转型。然而实际上这也不是向上转型—— **Apple** 的 **List** 不是 **Fruit** 的 **List**。**Apple** 的 **List** 将持有 **Apple** 和 **Apple** 的子类型,**Fruit** 的 **List** 将持有任何类型的 **Fruit**。是的,这包括 **Apple**,但是它不是一个 **Apple** 的 **List**,它仍然是 **Fruit** 的 **List**。**Apple** 的 **List** 在类型上不等价于 **Fruit** 的 **List**,即使 **Apple** 是一种 **Fruit** 类型。
真正的问题是我们在讨论的集合类型,而不是集合持有对象的类型。与数组不同,泛型没有内建的协变类型。这是因为数组是完全在语言中定义的,因此可以具有编译期和运行时的内建检查,但是在使用泛型时,编译器和运行时系统不知道你想用类型做什么,以及应该采用什么规则。
@@ -2736,7 +2738,7 @@ public class Holder {
Holder apple = new Holder<>(new Apple());
Apple d = apple.get();
apple.set(d);
-// Holder fruit = apple; // Cannot upcast
+ // Holder fruit = apple; // Cannot upcast
Holder extends Fruit> fruit = apple; // OK
Fruit p = fruit.get();
d = (Apple) fruit.get();
@@ -2745,8 +2747,8 @@ public class Holder {
} catch (Exception e) {
System.out.println(e);
}
-// fruit.set(new Apple()); // Cannot call set()
-// fruit.set(new Fruit()); // Cannot call set()
+ // fruit.set(new Apple()); // Cannot call set()
+ // fruit.set(new Fruit()); // Cannot call set()
System.out.println(fruit.equals(d)); // OK
}
}
@@ -3325,7 +3327,7 @@ Double
```
`f1()` 中的类型参数都是确切的,没有通配符或边界。在 `f2()` 中,**Holder** 参数是一个无界通配符,因此它看起来是未知的。但是,在 `f2()` 中调用了 `f1()`,而 `f1()` 需要一个已知参数。这里所发生的是:在调用 `f2()` 的过程中捕获了参数类型,并在调用 `f1()` 时使用了这种类型。
-你可能想知道这项技术是否可以用于写入,但是这要求在传递 `Holder>` 时同时传递一个具体类型。捕获转换只有在这样的情况下可以工作:即在方法内部,你需要使用确切的类型。注意,不能从 `f2()` 中返回 **T**,因为 **T ** 对于 `f2()` 来说是未知的。捕获转换十分有趣,但是非常受限。
+你可能想知道这项技术是否可以用于写入,但是这要求在传递 `Holder>` 时同时传递一个具体类型。捕获转换只有在这样的情况下可以工作:即在方法内部,你需要使用确切的类型。注意,不能从 `f2()` 中返回 **T**,因为 **T** 对于 `f2()` 来说是未知的。捕获转换十分有趣,但是非常受限。
From 59ec312823618a2c2662c8c69870405c88a53b5c Mon Sep 17 00:00:00 2001
From: FengBaoheng <344092466@qq.com>
Date: Tue, 14 Apr 2020 11:50:31 +0800
Subject: [PATCH 069/224] add examples link (#432)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
* fix typo 本章小节->本章小结
* fix typos on 20-Generics
修正20章的打字错误
* 添加示例代码链接
---
README.md | 7 +++++++
1 file changed, 7 insertions(+)
diff --git a/README.md b/README.md
index 7d5c6774..187c1a55 100644
--- a/README.md
+++ b/README.md
@@ -88,6 +88,13 @@
* 页数:2038
* 发行:仅电子版
+
+## 示例代码
+
+* [gradle: OnJava8-Examples](https://github.com/BruceEckel/OnJava8-Examples)
+* [maven: OnJava8-Examples-Maven](https://github.com/sjsdfg/OnJava8-Examples-Maven)
+
+
## 贡献者
* 主译:[LingCoder](https://github.com/LingCoder),[sjsdfg](https://github.com/sjsdfg),[xiangflight](https://github.com/xiangflight)
From 474fd6093c0bc5fcdcb82d3beef8b7d036ff1b44 Mon Sep 17 00:00:00 2001
From: springga
Date: Thu, 16 Apr 2020 23:53:34 +0800
Subject: [PATCH 070/224] Update 01-What-is-an-Object.md
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
logical assumption应为“合理假设”而不是“逻辑假设”
tend to be应为“通常”“往往”而不是“趋向于”
直译改为意译
---
docs/book/01-What-is-an-Object.md | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/docs/book/01-What-is-an-Object.md b/docs/book/01-What-is-an-Object.md
index 5b0d6ca5..3c9ba058 100644
--- a/docs/book/01-What-is-an-Object.md
+++ b/docs/book/01-What-is-an-Object.md
@@ -266,7 +266,7 @@ Java 的单继承结构有很多好处。由于所有对象都具有一个公共
第二种方法是在堆内存(Heap)中动态地创建对象。在这种方式下,直到程序运行我们才能确定需要创建的对象数量、生存时间和类型。什么时候需要,什么时候在堆内存中创建。 因为内存的占用是动态管理的,所以在运行时,在堆内存上开辟空间所需的时间可能比在栈内存上要长(但也不一定)。在栈内存开辟和释放空间通常是一条将栈指针向下移动和一条将栈指针向上移动的汇编指令。开辟堆内存空间的时间取决于内存机制的设计。
-动态方法有这样一个一般性的逻辑假设:对象趋向于变得复杂,因此额外的内存查找和释放对对象的创建影响不大。(原文:*The dynamic approach makes the generally logical assumption that objects tend to be complicated, so the extra overhead of finding storage and releasing that storage will not have an important impact on the creation of an object.*)此外,更好的灵活性对于问题的解决至关重要。
+动态方法有这样一个合理假设:对象通常是复杂的,相比于对象创建的整体开销,寻找和释放内存空间的开销微不足道。(原文:*The dynamic approach makes the generally logical assumption that objects tend to be complicated, so the extra overhead of finding storage and releasing that storage will not have an important impact on the creation of an object.*)此外,更好的灵活性对于问题的解决至关重要。
Java 使用动态内存分配。每次创建对象时,使用 `new` 关键字构建该对象的动态实例。这又带来另一个问题:对象的生命周期。较之堆内存,在栈内存中创建对象,编译器能够确定该对象的生命周期并自动销毁它;然而如果你在堆内存创建对象的话,编译器是不知道它的生命周期的。在 C++ 中你必须以编程方式确定何时销毁对象,否则可能导致内存泄漏。Java 的内存管理是建立在垃圾收集器上的,它能自动发现对象不再被使用并释放内存。垃圾收集器的存在带来了极大的便利,它减少了我们之前必须要跟踪的问题和编写相关代码的数量。因此,垃圾收集器提供了更高级别的保险,以防止潜在的内存泄漏问题,这个问题使得许多 C++ 项目没落。
@@ -291,4 +291,4 @@ Java 的异常处理机制在编程语言中脱颖而出。Java 从一开始就
OOP 和 Java 不一定适合每个人。评估自己的需求以及与现有方案作比较是很重要的。请充分考虑后再决定是不是选择 Java。如果在可预见的未来,Java 并不能很好的满足你的特定需求,那么你应该去寻找其他替代方案(特别是,我推荐看 Python)。如果你依然选择 Java 作为你的开发语言,我希望你至少应该清楚你选择的是什么,以及为什么选择这个方向。
-
\ No newline at end of file
+
From ce26e6efaf2545467a481fb6c24dc5624eb63350 Mon Sep 17 00:00:00 2001
From: iwangbingzhi
Date: Sun, 19 Apr 2020 16:07:44 +0800
Subject: [PATCH 071/224] update (#435)
---
docs/book/Appendix-New-IO.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/docs/book/Appendix-New-IO.md b/docs/book/Appendix-New-IO.md
index 04688e68..94e47660 100644
--- a/docs/book/Appendix-New-IO.md
+++ b/docs/book/Appendix-New-IO.md
@@ -133,7 +133,7 @@ public class ChannelCopy {
```
-**FileChannel** 用于读取;**FileChannel** 用于写入。当 **ByteBuffer** 分配好存储,调用 **FileChannel** 的 `read()` 方法返回 **-1**(毫无疑问,这是来源于 Unix 和 C 语言)时,说明输入流读取完了。在每次 `read()` 将数据放入缓冲区之后,`flip()` 都会准备好缓冲区,以便 `write()` 提取它的信息。在 `write()` 之后,数据仍然在缓冲区中,我们需要 `clear()` 来重置所有内部指针,以便在下一次 `read()` 中接受数据。
+**第一个FileChannel** 用于读取;**第二个FileChannel** 用于写入。当 **ByteBuffer** 分配好存储,调用 **FileChannel** 的 `read()` 方法返回 **-1**(毫无疑问,这是来源于 Unix 和 C 语言)时,说明输入流读取完了。在每次 `read()` 将数据放入缓冲区之后,`flip()` 都会准备好缓冲区,以便 `write()` 提取它的信息。在 `write()` 之后,数据仍然在缓冲区中,我们需要 `clear()` 来重置所有内部指针,以便在下一次 `read()` 中接受数据。
但是,上例并不是处理这种操作的理想方法。方法 `transferTo()` 和 `transferFrom()` 允许你直接连接此通道到彼通道:
From 86ebfd142abcac15068c8db1144faf3e82ce4b13 Mon Sep 17 00:00:00 2001
From: unclesesame
Date: Mon, 20 Apr 2020 12:47:29 +0800
Subject: [PATCH 072/224] =?UTF-8?q?=E7=AC=AC15=E7=AB=A0=20=E5=BC=82?=
=?UTF-8?q?=E5=B8=B8:=20=E9=94=99=E5=88=AB=E5=AD=97=E6=9B=B4=E6=94=B9=20(#?=
=?UTF-8?q?436)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
update typos
---
docs/book/15-Exceptions.md | 12 ++++++------
1 file changed, 6 insertions(+), 6 deletions(-)
diff --git a/docs/book/15-Exceptions.md b/docs/book/15-Exceptions.md
index 5ee290bb..f114f4e8 100644
--- a/docs/book/15-Exceptions.md
+++ b/docs/book/15-Exceptions.md
@@ -914,7 +914,7 @@ DynamicFields.setField(DynamicFields.java:67)
至于返回值,setField() 将用 getField() 方法把此位置的旧值取出,这个操作可能会抛出 NoSuchFieldException 异常。如果客户端程序员调用了 getField() 方法,那么他就有责任处理这个可能抛出的 NoSuchFieldException 异常,但如果异常是从 setField0 方法里抛出的,这种情况将被视为编程错误,所以就使用接受 cause 参数的构造器把 NoSuchFieldException 异常转换为 RuntimeException 异常。
-你会注意到,toString0 方法使用了一个 StringBuilder 来创建其结果。在 [字符串](./Strings.md) 这章中你将会了解到更多的关于 StringBuilder 的知识,但是只要你编写设计循环的 toString() 方法,通常都会想使用它,就像本例一样。
+你会注意到,toString() 方法使用了一个 StringBuilder 来创建其结果。在 [字符串](./Strings.md) 这章中你将会了解到更多的关于 StringBuilder 的知识,但是只要你编写设计循环的 toString() 方法,通常都会想使用它,就像本例一样。
主方法中的 catch 子句看起来不同 - 它使用相同的子句处理两种不同类型的异常,并结合“或(|)”符号。此 Java 7 功能有助于减少代码重复,并使你更容易指定要捕获的确切类型,而不是简单地捕获基本类型。你可以通过这种方式组合多种异常类型。
@@ -1100,7 +1100,7 @@ on
off
```
-程序的目的是要确保 main() 结束的时候开关必须是关闭的,所以在每个 try 块和异常处理程序的末尾都加入了对 sw.offo 方法的调用。但也可能有这种情况:异常被抛出,但没被处理程序捕获,这时 sw.off() 就得不到调用。但是有了 finally,只要把 try 块中的清理代码移放在一处即可:
+程序的目的是要确保 main() 结束的时候开关必须是关闭的,所以在每个 try 块和异常处理程序的末尾都加入了对 sw.off() 方法的调用。但也可能有这种情况:异常被抛出,但没被处理程序捕获,这时 sw.off() 就得不到调用。但是有了 finally,只要把 try 块中的清理代码移放在一处即可:
```java
// exceptions/WithFinally.java
@@ -1272,7 +1272,7 @@ public class LostMessage {
A trivial exception
```
-从输出中可以看到,VeryImportantException 不见了,它被 finally 子句里的 HoHumException 所取代。这是相当严重的缺陷,因为异常可能会以一种比前面例子所示更微妙和难以察党的方式完全丢失。相比之下,C++把“前一个异常还没处理就抛出下一个异常”的情形看成是糟糕的编程错误。也许在 Java 的未来版本中会修正这个问题(另一方面,要把所有抛出异常的方法,如上例中的 dispose() 方法,全部打包放到 try-catch 子句里面)。
+从输出中可以看到,VeryImportantException 不见了,它被 finally 子句里的 HoHumException 所取代。这是相当严重的缺陷,因为异常可能会以一种比前面例子所示更微妙和难以察觉的方式完全丢失。相比之下,C++把“前一个异常还没处理就抛出下一个异常”的情形看成是糟糕的编程错误。也许在 Java 的未来版本中会修正这个问题(另一方面,要把所有抛出异常的方法,如上例中的 dispose() 方法,全部打包放到 try-catch 子句里面)。
一种更加简单的丢失异常的方式是从 finally 子句中返回:
@@ -1953,7 +1953,7 @@ try {
## 其他可选方式
异常处理系统就像一个活门(trap door),使你能放弃程序的正常执行序列。当“异常情形”
-发生的时候,正常的执行已变得不可能或者不需要了,这时就要用到这个“活门"。异常代表了当前方法不能继续执行的情形。开发异常处理系统的原因是,如果为每个方法所有可能发生的错误都进行处理的话,任务就显得过于繁重了,程序员也不愿意这么做。结果常常是将错误忽格。应该注意到,开发异常处理的初衷是为了方便程序员处理错误。
+发生的时候,正常的执行已变得不可能或者不需要了,这时就要用到这个“活门"。异常代表了当前方法不能继续执行的情形。开发异常处理系统的原因是,如果为每个方法所有可能发生的错误都进行处理的话,任务就显得过于繁重了,程序员也不愿意这么做。结果常常是将错误忽略。应该注意到,开发异常处理的初衷是为了方便程序员处理错误。
异常处理的一个重要原则是“只有在你知道如何处理的情况下才捕获异常"。实际上,异常处理的一个重要目标就是把错误处理的代码同错误发生的地点相分离。这使你能在一段代码中专注于要完成的事情,至于如何处理错误,则放在另一段代码中完成。这样一来,主要代码就不会与错误处理逻辑混在一起,也更容易理解和维护。通过允许一个处理程序去处理多个出错点,异常处理还使得错误处理代码的数量趋于减少。
@@ -2056,7 +2056,7 @@ public class MainException {
在编写你自己使用的简单程序时,从主方法中抛出异常是很方便的,但这不是通用的方法。
-问题的实质是,当在一个普通方法里调用别的方法时,要考虑到“我不知道该这样处理这个异常,但是也不想把它‘吞’了,或若打印一些无用的消息”。异常链提供了一种新的思路来解决这个问题。可以直接把“被检查的异常”包装进 RuntimeException 里面,就像这样:
+问题的实质是,当在一个普通方法里调用别的方法时,要考虑到“我不知道该这样处理这个异常,但是也不想把它‘吞’了,或者打印一些无用的消息”。异常链提供了一种新的思路来解决这个问题。可以直接把“被检查的异常”包装进 RuntimeException 里面,就像这样:
```java
try {
@@ -2163,7 +2163,7 @@ WrapCheckedException.throwRuntimeException() 的代码可以生成不同类型
异常是 Java 程序设计不可分割的一部分,如果不了解如何使用它们,那你只能完成很有限的工作。正因为如此,本书专门在此介绍了异常——对于许多类库(例如提到过的 I/O 库),如果不处理异常,你就无法使用它们。
-异常处理的优点之一就是它使得你可以在某处集中精力处理你要解决的问题,而在另一处处理你编写的这段代码中产生的错误。尽管异常通常被认为是一种工具,使得你可以在运行时报告错误并从错误中恢复,但是我一直怀疑到底有多少时候“恢复”真正得以实现了,或者能够得以实现。我认为这种情况少于 10%,并且即便是这 10%,也只是将栈展开到某个已知的稳定状态,而并没有实际执行任何种类的恢复性行为。无论这是否正确,我一直相信“报告”功能是异常的精髓所在. Java 坚定地强调将所有的错误都以异常形式报告的这一事实,正是它远远超过语如 C++ 这类语言的长处之一,因为在 C++ 这类语言中,需要以大量不同的方式来报告错误,或者根本就没有提供错误报告功能。一致的错误报告系统意味着,你再也不必对所写的每一段代码,都质问自己“错误是否正在成为漏网之鱼?”(只要你没有“吞咽”异常,这是关键所在!)。
+异常处理的优点之一就是它使得你可以在某处集中精力处理你要解决的问题,而在另一处处理你编写的这段代码中产生的错误。尽管异常通常被认为是一种工具,使得你可以在运行时报告错误并从错误中恢复,但是我一直怀疑到底有多少时候“恢复”真正得以实现了,或者能够得以实现。我认为这种情况少于 10%,并且即便是这 10%,也只是将栈展开到某个已知的稳定状态,而并没有实际执行任何种类的恢复性行为。无论这是否正确,我一直相信“报告”功能是异常的精髓所在. Java 坚定地强调将所有的错误都以异常形式报告的这一事实,正是它远远超过如 C++ 这类语言的长处之一,因为在 C++ 这类语言中,需要以大量不同的方式来报告错误,或者根本就没有提供错误报告功能。一致的错误报告系统意味着,你再也不必对所写的每一段代码,都质问自己“错误是否正在成为漏网之鱼?”(只要你没有“吞咽”异常,这是关键所在!)。
就像你将要在后续章节中看到的,通过将这个问题甩给其他代码-即使你是通过抛出 RuntimeException 来实现这一点的--你在设计和实现时,便可以专注于更加有趣和富有挑战性的问题了。
From 28473b2ecd8b8aeaa7a5d83052642ba5727728ca Mon Sep 17 00:00:00 2001
From: Ryan Lee
Date: Tue, 21 Apr 2020 13:23:23 +0800
Subject: [PATCH 073/224] =?UTF-8?q?fix=20=E7=AC=AC=E4=B8=80=E7=AB=A0?=
=?UTF-8?q?=E6=8B=AC=E5=8F=B7=E4=BD=BF=E7=94=A8=E7=9A=84=E9=94=99=E8=AF=AF?=
=?UTF-8?q?=20(#437)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
docs/book/01-What-is-an-Object.md | 8 ++++----
1 file changed, 4 insertions(+), 4 deletions(-)
diff --git a/docs/book/01-What-is-an-Object.md b/docs/book/01-What-is-an-Object.md
index 3c9ba058..0f3fe968 100644
--- a/docs/book/01-What-is-an-Object.md
+++ b/docs/book/01-What-is-an-Object.md
@@ -74,7 +74,7 @@ lt.on();
2. 使类库的创建者(研发程序员)在不影响后者使用的情况下完善更新工具库。例如,我们开发了一个功能简单的工具类,后来发现可以通过优化代码来提高执行速度。假如工具类的接口和实现部分明确分开并受到保护,那我们就可以轻松地完成改造。
-Java 有三个显式关键字来设置类中的访问权限:`public`(公开),`private`(私有)和`protected`(受保护)。这些访问修饰符决定了谁能使用它们修饰的方法、变量或类。
+Java 有三个显式关键字来设置类中的访问权限:`public`(公开),`private`(私有)和`protected`(受保护)。这些访问修饰符决定了谁能使用它们修饰的方法、变量或类。
1. `public`(公开)表示任何人都可以访问和使用该元素;
@@ -116,7 +116,7 @@ Java 有三个显式关键字来设置类中的访问权限:`public`(公开

-例如,垃圾回收机对垃圾进行分类。基类是“垃圾”。每块垃圾都有重量、价值等特性,它们可以被切碎、熔化或分解。在此基础上,可以通过添加额外的特性(瓶子有颜色,钢罐有磁性)或行为(铝罐可以被压碎)派生出更具体的垃圾类型。此外,一些行为可以不同(纸张的价值取决于它的类型和状态)。使用继承,你将构建一个类型层次结构,来表示你试图解决的某种类型的问题。第二个例子是常见的“形状”例子,可能用于计算机辅助设计系统或游戏模拟。基类是“形状”,每个形状都有大小、颜色、位置等等。每个形状可以绘制、擦除、移动、着色等。由此,可以派生出(继承出)具体类型的形状——圆形、正方形、三角形等等——每个形状可以具有附加的特征和行为。
+例如,垃圾回收机对垃圾进行分类。基类是“垃圾”。每块垃圾都有重量、价值等特性,它们可以被切碎、熔化或分解。在此基础上,可以通过添加额外的特性(瓶子有颜色,钢罐有磁性)或行为(铝罐可以被压碎)派生出更具体的垃圾类型。此外,一些行为可以不同(纸张的价值取决于它的类型和状态)。使用继承,你将构建一个类型层次结构,来表示你试图解决的某种类型的问题。第二个例子是常见的“形状”例子,可能用于计算机辅助设计系统或游戏模拟。基类是“形状”,每个形状都有大小、颜色、位置等等。每个形状可以绘制、擦除、移动、着色等。由此,可以派生出(继承出)具体类型的形状——圆形、正方形、三角形等等——每个形状可以具有附加的特征和行为。

@@ -232,7 +232,7 @@ Java 的单继承结构有很多好处。由于所有对象都具有一个公共
还好,一般优秀的 OOP 语言都会将“集合”作为其基础包。在 C++ 中,“集合”是其标准库的一部分,通常被称为 STL(Standard Template Library,标准模板库)。SmallTalk 有一套非常完整的集合库。同样,Java 的标准库中也提供许多现成的集合类。
-在一些库中,一两个泛型集合就能满足我们所有的需求了,而在其他一些类库(Java)中,不同类型的集合对应不同的需求:常见的有 List,常用于保存序列;Map,也称为关联数组,常用于将对象与其他对象关联);Set,只能保存非重复的值;其他还包括如队列(Queue)、树(Tree)、栈(Stack)、堆(Heap)等等。从设计的角度来看,我们真正想要的是一个能够解决某个问题的集合。如果一种集合就满足所有需求,那么我们就不需要剩下的了。之所以选择集合有以下两个原因:
+在一些库中,一两个泛型集合就能满足我们所有的需求了,而在其他一些类库(Java)中,不同类型的集合对应不同的需求:常见的有 List,常用于保存序列;Map,也称为关联数组,常用于将对象与其他对象关联;Set,只能保存非重复的值;其他还包括如队列(Queue)、树(Tree)、栈(Stack)、堆(Heap)等等。从设计的角度来看,我们真正想要的是一个能够解决某个问题的集合。如果一种集合就满足所有需求,那么我们就不需要剩下的了。之所以选择集合有以下两个原因:
1. 集合可以提供不同类型的接口和外部行为。堆栈、队列的应用场景和集合、列表不同,它们中的一种提供的解决方案可能比其他灵活得多。
@@ -256,7 +256,7 @@ Java 的单继承结构有很多好处。由于所有对象都具有一个公共
在简单的编程场景下,对象的清理并不是问题。我们创建对象,按需使用,最后销毁它。然而,情况往往要比这更复杂:
-假设,我们正在为机场设计一个空中交通管制的系统(该例也适用于仓库货柜管理、影带出租或者宠物寄养仓库系统)。第一步比较简单:创建一个用来保存飞机的集合,每当有飞机进入交通管制区域时,我们就创建一个“飞机”对象并将其加入到集合中,等到飞机离开时将其从这个集合中清除。与此同时,我们还需要一个记录飞机信息的系统,也许这些数据不像主要控制功能那样引人注意。比如,我们要记录所有飞机中的小型飞机的的信息(比如飞行计划)。此时,我们又创建了第二个集合来记录所有小型飞机。 每当创建一个“飞机”对象的时候,将其放入第一个集合;若它属于小型飞机,也必须同时将其放入第二个集合里。
+假设,我们正在为机场设计一个空中交通管制的系统(该例也适用于仓库货柜管理、影带出租或者宠物寄养仓库系统)。第一步比较简单:创建一个用来保存飞机的集合,每当有飞机进入交通管制区域时,我们就创建一个“飞机”对象并将其加入到集合中,等到飞机离开时将其从这个集合中清除。与此同时,我们还需要一个记录飞机信息的系统,也许这些数据不像主要控制功能那样引人注意。比如,我们要记录所有飞机中的小型飞机的的信息(比如飞行计划)。此时,我们又创建了第二个集合来记录所有小型飞机。 每当创建一个“飞机”对象的时候,将其放入第一个集合;若它属于小型飞机,也必须同时将其放入第二个集合里。
现在问题开始棘手了:我们怎么知道何时该清理这些对象呢?当某一个系统处理完成,而其他系统可能还没有处理完成。这样的问题在其他的场景下也可能发生。在 C++ 程序设计中,当使用完一个对象后,必须明确将其删除,这就让问题变复杂了。
From 4bff99a74dee3c4d1dfb19d937de85f631b41b59 Mon Sep 17 00:00:00 2001
From: taolei <809210721@qq.com>
Date: Wed, 22 Apr 2020 14:08:02 +0800
Subject: [PATCH 074/224] =?UTF-8?q?=E4=BF=AE=E6=AD=A3=E9=94=99=E8=AF=AF?=
=?UTF-8?q?=E7=AB=A0=E8=8A=82=E6=A0=87=E9=A2=98=20(#440)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
修正第五章控制流 第一子节 'true和false'标题中错误拼写
close #439
---
SUMMARY.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/SUMMARY.md b/SUMMARY.md
index eea7c550..a15b48fc 100644
--- a/SUMMARY.md
+++ b/SUMMARY.md
@@ -72,7 +72,7 @@
* [运算符总结](docs/book/04-Operators.md#运算符总结)
* [本章小结](docs/book/04-Operators.md#本章小结)
* [第五章 控制流](docs/book/05-Control-Flow.md)
- * [true和flase](docs/book/05-Control-Flow.md#true和flase)
+ * [true和false](docs/book/05-Control-Flow.md#true和false)
* [if-else](docs/book/05-Control-Flow.md#if-else)
* [迭代语句](docs/book/05-Control-Flow.md#迭代语句)
* [for-in语法](docs/book/05-Control-Flow.md#for-in语法)
From 94f4e80b211c6b455fc0a9924abb937fb0e100d3 Mon Sep 17 00:00:00 2001
From: JasonFCN <34146521+JasonFCN@users.noreply.github.com>
Date: Thu, 23 Apr 2020 13:24:20 +0800
Subject: [PATCH 075/224] Update 22-Enumerations.md (#442)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
* Update 22-Enumerations.md
906行:“存人” 改为 “存入”。
* Update 22-Enumerations.md
mian0 --> main()
---
docs/book/22-Enumerations.md | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/docs/book/22-Enumerations.md b/docs/book/22-Enumerations.md
index 0259ec56..d4076f05 100644
--- a/docs/book/22-Enumerations.md
+++ b/docs/book/22-Enumerations.md
@@ -903,7 +903,7 @@ Expected: java.lang.NullPointerException
与 EnumSet 一样,enum 实例定义时的次序决定了其在 EnumMap 中的顺序。
-main0 方法的最后部分说明,enum 的每个实例作为一个键,总是存在的。但是,如果你没有为这个键调用 put() 方法来存人相应的值的话,其对应的值就是 null。
+main() 方法的最后部分说明,enum 的每个实例作为一个键,总是存在的。但是,如果你没有为这个键调用 put() 方法来存入相应的值的话,其对应的值就是 null。
与常量相关的方法(constant-specific methods 将在下一节中介绍)相比,EnumMap 有一个优点,那 EnumMap 允许程序员改变值对象,而常量相关的方法在编译期就被固定了。稍后你会看到,在你有多种类型的 enum,而且它们之间存在互操作的情况下,我们可以用 EnumMap 实现多路分发(multiple dispatching)。
@@ -2108,4 +2108,4 @@ table 与前一个例子中 initRow() 方法的调用次序完全相同。
-
\ No newline at end of file
+
From 5d7462a629a410cb29f98938387daa0d846d8e4b Mon Sep 17 00:00:00 2001
From: unclesesame
Date: Sun, 26 Apr 2020 17:37:46 +0800
Subject: [PATCH 076/224] =?UTF-8?q?16-19=E7=AB=A0=20=E7=BA=A0=E9=94=99=20(?=
=?UTF-8?q?#443)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
* 第15章 异常: 错别字更改
update typos
* 第19章 类型信息 纠错
第19章 类型信息 纠错
* 第17章 文件 纠错
第17章 文件 纠错
* 第16章 代码校验 纠错
第16章 代码校验 纠错
---
docs/book/16-Validating-Your-Code.md | 6 +++---
docs/book/17-Files.md | 10 +++++-----
docs/book/19-Type-Information.md | 10 +++++-----
3 files changed, 13 insertions(+), 13 deletions(-)
diff --git a/docs/book/16-Validating-Your-Code.md b/docs/book/16-Validating-Your-Code.md
index 46306059..527eef2e 100644
--- a/docs/book/16-Validating-Your-Code.md
+++ b/docs/book/16-Validating-Your-Code.md
@@ -180,7 +180,7 @@ Cleaning up 4
**gradlew test**
-尽管可以用最简单的方法,如 **CountedListTest.java** 所示没那样,JUnit 还包括大量的测试结构,你可以到[官网](junit.org)上学习它们。
+尽管可以用最简单的方法,如 **CountedListTest.java** 所示那样,JUnit 还包括大量的测试结构,你可以到[官网](junit.org)上学习它们。
JUnit 是 Java 最流行的单元测试框架,但也有其它可以替代的。你可以通过互联网发现更适合的那一个。
@@ -1240,7 +1240,7 @@ public class SLF4JLogging {
**Aug 16, 2016 5:40:31 PM InfoLogging main**
**INFO: hello logging**
-日志系统会检测日志消息处所在的的类名和方法名。 但它不能保证这些名称是正确的,所以不要纠结于其准确性。
+日志系统会检测日志消息处所在的类名和方法名。 但它不能保证这些名称是正确的,所以不要纠结于其准确性。
### 日志等级
@@ -1654,7 +1654,7 @@ N:数组的大小:**10^(2*k)**,通常来说,**k=1..7** 足够来练习
Q:setter 的操作成本
-这个 C/P/N/Q 模型在早期 JDK 8 的 Lambda 开发期间付出水面,大多数并行的 Stream 操作(**parallelSetAll()** 也基本相似)都满足这些结论:**N*Q**(主要工作量)对于并发性能尤为重要。并行算法在工作量较少时可能实际运行得更慢。
+这个 C/P/N/Q 模型在早期 JDK 8 的 Lambda 开发期间浮出水面,大多数并行的 Stream 操作(**parallelSetAll()** 也基本相似)都满足这些结论:**N*Q**(主要工作量)对于并发性能尤为重要。并行算法在工作量较少时可能实际运行得更慢。
在一些情况下操作竞争如此激烈使得并行毫无帮助,而不管 **N*Q** 有多大。当 **C** 很大时,**P** 就变得不太相关(内部并行在大量的外部并行面前显得多余)。此外,在一些情况下,并行分解会让相同的 **C** 个客户端运行得比它们顺序运行代码更慢。
diff --git a/docs/book/17-Files.md b/docs/book/17-Files.md
index c92cb291..7a22b051 100644
--- a/docs/book/17-Files.md
+++ b/docs/book/17-Files.md
@@ -5,7 +5,7 @@
# 第十七章 文件
>在丑陋的 Java I/O 编程方式诞生多年以后,Java终于简化了文件读写的基本操作。
-这种"困难方式"的全部细节都在 [Appendix: I/O Streams](./Appendix-IO-Streams.md)。如果你读过这个部分,就会认同 Java 设计者毫不在意他们的使用者的体验这一观念。打开并读取文件对于大多数编程语言来是非常常用的,由于 I/O 糟糕的设计以至于
+这种"困难方式"的全部细节都在 [Appendix: I/O Streams](./Appendix-IO-Streams.md)。如果你读过这个部分,就会认同 Java 设计者毫不在意他们的使用者的体验这一观念。打开并读取文件对于大多数编程语言来说是非常常用的,由于 I/O 糟糕的设计以至于
很少有人能够在不依赖其他参考代码的情况下完成打开文件的操作。
好像 Java 设计者终于意识到了 Java 使用者多年来的痛苦,在 Java7 中对此引入了巨大的改进。这些新元素被放在 **java.nio.file** 包下面,过去人们通常把 **nio** 中的 **n** 理解为 **new** 即新的 **io**,现在更应该当成是 **non-blocking** 非阻塞 **io**(**io**就是*input/output输入/输出*)。**java.nio.file** 库终于将 Java 文件操作带到与其他编程语言相同的水平。最重要的是 Java8 新增的 streams 与文件结合使得文件操作编程变得更加优雅。我们将看一下文件操作的两个基本组件:
@@ -124,7 +124,7 @@ true
我已经在这一章第一个程序的 **main()** 方法添加了第一行用于展示操作系统的名称,因此你可以看到不同操作系统之间存在哪些差异。理想情况下,差别会相对较小,并且使用 **/** 或者 **\\** 路径分隔符进行分隔。你可以看到我运行在Windows 10 上的程序输出。
当 **toString()** 方法生成完整形式的路径,你可以看到 **getFileName()** 方法总是返回当前文件名。
-通过使用 **Files** 工具类(我们接下类将会更多地使用它),可以测试一个文件是否存在,测试是否是一个"普通"文件还是一个目录等等。"Nofile.txt"这个示例展示我们描述的文件可能并不在指定的位置;这样可以允许你创建一个新的路径。"PathInfo.java"存在于当前目录中,最初它只是没有路径的文件名,但它仍然被检测为"存在"。一旦我们将其转换为绝对路径,我们将会得到一个从"C:"盘(因为我们是在Windows机器下进行测试)开始的完整路径,现在它也拥有一个父路径。“真实”路径的定义在文档中有点模糊,因为它取决于具体的文件系统。例如,如果文件名不区分大小写,即使路径由于大小写的缘故而不是完全相同,也可能得到肯定的匹配结果。在这样的平台上,**toRealPath()** 将返回实际情况下的 **Path**,并且还会删除任何冗余元素。
+通过使用 **Files** 工具类(我们接下来将会更多地使用它),可以测试一个文件是否存在,测试是否是一个"普通"文件还是一个目录等等。"Nofile.txt"这个示例展示我们描述的文件可能并不在指定的位置;这样可以允许你创建一个新的路径。"PathInfo.java"存在于当前目录中,最初它只是没有路径的文件名,但它仍然被检测为"存在"。一旦我们将其转换为绝对路径,我们将会得到一个从"C:"盘(因为我们是在Windows机器下进行测试)开始的完整路径,现在它也拥有一个父路径。“真实”路径的定义在文档中有点模糊,因为它取决于具体的文件系统。例如,如果文件名不区分大小写,即使路径由于大小写的缘故而不是完全相同,也可能得到肯定的匹配结果。在这样的平台上,**toRealPath()** 将返回实际情况下的 **Path**,并且还会删除任何冗余元素。
这里你会看到 **URI** 看起来只能用于描述文件,实际上 **URI** 可以用于描述更多的东西;通过 [维基百科](https://en.wikipedia.org/wiki/Uniform_Resource_Identifier) 可以了解更多细节。现在我们成功地将 **URI** 转为一个 **Path** 对象。
@@ -465,7 +465,7 @@ test\Hello.txt
我们尝试使用 **createDirectory()** 来创建多级路径,但是这样会抛出异常,因为这个方法只能创建单级路径。我已经将 **populateTestDir()** 作为一个单独的方法,因为它将在后面的例子中被重用。对于每一个变量 **variant**,我们都能使用 **createDirectories()** 创建完整的目录路径,然后使用此文件的副本以不同的目标名称填充该终端目录。然后我们使用 **createTempFile()** 生成一个临时文件。
-在调用 **populateTestDir()** 之后,我们在 **test** 目录下面下面创建一个临时目录。请注意,**createTempDirectory()** 只有名称的前缀选项。与 **createTempFile()** 不同,我们再次使用它将临时文件放入新的临时目录中。你可以从输出中看到,如果未指定后缀,它将默认使用".tmp"作为后缀。
+在调用 **populateTestDir()** 之后,我们在 **test** 目录下面创建一个临时目录。请注意,**createTempDirectory()** 只有名称的前缀选项。与 **createTempFile()** 不同,我们再次使用它将临时文件放入新的临时目录中。你可以从输出中看到,如果未指定后缀,它将默认使用".tmp"作为后缀。
为了展示结果,我们首次使用看起来很有希望的 **newDirectoryStream()**,但事实证明这个方法只是返回 **test** 目录内容的 Stream 流,并没有更多的内容。要获取目录树的全部内容的流,请使用 **Files.walk()**。
@@ -589,7 +589,7 @@ evt.kind(): ENTRY_DELETE
此时,**watcher.take()** 将等待并阻塞在这里。当目标事件发生时,会返回一个包含 **WatchEvent** 的 **Watchkey** 对象。展示的这三种方法是能对 **WatchEvent** 执行的全部操作。
-查看输出的具体内容。即使我们正在删除以 **.txt** 结尾的文件,在 **Hello.txt** 被删除之前,**WatchService** 也不会被触发。你可能认为,如果说"监视这个目录",自然会包含整个目录和下面子目录,但实际上的:只会监视给定的目录,而不是下面的所有内容。如果需要监视整个树目录,必须在整个树的每个子目录上放置一个 **Watchservice**。
+查看输出的具体内容。即使我们正在删除以 **.txt** 结尾的文件,在 **Hello.txt** 被删除之前,**WatchService** 也不会被触发。你可能认为,如果说"监视这个目录",自然会包含整个目录和下面子目录,但实际上:只会监视给定的目录,而不是下面的所有内容。如果需要监视整个树目录,必须在整个树的每个子目录上放置一个 **Watchservice**。
```java
// files/TreeWatcher.java
@@ -851,4 +851,4 @@ Java 7 和 8 对于处理文件和目录的类库做了大量改进。如果您
-
\ No newline at end of file
+
diff --git a/docs/book/19-Type-Information.md b/docs/book/19-Type-Information.md
index 3c70fe47..f013b51a 100644
--- a/docs/book/19-Type-Information.md
+++ b/docs/book/19-Type-Information.md
@@ -785,11 +785,11 @@ public class ForNameCreator extends PetCreator {
}
```
-`loader()` 方法使用 `Class.forName()` 创建了 `Class` 对象的 `List`。这可能会导致 `ClassNotFoundException` 异常,因为你传入的是一个 `String` 类型的参数,它不能再编译期间被确认是否合理。由于 `Pet` 相关的文件在 `typeinfo` 包里面,所以使用它们的时候需要填写完整的包名。
+`loader()` 方法使用 `Class.forName()` 创建了 `Class` 对象的 `List`。这可能会导致 `ClassNotFoundException` 异常,因为你传入的是一个 `String` 类型的参数,它不能在编译期间被确认是否合理。由于 `Pet` 相关的文件在 `typeinfo` 包里面,所以使用它们的时候需要填写完整的包名。
为了使得 `List` 装入的是具体的 `Class` 对象,类型转换是必须的,它会产生一个编译时警告。`loader()` 方法是分开编写的,然后它被放入到一个静态代码块里,因为 `@SuppressWarning` 注解不能够直接放置在静态代码块之上。
-为了对 `Pet` 进行计数,我们需要一个能跟踪不同类型的 `Pet` 的工具。`Map` 的是这个需求的首选,我们将 `Pet` 类型名作为键,将保存 `Pet` 数量的 `Integer` 作为值。通过这种方式,你就看可以询问:“有多少个 `Hamster` 对象?”我们可以使用 `instanceof` 来对 `Pet` 进行计数:
+为了对 `Pet` 进行计数,我们需要一个能跟踪不同类型的 `Pet` 的工具。`Map` 是这个需求的首选,我们将 `Pet` 类型名作为键,将保存 `Pet` 数量的 `Integer` 作为值。通过这种方式,你就可以询问:“有多少个 `Hamster` 对象?”我们可以使用 `instanceof` 来对 `Pet` 进行计数:
```java
// typeinfo/PetCount.java
@@ -1052,7 +1052,7 @@ EgyptianMau=2, Rodent=5, Hamster=1, Manx=7, Pet=20}
### 递归计数
-`PetCount3.Counter` 中的 `Map` 预先加载了所有不同的 `Pet` 类。我们可以使用 `Class.isAssignableFrom()` 而不是预加载地图,并创建一个不限于计数 `Pet` 的通用工具:
+`PetCount3.Counter` 中的 `Map` 预先加载了所有不同的 `Pet` 类。我们可以使用 `Class.isAssignableFrom()` 而不是预加载 `Map` ,并创建一个不限于计数 `Pet` 的通用工具:
```java
// onjava/TypeCounter.java
@@ -1336,7 +1336,7 @@ x.getClass().equals(Derived.class)) true
如果你不知道对象的确切类型,RTTI 会告诉你。但是,有一个限制:必须在编译时知道类型,才能使用 RTTI 检测它,并对信息做一些有用的事情。换句话说,编译器必须知道你使用的所有类。
-起初,这看起来并没有那么大的限制,但是假设你引用了一个对不在程序空间中的对象。实际上,该对象的类在编译时甚至对程序都不可用。也许你从磁盘文件或网络连接中获得了大量的字节,并被告知这些字节代表一个类。由于这个类在编译器为你的程序生成代码后很长时间才会出现,你如何使用这样的类?
+起初,这看起来并没有那么大的限制,但是假设你引用了一个不在程序空间中的对象。实际上,该对象的类在编译时甚至对程序都不可用。也许你从磁盘文件或网络连接中获得了大量的字节,并被告知这些字节代表一个类。由于这个类在编译器为你的程序生成代码后很长时间才会出现,你如何使用这样的类?
在传统编程环境中,这是一个牵强的场景。但是,当我们进入一个更大的编程世界时,会有一些重要的情况发生。第一个是基于组件的编程,你可以在应用程序构建器*集成开发环境*中使用*快速应用程序开发*(RAD)构建项目。这是一种通过将表示组件的图标移动到窗体上来创建程序的可视化方法。然后,通过在编程时设置这些组件的一些值来配置这些组件。这种设计时配置要求任何组件都是可实例化的,它公开自己的部分,并且允许读取和修改其属性。此外,处理*图形用户界面*(GUI)事件的组件必须公开有关适当方法的信息,以便 IDE 可以帮助程序员覆写这些事件处理方法。反射提供了检测可用方法并生成方法名称的机制。
@@ -1826,7 +1826,7 @@ caught EmptyTitleException
`EmptyTitleException` 是一个 `RuntimeException`,因为它意味着程序存在错误。在这个方案里边,你仍然可能会得到一个异常。但不同的是,在错误产生的那一刻(向 `setTitle()` 传 `null` 值时)就会抛出异常,而不是发生在其它时刻,需要你通过调试才能发现问题所在。另外,使用 `EmptyTitleException` 还有助于定位 BUG。
-`Person` 字段的限制又不太一样:如果你把它的值设为 `null`,程序会自动把将它赋值成一个空的 `Person` 对象。先前我们也用过类似的方法把字段转换成 `Option`,但这里我们是在返回结果的时候使用 `orElse(new Person())` 插入一个空的 `Person` 对象替代了 `null`。
+`Person` 字段的限制又不太一样:如果你把它的值设为 `null`,程序会自动把将它赋值成一个空的 `Person` 对象。先前我们也用过类似的方法把字段转换成 `Optional`,但这里我们是在返回结果的时候使用 `orElse(new Person())` 插入一个空的 `Person` 对象替代了 `null`。
在 `Position` 里边,我们没有创建一个表示“空”的标志位或者方法,因为 `person` 字段的 `Person` 对象为空,就表示这个 `Position` 是个空缺位置。之后,你可能会发现你必须添加一个显式的表示“空位”的方法,但是正如 YAGNI[^2] (You Aren't Going to Need It,你永远不需要它)所言,在初稿时“实现尽最大可能的简单”,直到程序在某些方面要求你为其添加一些额外的特性,而不是假设这是必要的。
From 5c3e99026911674a806bc2f28aa2755e561fcca7 Mon Sep 17 00:00:00 2001
From: rocLv
Date: Wed, 29 Apr 2020 21:03:24 +0800
Subject: [PATCH 077/224] Add jupyter format files (#445)
* add jupyter version
* update readme
---
README.md | 23 +
.../01-What-is-an-Object-checkpoint.ipynb | 408 +
.../04-Operators-checkpoint.ipynb | 2214 +++++
jupyter/00-Introduction.ipynb | 111 +
jupyter/00-On-Java-8.ipynb | 76 +
jupyter/00-Preface.ipynb | 112 +
jupyter/01-What-is-an-Object.ipynb | 422 +
...nstalling-Java-and-the-Book-Examples.ipynb | 305 +
jupyter/03-Objects-Everywhere.ipynb | 1386 +++
jupyter/04-Operators.ipynb | 2247 +++++
jupyter/05-Control-Flow.ipynb | 1436 ++++
jupyter/06-Housekeeping.ipynb | 3097 +++++++
jupyter/07-Implementation-Hiding.ipynb | 1325 +++
jupyter/08-Reuse.ipynb | 1661 ++++
jupyter/09-Polymorphism.ipynb | 1809 ++++
jupyter/10-Interfaces.ipynb | 2640 ++++++
jupyter/11-Inner-Classes.ipynb | 2189 +++++
jupyter/12-Collections.ipynb | 2589 ++++++
jupyter/13-Functional-Programming.ipynb | 2395 ++++++
jupyter/14-Streams.ipynb | 3492 ++++++++
jupyter/15-Exceptions.ipynb | 3518 ++++++++
jupyter/16-Validating-Your-Code.ipynb | 2480 ++++++
jupyter/17-Files.ipynb | 1123 +++
jupyter/18-Strings.ipynb | 2481 ++++++
jupyter/19-Type-Information.ipynb | 3701 ++++++++
jupyter/20-Generics.ipynb | 7433 +++++++++++++++++
jupyter/21-Arrays.ipynb | 3498 ++++++++
jupyter/22-Enumerations.ipynb | 3076 +++++++
jupyter/23-Annotations.ipynb | 2737 ++++++
jupyter/24-Concurrent-Programming.ipynb | 4536 ++++++++++
jupyter/25-Patterns.ipynb | 1674 ++++
jupyter/Appendix-Becoming-a-Programmer.ipynb | 132 +
...ts-and-Costs-of-Static-Type-Checking.ipynb | 42 +
jupyter/Appendix-Collection-Topics.ipynb | 3861 +++++++++
jupyter/Appendix-Data-Compression.ipynb | 417 +
jupyter/Appendix-IO-Streams.ipynb | 716 ++
jupyter/Appendix-Javadoc.ipynb | 400 +
jupyter/Appendix-Low-Level-Concurrency.ipynb | 2446 ++++++
jupyter/Appendix-New-IO.ipynb | 1446 ++++
jupyter/Appendix-Object-Serialization.ipynb | 1374 +++
...pendix-Passing-and-Returning-Objects.ipynb | 88 +
jupyter/Appendix-Programming-Guidelines.ipynb | 187 +
jupyter/Appendix-Standard-IO.ipynb | 335 +
jupyter/Appendix-Supplements.ipynb | 39 +
...itive-Legacy-of-C-plus-plus-and-Java.ipynb | 44 +
...ix-Understanding-equals-and-hashCode.ipynb | 1340 +++
jupyter/GLOSSARY.ipynb | 23 +
47 files changed, 79084 insertions(+)
create mode 100644 jupyter/.ipynb_checkpoints/01-What-is-an-Object-checkpoint.ipynb
create mode 100644 jupyter/.ipynb_checkpoints/04-Operators-checkpoint.ipynb
create mode 100644 jupyter/00-Introduction.ipynb
create mode 100644 jupyter/00-On-Java-8.ipynb
create mode 100644 jupyter/00-Preface.ipynb
create mode 100644 jupyter/01-What-is-an-Object.ipynb
create mode 100644 jupyter/02-Installing-Java-and-the-Book-Examples.ipynb
create mode 100644 jupyter/03-Objects-Everywhere.ipynb
create mode 100644 jupyter/04-Operators.ipynb
create mode 100644 jupyter/05-Control-Flow.ipynb
create mode 100644 jupyter/06-Housekeeping.ipynb
create mode 100644 jupyter/07-Implementation-Hiding.ipynb
create mode 100644 jupyter/08-Reuse.ipynb
create mode 100644 jupyter/09-Polymorphism.ipynb
create mode 100644 jupyter/10-Interfaces.ipynb
create mode 100644 jupyter/11-Inner-Classes.ipynb
create mode 100644 jupyter/12-Collections.ipynb
create mode 100644 jupyter/13-Functional-Programming.ipynb
create mode 100644 jupyter/14-Streams.ipynb
create mode 100644 jupyter/15-Exceptions.ipynb
create mode 100644 jupyter/16-Validating-Your-Code.ipynb
create mode 100644 jupyter/17-Files.ipynb
create mode 100644 jupyter/18-Strings.ipynb
create mode 100644 jupyter/19-Type-Information.ipynb
create mode 100644 jupyter/20-Generics.ipynb
create mode 100644 jupyter/21-Arrays.ipynb
create mode 100644 jupyter/22-Enumerations.ipynb
create mode 100644 jupyter/23-Annotations.ipynb
create mode 100644 jupyter/24-Concurrent-Programming.ipynb
create mode 100644 jupyter/25-Patterns.ipynb
create mode 100644 jupyter/Appendix-Becoming-a-Programmer.ipynb
create mode 100644 jupyter/Appendix-Benefits-and-Costs-of-Static-Type-Checking.ipynb
create mode 100644 jupyter/Appendix-Collection-Topics.ipynb
create mode 100644 jupyter/Appendix-Data-Compression.ipynb
create mode 100644 jupyter/Appendix-IO-Streams.ipynb
create mode 100644 jupyter/Appendix-Javadoc.ipynb
create mode 100644 jupyter/Appendix-Low-Level-Concurrency.ipynb
create mode 100644 jupyter/Appendix-New-IO.ipynb
create mode 100644 jupyter/Appendix-Object-Serialization.ipynb
create mode 100644 jupyter/Appendix-Passing-and-Returning-Objects.ipynb
create mode 100644 jupyter/Appendix-Programming-Guidelines.ipynb
create mode 100644 jupyter/Appendix-Standard-IO.ipynb
create mode 100644 jupyter/Appendix-Supplements.ipynb
create mode 100644 jupyter/Appendix-The-Positive-Legacy-of-C-plus-plus-and-Java.ipynb
create mode 100644 jupyter/Appendix-Understanding-equals-and-hashCode.ipynb
create mode 100644 jupyter/GLOSSARY.ipynb
diff --git a/README.md b/README.md
index 187c1a55..750efa20 100644
--- a/README.md
+++ b/README.md
@@ -61,6 +61,29 @@
- [x] [附录:C++和Java的优良传统](docs/book/Appendix-The-Positive-Legacy-of-C-plus-plus-and-Java.md)
- [ ] [附录:成为一名程序员](docs/book/Appendix-Becoming-a-Programmer.md)
+## INSTALL
+
+1. 首先安装[Jupyter Lab](https://jupyter.org/)
+2. 安装[Java Kernel](https://github.com/SpencerPark/IJava)
+
+注意: 打开文件后,在工具栏最右边选择`Java`。 Mac下按`CMD + Enter`可以运行Code。
+
+ Java SDK需要1.9及以上。可以用[sdkman](sdkman.io)安装.
+
+3. 代码运行。
+ ```java
+ public class Hello {
+ public static void main(String [] args){
+ System.out.println("Hello, world!")
+ }
+
+ }
+
+ //调用静态方法main
+ Hello.main(new String [0]);
+
+
+ ```
## 一起交流
diff --git a/jupyter/.ipynb_checkpoints/01-What-is-an-Object-checkpoint.ipynb b/jupyter/.ipynb_checkpoints/01-What-is-an-Object-checkpoint.ipynb
new file mode 100644
index 00000000..897dcd4d
--- /dev/null
+++ b/jupyter/.ipynb_checkpoints/01-What-is-an-Object-checkpoint.ipynb
@@ -0,0 +1,408 @@
+{
+ "cells": [
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "# 第一章 对象的概念\n",
+ "\n",
+ "> “我们没有意识到惯用语言的结构有多大的力量。可以毫不夸张地说,它通过语义反应机制奴役我们。语言表现出来并在无意识中给我们留下深刻印象的结构会自动投射到我们周围的世界。” -- Alfred Korzybski (1930)\n",
+ "\n",
+ "计算机革命的起源来自机器。编程语言就像是那台机器。它不仅是我们思维放大的工具与另一种表达媒介,更像是我们思想的一部分。语言的灵感来自其他形式的表达,如写作,绘画,雕塑,动画和电影制作。编程语言就是创建应用程序的思想结构。\n",
+ "\n",
+ "面向对象编程(Object-Oriented Programming OOP)是一种编程思维方式和编码架构。本章讲述 OOP 的基本概述。如果读者对此不太理解,可先行跳过本章。等你具备一定编程基础后,请务必再回头看。只有这样你才能深刻理解面向对象编程的重要性及设计方式。\n",
+ "\n",
+ "## 抽象\n",
+ "\n",
+ "所有编程语言都提供抽象机制。从某种程度上来说,问题的复杂度直接取决于抽象的类型和质量。这里的“类型”意思是:抽象的内容是什么?汇编语言是对底层机器的轻微抽象。接着出现的“命令式”语言(如 FORTRAN,BASIC 和 C)是对汇编语言的抽象。与汇编相比,这类语言已有了长足的改进,但它们的抽象原理依然要求我们着重考虑计算机的结构,而非问题本身的结构。\n",
+ "\n",
+ "程序员必须要在机器模型(“解决方案空间”)和实际解决的问题模型(“问题空间”)之间建立起一种关联。这个过程既费精力,又脱离编程语言本身的范畴。这使得程序代码很难编写,维护代价高昂。同时还造就了一个副产业“编程方法”学科。\n",
+ "\n",
+ "为机器建模的另一个方法是为要解决的问题制作模型。对一些早期语言来说,如 LISP 和 APL,它们的做法是“从不同的角度观察世界”——“所有问题都归纳为列表”或“所有问题都归纳为算法”。PROLOG 则将所有\n",
+ "问题都归纳为决策链。对于这些语言,我们认为它们一部分是“基于约束”的编程,另一部分则是专为\n",
+ "处理图形符号设计的(后者被证明限制性太强)。每种方法都有自己特殊的用途,适合解决某一类的问题。只要超出了它们力所能及的范围,就会显得非常笨拙。\n",
+ "\n",
+ "面向对象的程序设计在此基础上跨出了一大步,程序员可利用一些工具表达“问题空间”内的元素。由于这种表达非常具有普遍性,所以不必受限于特定类型的问题。我们将问题空间中的元素以及它们在解决方案空间的表示称作“对象”(**Object**)。当然,还有一些在问题空间没有对应的对象体。通过添加新的对象类型,程序可进行灵活的调整,以便与特定的问题配合。所以当你在阅读描述解决方案的代码时,也是在阅读问题的表述。与我们以前见过的相比,这无疑是一种更加灵活、更加强大的语言抽象方法。总之,OOP 允许我们根据问题来描述问题,而不是根据运行解决方案的计算机。然而,它仍然与计算机有联系,每个对象都类似一台小计算机:它们有自己的状态并且可以进行特定的操作。这与现实世界的“对象”或者“物体”相似:它们都有自己的特征和行为。\n",
+ "\n",
+ "Smalltalk 作为第一个成功的面向对象并影响了 Java 的程序设计语言 ,*Alan Kay* 总结了其五大基本特征。通过这些特征,我们可理解“纯粹”的面向对象程序设计方法是什么样的:\n",
+ "\n",
+ "> 1. **万物皆对象**。你可以将对象想象成一种特殊的变量。它存储数据,但可以在你对其“发出请求”时执行本身的操作。理论上讲,你总是可以从要解决的问题身上抽象出概念性的组件,然后在程序中将其表示为一个对象。\n",
+ "> 2. **程序是一组对象,通过消息传递来告知彼此该做什么**。要请求调用一个对象的方法,你需要向该对象发送消息。\n",
+ "> 3. **每个对象都有自己的存储空间,可容纳其他对象**。或者说,通过封装现有对象,可制作出新型对象。所以,尽管对象的概念非常简单,但在程序中却可达到任意高的复杂程度。\n",
+ "> 4. **每个对象都有一种类型**。根据语法,每个对象都是某个“类”的一个“实例”。其中,“类”(Class)是“类型”(Type)的同义词。一个类最重要的特征就是“能将什么消息发给它?”。\n",
+ "> 5. **同一类所有对象都能接收相同的消息**。这实际是别有含义的一种说法,大家不久便能理解。由于类型为“圆”(Circle)的一个对象也属于类型为“形状”(Shape)的一个对象,所以一个圆完全能接收发送给\"形状”的消息。这意味着可让程序代码统一指挥“形状”,令其自动控制所有符合“形状”描述的对象,其中自然包括“圆”。这一特性称为对象的“可替换性”,是OOP最重要的概念之一。\n",
+ "\n",
+ "*Grady Booch* 提供了对对象更简洁的描述:一个对象具有自己的状态,行为和标识。这意味着对象有自己的内部数据(提供状态)、方法 (产生行为),并彼此区分(每个对象在内存中都有唯一的地址)。\n",
+ "\n",
+ "## 接口\n",
+ "\n",
+ "亚里士多德(*Aristotle*)大概是第一个认真研究“类型”的哲学家,他曾提出过“鱼类和鸟类”这样的概念。所有对象都是唯一的,但同时也是具有相同的特性和行为的对象所归属的类的一部分。这种思想被首次应用于第一个面向对象编程语言 Simula-67,它在程序中使用基本关键字 **class** 来引入新的类型(class 和 type 通常可互换使用,有些人对它们进行了进一步区分,他们强调 type 决定了接口,而 class 是那个接口的一种特殊实现方式)。\n",
+ "\n",
+ "Simula 是一个很好的例子。正如这个名字所暗示的,它的作用是“模拟”(Simulate)类似“银行出纳员”这样的经典问题。在这个例子里,我们有一系列出纳员、客户、帐号、交易和货币单位等许多\"对象”。每类成员(元素)都具有一些通用的特征:每个帐号都有一定的余额;每名出纳都能接收客户的存款;等等。与此同时,每个成员都有自己的状态;每个帐号都有不同的余额;每名出纳都有一个名字。所以在计算机程序中,能用独一无二的实体分别表示出纳员、客户、帐号以及交易。这个实体便是“对象”,而且每个对象都隶属一个特定的“类”,那个类具有自己的通用特征与行为。\n",
+ "\n",
+ "因此,在面向对象的程序设计中,尽管我们真正要做的是新建各种各样的数据“类型”(Type),但几乎所有面向对象的程序设计语言都采用了 `class` 关键字。当你看到 “type” 这个词的时候,请同时想到 `class`;反之亦然。\n",
+ "\n",
+ "创建好一个类后,可根据情况生成许多对象。随后,可将那些对象作为要解决问题中存在的元素进行处理。事实上,当我们进行面向对象的程序设计时,面临的最大一项挑战是:如何在“问题空间”(问题实际存在的地方)的元素与“方案空间”(对实际问题进行建模的地方,如计算机)的元素之间建立理想的“一对一”的映射关系。\n",
+ "\n",
+ "那么如何利用对象完成真正有用的工作呢?必须有一种办法能向对象发出请求,令其解决一些实际的问题,比如完成一次交易、在屏幕上画一些东西或者打开一个开关等等。每个对象仅能接受特定的请求。我们向对象发出的请求是通过它的“接口”(Interface)定义的,对象的“类型”或“类”则规定了它的接口形式。“类型”与“接口”的对应关系是面向对象程序设计的基础。\n",
+ "\n",
+ "下面让我们以电灯泡为例:\n",
+ "\n",
+ ""
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "Light lt = new Light();\n",
+ "lt.on();"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "在这个例子中,类型/类的名称是 **Light**,可向 **Light** 对象发出的请求包括打开 `on`、关闭 `off`、变得更明亮 `brighten` 或者变得更暗淡 `dim`。通过声明一个引用,如 `lt` 和 `new` 关键字,我们创建了一个 **Light** 类型的对象,再用等号将其赋给引用。\n",
+ "\n",
+ "为了向对象发送消息,我们使用句点符号 `.` 将 `lt` 和消息名称 `on` 连接起来。可以看出,使用一些预先定义好的类时,我们在程序里采用的代码是非常简单直观的。\n",
+ "\n",
+ "上图遵循 **UML**(Unified Modeling Language,统一建模语言)的格式。每个类由一个框表示,框的顶部有类型名称,框中间部分是要描述的任何数据成员,方法(属于此对象的方法,它们接收任何发送到该对象的消息)在框的底部。通常,只有类的名称和公共方法在 **UML** 设计图中显示,因此中间部分未显示,如本例所示。如果你只对类名感兴趣,则也不需要显示方法信息。\n",
+ "\n",
+ "## 服务提供\n",
+ "\n",
+ "在开发或理解程序设计时,我们可以将对象看成是“服务提供者”。你的程序本身将为用户提供服务,并且它能通过调用其他对象提供的服务来实现这一点。我们的最终目标是开发或调用工具库中已有的一些对象,提供理想的服务来解决问题。\n",
+ "\n",
+ "那么问题来了:我们该选择哪个对象来解决问题呢?例如,你正在开发一个记事本程序。*你可能会想到在屏幕输入默认的记事本对象*,一个用于检测不同类型打印机并执行打印的对象。这些对象中的某些已经有了。那对于还没有的对象,我们该设计成啥样呢?这些对象需要提供哪些服务,以及还需要调用其他哪些对象?\n",
+ "\n",
+ "我们可以将这些问题一一分解,抽象成一组服务。软件设计的基本原则是高内聚:每个组件的内部作用明确,功能紧密相关。然而经常有人将太多功能塞进一个对象中。例如:在支票打印模块中,你需要设计一个可以同时读取文本格式又能正确识别不同打印机型号的对象。正确的做法是提供三个或更多对象:一个对象检查所有排版布局的目录;一个或一组可以识别不同打印机型号的对象展示通用的打印界面;第三个对象组合上述两个服务来完成任务。这样,每个对象都提供了一组紧密的服务。在良好的面向对象设计中,每个对象功能单一且高效。这样的程序设计可以提高我们代码的复用性,同时也方便别人阅读和理解我们的代码。只有让人知道你提供什么服务,别人才能更好地将其应用到其他模块或程序中。\n",
+ "\n",
+ "## 封装\n",
+ "\n",
+ "我们可以把编程的侧重领域划分为研发和应用。应用程序员调用研发程序员构建的基础工具类来做快速开发。研发程序员开发一个工具类,该工具类仅向应用程序员公开必要的内容,并隐藏内部实现的细节。这样可以有效地避免该工具类被错误的使用和更改,从而减少程序出错的可能。彼此职责划分清晰,相互协作。当应用程序员调用研发程序员开发的工具类时,双方建立了关系。应用程序员通过使用现成的工具类组装应用程序或者构建更大的工具库。如果工具类的创建者将类的内部所有信息都公开给调用者,那么有些使用规则就不容易被遵守。因为前者无法保证后者是否会按照正确的规则来使用,甚至是改变该工具类。只有设定访问控制,才能从根本上阻止这种情况的发生。\n",
+ "\n",
+ "因此,使用访问控制的原因有以下两点:\n",
+ "\n",
+ "1. 让应用程序员不要触摸他们不应该触摸的部分。(请注意,这也是一个哲学决策。部分编程语言认为如果程序员有需要,则应该让他们访问细节部分。);\n",
+ "\n",
+ "2. 使类库的创建者(研发程序员)在不影响后者使用的情况下完善更新工具库。例如,我们开发了一个功能简单的工具类,后来发现可以通过优化代码来提高执行速度。假如工具类的接口和实现部分明确分开并受到保护,那我们就可以轻松地完成改造。\n",
+ "\n",
+ "Java 有三个显式关键字来设置类中的访问权限:`public`(公开),`private`(私有)和`protected`(受保护)。这些访问修饰符决定了谁能使用它们修饰的方法、变量或类。\n",
+ "\n",
+ " 1. `public`(公开)表示任何人都可以访问和使用该元素;\n",
+ "\n",
+ " 2. `private`(私有)除了类本身和类内部的方法,外界无法直接访问该元素。`private` 是类和调用者之间的屏障。任何试图访问私有成员的行为都会报编译时错误;\n",
+ "\n",
+ " 3. `protected`(受保护)类似于 `private`,区别是子类(下一节就会引入继承的概念)可以访问 `protected` 的成员,但不能访问 `private` 成员;\n",
+ "\n",
+ " 4. `default`(默认)如果你不使用前面的三者,默认就是 `default` 访问权限。`default` 被称为包访问,因为该权限下的资源可以被同一包(库组件)中其他类的成员访问。\n",
+ "\n",
+ "## 复用\n",
+ "\n",
+ "一个类经创建和测试后,理应是可复用的。然而很多时候,由于程序员没有足够的编程经验和远见,我们的代码复用性并不强。\n",
+ "\n",
+ "代码和设计方案的复用性是面向对象程序设计的优点之一。我们可以通过重复使用某个类的对象来达到这种复用性。同时,我们也可以将一个类的对象作为另一个类的成员变量使用。新的类可以是由任意数量和任意类型的其他对象构成。这里涉及到“组合”和“聚合”的概念:\n",
+ "\n",
+ "* **组合**(Composition)经常用来表示“拥有”关系(has-a relationship)。例如,“汽车拥有引擎”。\n",
+ "\n",
+ "* **聚合**(Aggregation)动态的**组合**。\n",
+ "\n",
+ "\n",
+ "\n",
+ "上图中实心三角形指向“ **Car** ”表示 **组合** 的关系;如果是 **聚合** 关系,可以使用空心三角形。\n",
+ "\n",
+ "(**译者注**:组合和聚合都属于关联关系的一种,只是额外具有整体-部分的意义。至于是聚合还是组合,需要根据实际的业务需求来判断。可能相同超类和子类,在不同的业务场景,关联关系会发生变化。只看代码是无法区分聚合和组合的,具体是哪一种关系,只能从语义级别来区分。聚合关系中,整件不会拥有部件的生命周期,所以整件删除时,部件不会被删除。再者,多个整件可以共享同一个部件。组合关系中,整件拥有部件的生命周期,所以整件删除时,部件一定会跟着删除。而且,多个整件不可以同时共享同一个部件。这个区别可以用来区分某个关联关系到底是组合还是聚合。两个类生命周期不同步,则是聚合关系,生命周期同步就是组合关系。)\n",
+ "\n",
+ "使用“组合”关系给我们的程序带来极大的灵活性。通常新建的类中,成员对象会使用 `private` 访问权限,这样应用程序员则无法对其直接访问。我们就可以在不影响客户代码的前提下,从容地修改那些成员。我们也可以在“运行时\"改变成员对象从而动态地改变程序的行为,这进一步增大了灵活性。下面一节要讲到的“继承”并不具备这种灵活性,因为编译器对通过继承创建的类进行了限制。\n",
+ "\n",
+ "在面向对象编程中经常重点强调“继承”。在新手程序员的印象里,或许先入为主地认为“继承应当随处可见”。沿着这种思路产生的程序设计通常拙劣又复杂。相反,在创建新类时首先要考虑“组合”,因为它更简单灵活,而且设计更加清晰。等我们有一些编程经验后,一旦需要用到继承,就会明显意识到这一点。\n",
+ "\n",
+ "## 继承\n",
+ "\n",
+ "“继承”给面向对象编程带来极大的便利。它在概念上允许我们将各式各样的数据和功能封装到一起,这样便可恰当表达“问题空间”的概念,而不用受制于必须使用底层机器语言。\n",
+ "\n",
+ "通过使用 `class` 关键字,这些概念形成了编程语言中的基本单元。遗憾的是,这么做还是有很多麻烦:在创建了一个类之后,即使另一个新类与其具有相似的功能,你还是得重新创建一个新类。但我们若能利用现成的数据类型,对其进行“克隆”,再根据情况进行添加和修改,情况就显得理想多了。“继承”正是针对这个目标而设计的。但继承并不完全等价于克隆。在继承过程中,若原始类(正式名称叫作基类、超类或父类)发生了变化,修改过的“克隆”类(正式名称叫作继承类或者子类)也会反映出这种变化。\n",
+ "\n",
+ "\n",
+ "\n",
+ "这个图中的箭头从派生类指向基类。正如你将看到的,通常有多个派生类。类型不仅仅描述一组对象的约束,它还涉及其他类型。两种类型可以具有共同的特征和行为,但是一种类型可能包含比另一种类型更多的特征,并且还可以处理更多的消息(或者以不同的方式处理它们)。继承通过基类和派生类的概念来表达这种相似性。基类包含派生自它的类型之间共享的所有特征和行为。创建基类以表示思想的核心。从基类中派生出其他类型来表示实现该核心的不同方式。\n",
+ "\n",
+ "\n",
+ "\n",
+ "例如,垃圾回收机对垃圾进行分类。基类是“垃圾”。每块垃圾都有重量、价值等特性,它们可以被切碎、熔化或分解。在此基础上,可以通过添加额外的特性(瓶子有颜色,钢罐有磁性)或行为(铝罐可以被压碎)派生出更具体的垃圾类型。此外,一些行为可以不同(纸张的价值取决于它的类型和状态)。使用继承,你将构建一个类型层次结构,来表示你试图解决的某种类型的问题。第二个例子是常见的“形状”例子,可能用于计算机辅助设计系统或游戏模拟。基类是“形状”,每个形状都有大小、颜色、位置等等。每个形状可以绘制、擦除、移动、着色等。由此,可以派生出(继承出)具体类型的形状——圆形、正方形、三角形等等——每个形状可以具有附加的特征和行为。\n",
+ "\n",
+ "\n",
+ "\n",
+ "例如,某些形状可以翻转。有些行为可能不同,比如计算形状的面积。类型层次结构体现了形状之间的相似性和差异性。以相同的术语将解决方案转换成问题是有用的,因为你不需要在问题描述和解决方案描述之间建立许多中间模型。通过使用对象,类型层次结构成为了主要模型,因此你可以直接从真实世界中对系统的描述过渡到用代码对系统进行描述。事实上,有时候,那些善于寻找复杂解决方案的人会被面向对象设计的简单性难倒。从现有类型继承创建新类型。这种新类型不仅包含现有类型的所有成员(尽管私有成员被隐藏起来并且不可访问),而且更重要的是它复制了基类的接口。也就是说,基类对象接收的所有消息也能被派生类对象接收。根据类接收的消息,我们知道类的类型,因此派生类与基类是相同的类型。\n",
+ "\n",
+ "在前面的例子中,“圆是形状”。这种通过继承的类型等价性是理解面向对象编程含义的基本门槛之一。因为基类和派生类都具有相同的基本接口,所以伴随此接口的必定有某些具体实现。也就是说,当对象接收到特定消息时,必须有可执行代码。如果继承一个类而不做其他任何事,则来自基类接口的方法直接进入派生类。这意味着派生类和基类不仅具有相同的类型,而且具有相同的行为,这么做没什么特别意义。\n",
+ "\n",
+ "有两种方法可以区分新的派生类与原始的基类。第一种方法很简单:在派生类中添加新方法。这些新方法不是基类接口的一部分。这意味着基类不能满足你的所有需求,所以你添加了更多的方法。继承的这种简单而原始的用途有时是解决问题的完美解决方案。然而,还是要仔细考虑是否在基类中也要有这些额外的方法。这种设计的发现与迭代过程在面向对象程序设计中会经常发生。\n",
+ "\n",
+ "尽管继承有时意味着你要在接口中添加新方法(尤其是在以 **extends** 关键字表示继承的 Java 中),但并非总需如此。第二种也是更重要地区分派生类和基类的方法是改变现有基类方法的行为,这被称为覆盖 (overriding)。要想覆盖一个方法,只需要在派生类中重新定义这个方法即可。\n",
+ "\n",
+ "### \"是一个\"与\"像是一个\"的关系\n",
+ "\n",
+ "对于继承可能会引发争论:继承应该只覆盖基类的方法(不应该添加基类中没有的方法)吗?如果这样的话,基类和派生类就是相同的类型了,因为它们具有相同的接口。这会造成,你可以用一个派生类对象完全替代基类对象,这叫作\"纯粹替代\",也经常被称作\"替代原则\"。在某种意义上,这是一种处理继承的理想方式。我们经常把这种基类和派生类的关系称为是一个(is-a)关系,因为可以说\"圆是一个形状\"。判断是否继承,就看在你的类之间有无这种 is-a 关系。\n",
+ "\n",
+ "有时你在派生类添加了新的接口元素,从而扩展接口。虽然新类型仍然可以替代基类,但是这种替代不完美,原因在于基类无法访问新添加的方法。这种关系称为像是一个(is-like-a)关系。新类型不但拥有旧类型的接口,而且包含其他方法,所以不能说新旧类型完全相同。\n",
+ "\n",
+ "\n",
+ "\n",
+ "以空调为例,假设房间里已经安装好了制冷设备的控制器,即你有了控制制冷设备的接口。想象一下,现在空调坏了,你重新安装了一个既制冷又制热的热力泵。热力泵就像是一个(is-like-a)空调,但它可以做更多。因为当初房间的控制系统被设计成只能控制制冷设备,所以它只能与新对象(热力泵)的制冷部分通信。新对象的接口已经扩展了,现有控制系统却只知道原来的接口,一旦看到这个设计,你就会发现,作为基类的制冷系统不够一般化,应该被重新命名为\"温度控制系统\",也应该包含制热功能,这样的话,我们就可以使用替代原则了。上图反映了在现实世界中进行设计时可能会发生的事情。\n",
+ "\n",
+ "当你看到替代原则时,很容易会认为纯粹替代是唯一可行的方式,并且使用纯粹替代的设计是很好的。但有些时候,你会发现必须得在派生(扩展)类中添加新方法(提供新的接口)。只要仔细审视,你可以很明显地区分两种设计方式的使用场合。\n",
+ "\n",
+ "## 多态\n",
+ "\n",
+ "我们在处理类的层次结构时,通常把一个对象看成是它所属的基类,而不是把它当成具体类。通过这种方式,我们可以编写出不局限于特定类型的代码。在上个“形状”的例子中,“方法”(method)操纵的是通用“形状”,而不关心它们是“圆”、“正方形”、“三角形”还是某种尚未定义的形状。所有的形状都可以被绘制、擦除和移动,因此“方法”向其中的任何代表“形状”的对象发送消息都不必担心对象如何处理信息。\n",
+ "\n",
+ "这样的代码不会受添加的新类型影响,并且添加新类型是扩展面向对象程序以处理新情况的常用方法。 例如,你可以通过通用的“形状”基类派生出新的“五角形”形状的子类,而不需要修改通用\"形状\"基类的方法。通过派生新的子类来扩展设计的这种能力是封装变化的基本方法之一。\n",
+ "\n",
+ "这种能力改善了我们的设计,且减少了软件的维护代价。如果我们把派生的对象类型统一看成是它本身的基类(“圆”当作“形状”,“自行车”当作“车”,“鸬鹚”当作“鸟”等等),编译器(compiler)在编译时期就无法准确地知道什么“形状”被擦除,哪一种“车”在行驶,或者是哪种“鸟”在飞行。这就是关键所在:当程序接收这种消息时,程序员并不想知道哪段代码会被执行。“绘图”的方法可以平等地应用到每种可能的“形状”上,形状会依据自身的具体类型执行恰当的代码。\n",
+ "\n",
+ "如果不需要知道执行了哪部分代码,那我们就能添加一个新的不同执行方式的子类而不需要更改调用它的方法。那么编译器在不确定该执行哪部分代码时是怎么做的呢?举个例子,下图的 **BirdController** 对象和通用 **Bird** 对象中,**BirdController** 不知道 **Bird** 的确切类型却还能一起工作。从 **BirdController** 的角度来看,这是很方便的,因为它不需要编写特别的代码来确定 **Bird** 对象的确切类型或行为。那么,在调用 **move()** 方法时是如何保证发生正确的行为(鹅走路、飞或游泳、企鹅走路或游泳)的呢?\n",
+ "\n",
+ "\n",
+ "\n",
+ "这个问题的答案,是面向对象程序设计的妙诀:在传统意义上,编译器不能进行函数调用。由非 OOP 编译器产生的函数调用会引起所谓的**早期绑定**,这个术语你可能从未听说过,不会想过其他的函数调用方式。这意味着编译器生成对特定函数名的调用,该调用会被解析为将执行的代码的绝对地址。\n",
+ "\n",
+ "通过继承,程序直到运行时才能确定代码的地址,因此发送消息给对象时,还需要其他一些方案。为了解决这个问题,面向对象语言使用**后期绑定**的概念。当向对象发送信息时,被调用的代码直到运行时才确定。编译器确保方法存在,并对参数和返回值执行类型检查,但是它不知道要执行的确切代码。\n",
+ "\n",
+ "为了执行后期绑定,Java 使用一个特殊的代码位来代替绝对调用。这段代码使用对象中存储的信息来计算方法主体的地址(此过程在多态性章节中有详细介绍)。因此,每个对象的行为根据特定代码位的内容而不同。当你向对象发送消息时,对象知道该如何处理这条消息。在某些语言中,必须显式地授予方法后期绑定属性的灵活性。例如,C++ 使用 **virtual** 关键字。在这些语言中,默认情况下方法不是动态绑定的。在 Java 中,动态绑定是默认行为,不需要额外的关键字来实现多态性。\n",
+ "\n",
+ "为了演示多态性,我们编写了一段代码,它忽略了类型的具体细节,只与基类对话。该代码与具体类型信息分离,因此更易于编写和理解。而且,如果通过继承添加了一个新类型(例如,一个六边形),那么代码对于新类型的 Shape 就像对现有类型一样有效。因此,该程序是可扩展的。\n",
+ "\n",
+ "代码示例:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "void doSomething(Shape shape) {\n",
+ " shape.erase();\n",
+ " // ...\n",
+ " shape.draw();\n",
+ "}"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "此方法与任何 **Shape** 对话,因此它与所绘制和擦除的对象的具体类型无关。如果程序的其他部分使用 `doSomething()` 方法:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ " Circle circle = new Circle();\n",
+ " Triangle triangle = new Triangle();\n",
+ " Line line = new Line();\n",
+ " doSomething(circle);\n",
+ " doSomething(triangle);\n",
+ " doSomething(line);\n"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "可以看到无论传入的“形状”是什么,程序都正确的执行了。\n",
+ "\n",
+ "\n",
+ "\n",
+ "这是一个非常令人惊奇的编程技巧。分析下面这行代码:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ " doSomething(circle);"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "当预期接收 **Shape** 的方法被传入了 **Circle**,会发生什么。由于 **Circle** 也是一种 **Shape**,所\n",
+ "以 `doSomething(circle)` 能正确地执行。也就是说,`doSomething()` 能接收任意发送给 **Shape** 的消息。这是完全安全和合乎逻辑的事情。\n",
+ "\n",
+ "这种把子类当成其基类来处理的过程叫做“向上转型”(**upcasting**)。在面向对象的编程里,经常利用这种方法来给程序解耦。再看下面的 `doSomething()` 代码示例:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ " shape.erase();\n",
+ " // ...\n",
+ " shape.draw();"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "我们可以看到程序并未这样表达:“如果你是一个 Circle ,就这样做;如果你是一个 Square,就那样做...”。若那样编写代码,就需检查 Shape 所有可能的类型,如圆、矩形等等。这显然是非常麻烦的,而且每次添加了一种新的 Shape 类型后,都要相应地进行修改。在这里,我们只需说:“你是一种几何形状,我知道你能删掉 `erase()` 和绘制 `draw()`,你自己去做吧,注意细节。”\n",
+ "\n",
+ "尽管我们没作出任何特殊指示,程序的操作也是完全正确和恰当的。我们知道,为 Circle 调用`draw()` 时执行的代码与为一个 Square 或 Line 调用 `draw()` 时执行的代码是不同的。但在将 `draw()` 信息发给一个匿名 Shape 时,根据 Shape 句柄当时连接的实际类型,会相应地采取正确的操作。这非常神奇,因为当 Java 编译器为 `doSomething()` 编译代码时,它并不知道自己要操作的准确类型是什么。\n",
+ "\n",
+ "尽管我们确实可以保证最终会为 Shape 调用 `erase()` 和 `draw()`,但并不能确定特定的 Circle,Square 或者 Line 调用什么。最后,程序执行的操作却依然是正确的,这是怎么做到的呢?\n",
+ "\n",
+ "发送消息给对象时,如果程序不知道接收的具体类型是什么,但最终执行是正确的,这就是对象的“多态性”(Polymorphism)。面向对象的程序设计语言是通过“动态绑定”的方式来实现对象的多态性的。编译器和运行时系统会负责对所有细节的控制;我们只需知道要做什么,以及如何利用多态性来更好地设计程序。\n",
+ "\n",
+ "## 单继承结构\n",
+ "\n",
+ "自从 C++ 引入以来,一个 OOP 问题变得尤为突出:是否所有的类都应该默认从一个基类继承呢?这个答案在 Java 中是肯定的(实际上,除 C++ 以外的几乎所有OOP语言中也是这样)。在 Java 中,这个最终基类的名字就是 `Object`。\n",
+ "\n",
+ "Java 的单继承结构有很多好处。由于所有对象都具有一个公共接口,因此它们最终都属于同一个基类。相反的,对于 C++ 所使用的多继承的方案则是不保证所有的对象都属于同一个基类。从向后兼容的角度看,多继承的方案更符合 C 的模型,而且受限较少。\n",
+ "\n",
+ "对于完全面向对象编程,我们必须要构建自己的层次结构,以提供与其他 OOP 语言同样的便利。我们经常会使用到新的类库和不兼容的接口。为了整合它们而花费大气力(有可能还要用上多继承)以获得 C++ 样的“灵活性”值得吗?如果从零开始,Java 这样的替代方案会是更好的选择。\n",
+ "\n",
+ "另外,单继承的结构使得垃圾收集器的实现更为容易。这也是 Java 在 C++ 基础上的根本改进之一。\n",
+ "\n",
+ "由于运行期的类型信息会存在于所有对象中,所以我们永远不会遇到判断不了对象类型的情况。这对于系统级操作尤其重要,例如[异常处理](#异常处理)。同时,这也让我们的编程具有更大的灵活性。\n",
+ "\n",
+ "## 集合\n",
+ "\n",
+ "通常,我们并不知道解决某个具体问题需要的对象数量和持续时间,以及对象的存储方式。那么我们如何知悉程序在运行时需要分配的内存空间呢?\n",
+ "\n",
+ "在面向对象的设计中,问题的解决方案有些过于轻率:创建一个新类型的对象来引用、容纳其他的对象。当然,我们也可以使用多数编程语言都支持的“数组”(array)。在 Java 中“集合”(Collection)的使用率更高。(也可称之为“容器”,但“集合”这个称呼更通用。)\n",
+ "\n",
+ "“集合”这种类型的对象可以存储任意类型、数量的其他对象。它能根据需要自动扩容,我们不用关心过程是如何实现的。\n",
+ "\n",
+ "还好,一般优秀的 OOP 语言都会将“集合”作为其基础包。在 C++ 中,“集合”是其标准库的一部分,通常被称为 STL(Standard Template Library,标准模板库)。SmallTalk 有一套非常完整的集合库。同样,Java 的标准库中也提供许多现成的集合类。\n",
+ "\n",
+ "在一些库中,一两个泛型集合就能满足我们所有的需求了,而在其他一些类库(Java)中,不同类型的集合对应不同的需求:常见的有 List,常用于保存序列;Map,也称为关联数组,常用于将对象与其他对象关联;Set,只能保存非重复的值;其他还包括如队列(Queue)、树(Tree)、栈(Stack)、堆(Heap)等等。从设计的角度来看,我们真正想要的是一个能够解决某个问题的集合。如果一种集合就满足所有需求,那么我们就不需要剩下的了。之所以选择集合有以下两个原因:\n",
+ "\n",
+ "1. 集合可以提供不同类型的接口和外部行为。堆栈、队列的应用场景和集合、列表不同,它们中的一种提供的解决方案可能比其他灵活得多。\n",
+ "\n",
+ "2. 不同的集合对某些操作有不同的效率。例如,List 的两种基本类型:ArrayList 和 LinkedList。虽然两者具有相同接口和外部行为,但是在某些操作中它们的效率差别很大。在 ArrayList 中随机查找元素是很高效的,而 LinkedList 随机查找效率低下。反之,在 LinkedList 中插入元素的效率要比在 ArrayList 中高。由于底层数据结构的不同,每种集合类型在执行相同的操作时会表现出效率上的差异。\n",
+ "\n",
+ "我们可以一开始使用 LinkedList 构建程序,在优化系统性能时改用 ArrayList。通过对 List 接口的抽象,我们可以很容易地将 LinkedList 改为 ArrayList。\n",
+ "\n",
+ "在 Java 5 泛型出来之前,集合中保存的是通用类型 `Object`。Java 单继承的结构意味着所有元素都基于 `Object` 类,所以在集合中可以保存任何类型的数据,易于重用。要使用这样的集合,我们先要往集合添加元素。由于 Java 5 版本前的集合只保存 `Object`,当我们往集合中添加元素时,元素便向上转型成了 `Object`,从而丢失自己原有的类型特性。这时我们再从集合中取出该元素时,元素的类型变成了 `Object`。那么我们该怎么将其转回原先具体的类型呢?这里,我们使用了强制类型转换将其转为更具体的类型,这个过程称为对象的“向下转型”。通过“向上转型”,我们知道“圆形”也是一种“形状”,这个过程是安全的。可是我们不能从“Object”看出其就是“圆形”或“形状”,所以除非我们能确定元素的具体类型信息,否则“向下转型”就是不安全的。也不能说这样的错误就是完全危险的,因为一旦我们转化了错误的类型,程序就会运行出错,抛出“运行时异常”(RuntimeException)。(后面的章节会提到) 无论如何,我们要寻找一种在取出集合元素时确定其具体类型的方法。另外,每次取出元素都要做额外的“向下转型”对程序和程序员都是一种开销。以某种方式创建集合,以确认保存元素的具体类型,减少集合元素“向下转型”的开销和可能出现的错误难道不好吗?这种解决方案就是:参数化类型机制(Parameterized Type Mechanism)。\n",
+ "\n",
+ "参数化类型机制可以使得编译器能够自动识别某个 `class` 的具体类型并正确地执行。举个例子,对集合的参数化类型机制可以让集合仅接受“形状”这种类型的元素,并以“形状”类型取出元素。Java 5 版本支持了参数化类型机制,称之为“泛型”(Generic)。泛型是 Java 5 的主要特性之一。你可以按以下方式向 ArrayList 中添加 Shape(形状):"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ " ArrayList shapes = new ArrayList<>();"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "泛型的应用,让 Java 的许多标准库和组件都发生了改变。在本书的代码示例中,你也会经常看到泛型的身影。\n",
+ "\n",
+ "## 对象创建与生命周期\n",
+ "\n",
+ "我们在使用对象时要注意的一个关键问题就是对象的创建和销毁方式。每个对象的生存都需要资源,尤其是内存。为了资源的重复利用,当对象不再被使用时,我们应该及时释放资源,清理内存。\n",
+ "\n",
+ "在简单的编程场景下,对象的清理并不是问题。我们创建对象,按需使用,最后销毁它。然而,情况往往要比这更复杂:\n",
+ "\n",
+ "假设,我们正在为机场设计一个空中交通管制的系统(该例也适用于仓库货柜管理、影带出租或者宠物寄养仓库系统)。第一步比较简单:创建一个用来保存飞机的集合,每当有飞机进入交通管制区域时,我们就创建一个“飞机”对象并将其加入到集合中,等到飞机离开时将其从这个集合中清除。与此同时,我们还需要一个记录飞机信息的系统,也许这些数据不像主要控制功能那样引人注意。比如,我们要记录所有飞机中的小型飞机的的信息(比如飞行计划)。此时,我们又创建了第二个集合来记录所有小型飞机。 每当创建一个“飞机”对象的时候,将其放入第一个集合;若它属于小型飞机,也必须同时将其放入第二个集合里。\n",
+ "\n",
+ "现在问题开始棘手了:我们怎么知道何时该清理这些对象呢?当某一个系统处理完成,而其他系统可能还没有处理完成。这样的问题在其他的场景下也可能发生。在 C++ 程序设计中,当使用完一个对象后,必须明确将其删除,这就让问题变复杂了。\n",
+ "\n",
+ "对象的数据在哪?它的生命周期是怎么被控制的? 在 C++ 设计中采用的观点是效率第一,因此它将选择权交给了程序员。为了获得最大的运行时速度,程序员可以在编写程序时,通过将对象放在栈(Stack,有时称为自动变量或作用域变量)或静态存储区域(static storage area)中来确定内存占用和生存时间。这些区域的对象会被优先分配内存和释放。这种控制在某些情况下非常有用。\n",
+ "\n",
+ "然而相对的,我们也牺牲了程序的灵活性。因为在编写代码时,我们必须要弄清楚对象的数量、生存时间还有类型。如果我们要用它来解决一个相当普遍的问题时(如计算机辅助设计、仓库管理或空中交通管制等),限制就太大了。\n",
+ "\n",
+ "第二种方法是在堆内存(Heap)中动态地创建对象。在这种方式下,直到程序运行我们才能确定需要创建的对象数量、生存时间和类型。什么时候需要,什么时候在堆内存中创建。 因为内存的占用是动态管理的,所以在运行时,在堆内存上开辟空间所需的时间可能比在栈内存上要长(但也不一定)。在栈内存开辟和释放空间通常是一条将栈指针向下移动和一条将栈指针向上移动的汇编指令。开辟堆内存空间的时间取决于内存机制的设计。\n",
+ "\n",
+ "动态方法有这样一个合理假设:对象通常是复杂的,相比于对象创建的整体开销,寻找和释放内存空间的开销微不足道。(原文:*The dynamic approach makes the generally logical assumption that objects tend to be complicated, so the extra overhead of finding storage and releasing that storage will not have an important impact on the creation of an object.*)此外,更好的灵活性对于问题的解决至关重要。\n",
+ "\n",
+ "Java 使用动态内存分配。每次创建对象时,使用 `new` 关键字构建该对象的动态实例。这又带来另一个问题:对象的生命周期。较之堆内存,在栈内存中创建对象,编译器能够确定该对象的生命周期并自动销毁它;然而如果你在堆内存创建对象的话,编译器是不知道它的生命周期的。在 C++ 中你必须以编程方式确定何时销毁对象,否则可能导致内存泄漏。Java 的内存管理是建立在垃圾收集器上的,它能自动发现对象不再被使用并释放内存。垃圾收集器的存在带来了极大的便利,它减少了我们之前必须要跟踪的问题和编写相关代码的数量。因此,垃圾收集器提供了更高级别的保险,以防止潜在的内存泄漏问题,这个问题使得许多 C++ 项目没落。\n",
+ "\n",
+ "Java 的垃圾收集器被设计用来解决内存释放的问题(虽然这不包括对象清理的其他方面)。垃圾收集器知道对象什么时候不再被使用并且自动释放内存。结合单继承和仅可在堆中创建对象的机制,Java 的编码过程比用 C++ 要简单得多。我们所要做的决定和要克服的障碍也会少很多!\n",
+ "\n",
+ "## 异常处理\n",
+ "\n",
+ "自编程语言被发明以来,程序的错误处理一直都是个难题。因为很难设计出一个好的错误处理方案,所以许多编程语言都忽略了这个问题,把这个问题丢给了程序类库的设计者。他们提出了在许多情况下都可以工作但很容易被规避的半途而废的措施,通常只需忽略错误。多数错误处理方案的主要问题是:它们依赖程序员之间的约定俗成而不是语言层面的限制。换句话说,如果程序员赶时间或没想起来,这些方案就很容易被忘记。\n",
+ "\n",
+ "异常处理机制将程序错误直接交给编程语言甚至是操作系统。“异常”(Exception)是一个从出错点“抛出”(thrown)后能被特定类型的异常处理程序捕获(catch)的一个对象。它不会干扰程序的正常运行,仅当程序出错的时候才被执行。这让我们的编码更简单:不用再反复检查错误了。另外,异常不像方法返回的错误值和方法设置用来表示发生错误的标志位那样可以被忽略。异常的发生是不会被忽略的,它终究会在某一时刻被处理。\n",
+ "\n",
+ "最后,“异常机制”提供了一种可靠地从错误状况中恢复的方法,使得我们可以编写出更健壮的程序。有时你只要处理好抛出的异常情况并恢复程序的运行即可,无需退出。\n",
+ "\n",
+ "Java 的异常处理机制在编程语言中脱颖而出。Java 从一开始就内置了异常处理,因此你不得不使用它。这是 Java 语言唯一接受的错误报告方法。如果没有编写适当的异常处理代码,你将会收到一条编译时错误消息。这种有保障的一致性有时会让程序的错误处理变得更容易。值得注意的是,异常处理并不是面向对象的特性。尽管在面向对象的语言中异常通常由对象表示,但是在面向对象语言之前也存在异常处理。\n",
+ "\n",
+ "## 本章小结\n",
+ "\n",
+ "面向过程程序包含数据定义和函数调用。要找到程序的意图,你必须要在脑中建立一个模型,弄清函数调用和更底层的概念。这些程序令人困扰,因为它们的表示更多地面向计算机而不是我们要解决的问题,这就是我们在设计程序时需要中间表示的原因。OOP 在面向过程编程的基础上增加了许多新的概念,所以有人会认为使用 Java 来编程会比同等的面向过程编程要更复杂。在这里,我想给大家一个惊喜:通常按照 Java 规范编写的程序会比面向过程程序更容易被理解。\n",
+ "\n",
+ "你看到的是对象的概念,这些概念是站在“问题空间”的(而不是站在计算机角度的“解决方案空间”),以及发送消息给对象以指示该空间中的活动。面向对象编程的一个优点是:设计良好的 Java 程序代码更容易被人阅读理解。由于 Java 类库的复用性,通常程序要写的代码也会少得多。\n",
+ "\n",
+ "OOP 和 Java 不一定适合每个人。评估自己的需求以及与现有方案作比较是很重要的。请充分考虑后再决定是不是选择 Java。如果在可预见的未来,Java 并不能很好的满足你的特定需求,那么你应该去寻找其他替代方案(特别是,我推荐看 Python)。如果你依然选择 Java 作为你的开发语言,我希望你至少应该清楚你选择的是什么,以及为什么选择这个方向。\n",
+ "\n",
+ "\n",
+ ""
+ ]
+ }
+ ],
+ "metadata": {},
+ "nbformat": 4,
+ "nbformat_minor": 4
+}
diff --git a/jupyter/.ipynb_checkpoints/04-Operators-checkpoint.ipynb b/jupyter/.ipynb_checkpoints/04-Operators-checkpoint.ipynb
new file mode 100644
index 00000000..655a709c
--- /dev/null
+++ b/jupyter/.ipynb_checkpoints/04-Operators-checkpoint.ipynb
@@ -0,0 +1,2214 @@
+{
+ "cells": [
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "[TOC]\n",
+ "\n",
+ "\n",
+ "# 第四章 运算符\n",
+ "\n",
+ ">运算符操纵数据。\n",
+ "\n",
+ "Java 是从 C++ 的基础上做了一些改进和简化发展而成的。对于 C/C++ 程序员来说,Java 的运算符并不陌生。如果你已了解 C 或 C++,大可以跳过本章和下一章,直接阅读 Java 与 C/C++ 不同的地方。\n",
+ "\n",
+ "如果理解这两章的内容对你来说还有点困难,那么我推荐你先了解下 《Thinking in C》 再继续后面的学习。 这本书现在可以在 [www.OnJava8.com](http://www.OnJava8.com]) 上免费下载。它的内容包含音频讲座、幻灯片、练习和解答,专门用于帮助你快速掌握学习 Java 所需的基础知识。\n",
+ "\n",
+ "\n",
+ "## 开始使用\n",
+ "\n",
+ "运算符接受一个或多个参数并生成新值。这个参数与普通方法调用的形式不同,但效果是相同的。加法 `+`、减法 `-`、乘法 `*`、除法 `/` 以及赋值 `=` 在任何编程语言中的工作方式都是类似的。所有运算符都能根据自己的运算对象生成一个值。除此以外,一些运算符可改变运算对象的值,这叫作“副作用”(**Side Effect**)。运算符最常见的用途就是修改自己的运算对象,从而产生副作用。但要注意生成的值亦可由没有副作用的运算符生成。\n",
+ "\n",
+ "几乎所有运算符都只能操作基本类型(Primitives)。唯一的例外是 `=`、`==` 和 `!=`,它们能操作所有对象(这也是令人混淆的一个地方)。除此以外,**String** 类支持 `+` 和 `+=`。\n",
+ "\n",
+ "\n",
+ "## 优先级\n",
+ "\n",
+ "运算符的优先级决定了存在多个运算符时一个表达式各部分的运算顺序。Java 对运算顺序作出了特别的规定。其中,最简单的规则就是乘法和除法在加法和减法之前完成。程序员经常都会忘记其他优先级规则,所以应该用括号明确规定运算顺序。代码示例:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "// operators/Precedence.java\n",
+ "public class Precedence {\n",
+ " \n",
+ " public static void main(String[] args) {\n",
+ " int x = 1, y = 2, z = 3;\n",
+ " int a = x + y - 2/2 + z; // [1]\n",
+ " int b = x + (y - 2)/(2 + z); // [2]\n",
+ " System.out.println(\"a = \" + a);\n",
+ " System.out.println(\"b = \" + b);\n",
+ " }\n",
+ "}"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "输出结果:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ " a = 5\n",
+ " b = 1"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "这些语句看起来大致相同,但从输出中我们可以看出它们具有非常不同的含义,具体取决于括号的使用。\n",
+ "\n",
+ "我们注意到,在 `System.out.println()` 语句中使用了 `+` 运算符。 但是在这里 `+` 代表的意思是字符串连接符。编译器会将 `+` 连接的非字符串尝试转换为字符串。上例中的输出结果说明了 a 和 b 都已经被转化成了字符串。\n",
+ "\n",
+ "\n",
+ "## 赋值\n",
+ "\n",
+ "运算符的赋值是由符号 `=` 完成的。它代表着获取 `=` 右边的值并赋给左边的变量。右边可以是任何常量、变量或者可产生一个返回值的表达式。但左边必须是一个明确的、已命名的变量。也就是说,必须要有一个物理的空间来存放右边的值。举个例子来说,可将一个常数赋给一个变量(A = 4),但不可将任何东西赋给一个常数(比如不能 4 = A)。\n",
+ "\n",
+ "基本类型的赋值都是直接的,而不像对象,赋予的只是其内存的引用。举个例子,a = b ,如果 b 是基本类型,那么赋值操作会将 b 的值复制一份给变量 a, 此后若 a 的值发生改变是不会影响到 b 的。作为一名程序员,这应该成为我们的常识。\n",
+ "\n",
+ "如果是为对象赋值,那么结果就不一样了。对一个对象进行操作时,我们实际上操作的是它的引用。所以我们将右边的对象赋予给左边时,赋予的只是该对象的引用。此时,两者指向的堆中的对象还是同一个。代码示例:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java "
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "// operators/Assignment.java\n",
+ "// Assignment with objects is a bit tricky\n",
+ "class Tank {\n",
+ " int level;\n",
+ "}\n",
+ "\n",
+ "public class Assignment {\n",
+ "\n",
+ " public static void main(String[] args) {\n",
+ " Tank t1 = new Tank();\n",
+ " Tank t2 = new Tank();\n",
+ " t1.level = 9;\n",
+ " t2.level = 47;\n",
+ " System.out.println(\"1: t1.level: \" + t1.level +\n",
+ " \", t2.level: \" + t2.level);\n",
+ " t1 = t2;\n",
+ " System.out.println(\"2: t1.level: \" + t1.level +\n",
+ " \", t2.level: \" + t2.level);\n",
+ " t1.level = 27;\n",
+ " System.out.println(\"3: t1.level: \" + t1.level +\n",
+ " \", t2.level: \" + t2.level);\n",
+ " }\n",
+ "}"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "输出结果:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "1: t1.level: 9, t2.level: 47\n",
+ "2: t1.level: 47, t2.level: 47\n",
+ "3: t1.level: 27, t2.level: 27"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "这是一个简单的 `Tank` 类,在 `main()` 方法创建了两个实例对象。 两个对象的 `level` 属性分别被赋予不同的值。 然后,t2 的值被赋予给 t1。在许多编程语言里,预期的结果是 t1 和 t2 的值会一直相对独立。但是,在 Java 中,由于赋予的只是对象的引用,改变 t1 也就改变了 t2。 这是因为 t1 和 t2 此时指向的是堆中同一个对象。(t1 原始对象的引用在 t2 赋值给其时丢失,它引用的对象会在垃圾回收时被清理)。\n",
+ "\n",
+ "这种现象通常称为别名(aliasing),这是 Java 处理对象的一种基本方式。但是假若你不想出现这里的别名引起混淆的话,你可以这么做。代码示例:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "t1.level = t2.level;"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "较之前的做法,这样做保留了两个单独的对象,而不是丢弃一个并将 t1 和 t2 绑定到同一个对象。但是这样的操作有点违背 Java 的设计原则。对象的赋值是个需要重视的环节,否则你可能收获意外的“惊喜”。\n",
+ "\n",
+ " \n",
+ "### 方法调用中的别名现象\n",
+ "\n",
+ "当我们把对象传递给方法时,会发生别名现象。"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "// operators/PassObject.java\n",
+ "// 正在传递的对象可能不是你之前使用的\n",
+ "class Letter {\n",
+ " char c;\n",
+ "}\n",
+ "\n",
+ "public class PassObject {\n",
+ " static void f(Letter y) {\n",
+ " y.c = 'z';\n",
+ " }\n",
+ " \n",
+ " public static void main(String[] args) {\n",
+ " Letter x = new Letter();\n",
+ " x.c = 'a';\n",
+ " System.out.println(\"1: x.c: \" + x.c);\n",
+ " f(x);\n",
+ " System.out.println(\"2: x.c: \" + x.c);\n",
+ " }\n",
+ "}"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "输出结果:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "1: x.c: a\n",
+ "2: x.c: z"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "在许多编程语言中,方法 `f()` 似乎会在内部复制其参数 **Letter y**。但是一旦传递了一个引用,那么实际上 `y.c ='z';` 是在方法 `f()` 之外改变对象。别名现象以及其解决方案是个复杂的问题,在附录中有包含:[对象传递和返回](./Appendix-Passing-and-Returning-Objects.md)。意识到这一点,我们可以警惕类似的陷阱。\n",
+ "\n",
+ "\n",
+ "## 算术运算符\n",
+ "\n",
+ "Java 的基本算术运算符与其他大多编程语言是相同的。其中包括加号 `+`、减号 `-`、除号 `/`、乘号 `*` 以及取模 `%`(从整数除法中获得余数)。整数除法会直接砍掉小数,而不是进位。\n",
+ "\n",
+ "Java 也用一种与 C++ 相同的简写形式同时进行运算和赋值操作,由运算符后跟等号表示,并且与语言中的所有运算符一致(只要有意义)。 可用 x += 4 来表示:将 x 的值加上4的结果再赋值给 x。更多代码示例:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "// operators/MathOps.java\n",
+ "// The mathematical operators\n",
+ "import java.util.*;\n",
+ "\n",
+ "public class MathOps {\n",
+ " public static void main(String[] args) {\n",
+ " // Create a seeded random number generator:\n",
+ " Random rand = new Random(47);\n",
+ " int i, j, k;\n",
+ " // Choose value from 1 to 100:\n",
+ " j = rand.nextInt(100) + 1;\n",
+ " System.out.println(\"j : \" + j);\n",
+ " k = rand.nextInt(100) + 1;\n",
+ " System.out.println(\"k : \" + k);\n",
+ " i = j + k;\n",
+ " System.out.println(\"j + k : \" + i);\n",
+ " i = j - k;\n",
+ " System.out.println(\"j - k : \" + i);\n",
+ " i = k / j;\n",
+ " System.out.println(\"k / j : \" + i);\n",
+ " i = k * j;\n",
+ " System.out.println(\"k * j : \" + i);\n",
+ " i = k % j;\n",
+ " System.out.println(\"k % j : \" + i);\n",
+ " j %= k;\n",
+ " System.out.println(\"j %= k : \" + j);\n",
+ " // 浮点运算测试\n",
+ " float u, v, w; // Applies to doubles, too\n",
+ " v = rand.nextFloat();\n",
+ " System.out.println(\"v : \" + v);\n",
+ " w = rand.nextFloat();\n",
+ " System.out.println(\"w : \" + w);\n",
+ " u = v + w;\n",
+ " System.out.println(\"v + w : \" + u);\n",
+ " u = v - w;\n",
+ " System.out.println(\"v - w : \" + u);\n",
+ " u = v * w;\n",
+ " System.out.println(\"v * w : \" + u);\n",
+ " u = v / w;\n",
+ " System.out.println(\"v / w : \" + u);\n",
+ " // 下面的操作同样适用于 char, \n",
+ " // byte, short, int, long, and double:\n",
+ " u += v;\n",
+ " System.out.println(\"u += v : \" + u);\n",
+ " u -= v;\n",
+ " System.out.println(\"u -= v : \" + u);\n",
+ " u *= v;\n",
+ " System.out.println(\"u *= v : \" + u);\n",
+ " u /= v;\n",
+ " System.out.println(\"u /= v : \" + u); \n",
+ " }\n",
+ "}\n"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "输出结果:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "j : 59\n",
+ "k : 56\n",
+ "j + k : 115\n",
+ "j - k : 3\n",
+ "k / j : 0\n",
+ "k * j : 3304\n",
+ "k % j : 56\n",
+ "j %= k : 3\n",
+ "v : 0.5309454\n",
+ "w : 0.0534122\n",
+ "v + w : 0.5843576\n",
+ "v - w : 0.47753322\n",
+ "v * w : 0.028358962\n",
+ "v / w : 9.940527\n",
+ "u += v : 10.471473\n",
+ "u -= v : 9.940527\n",
+ "u *= v : 5.2778773\n",
+ "u /= v : 9.940527"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "为了生成随机数字,程序首先创建一个 **Random** 对象。不带参数的 **Random** 对象会利用当前的时间用作随机数生成器的“种子”(seed),从而为程序的每次执行生成不同的输出。在本书的示例中,重要的是每个示例末尾的输出尽可能一致,以便可以使用外部工具进行验证。所以我们通过在创建 **Random** 对象时提供种子(随机数生成器的初始化值,其始终为特定种子值产生相同的序列),让程序每次执行都生成相同的随机数,如此以来输出结果就是可验证的 [^1]。 若需要生成随机值,可删除代码示例中的种子参数。该对象通过调用方法 `nextInt()` 和 `nextFloat()`(还可以调用 `nextLong()` 或 `nextDouble()`),使用 **Random** 对象生成许多不同类型的随机数。`nextInt()` 的参数设置生成的数字的上限,下限为零,为了避免零除的可能性,结果偏移1。\n",
+ "\n",
+ "\n",
+ "### 一元加减运算符\n",
+ "\n",
+ "一元加 `+` 减 `-` 运算符的操作和二元是相同的。编译器可自动识别使用何种方式解析运算:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "x = -a;"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "上例的代码表意清晰,编译器可正确识别。下面再看一个示例:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "x = a * -b;"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "虽然编译器可以正确的识别,但是程序员可能会迷惑。为了避免混淆,推荐下面的写法:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "x = a * (-b);"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "一元减号可以得到数据的负值。一元加号的作用相反,不过它唯一能影响的就是把较小的数值类型自动转换为 **int** 类型。\n",
+ "\n",
+ "\n",
+ "## 递增和递减\n",
+ "\n",
+ "和 C 语言类似,Java 提供了许多快捷运算方式。快捷运算可使代码可读性,可写性都更强。其中包括递增 `++` 和递减 `--`,意为“增加或减少一个单位”。举个例子来说,假设 a 是一个 **int** 类型的值,则表达式 `++a` 就等价于 `a = a + 1`。 递增和递减运算符不仅可以修改变量,还可以生成变量的值。\n",
+ "\n",
+ "每种类型的运算符,都有两个版本可供选用;通常将其称为“前缀”和“后缀”。“前递增”表示 `++` 运算符位于变量或表达式的前面;而“后递增”表示 `++` 运算符位于变量的后面。类似地,“前递减”意味着 `--` 运算符位于变量的前面;而“后递减”意味着 `--` 运算符位于变量的后面。对于前递增和前递减(如 `++a` 或 `--a`),会先执行递增/减运算,再返回值。而对于后递增和后递减(如 `a++` 或 `a--`),会先返回值,再执行递增/减运算。代码示例:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "// operators/AutoInc.java\n",
+ "// 演示 ++ 和 -- 运算符\n",
+ "public class AutoInc {\n",
+ " public static void main(String[] args) {\n",
+ " int i = 1;\n",
+ " System.out.println(\"i: \" + i);\n",
+ " System.out.println(\"++i: \" + ++i); // 前递增\n",
+ " System.out.println(\"i++: \" + i++); // 后递增\n",
+ " System.out.println(\"i: \" + i);\n",
+ " System.out.println(\"--i: \" + --i); // 前递减\n",
+ " System.out.println(\"i--: \" + i--); // 后递减\n",
+ " System.out.println(\"i: \" + i);\n",
+ " }\n",
+ "}"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "输出结果:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "i: 1\n",
+ "++i: 2\n",
+ "i++: 2\n",
+ "i: 3\n",
+ "--i: 2\n",
+ "i--: 2\n",
+ "i: 1"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "对于前缀形式,我们将在执行递增/减操作后获取值;使用后缀形式,我们将在执行递增/减操作之前获取值。它们是唯一具有“副作用”的运算符(除那些涉及赋值的以外) —— 它们修改了操作数的值。\n",
+ "\n",
+ "C++ 名称来自于递增运算符,暗示着“比 C 更进一步”。在早期的 Java 演讲中,*Bill Joy*(Java 作者之一)说“**Java = C++ --**”(C++ 减减),意味着 Java 在 C++ 的基础上减少了许多不必要的东西,因此语言更简单。随着进一步地学习,我们会发现 Java 的确有许多地方相对 C++ 来说更简便,但是在其他方面,难度并不会比 C++ 小多少。\n",
+ "\n",
+ "\n",
+ "## 关系运算符\n",
+ "\n",
+ "关系运算符会通过产生一个布尔(**boolean**)结果来表示操作数之间的关系。如果关系为真,则结果为 **true**,如果关系为假,则结果为 **false**。关系运算符包括小于 `<`,大于 `>`,小于或等于 `<=`,大于或等于 `>=`,等于 `==` 和不等于 `!=`。`==` 和 `!=` 可用于所有基本类型,但其他运算符不能用于基本类型 **boolean**,因为布尔值只能表示 **true** 或 **false**,所以比较它们之间的“大于”或“小于”没有意义。\n",
+ "\n",
+ "\n",
+ "### 测试对象等价\n",
+ "\n",
+ "关系运算符 `==` 和 `!=` 同样适用于所有对象之间的比较运算,但它们比较的内容却经常困扰 Java 的初学者。下面是代码示例:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "// operators/Equivalence.java\n",
+ "public class Equivalence {\n",
+ " public static void main(String[] args) {\n",
+ " Integer n1 = 47;\n",
+ " Integer n2 = 47;\n",
+ " System.out.println(n1 == n2);\n",
+ " System.out.println(n1 != n2);\n",
+ " }\n",
+ "}"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "输出结果:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "true\n",
+ "false"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "表达式 `System.out.println(n1 == n2)` 将会输出比较的结果。因为两个 **Integer** 对象相同,所以先输出 **true**,再输出 **false**。但是,尽管对象的内容一样,对象的引用却不一样。`==` 和 `!=` 比较的是对象引用,所以输出实际上应该是先输出 **false**,再输出 **true**(译者注:如果你把 47 改成 128,那么打印的结果就是这样,因为 Integer 内部维护着一个 IntegerCache 的缓存,默认缓存范围是 [-128, 127],所以 [-128, 127] 之间的值用 `==` 和 `!=` 比较也能能到正确的结果,但是不推荐用关系运算符比较,具体见 JDK 中的 Integer 类源码)。\n",
+ "\n",
+ "那么怎么比较两个对象的内容是否相同呢?你必须使用所有对象(不包括基本类型)中都存在的 `equals()` 方法,下面是如何使用 `equals()` 方法的示例:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "// operators/EqualsMethod.java\n",
+ "public class EqualsMethod {\n",
+ " public static void main(String[] args) {\n",
+ " Integer n1 = 47;\n",
+ " Integer n2 = 47;\n",
+ " System.out.println(n1.equals(n2));\n",
+ " }\n",
+ "}"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "输出结果:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "true"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "上例的结果看起来是我们所期望的。但其实事情并非那么简单。下面我们来创建自己的类:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "// operators/EqualsMethod2.java\n",
+ "// 默认的 equals() 方法没有比较内容\n",
+ "class Value {\n",
+ " int i;\n",
+ "}\n",
+ "\n",
+ "public class EqualsMethod2 {\n",
+ " public static void main(String[] args) {\n",
+ " Value v1 = new Value();\n",
+ " Value v2 = new Value();\n",
+ " v1.i = v2.i = 100;\n",
+ " System.out.println(v1.equals(v2));\n",
+ " }\n",
+ "}"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "输出结果:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "false"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "上例的结果再次令人困惑:结果是 **false**。原因: `equals()` 的默认行为是比较对象的引用而非具体内容。因此,除非你在新类中覆写 `equals()` 方法,否则我们将获取不到想要的结果。不幸的是,在学习 [复用](./08-Reuse.md)(**Reuse**) 章节后我们才能接触到“覆写”(**Override**),并且直到 [附录:集合主题](./Appendix-Collection-Topics.md),才能知道定义 `equals()` 方法的正确方式,但是现在明白 `equals()` 行为方式也可能为你节省一些时间。\n",
+ "\n",
+ "大多数 Java 库类通过覆写 `equals()` 方法比较对象的内容而不是其引用。\n",
+ "\n",
+ "\n",
+ "## 逻辑运算符\n",
+ "\n",
+ "每个逻辑运算符 `&&` (**AND**)、`||`(**OR**)和 `!`(**非**)根据参数的逻辑关系生成布尔值 `true` 或 `false`。下面的代码示例使用了关系运算符和逻辑运算符:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "// operators/Bool.java\n",
+ "// 关系运算符和逻辑运算符\n",
+ "import java.util.*;\n",
+ "public class Bool {\n",
+ " public static void main(String[] args) {\n",
+ " Random rand = new Random(47);\n",
+ " int i = rand.nextInt(100);\n",
+ " int j = rand.nextInt(100);\n",
+ " System.out.println(\"i = \" + i);\n",
+ " System.out.println(\"j = \" + j);\n",
+ " System.out.println(\"i > j is \" + (i > j));\n",
+ " System.out.println(\"i < j is \" + (i < j));\n",
+ " System.out.println(\"i >= j is \" + (i >= j));\n",
+ " System.out.println(\"i <= j is \" + (i <= j));\n",
+ " System.out.println(\"i == j is \" + (i == j));\n",
+ " System.out.println(\"i != j is \" + (i != j));\n",
+ " // 将 int 作为布尔处理不是合法的 Java 写法\n",
+ " //- System.out.println(\"i && j is \" + (i && j));\n",
+ " //- System.out.println(\"i || j is \" + (i || j));\n",
+ " //- System.out.println(\"!i is \" + !i);\n",
+ " System.out.println(\"(i < 10) && (j < 10) is \"\n",
+ " + ((i < 10) && (j < 10)) );\n",
+ " System.out.println(\"(i < 10) || (j < 10) is \"\n",
+ " + ((i < 10) || (j < 10)) );\n",
+ " }\n",
+ "}"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "输出结果:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "i = 58\n",
+ "j = 55\n",
+ "i > j is true\n",
+ "i < j is false\n",
+ "i >= j is true\n",
+ "i <= j is false\n",
+ "i == j is false\n",
+ "i != j is true\n",
+ "(i < 10) && (j < 10) is false\n",
+ "(i < 10) || (j < 10) is false"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "在 Java 逻辑运算中,我们不能像 C/C++ 那样使用非布尔值, 而仅能使用 **AND**、 **OR**、 **NOT**。上面的例子中,我们将使用非布尔值的表达式注释掉了(你可以看到表达式前面是 //-)。但是,后续的表达式使用关系比较生成布尔值,然后对结果使用了逻辑运算。请注意,如果在预期为 **String** 类型的位置使用 **boolean** 类型的值,则结果会自动转为适当的文本格式(即 \"true\" 或 \"false\" 字符串)。\n",
+ "\n",
+ "我们可以将前一个程序中 **int** 的定义替换为除 **boolean** 之外的任何其他基本数据类型。但请注意,**float** 类型的数值比较非常严格,只要两个数字的最小位不同则两个数仍然不相等;只要数字最小位是大于 0 的,那么它就不等于 0。\n",
+ "\n",
+ "\n",
+ "### 短路\n",
+ "\n",
+ "逻辑运算符支持一种称为“短路”(short-circuiting)的现象。整个表达式会在运算到可以明确结果时就停止并返回结果,这意味着该逻辑表达式的后半部分不会被执行到。代码示例:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "// operators / ShortCircuit.java \n",
+ "// 逻辑运算符的短路行为\n",
+ "public class ShortCircuit {\n",
+ "\n",
+ " static boolean test1(int val) {\n",
+ " System.out.println(\"test1(\" + val + \")\");\n",
+ " System.out.println(\"result: \" + (val < 1));\n",
+ " return val < 1;\n",
+ " }\n",
+ "\n",
+ " static boolean test2(int val) {\n",
+ " System.out.println(\"test2(\" + val + \")\");\n",
+ " System.out.println(\"result: \" + (val < 2));\n",
+ " return val < 2;\n",
+ " }\n",
+ "\n",
+ " static boolean test3(int val) {\n",
+ " System.out.println(\"test3(\" + val + \")\");\n",
+ " System.out.println(\"result: \" + (val < 3));\n",
+ " return val < 3;\n",
+ " }\n",
+ "\n",
+ " public static void main(String[] args) {\n",
+ " boolean b = test1(0) && test2(2) && test3(2);\n",
+ " System.out.println(\"expression is \" + b);\n",
+ " }\n",
+ "}"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "输出结果:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "test1(0)\n",
+ "result: true\n",
+ "test2(2)\n",
+ "result: false\n",
+ "expression is false"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "每个测试都对参数执行比较并返回 `true` 或 `false`。同时控制台也会在方法执行时打印他们的执行状态。 下面的表达式:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "test1(0)&& test2(2)&& test3(2)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "可能你的预期是程序会执行 3 个 **test** 方法并返回。我们来分析一下:第一个方法的结果返回 `true`,因此表达式会继续走下去。紧接着,第二个方法的返回结果是 `false`。这就代表这整个表达式的结果肯定为 `false`,所以就没有必要再判断剩下的表达式部分了。\n",
+ "\n",
+ "所以,运用“短路”可以节省部分不必要的运算,从而提高程序潜在的性能。\n",
+ "\n",
+ "\n",
+ "## 字面值常量\n",
+ "\n",
+ "通常,当我们向程序中插入一个字面值常量(**Literal**)时,编译器会确切地识别它的类型。当类型不明确时,必须辅以字面值常量关联来帮助编译器识别。代码示例:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "// operators/Literals.java\n",
+ "public class Literals {\n",
+ " public static void main(String[] args) {\n",
+ " int i1 = 0x2f; // 16进制 (小写)\n",
+ " System.out.println(\n",
+ " \"i1: \" + Integer.toBinaryString(i1));\n",
+ " int i2 = 0X2F; // 16进制 (大写)\n",
+ " System.out.println(\n",
+ " \"i2: \" + Integer.toBinaryString(i2));\n",
+ " int i3 = 0177; // 8进制 (前导0)\n",
+ " System.out.println(\n",
+ " \"i3: \" + Integer.toBinaryString(i3));\n",
+ " char c = 0xffff; // 最大 char 型16进制值\n",
+ " System.out.println(\n",
+ " \"c: \" + Integer.toBinaryString(c));\n",
+ " byte b = 0x7f; // 最大 byte 型16进制值 10101111;\n",
+ " System.out.println(\n",
+ " \"b: \" + Integer.toBinaryString(b));\n",
+ " short s = 0x7fff; // 最大 short 型16进制值\n",
+ " System.out.println(\n",
+ " \"s: \" + Integer.toBinaryString(s));\n",
+ " long n1 = 200L; // long 型后缀\n",
+ " long n2 = 200l; // long 型后缀 (容易与数值1混淆)\n",
+ " long n3 = 200;\n",
+ " \n",
+ " // Java 7 二进制字面值常量:\n",
+ " byte blb = (byte)0b00110101;\n",
+ " System.out.println(\n",
+ " \"blb: \" + Integer.toBinaryString(blb));\n",
+ " short bls = (short)0B0010111110101111;\n",
+ " System.out.println(\n",
+ " \"bls: \" + Integer.toBinaryString(bls));\n",
+ " int bli = 0b00101111101011111010111110101111;\n",
+ " System.out.println(\n",
+ " \"bli: \" + Integer.toBinaryString(bli));\n",
+ " long bll = 0b00101111101011111010111110101111;\n",
+ " System.out.println(\n",
+ " \"bll: \" + Long.toBinaryString(bll));\n",
+ " float f1 = 1;\n",
+ " float f2 = 1F; // float 型后缀\n",
+ " float f3 = 1f; // float 型后缀\n",
+ " double d1 = 1d; // double 型后缀\n",
+ " double d2 = 1D; // double 型后缀\n",
+ " // (long 型的字面值同样适用于十六进制和8进制 )\n",
+ " }\n",
+ "}"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "输出结果:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "i1: 101111\n",
+ "i2: 101111\n",
+ "i3: 1111111\n",
+ "c: 1111111111111111\n",
+ "b: 1111111\n",
+ "s: 111111111111111\n",
+ "blb: 110101\n",
+ "bls: 10111110101111\n",
+ "bli: 101111101011111010111110101111\n",
+ "bll: 101111101011111010111110101111"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "在文本值的后面添加字符可以让编译器识别该文本值的类型。对于 **Long** 型数值,结尾使用大写 `L` 或小写 `l` 皆可(不推荐使用 `l`,因为容易与阿拉伯数值 1 混淆)。大写 `F` 或小写 `f` 表示 **float** 浮点数。大写 `D` 或小写 `d` 表示 **double** 双精度。\n",
+ "\n",
+ "十六进制(以 16 为基数),适用于所有整型数据类型,由前导 `0x` 或 `0X` 表示,后跟 0-9 或 a-f (大写或小写)。如果我们在初始化某个类型的数值时,赋值超出其范围,那么编译器会报错(不管值的数字形式如何)。在上例的代码中,**char**、**byte** 和 **short** 的值已经是最大了。如果超过这些值,编译器将自动转型为 **int**,并且提示我们需要声明强制转换(强制转换将在本章后面定义),意味着我们已越过该类型的范围界限。\n",
+ "\n",
+ "八进制(以 8 为基数)由 0~7 之间的数字和前导零 `0` 表示。\n",
+ "\n",
+ "Java 7 引入了二进制的字面值常量,由前导 `0b` 或 `0B` 表示,它可以初始化所有的整数类型。\n",
+ "\n",
+ "使用整型数值类型时,显示其二进制形式会很有用。在 Long 型和 Integer 型中这很容易实现,调用其静态的 `toBinaryString()` 方法即可。 但是请注意,若将较小的类型传递给 **Integer.**`tobinarystring()` 时,类型将自动转换为 **int**。\n",
+ "\n",
+ "\n",
+ "### 下划线\n",
+ "\n",
+ "Java 7 中有一个深思熟虑的补充:我们可以在数字字面量中包含下划线 `_`,以使结果更清晰。这对于大数值的分组特别有用。代码示例:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "// operators/Underscores.java\n",
+ "public class Underscores {\n",
+ " public static void main(String[] args) {\n",
+ " double d = 341_435_936.445_667;\n",
+ " System.out.println(d);\n",
+ " int bin = 0b0010_1111_1010_1111_1010_1111_1010_1111;\n",
+ " System.out.println(Integer.toBinaryString(bin));\n",
+ " System.out.printf(\"%x%n\", bin); // [1]\n",
+ " long hex = 0x7f_e9_b7_aa;\n",
+ " System.out.printf(\"%x%n\", hex);\n",
+ " }\n",
+ "}"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "输出结果:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "3.41435936445667E8\n",
+ "101111101011111010111110101111\n",
+ "2fafafaf\n",
+ "7fe9b7aa"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "下面是合理使用的规则:\n",
+ "\n",
+ "1. 仅限单 `_`,不能多条相连。\n",
+ "2. 数值开头和结尾不允许出现 `_`。\n",
+ "3. `F`、`D` 和 `L`的前后禁止出现 `_`。\n",
+ "4. 二进制前导 `b` 和 十六进制 `x` 前后禁止出现 `_`。\n",
+ "\n",
+ "[1] 注意 `%n`的使用。熟悉 C 风格的程序员可能习惯于看到 `\\n` 来表示换行符。问题在于它给你的是一个“Unix风格”的换行符。此外,如果我们使用的是 Windows,则必须指定 `\\r\\n`。这种差异的包袱应该由编程语言来解决。这就是 Java 用 `%n` 实现的可以忽略平台间差异而生成适当的换行符,但只有当你使用 `System.out.printf()` 或 `System.out.format()` 时。对于 `System.out.println()`,我们仍然必须使用 `\\n`;如果你使用 `%n`,`println()` 只会输出 `%n` 而不是换行符。\n",
+ "\n",
+ "\n",
+ "### 指数计数法\n",
+ "\n",
+ "指数总是采用一种我认为很不直观的记号方法:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "// operators/Exponents.java\n",
+ "// \"e\" 表示 10 的几次幂\n",
+ "public class Exponents {\n",
+ " public static void main(String[] args) {\n",
+ " // 大写 E 和小写 e 的效果相同:\n",
+ " float expFloat = 1.39e-43f;\n",
+ " expFloat = 1.39E-43f;\n",
+ " System.out.println(expFloat);\n",
+ " double expDouble = 47e47d; // 'd' 是可选的\n",
+ " double expDouble2 = 47e47; // 自动转换为 double\n",
+ " System.out.println(expDouble);\n",
+ " }\n",
+ "}"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "输出结果:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "1.39E-43\n",
+ "4.7E48"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "在科学与工程学领域,**e** 代表自然对数的基数,约等于 2.718 (Java 里用一种更精确的 **double** 值 **Math.E** 来表示自然对数)。指数表达式 \"1.39 x e-43\",意味着 “1.39 × 2.718 的 -43 次方”。然而,自 FORTRAN 语言发明后,人们自然而然地觉得e 代表 “10 的几次幂”。这种做法显得颇为古怪,因为 FORTRAN 最初是为科学与工程领域设计的。\n",
+ "\n",
+ "理所当然,它的设计者应对这样的混淆概念持谨慎态度 [^2]。但不管怎样,这种特别的表达方法在 C,C++ 以及现在的 Java 中顽固地保留下来了。所以倘若习惯 e 作为自然对数的基数使用,那么在 Java 中看到类似“1.39e-43f”这样的表达式时,请转换你的思维,从程序设计的角度思考它;它真正的含义是 “1.39 × 10 的 -43 次方”。\n",
+ "\n",
+ "注意如果编译器能够正确地识别类型,就不必使用后缀字符。对于下述语句:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "long n3 = 200;"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "它并不存在含糊不清的地方,所以 200 后面的 L 大可省去。然而,对于下述语句:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "float f4 = 1e-43f; //10 的幂数"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "编译器通常会将指数作为 **double** 类型来处理,所以假若没有这个后缀字符 `f`,编译器就会报错,提示我们应该将 **double** 型转换成 **float** 型。\n",
+ "\n",
+ "\n",
+ "## 位运算符\n",
+ "\n",
+ "位运算符允许我们操作一个整型数字中的单个二进制位。位运算符会对两个整数对应的位执行布尔代数,从而产生结果。\n",
+ "\n",
+ "位运算源自 C 语言的底层操作。我们经常要直接操纵硬件,频繁设置硬件寄存器内的二进制位。Java 的设计初衷是电视机顶盒嵌入式开发,所以这种底层的操作仍被保留了下来。但是,你可能不会使用太多位运算。\n",
+ "\n",
+ "若两个输入位都是 1,则按位“与运算符” `&` 运算后结果是 1,否则结果是 0。若两个输入位里至少有一个是 1,则按位“或运算符” `|` 运算后结果是 1;只有在两个输入位都是 0 的情况下,运算结果才是 0。若两个输入位的某一个是 1,另一个不是 1,那么按位“异或运算符” `^` 运算后结果才是 1。按位“非运算符” `~` 属于一元运算符;它只对一个自变量进行操作(其他所有运算符都是二元运算符)。按位非运算后结果与输入位相反。例如输入 0,则输出 1;输入 1,则输出 0。\n",
+ "\n",
+ "位运算符和逻辑运算符都使用了同样的字符,只不过数量不同。位短,所以位运算符只有一个字符。位运算符可与等号 `=` 联合使用以接收结果及赋值:`&=`,`|=` 和 `^=` 都是合法的(由于 `~` 是一元运算符,所以不可与 `=` 联合使用)。\n",
+ "\n",
+ "我们将 **Boolean** 类型被视为“单位值”(one-bit value),所以它多少有些独特的地方。我们可以对 boolean 型变量执行与、或、异或运算,但不能执行非运算(大概是为了避免与逻辑“非”混淆)。对于布尔值,位运算符具有与逻辑运算符相同的效果,只是它们不会中途“短路”。此外,针对布尔值进行的位运算为我们新增了一个“异或”逻辑运算符,它并未包括在逻辑运算符的列表中。在移位表达式中,禁止使用布尔值,原因将在下面解释。\n",
+ "\n",
+ "\n",
+ "## 移位运算符\n",
+ "\n",
+ "移位运算符面向的运算对象也是二进制的“位”。它们只能用于处理整数类型(基本类型的一种)。左移位运算符 `<<` 能将其左边的运算对象向左移动右侧指定的位数(在低位补 0)。右移位运算符 `>>` 则相反。右移位运算符有“正”、“负”值:若值为正,则在高位插入 0;若值为负,则在高位插入 1。Java 也添加了一种“不分正负”的右移位运算符(>>>),它使用了“零扩展”(zero extension):无论正负,都在高位插入 0。这一运算符是 C/C++ 没有的。\n",
+ "\n",
+ "如果移动 **char**、**byte** 或 **short**,则会在移动发生之前将其提升为 **int**,结果为 **int**。仅使用右值(rvalue)的 5 个低阶位。这可以防止我们移动超过 **int** 范围的位数。若对一个 **long** 值进行处理,最后得到的结果也是 **long**。\n",
+ "\n",
+ "移位可以与等号 `<<=` 或 `>>=` 或 `>>>=` 组合使用。左值被替换为其移位运算后的值。但是,问题来了,当无符号右移与赋值相结合时,若将其与 **byte** 或 **short** 一起使用的话,则结果错误。取而代之的是,它们被提升为 **int** 型并右移,但在重新赋值时被截断。在这种情况下,结果为 -1。下面是代码示例:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "// operators/URShift.java\n",
+ "// 测试无符号右移\n",
+ "public class URShift {\n",
+ " public static void main(String[] args) {\n",
+ " int i = -1;\n",
+ " System.out.println(Integer.toBinaryString(i));\n",
+ " i >>>= 10;\n",
+ " System.out.println(Integer.toBinaryString(i));\n",
+ " long l = -1;\n",
+ " System.out.println(Long.toBinaryString(l));\n",
+ " l >>>= 10;\n",
+ " System.out.println(Long.toBinaryString(l));\n",
+ " short s = -1;\n",
+ " System.out.println(Integer.toBinaryString(s));\n",
+ " s >>>= 10;\n",
+ " System.out.println(Integer.toBinaryString(s));\n",
+ " byte b = -1;\n",
+ " System.out.println(Integer.toBinaryString(b));\n",
+ " b >>>= 10;\n",
+ " System.out.println(Integer.toBinaryString(b));\n",
+ " b = -1;\n",
+ " System.out.println(Integer.toBinaryString(b));\n",
+ " System.out.println(Integer.toBinaryString(b>>>10));\n",
+ " }\n",
+ "}"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "输出结果:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "11111111111111111111111111111111\n",
+ "1111111111111111111111\n",
+ "1111111111111111111111111111111111111111111111111111111111111111\n",
+ "111111111111111111111111111111111111111111111111111111\n",
+ "11111111111111111111111111111111\n",
+ "11111111111111111111111111111111\n",
+ "11111111111111111111111111111111\n",
+ "11111111111111111111111111111111\n",
+ "11111111111111111111111111111111\n",
+ "1111111111111111111111"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "在上例中,结果并未重新赋值给变量 **b** ,而是直接打印出来,因此一切正常。下面是一个涉及所有位运算符的代码示例:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "// operators/BitManipulation.java\n",
+ "// 使用位运算符\n",
+ "import java.util.*;\n",
+ "public class BitManipulation {\n",
+ " public static void main(String[] args) {\n",
+ " Random rand = new Random(47);\n",
+ " int i = rand.nextInt();\n",
+ " int j = rand.nextInt();\n",
+ " printBinaryInt(\"-1\", -1);\n",
+ " printBinaryInt(\"+1\", +1);\n",
+ " int maxpos = 2147483647;\n",
+ " printBinaryInt(\"maxpos\", maxpos);\n",
+ " int maxneg = -2147483648;\n",
+ " printBinaryInt(\"maxneg\", maxneg);\n",
+ " printBinaryInt(\"i\", i);\n",
+ " printBinaryInt(\"~i\", ~i);\n",
+ " printBinaryInt(\"-i\", -i);\n",
+ " printBinaryInt(\"j\", j);\n",
+ " printBinaryInt(\"i & j\", i & j);\n",
+ " printBinaryInt(\"i | j\", i | j);\n",
+ " printBinaryInt(\"i ^ j\", i ^ j);\n",
+ " printBinaryInt(\"i << 5\", i << 5);\n",
+ " printBinaryInt(\"i >> 5\", i >> 5);\n",
+ " printBinaryInt(\"(~i) >> 5\", (~i) >> 5);\n",
+ " printBinaryInt(\"i >>> 5\", i >>> 5);\n",
+ " printBinaryInt(\"(~i) >>> 5\", (~i) >>> 5);\n",
+ " long l = rand.nextLong();\n",
+ " long m = rand.nextLong();\n",
+ " printBinaryLong(\"-1L\", -1L);\n",
+ " printBinaryLong(\"+1L\", +1L);\n",
+ " long ll = 9223372036854775807L;\n",
+ " printBinaryLong(\"maxpos\", ll);\n",
+ " long lln = -9223372036854775808L;\n",
+ " printBinaryLong(\"maxneg\", lln);\n",
+ " printBinaryLong(\"l\", l);\n",
+ " printBinaryLong(\"~l\", ~l);\n",
+ " printBinaryLong(\"-l\", -l);\n",
+ " printBinaryLong(\"m\", m);\n",
+ " printBinaryLong(\"l & m\", l & m);\n",
+ " printBinaryLong(\"l | m\", l | m);\n",
+ " printBinaryLong(\"l ^ m\", l ^ m);\n",
+ " printBinaryLong(\"l << 5\", l << 5);\n",
+ " printBinaryLong(\"l >> 5\", l >> 5);\n",
+ " printBinaryLong(\"(~l) >> 5\", (~l) >> 5);\n",
+ " printBinaryLong(\"l >>> 5\", l >>> 5);\n",
+ " printBinaryLong(\"(~l) >>> 5\", (~l) >>> 5);\n",
+ " }\n",
+ "\n",
+ " static void printBinaryInt(String s, int i) {\n",
+ " System.out.println(\n",
+ " s + \", int: \" + i + \", binary:\\n \" +\n",
+ " Integer.toBinaryString(i));\n",
+ " }\n",
+ "\n",
+ " static void printBinaryLong(String s, long l) {\n",
+ " System.out.println(\n",
+ " s + \", long: \" + l + \", binary:\\n \" +\n",
+ " Long.toBinaryString(l));\n",
+ " }\n",
+ "}"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "输出结果(前 32 行):"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "-1, int: -1, binary:\n",
+ "11111111111111111111111111111111\n",
+ "+1, int: 1, binary:\n",
+ "1\n",
+ "maxpos, int: 2147483647, binary:\n",
+ "1111111111111111111111111111111\n",
+ "maxneg, int: -2147483648, binary:\n",
+ "10000000000000000000000000000000\n",
+ "i, int: -1172028779, binary:\n",
+ "10111010001001000100001010010101\n",
+ "~i, int: 1172028778, binary:\n",
+ " 1000101110110111011110101101010\n",
+ "-i, int: 1172028779, binary:\n",
+ "1000101110110111011110101101011\n",
+ "j, int: 1717241110, binary:\n",
+ "1100110010110110000010100010110\n",
+ "i & j, int: 570425364, binary:\n",
+ "100010000000000000000000010100\n",
+ "i | j, int: -25213033, binary:\n",
+ "11111110011111110100011110010111\n",
+ "i ^ j, int: -595638397, binary:\n",
+ "11011100011111110100011110000011\n",
+ "i << 5, int: 1149784736, binary:\n",
+ "1000100100010000101001010100000\n",
+ "i >> 5, int: -36625900, binary:\n",
+ "11111101110100010010001000010100\n",
+ "(~i) >> 5, int: 36625899, binary:\n",
+ "10001011101101110111101011\n",
+ "i >>> 5, int: 97591828, binary:\n",
+ "101110100010010001000010100\n",
+ "(~i) >>> 5, int: 36625899, binary:\n",
+ "10001011101101110111101011\n",
+ " ..."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "结尾的两个方法 `printBinaryInt()` 和 `printBinaryLong()` 分别操作一个 **int** 和 **long** 值,并转换为二进制格式输出,同时附有简要的文字说明。除了演示 **int** 和 **long** 的所有位运算符的效果之外,本示例还显示 **int** 和 **long** 的最小值、最大值、+1 和 -1 值,以便我们了解它们的形式。注意高位代表符号:0 表示正,1 表示负。上面显示了 **int** 部分的输出。以上数字的二进制表示形式是带符号的补码(2's complement)。\n",
+ "\n",
+ "\n",
+ "## 三元运算符\n",
+ "\n",
+ "三元运算符,也称为条件运算符。这种运算符比较罕见,因为它有三个运算对象。但它确实属于运算符的一种,因为它最终也会生成一个值。这与本章后一节要讲述的普通 **if-else** 语句是不同的。下面是它的表达式格式:\n",
+ "\n",
+ "**布尔表达式 ? 值 1 : 值 2**\n",
+ "\n",
+ "若表达式计算为 **true**,则返回结果 **值 1** ;如果表达式的计算为 **false**,则返回结果 **值 2**。\n",
+ "\n",
+ "当然,也可以换用普通的 **if-else** 语句(在后面介绍),但三元运算符更加简洁。作为三元运算符的创造者, C 自诩为一门简练的语言。三元运算符的引入多半就是为了高效编程,但假若我们打算频繁使用它的话,还是先多作一些思量: 它易于产生可读性差的代码。与 **if-else** 不同的是,三元运算符是有返回结果的。请看下面的代码示例:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "// operators/TernaryIfElse.java\n",
+ "public class TernaryIfElse {\n",
+ " \n",
+ "static int ternary(int i) {\n",
+ " return i < 10 ? i * 100 : i * 10;\n",
+ "}\n",
+ "\n",
+ "static int standardIfElse(int i) {\n",
+ " if(i < 10)\n",
+ " return i * 100;\n",
+ " else\n",
+ " return i * 10;\n",
+ "}\n",
+ "\n",
+ " public static void main(String[] args) {\n",
+ " System.out.println(ternary(9));\n",
+ " System.out.println(ternary(10));\n",
+ " System.out.println(standardIfElse(9));\n",
+ " System.out.println(standardIfElse(10));\n",
+ " }\n",
+ "}"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "输出结果:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "900\n",
+ "100\n",
+ "900\n",
+ "100"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "可以看出,`ternary()` 中的代码更简短。然而,**standardIfElse()** 中的代码更易理解且不要求更多的录入。所以我们在挑选三元运算符时,请务必权衡一下利弊。\n",
+ "\n",
+ "\n",
+ "## 字符串运算符\n",
+ "\n",
+ "这个运算符在 Java 里有一项特殊用途:连接字符串。这点已在前面展示过了。尽管与 `+` 的传统意义不符,但如此使用也还是比较自然的。这一功能看起来还不错,于是在 C++ 里引入了“运算符重载”机制,以便 C++ 程序员为几乎所有运算符增加特殊的含义。但遗憾得是,与 C++ 的一些限制结合以后,它变得复杂。这要求程序员在设计自己的类时必须对此有周全的考虑。虽然在 Java 中实现运算符重载机制并非难事(如 C# 所展示的,它具有简单的运算符重载),但因该特性过于复杂,因此 Java 并未实现它。\n",
+ "\n",
+ "我们注意到运用 `String +` 时有一些有趣的现象。若表达式以一个 **String** 类型开头(编译器会自动将双引号 `\"\"` 标注的的字符序列转换为字符串),那么后续所有运算对象都必须是字符串。代码示例:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "// operators/StringOperators.java\n",
+ "public class StringOperators {\n",
+ " public static void main(String[] args) {\n",
+ " int x = 0, y = 1, z = 2;\n",
+ " String s = \"x, y, z \";\n",
+ " System.out.println(s + x + y + z);\n",
+ " // 将 x 转换为字符串\n",
+ " System.out.println(x + \" \" + s);\n",
+ " s += \"(summed) = \"; \n",
+ " // 级联操作\n",
+ " System.out.println(s + (x + y + z));\n",
+ " // Integer.toString()方法的简写:\n",
+ " System.out.println(\"\" + x);\n",
+ " }\n",
+ "}"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "输出结果:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "x, y, z 012\n",
+ "0 x, y, z\n",
+ "x, y, z (summed) = 3\n",
+ "0"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "**注意**:上例中第 1 输出语句的执行结果是 `012` 而并非 `3`,这是因为编译器将其分别转换为其字符串形式然后与字符串变量 **s** 连接。在第 2 条输出语句中,编译器将开头的变量转换为了字符串,由此可以看出,这种转换与数据的位置无关,只要当中有一条数据是字符串类型,其他非字符串数据都将被转换为字符串形式并连接。最后一条输出语句,我们可以看出 `+=` 运算符可以拼接其右侧的字符串连接结果并重赋值给自身变量 `s`。括号 `()` 可以控制表达式的计算顺序,以便在显示 **int** 之前对其进行实际求和。\n",
+ "\n",
+ "请注意主方法中的最后一个例子:我们经常会看到一个空字符串 `\"\"` 跟着一个基本类型的数据。这样可以隐式地将其转换为字符串,以代替繁琐的显式调用方法(如这里可以使用 **Integer.toString()**)。\n",
+ "\n",
+ "\n",
+ "## 常见陷阱\n",
+ "\n",
+ "使用运算符时很容易犯的一个错误是,在还没搞清楚表达式的计算方式时就试图忽略括号 `()`。在 Java 中也一样。 在 C++ 中你甚至可能犯这样极端的错误.代码示例:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "while(x = y) {\n",
+ "// ...\n",
+ "}"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "显然,程序员原意是测试等价性 `==`,而非赋值 `=`。若变量 **y** 非 0 的话,在 C/C++ 中,这样的赋值操作总会返回 `true`。于是,上面的代码示例将会无限循环。而在 Java 中,这样的表达式结果并不会转化为一个布尔值。 而编译器会试图把这个 **int** 型数据转换为预期应接收的布尔类型。最后,我们将会在试图运行前收到编译期错误。因此,Java 天生避免了这种陷阱发生的可能。\n",
+ "\n",
+ "唯一有种情况例外:当变量 `x` 和 `y` 都是布尔值,例如 `x=y` 是一个逻辑表达式。除此之外,之前的那个例子,很大可能是错误。\n",
+ "\n",
+ "在 C/C++ 里,类似的一个问题还有使用按位“与” `&` 和“或” `|` 运算,而非逻辑“与” `&&` 和“或” `||`。就象 `=` 和 `==` 一样,键入一个字符当然要比键入两个简单。在 Java 中,编译器同样可防止这一点,因为它不允许我们强行使用另一种并不符的类型。\n",
+ "\n",
+ "\n",
+ "## 类型转换\n",
+ "\n",
+ "“类型转换”(Casting)的作用是“与一个模型匹配”。在适当的时候,Java 会将一种数据类型自动转换成另一种。例如,假设我们为 **float** 变量赋值一个整数值,计算机会将 **int** 自动转换成 **float**。我们可以在程序未自动转换时显式、强制地使此类型发生转换。\n",
+ "\n",
+ "要执行强制转换,需要将所需的数据类型放在任何值左侧的括号内,如下所示:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "// operators/Casting.java\n",
+ "public class Casting {\n",
+ " public static void main(String[] args) {\n",
+ " int i = 200;\n",
+ " long lng = (long)i;\n",
+ " lng = i; // 没有必要的类型提升\n",
+ " long lng2 = (long)200;\n",
+ " lng2 = 200;\n",
+ " // 类型收缩\n",
+ " i = (int)lng2; // Cast required\n",
+ " }\n",
+ "}"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "诚然,你可以这样地去转换一个数值类型的变量。但是上例这种做法是多余的:因为编译器会在必要时自动提升 **int** 型数据为 **long** 型。\n",
+ "\n",
+ "当然,为了程序逻辑清晰或提醒自己留意,我们也可以显式地类型转换。在其他情况下,类型转换型只有在代码编译时才显出其重要性。在 C/C++ 中,类型转换有时会让人头痛。在 Java 里,类型转换则是一种比较安全的操作。但是,若将数据类型进行“向下转换”(**Narrowing Conversion**)的操作(将容量较大的数据类型转换成容量较小的类型),可能会发生信息丢失的危险。此时,编译器会强迫我们进行转型,好比在提醒我们:该操作可能危险,若你坚持让我这么做,那么对不起,请明确需要转换的类型。 对于“向上转换”(**Widening conversion**),则不必进行显式的类型转换,因为较大类型的数据肯定能容纳较小类型的数据,不会造成任何信息的丢失。\n",
+ "\n",
+ "除了布尔类型的数据,Java 允许任何基本类型的数据转换为另一种基本类型的数据。此外,类是不能进行类型转换的。为了将一个类转换为另一个类型,需要使用特殊的方法(后面将会学习到如何在父子类之间进行向上/向下转型,例如,“橡树”可以转换为“树”,反之亦然。而对于“岩石”是无法转换为“树”的)。\n",
+ "\n",
+ "\n",
+ "### 截断和舍入\n",
+ "\n",
+ "在执行“向下转换”时,必须注意数据的截断和舍入问题。若从浮点值转换为整型值,Java 会做什么呢?例如:浮点数 29.7 被转换为整型值,结果会是 29 还是 30 呢?下面是代码示例:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "// operators/CastingNumbers.java\n",
+ "// 尝试转换 float 和 double 型数据为整型数据\n",
+ "public class CastingNumbers {\n",
+ " public static void main(String[] args) {\n",
+ " double above = 0.7, below = 0.4;\n",
+ " float fabove = 0.7f, fbelow = 0.4f;\n",
+ " System.out.println(\"(int)above: \" + (int)above);\n",
+ " System.out.println(\"(int)below: \" + (int)below);\n",
+ " System.out.println(\"(int)fabove: \" + (int)fabove);\n",
+ " System.out.println(\"(int)fbelow: \" + (int)fbelow);\n",
+ " }\n",
+ "}"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "输出结果:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "(int)above: 0\n",
+ "(int)below: 0\n",
+ "(int)fabove: 0\n",
+ "(int)fbelow: 0"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "因此,答案是,从 **float** 和 **double** 转换为整数值时,小数位将被截断。若你想对结果进行四舍五入,可以使用 `java.lang.Math` 的 ` round()` 方法:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "// operators/RoundingNumbers.java\n",
+ "// float 和 double 类型数据的四舍五入\n",
+ "public class RoundingNumbers {\n",
+ " public static void main(String[] args) {\n",
+ " double above = 0.7, below = 0.4;\n",
+ " float fabove = 0.7f, fbelow = 0.4f;\n",
+ " System.out.println(\n",
+ " \"Math.round(above): \" + Math.round(above));\n",
+ " System.out.println(\n",
+ " \"Math.round(below): \" + Math.round(below));\n",
+ " System.out.println(\n",
+ " \"Math.round(fabove): \" + Math.round(fabove));\n",
+ " System.out.println(\n",
+ " \"Math.round(fbelow): \" + Math.round(fbelow));\n",
+ " }\n",
+ "}"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "输出结果:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "Math.round(above): 1\n",
+ "Math.round(below): 0\n",
+ "Math.round(fabove): 1\n",
+ "Math.round(fbelow): 0"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "因为 `round()` 方法是 `java.lang` 的一部分,所以我们无需通过 `import` 就可以使用。\n",
+ "\n",
+ "\n",
+ "### 类型提升\n",
+ "\n",
+ "你会发现,如果我们对小于 **int** 的基本数据类型(即 **char**、**byte** 或 **short**)执行任何算术或按位操作,这些值会在执行操作之前类型提升为 **int**,并且结果值的类型为 **int**。若想重新使用较小的类型,必须使用强制转换(由于重新分配回一个较小的类型,结果可能会丢失精度)。通常,表达式中最大的数据类型是决定表达式结果的数据类型。**float** 型和 **double** 型相乘,结果是 **double** 型的;**int** 和 **long** 相加,结果是 **long** 型。\n",
+ "\n",
+ "\n",
+ "## Java没有sizeof\n",
+ "\n",
+ "在 C/C++ 中,经常需要用到 `sizeof()` 方法来获取数据项被分配的字节大小。C/C++ 中使用 `sizeof()` 最有说服力的原因是为了移植性,不同数据在不同机器上可能有不同的大小,所以在进行大小敏感的运算时,程序员必须对这些类型有多大做到心中有数。例如,一台计算机可用 32 位来保存整数,而另一台只用 16 位保存。显然,在第一台机器中,程序可保存更大的值。所以,移植是令 C/C++ 程序员颇为头痛的一个问题。\n",
+ "\n",
+ "Java 不需要 ` sizeof()` 方法来满足这种需求,因为所有类型的大小在不同平台上是相同的。我们不必考虑这个层次的移植问题 —— Java 本身就是一种“与平台无关”的语言。\n",
+ "\n",
+ "\n",
+ "## 运算符总结\n",
+ "\n",
+ "上述示例分别向我们展示了哪些基本类型能被用于特定的运算符。基本上,下面的代码示例是对上述所有示例的重复,只不过概括了所有的基本类型。这个文件能被正确地编译,因为我已经把编译不通过的那部分用注释 `//` 过滤了。代码示例:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "// operators/AllOps.java\n",
+ "// 测试所有基本类型的运算符操作\n",
+ "// 看看哪些是能被 Java 编译器接受的\n",
+ "public class AllOps {\n",
+ " // 布尔值的接收测试:\n",
+ " void f(boolean b) {}\n",
+ " void boolTest(boolean x, boolean y) {\n",
+ " // 算数运算符:\n",
+ " //- x = x * y;\n",
+ " //- x = x / y;\n",
+ " //- x = x % y;\n",
+ " //- x = x + y;\n",
+ " //- x = x - y;\n",
+ " //- x++;\n",
+ " //- x--;\n",
+ " //- x = +y;\n",
+ " //- x = -y;\n",
+ " // 关系运算符和逻辑运算符:\n",
+ " //- f(x > y);\n",
+ " //- f(x >= y);\n",
+ " //- f(x < y);\n",
+ " //- f(x <= y);\n",
+ " f(x == y);\n",
+ " f(x != y);\n",
+ " f(!y);\n",
+ " x = x && y;\n",
+ " x = x || y;\n",
+ " // 按位运算符:\n",
+ " //- x = ~y;\n",
+ " x = x & y;\n",
+ " x = x | y;\n",
+ " x = x ^ y;\n",
+ " //- x = x << 1;\n",
+ " //- x = x >> 1;\n",
+ " //- x = x >>> 1;\n",
+ " // 联合赋值:\n",
+ " //- x += y;\n",
+ " //- x -= y;\n",
+ " //- x *= y;\n",
+ " //- x /= y;\n",
+ " //- x %= y;\n",
+ " //- x <<= 1;\n",
+ " //- x >>= 1;\n",
+ " //- x >>>= 1;\n",
+ " x &= y;\n",
+ " x ^= y;\n",
+ " x |= y;\n",
+ " // 类型转换:\n",
+ " //- char c = (char)x;\n",
+ " //- byte b = (byte)x;\n",
+ " //- short s = (short)x;\n",
+ " //- int i = (int)x;\n",
+ " //- long l = (long)x;\n",
+ " //- float f = (float)x;\n",
+ " //- double d = (double)x;\n",
+ " }\n",
+ "\n",
+ " void charTest(char x, char y) {\n",
+ " // 算数运算符:\n",
+ " x = (char)(x * y);\n",
+ " x = (char)(x / y);\n",
+ " x = (char)(x % y);\n",
+ " x = (char)(x + y);\n",
+ " x = (char)(x - y);\n",
+ " x++;\n",
+ " x--;\n",
+ " x = (char) + y;\n",
+ " x = (char) - y;\n",
+ " // 关系和逻辑运算符:\n",
+ " f(x > y);\n",
+ " f(x >= y);\n",
+ " f(x < y);\n",
+ " f(x <= y);\n",
+ " f(x == y);\n",
+ " f(x != y);\n",
+ " //- f(!x);\n",
+ " //- f(x && y);\n",
+ " //- f(x || y);\n",
+ " // 按位运算符:\n",
+ " x= (char)~y;\n",
+ " x = (char)(x & y);\n",
+ " x = (char)(x | y);\n",
+ " x = (char)(x ^ y);\n",
+ " x = (char)(x << 1);\n",
+ " x = (char)(x >> 1);\n",
+ " x = (char)(x >>> 1);\n",
+ " // 联合赋值:\n",
+ " x += y;\n",
+ " x -= y;\n",
+ " x *= y;\n",
+ " x /= y;\n",
+ " x %= y;\n",
+ " x <<= 1;\n",
+ " x >>= 1;\n",
+ " x >>>= 1;\n",
+ " x &= y;\n",
+ " x ^= y;\n",
+ " x |= y;\n",
+ " // 类型转换\n",
+ " //- boolean bl = (boolean)x;\n",
+ " byte b = (byte)x;\n",
+ " short s = (short)x;\n",
+ " int i = (int)x;\n",
+ " long l = (long)x;\n",
+ " float f = (float)x;\n",
+ " double d = (double)x;\n",
+ " }\n",
+ "\n",
+ " void byteTest(byte x, byte y) {\n",
+ " // 算数运算符:\n",
+ " x = (byte)(x* y);\n",
+ " x = (byte)(x / y);\n",
+ " x = (byte)(x % y);\n",
+ " x = (byte)(x + y);\n",
+ " x = (byte)(x - y);\n",
+ " x++;\n",
+ " x--;\n",
+ " x = (byte) + y;\n",
+ " x = (byte) - y;\n",
+ " // 关系和逻辑运算符:\n",
+ " f(x > y);\n",
+ " f(x >= y);\n",
+ " f(x < y);\n",
+ " f(x <= y);\n",
+ " f(x == y);\n",
+ " f(x != y);\n",
+ " //- f(!x);\n",
+ " //- f(x && y);\n",
+ " //- f(x || y);\n",
+ " //按位运算符:\n",
+ " x = (byte)~y;\n",
+ " x = (byte)(x & y);\n",
+ " x = (byte)(x | y);\n",
+ " x = (byte)(x ^ y);\n",
+ " x = (byte)(x << 1);\n",
+ " x = (byte)(x >> 1);\n",
+ " x = (byte)(x >>> 1);\n",
+ " // 联合赋值:\n",
+ " x += y;\n",
+ " x -= y;\n",
+ " x *= y;\n",
+ " x /= y;\n",
+ " x %= y;\n",
+ " x <<= 1;\n",
+ " x >>= 1;\n",
+ " x >>>= 1;\n",
+ " x &= y;\n",
+ " x ^= y;\n",
+ " x |= y;\n",
+ " // 类型转换:\n",
+ " //- boolean bl = (boolean)x;\n",
+ " char c = (char)x;\n",
+ " short s = (short)x;\n",
+ " int i = (int)x;\n",
+ " long l = (long)x;\n",
+ " float f = (float)x;\n",
+ " double d = (double)x;\n",
+ " }\n",
+ "\n",
+ " void shortTest(short x, short y) {\n",
+ " // 算术运算符:\n",
+ " x = (short)(x * y);\n",
+ " x = (short)(x / y);\n",
+ " x = (short)(x % y);\n",
+ " x = (short)(x + y);\n",
+ " x = (short)(x - y);\n",
+ " x++;\n",
+ " x--;\n",
+ " x = (short) + y;\n",
+ " x = (short) - y;\n",
+ " // 关系和逻辑运算符:\n",
+ " f(x > y);\n",
+ " f(x >= y);\n",
+ " f(x < y);\n",
+ " f(x <= y);\n",
+ " f(x == y);\n",
+ " f(x != y);\n",
+ " //- f(!x);\n",
+ " //- f(x && y);\n",
+ " //- f(x || y);\n",
+ " // 按位运算符:\n",
+ " x = (short) ~ y;\n",
+ " x = (short)(x & y);\n",
+ " x = (short)(x | y);\n",
+ " x = (short)(x ^ y);\n",
+ " x = (short)(x << 1);\n",
+ " x = (short)(x >> 1);\n",
+ " x = (short)(x >>> 1);\n",
+ " // Compound assignment:\n",
+ " x += y;\n",
+ " x -= y;\n",
+ " x *= y;\n",
+ " x /= y;\n",
+ " x %= y;\n",
+ " x <<= 1;\n",
+ " x >>= 1;\n",
+ " x >>>= 1;\n",
+ " x &= y;\n",
+ " x ^= y;\n",
+ " x |= y;\n",
+ " // 类型转换:\n",
+ " //- boolean bl = (boolean)x;\n",
+ " char c = (char)x;\n",
+ " byte b = (byte)x;\n",
+ " int i = (int)x;\n",
+ " long l = (long)x;\n",
+ " float f = (float)x;\n",
+ " double d = (double)x;\n",
+ " }\n",
+ "\n",
+ " void intTest(int x, int y) {\n",
+ " // 算术运算符:\n",
+ " x = x * y;\n",
+ " x = x / y;\n",
+ " x = x % y;\n",
+ " x = x + y;\n",
+ " x = x - y;\n",
+ " x++;\n",
+ " x--;\n",
+ " x = +y;\n",
+ " x = -y;\n",
+ " // 关系和逻辑运算符:\n",
+ " f(x > y);\n",
+ " f(x >= y);\n",
+ " f(x < y);\n",
+ " f(x <= y);\n",
+ " f(x == y);\n",
+ " f(x != y);\n",
+ " //- f(!x);\n",
+ " //- f(x && y);\n",
+ " //- f(x || y);\n",
+ " // 按位运算符:\n",
+ " x = ~y;\n",
+ " x = x & y;\n",
+ " x = x | y;\n",
+ " x = x ^ y;\n",
+ " x = x << 1;\n",
+ " x = x >> 1;\n",
+ " x = x >>> 1;\n",
+ " // 联合赋值:\n",
+ " x += y;\n",
+ " x -= y;\n",
+ " x *= y;\n",
+ " x /= y;\n",
+ " x %= y;\n",
+ " x <<= 1;\n",
+ " x >>= 1;\n",
+ " x >>>= 1;\n",
+ " x &= y;\n",
+ " x ^= y;\n",
+ " x |= y;\n",
+ " // 类型转换:\n",
+ " //- boolean bl = (boolean)x;\n",
+ " char c = (char)x;\n",
+ " byte b = (byte)x;\n",
+ " short s = (short)x;\n",
+ " long l = (long)x;\n",
+ " float f = (float)x;\n",
+ " double d = (double)x;\n",
+ " }\n",
+ "\n",
+ " void longTest(long x, long y) {\n",
+ " // 算数运算符:\n",
+ " x = x * y;\n",
+ " x = x / y;\n",
+ " x = x % y;\n",
+ " x = x + y;\n",
+ " x = x - y;\n",
+ " x++;\n",
+ " x--;\n",
+ " x = +y;\n",
+ " x = -y;\n",
+ " // 关系和逻辑运算符:\n",
+ " f(x > y);\n",
+ " f(x >= y);\n",
+ " f(x < y);\n",
+ " f(x <= y);\n",
+ " f(x == y);\n",
+ " f(x != y);\n",
+ " //- f(!x);\n",
+ " //- f(x && y);\n",
+ " //- f(x || y);\n",
+ " // 按位运算符:\n",
+ " x = ~y;\n",
+ " x = x & y;\n",
+ " x = x | y;\n",
+ " x = x ^ y;\n",
+ " x = x << 1;\n",
+ " x = x >> 1;\n",
+ " x = x >>> 1;\n",
+ " // 联合赋值:\n",
+ " x += y;\n",
+ " x -= y;\n",
+ " x *= y;\n",
+ " x /= y;\n",
+ " x %= y;\n",
+ " x <<= 1;\n",
+ " x >>= 1;\n",
+ " x >>>= 1;\n",
+ " x &= y;\n",
+ " x ^= y;\n",
+ " x |= y;\n",
+ " // 类型转换:\n",
+ " //- boolean bl = (boolean)x;\n",
+ " char c = (char)x;\n",
+ " byte b = (byte)x;\n",
+ " short s = (short)x;\n",
+ " int i = (int)x;\n",
+ " float f = (float)x;\n",
+ " double d = (double)x;\n",
+ " }\n",
+ "\n",
+ " void floatTest(float x, float y) {\n",
+ " // 算数运算符:\n",
+ " x = x * y;\n",
+ " x = x / y;\n",
+ " x = x % y;\n",
+ " x = x + y;\n",
+ " x = x - y;\n",
+ " x++;\n",
+ " x--;\n",
+ " x = +y;\n",
+ " x = -y;\n",
+ " // 关系和逻辑运算符:\n",
+ " f(x > y);\n",
+ " f(x >= y);\n",
+ " f(x < y);\n",
+ " f(x <= y);\n",
+ " f(x == y);\n",
+ " f(x != y);\n",
+ " //- f(!x);\n",
+ " //- f(x && y);\n",
+ " //- f(x || y);\n",
+ " // 按位运算符:\n",
+ " //- x = ~y;\n",
+ " //- x = x & y;\n",
+ " //- x = x | y;\n",
+ " //- x = x ^ y;\n",
+ " //- x = x << 1;\n",
+ " //- x = x >> 1;\n",
+ " //- x = x >>> 1;\n",
+ " // 联合赋值:\n",
+ " x += y;\n",
+ " x -= y;\n",
+ " x *= y;\n",
+ " x /= y;\n",
+ " x %= y;\n",
+ " //- x <<= 1;\n",
+ " //- x >>= 1;\n",
+ " //- x >>>= 1;\n",
+ " //- x &= y;\n",
+ " //- x ^= y;\n",
+ " //- x |= y;\n",
+ " // 类型转换:\n",
+ " //- boolean bl = (boolean)x;\n",
+ " char c = (char)x;\n",
+ " byte b = (byte)x;\n",
+ " short s = (short)x;\n",
+ " int i = (int)x;\n",
+ " long l = (long)x;\n",
+ " double d = (double)x;\n",
+ " }\n",
+ "\n",
+ " void doubleTest(double x, double y) {\n",
+ " // 算术运算符:\n",
+ " x = x * y;\n",
+ " x = x / y;\n",
+ " x = x % y;\n",
+ " x = x + y;\n",
+ " x = x - y;\n",
+ " x++;\n",
+ " x--;\n",
+ " x = +y;\n",
+ " x = -y;\n",
+ " // 关系和逻辑运算符:\n",
+ " f(x > y);\n",
+ " f(x >= y);\n",
+ " f(x < y);\n",
+ " f(x <= y);\n",
+ " f(x == y);\n",
+ " f(x != y);\n",
+ " //- f(!x);\n",
+ " //- f(x && y);\n",
+ " //- f(x || y);\n",
+ " // 按位运算符:\n",
+ " //- x = ~y;\n",
+ " //- x = x & y;\n",
+ " //- x = x | y;\n",
+ " //- x = x ^ y;\n",
+ " //- x = x << 1;\n",
+ " //- x = x >> 1;\n",
+ " //- x = x >>> 1;\n",
+ " // 联合赋值:\n",
+ " x += y;\n",
+ " x -= y;\n",
+ " x *= y;\n",
+ " x /= y;\n",
+ " x %= y;\n",
+ " //- x <<= 1;\n",
+ " //- x >>= 1;\n",
+ " //- x >>>= 1;\n",
+ " //- x &= y;\n",
+ " //- x ^= y;\n",
+ " //- x |= y;\n",
+ " // 类型转换:\n",
+ " //- boolean bl = (boolean)x;\n",
+ " char c = (char)x;\n",
+ " byte b = (byte)x;\n",
+ " short s = (short)x;\n",
+ " int i = (int)x;\n",
+ " long l = (long)x;\n",
+ " float f = (float)x;\n",
+ " }\n",
+ "}"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "**注意** :**boolean** 类型的运算是受限的。你能为其赋值 `true` 或 `false`,也可测试它的值是否是 `true` 或 `false`。但你不能对其作加减等其他运算。\n",
+ "\n",
+ "在 **char**,**byte** 和 **short** 类型中,我们可以看到算术运算符的“类型转换”效果。我们必须要显式强制类型转换才能将结果重新赋值为原始类型。对于 **int** 类型的运算则不用转换,因为默认就是 **int** 型。虽然我们不用再停下来思考这一切是否安全,但是两个大的 int 型整数相乘时,结果有可能超出 **int** 型的范围,这种情况下结果会发生溢出。下面的代码示例:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "// operators/Overflow.java\n",
+ "// 厉害了!内存溢出\n",
+ "public class Overflow {\n",
+ " public static void main(String[] args) {\n",
+ " int big = Integer.MAX_VALUE;\n",
+ " System.out.println(\"big = \" + big);\n",
+ " int bigger = big * 4;\n",
+ " System.out.println(\"bigger = \" + bigger);\n",
+ " }\n",
+ "}"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "输出结果:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "text"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "big = 2147483647\n",
+ "bigger = -4"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "编译器没有报错或警告,运行时一切看起来都无异常。诚然,Java 是优秀的,但是还不足够优秀。\n",
+ "\n",
+ "对于 **char**,**byte** 或者 **short**,混合赋值并不需要类型转换。即使为它们执行转型操作,也会获得与直接算术运算相同的结果。另外,省略类型转换可以使代码显得更加简练。总之,除 **boolean** 以外,其他任何两种基本类型间都可进行类型转换。当我们进行向下转换类型时,需要注意结果的范围是否溢出,否则我们就很可能在不知不觉中丢失精度。\n",
+ "\n",
+ "\n",
+ "## 本章小结\n",
+ "\n",
+ "如果你已接触过一门 C 语法风格编程语言,那么你在学习 Java 的运算符时实际上没有任何曲线。如果你觉得有难度,那么我推荐你要先去 www.OnJava8.com 观看 《Thinking in C》 的视频教程来补充一些前置知识储备。\n",
+ "\n",
+ "[^1]: 我在 *Pomona College* 大学读过两年本科,在那里 47 被称之为“魔法数字”(*magic number*),详见 [维基百科](https://en.wikipedia.org/wiki/47_(number)) 。\n",
+ "\n",
+ "[^2]: *John Kirkham* 说过:“自 1960 年我开始在 IBM 1620 上开始编程起,至 1970 年之间,FORTRAN 一直都是一种全大写的编程语言。这可能是因为许多早期的输入设备都是旧的电传打字机,使用了 5 位波特码,没有小写字母的功能。指数符号中的 e 也总是大写的,并且从未与自然对数底数 e 混淆,自然对数底数 e 总是小写的。 e 简单地代表指数,通常 10 是基数。那时,八进制也被程序员广泛使用。虽然我从未见过它的用法,但如果我看到一个指数符号的八进制数,我会认为它是以 8 为基数的。我记得第一次看到指数使用小写字母 e 是在 20 世纪 70 年代末,我也发现它令人困惑。这个问题出现的时候,小写字母悄悄进入了 Fortran。如果你真的想使用自然对数底,我们实际上有一些函数要使用,但是它们都是大写的。”\n",
+ "\n",
+ "\n",
+ ""
+ ]
+ }
+ ],
+ "metadata": {},
+ "nbformat": 4,
+ "nbformat_minor": 4
+}
diff --git a/jupyter/00-Introduction.ipynb b/jupyter/00-Introduction.ipynb
new file mode 100644
index 00000000..01651402
--- /dev/null
+++ b/jupyter/00-Introduction.ipynb
@@ -0,0 +1,111 @@
+{
+ "cells": [
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "# 简介\n",
+ "\n",
+ "> “我的语言极限,即是我的世界的极限。” ——路德维希·维特根斯坦(*Wittgenstein*)\n",
+ "\n",
+ "这句话无论对于自然语言还是编程语言来说都是一样的。你所使用的编程语言会将你的思维模式固化并逐渐远离其他语言,而且往往发生在潜移默化中。Java 作为一门傲娇的语言尤其如此。\n",
+ "\n",
+ "Java 是一门派生语言,早期语言设计者为了不想在项目中使用 C++ 而创造了这种看起来很像 C++,却比 C++ 有了改进的新语言(原始的项目并未成功)。Java 最核心的变化就是加入了“虚拟机”和“垃圾回收机制”,这两个概念在之后的章节会有详细描述。 此外,Java 还在其他方面推动了行业发展。例如,现在绝大多数编程语言都支持文档注释语法和 HTML 文档生成工具。\n",
+ "\n",
+ "Java 最主要的概念之一“对象”来自 SmallTalk 语言。SmallTalk 语言恪守“对象”(在下一章中描述)是编程的最基本单元。于是,万物皆对象。历经时间的检验,人们发现这种信念太过狂热。有些人甚至认为“对象”的概念是完全错误的,应该舍弃。就我个人而言,把一切事物都抽象成对象不仅是一项不必要的负担,同时还会招致许多设计朝着不好的方向发展。尽管如此,“对象”的概念依然有其闪光点。固执地要求所有东西都是一个对象(特别是一直到最底层级别)是一种设计错误;相反,完全逃避“对象”的概念似乎同样太过苛刻。\n",
+ "\n",
+ "Java 语言曾规划设计的许多功能并未按照承诺兑现。本书中,我将尝试解释这些原因,力争让读者知晓这些功能,并明白为什么这些功能最终并不适用。这无关 Java 是一种好语言或者坏语言,一旦你了解了该语言的缺陷和局限性,你就能够:\n",
+ "\n",
+ "1. 明白有些功能特性为什么会被“废弃”。\n",
+ "\n",
+ "2. 熟悉语言边界,更好地设计和编码。\n",
+ "\n",
+ "编程的过程就是复杂性管理的过程:业务问题的复杂性,以及依赖的计算机的复杂性。由于这种复杂性,我们的大多数软件项目都失败了。\n",
+ "\n",
+ "许多语言设计决策时都考虑到了复杂性,并试图降低语言的复杂性,但在设计过程中遇到了一些更棘手的问题,最终导致语言设计不可避免地“碰壁”,复杂性增加。例如,C++ 必须向后兼容 C(允许 C 程序员轻松迁移),并且效率很高。这些目标非常实用,并且也是 C++ 在编程界取得了成功的原因之一,但同时也引入了额外的复杂性,导致某些用 C++ 编写的项目开发失败。当然,你可以责怪程序员和管理人员手艺不精,但如果有一种编程语言可以帮助你在开发过程中发现错误,那岂不是更好?\n",
+ "\n",
+ "虽然 VB(Visual BASIC)绑定在 BASIC 上,但 BASIC 实际上并不是一种可扩展的语言。大量扩展的堆积造成 VB 的语法难以维护。Perl 向后兼容 awk、sed、grep 以及其它要替换的 Unix 工具。因此它常常被诟病产生了一堆“只写代码”(*write-only code*,写代码的人自己都看不懂的代码)。另一方面,C ++,VB,Perl 和其他语言(如 SmallTalk)在设计时重点放在了对某些复杂问题的处理上,因而在解决这些特定类型的问题方面非常成功。\n",
+ "\n",
+ "通信革命使我们相互沟通更加便利。无论是一对一沟通,还是团队里的互相沟通,甚至是地球上不同地区的沟通。据说下一次革命需要的是一种全球性的思维,这种思维源于足量的人以及足量相互连接。我不知道 Java 是否能成为这场革命的工具之一,但至少这种可能性让我觉得:我现在正在做的传道授业的事情是有意义的!\n",
+ "\n",
+ "## 前提条件\n",
+ "\n",
+ "阅读本书需要读者对编程有基本的了解:\n",
+ "\n",
+ "- 程序是一系列“陈述(语句、代码)”构成\n",
+ "\n",
+ "- 子程序、方法、宏的概念\n",
+ "\n",
+ "- 控制语句(例如 **if**),循环结构(例如 **while**)\n",
+ "\n",
+ "可能你已在学校、书籍或网络上了学过这些。只要你觉得对上述的编程基本概念熟悉,你就可以完成本书的学习。\n",
+ "\n",
+ "你可以通过在 On Java 8 的网站上免费下载 《Think in C》来补充学习 Java 所需要的前置知识。本书介绍了 Java 语言的基本控制机制以及面向对象编程(OOP)的概念。在本书中我引述了一些 C/C++ 语言中的一些特性来帮助读者更好的理解 Java。毕竟 Java 是在它们的基础之上发明的,理解他们之间的区别,有助于读者更好地学习 Java。我会试图简化这些引述,尽量让没有 C/C++ 基础的读者也能很好地理解。\n",
+ "\n",
+ "## JDK文档\n",
+ "\n",
+ "甲骨文公司已经提供了免费的标准 JDK 文档。除非有必要,否则本书中将不再赘述 API 相关的使用细节。使用浏览器来即时搜索最新最全的 JDK 文档好过翻阅本书来查找。只有在需要补充特定的示例时,我才会提供有关的额外描述。\n",
+ "\n",
+ "## C编程思想\n",
+ "\n",
+ "*Thinking in C* 已经可以在 [www.OnJava8.com](https://archive.org/details/ThinkingInC) 免费下载。Java 的基础语法是基于 C 语言的。*Thinking in C* 中有更适合初学者的编程基础介绍。 我已经委托 Chuck Allison 将这本 C 基础的书籍作为独立产品附赠于本书的 CD 中。希望大家在阅读本书时,都已具备了学习 Java 的良好基础。\n",
+ "\n",
+ "## 源码下载\n",
+ "\n",
+ "本书中所有源代码的示例都在版权保护的前提下通过 GitHub 免费提供。你可以将这些代码用于教育。任何人不得在未经正确引用代码来源的情况下随意重新发布此代码示例。在每个代码文件中,你都可以找到以下版权声明文件作为参考:\n",
+ "\n",
+ "**Copyright.txt**\n",
+ "\n",
+ "©2017 MindView LLC。版权所有。如果上述版权声明,本段和以下内容,特此授予免费使用,复制,修改和分发此计算机源代码(源代码)及其文档的许可,且无需出于下述目的的书面协议所有副本中都有五个编号的段落。\n",
+ "\n",
+ "1. 允许编译源代码并将编译代码仅以可执行格式包含在个人和商业软件程序中。\n",
+ "\n",
+ "2. 允许在课堂情况下使用源代码而不修改源代码,包括在演示材料中,前提是 “On Java 8” 一书被引用为原点。\n",
+ "\n",
+ "3. 可以通过以下方式获得将源代码合并到印刷媒体中的许可:MindView LLC,PO Box 969,Crested Butte,CO 81224 MindViewInc@gmail.com \n",
+ "\n",
+ "4. 源代码和文档的版权归 MindView LLC 所有。提供的源代码没有任何明示或暗示的担保,包括任何适销性,适用于特定用途或不侵权的默示担保。MindView LLC 不保证任何包含源代码的程序的运行不会中断或没有错误。MindView LLC 不对任何目的的源代码或包含源代码的任何软件的适用性做出任何陈述。包含源代码的任何程序的质量和性能的全部风险来自源代码的用户。用户理解源代码是为研究和教学目的而开发的,建议不要仅仅因任何原因依赖源代码或任何包含源代码的程序。如果源代码或任何产生的软件证明有缺陷,则用户承担所有必要的维修,修理或更正的费用。\n",
+ "\n",
+ "5. 在任何情况下,MINDVIEW LLC 或其出版商均不对任何一方根据任何法律理论对直接,间接,特殊,偶发或后果性损害承担任何责任,包括利润损失,业务中断,商业信息丢失或任何其他保险公司。由于 MINDVIEW LLC 或其出版商已被告知此类损害的可能性,因此使用本源代码及其文档或因无法使用任何结果程序而导致的个人受伤或者个人受伤。MINDVIEW LLC 特别声明不提供任何担保,包括但不限于对适销性和特定用途适用性的暗示担保。此处提供的源代码和文档基于“原样”基础,没有MINDVIEW LLC的任何随附服务,MINDVIEW LLC 没有义务提供维护,支持,更新,增强或修改。\n",
+ "\n",
+ "**请注意**,MindView LLC 仅提供以下唯一网址发布更新书中的代码示例,https://github.com/BruceEckel/OnJava8-examples 。你可在上述条款范围内将示例免费使用于项目和课堂中。\n",
+ "\n",
+ "如果你在源代码中发现错误,请在下面的网址提交更正:https://github.com/BruceEckel/OnJava8-examples/issues \n",
+ "\n",
+ "## 编码样式\n",
+ "\n",
+ "本书中代码标识符(关键字,方法,变量和类名)以粗体,固定宽度代码字体显示。像 “*class” 这种在代码中高频率出现的关键字可能让你觉得粗体有点乏味。(译者注:由于中英排版差异,中文翻译过程并未完全参照原作者的说明。具体排版格式请参考[此处](https://github.com/ruanyf/document-style-guide))其他显示为正常字体。本书文本格式尽可能遵循 Oracle 常见样式,并保证在大多数 Java 开发环境中被支持。书中我使用了自己喜欢的字体风格。Java 是一种自由的编程语言,你也可以使用 IDE(集成开发环境)工具(如 IntelliJ IDEA,Eclipse 或 NetBeans)将格式更改为适合你的格式。\n",
+ "\n",
+ "本书代码文件使用自动化工具进行测试,并在最新版本的 Java 编译通过(除了那些特别标记的错误之外)。本书重点介绍并使用 Java 8 进行测试。如果你必须了解更早的语言版本,可以在 [www.OnJava8.com](http://www.OnJava8.com) 免费下载 《Thinking in Java》。\n",
+ "\n",
+ "## BUG提交\n",
+ "\n",
+ "本书经过多重校订,但还是难免有所遗漏被新读者发现。如果你在正文或示例中发现任何错误的内容,请在[此处](https://github.com/BruceEckel/OnJava8-examples/issues)提交错误以及建议更正,作者感激不尽。\n",
+ "\n",
+ "## 邮箱订阅\n",
+ "\n",
+ "你可以在 [www.OnJava8.com上](http://www.OnJava8.com) 订阅邮件。邮件不含广告并尽量提供干货。\n",
+ "\n",
+ "## Java图形界面\n",
+ "\n",
+ "Java 在图形用户界面和桌面程序方面的发展可以说是一段悲伤的历史。Java 1.0 中图形用户界面(GUI)库的原始设计目标是让用户能在所有平台提供一个漂亮的界面。但遗憾的是,这个理想没有实现。相反,Java 1.0 AWT(抽象窗口工具包)在所有平台都表现平平,并且有诸多限制。你只能使用四种字体。另外,Java 1.0 AWT 编程模型也很笨拙且非面向对象。我的一个曾在 Java 设计期间工作过的学生道出了缘由:早期的 AWT 设计是在仅仅在一个月内构思、设计和实施的。不得不说这是一个“奇迹”,但同时更是“设计失败”的绝佳教材。\n",
+ "\n",
+ "在 Java 1.1 版本的 AWT 中 情况有所改善,事件模型带来更加清晰的面向对象方法,并添加了JavaBeans,致力于面向易于创建可视化编程环境的组件编程模型(已废弃)。\n",
+ "\n",
+ "Java 2(Java 1.2)通过使用 Java 基类(JFC)内容替换来完成从旧版 Java 1.0 AWT 的转换。其中 GUI 部分称为 Swing。这是一组丰富的 JavaBeans,它们创建了一个合理的 GUI。修订版 3(3之前都不好)比以往更适用于开发图形界面程序。\n",
+ "\n",
+ "Sun 在图形界面的最后一次尝试,称为 JavaFX。当 Oracle 收购 Sun 时,他们将原来雄心勃勃的项目(包括脚本语言)改为库,现在它似乎是 Java 官方唯一还在开发中的 UI 工具包(参见维基百科关于 JavaFX 的文章) - 但即使如此,JavaFX 最终似乎也失败了。\n",
+ "\n",
+ "现今 Swing 依然是 Java 发行版的一部分(只接受维护,不再有新功能开发)。而 Java 现在是一个开源项目,它应该始终可用。此外,Swing 和 JavaFX 有一些有限的交互性。这些可能是为了帮助开发者过渡到 JavaFX。\n",
+ "\n",
+ "桌面程序领域似乎从未尝勾起 Java 设计师的野心。Java 没有在图形界面取得该有的一席之地。另外,曾被大肆吹嘘的 JavaBeans 也没有获得任何影响力。(许多不幸的作者花了很多精力在 Swing 上编写书籍,甚至只用 JavaBeans 编写书籍)。Java 图形界面程序大多数情况下仅用于 IDE(集成开发环境)和一些企业内部应用程序。你可以采用 Java 开发图形界面,但这并非 Java 最擅长的领域。如果你必须学习 Swing,可以参考 *Thinking in Java* 第4版(可从 www.OnJava8.com 获得)或者通过其他专门的书籍学习。\n",
+ "\n",
+ "\n",
+ ""
+ ]
+ }
+ ],
+ "metadata": {},
+ "nbformat": 4,
+ "nbformat_minor": 4
+}
diff --git a/jupyter/00-On-Java-8.ipynb b/jupyter/00-On-Java-8.ipynb
new file mode 100644
index 00000000..c16c3177
--- /dev/null
+++ b/jupyter/00-On-Java-8.ipynb
@@ -0,0 +1,76 @@
+{
+ "cells": [
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "\n",
+ "

\n",
+ "
\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ " On Java 8
\n",
+ "\n",
+ "\n",
+ "\n",
+ "Bruce Eckel
\n",
+ "\n",
+ "\n",
+ "\n",
+ " MindView LLC \n",
+ "\n",
+ "\n",
+ "2017\n",
+ "\n",
+ "\n",
+ "©MindView LLC 版权所有\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "# On Java 8\n",
+ "\n",
+ "\n",
+ "\n",
+ "**版权©2017**\n",
+ "\n",
+ "\n",
+ "**作者 Bruce Eckel, President, MindView LLC.**\n",
+ "\n",
+ "\n",
+ "**版本号:7**\n",
+ "\n",
+ "\n",
+ "**ISBN 978-0-9818725-2-0**\n",
+ "\n",
+ "\n",
+ "**原书可在该网站购买 [www.OnJava8.com](http://www.OnJava8.com)** \n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "本书出版自美国,版权所有,翻版必究。未经授权不得非法存储在检索系统中,或以电子,机械,影印,录制任何形式传输等。制造商和销售商使用商标用来区分其产品标识。如果这些名称出现在这本书中,并且出版商知道商标要求,则这些名称已经用大写字母或所有大写字母打印。\n",
+ "\n",
+ "Java 是甲骨文公司(Oracle. Inc.)的商标。Windows 95,Windows NT,Windows 2000,Windows XP,Windows 7,Windows 8 和 Windows 10 是微软公司(Microsoft Corporation)的商标。\n",
+ "此处提及的所有其他产品名称和公司名称均为其各自所有者的财产。作者和出版商在编写本书时已经仔细校对过,但不作任何明示或暗示的保证,对错误或遗漏不承担任何责任。对于因使用此处包含的信息或程序而产生的偶然或间接损失,我们不承担任何责任。\n",
+ "\n",
+ "这本书是以平板电脑和计算机为载体的电子书,非传统纸质版书籍。 \n",
+ "故所有布局和格式设计旨在优化您在各种电子书阅读平台和系统上的观看体验。\n",
+ "封面由 Daniel Will-Harris 设计,[www.Will-Harris.com](http://www.Will-Harris.com)。\n",
+ "\n",
+ ""
+ ]
+ }
+ ],
+ "metadata": {},
+ "nbformat": 4,
+ "nbformat_minor": 4
+}
diff --git a/jupyter/00-Preface.ipynb b/jupyter/00-Preface.ipynb
new file mode 100644
index 00000000..e97ab2c8
--- /dev/null
+++ b/jupyter/00-Preface.ipynb
@@ -0,0 +1,112 @@
+{
+ "cells": [
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "# 前言\n",
+ "\n",
+ "> 本书基于 Java 8 版本来教授当前 Java 编程的最优实践。\n",
+ "\n",
+ "此前,我的另一本 Java 书籍 *Thinking in Java, 4th Edition*(《Java编程思想》 第 4 版 Prentice Hall 2006)依然适用于 Java 5 编程。Android 编程就是始于此语言版本。\n",
+ "\n",
+ "随着 Java 8 的出现,这门语言在许多地方发生了翻天覆地的变化。在新的版本中,代码的运用和实现上与以往不尽相同。这也促使了我时隔多年后再次创作了这本新书。《On Java 8》旨在面向已具有编程基础的开发者们。对于初学者,可以先在 [Code.org](http://Code.org) 或者 [Khan Academy](https://www.khanacademy.org/computing/computer-programming) 等网站上补充必要的前置知识。同时,[OnJava8.com](http://www.OnJava8.com) 上也有免费的 Thinking in C(《C编程思想》)专题知识。\n",
+ "\n",
+ "与几年前我们依赖印刷媒体相比,YouTube,博客和 StackOverflow 等网站的出现让寻找答案变得简单。请结合这些学习途径和努力坚持下去。本书可作为编程入门书籍,同时也适用于想要扩展知识的在职程序员。每次在世界各地的演讲中,我都非常感谢 《*Thinking in Java*》 这本书给我带来的所有荣誉。它对于我重塑 [Reinventing Business](http://www.reinventing-business.com) 项目和促进交流是非常宝贵的。最后,写这本书的原因之一 希望这本书可以为我的这个项目众筹。似乎下一步要创建一个所谓的蓝绿色组织(Teal Organization)才合乎逻辑的。\n",
+ "\n",
+ "## 教学目标\n",
+ "\n",
+ "每章教授一个或一组相关的概念,并且这些知识不依赖于尚未学习到的章节。如此,学习者可以在当前知识的背景框架下循序渐进地掌握 Java。\n",
+ "\n",
+ "本书的教学目标:\n",
+ "\n",
+ "1. 循序渐进地呈现学习内容,以便于你在不依赖后置知识框架的情况下轻松完成现有的学习任务,同时尽量保证前面章节的内容在后面的学习中得到运用。如果确有必要引入我们还没学习到的知识概念,我会做个简短地介绍。\n",
+ "\n",
+ "2. 尽可能地使用简单和简短的示例,方便读者理解。而不强求引入解决实际问题的例子。因为我发现,相比解决某个实际问题,读者更乐于看到自己真正理解了示例的每个细节。或许我会因为这些“玩具示例”而被一些人所诟病,但我更愿意看到我的读者们因此能保持饶有兴趣地学习。\n",
+ "\n",
+ "3. 把我知道以及我认为对于你学习语言很重要的东西都告诉你。我认为信息的重要性是分层次结构的。绝大多数情况下,我们没必要弄清问题的所有本质。好比编程语言中的某些特性和实现细节,95% 的程序员都不需要去知道。这些细节除了会加重你的学习成本,还让你更觉得这门语言好复杂。如果你非要考虑这些细节,那么它还会迷惑该代码的阅读者/维护者,所以我主张选择简单的方法解决问题。\n",
+ "\n",
+ "4. 希望本书能为你打下坚实的基础,方便你将来学习更难的课程和书籍。\n",
+ "\n",
+ "## 语言设计错误\n",
+ "\n",
+ "每种语言都有设计错误。当新手程序员涉足语言特性并猜测应用场景和使用方式时,他们体验到极大的不确定性和挫折感。承认错误令人尴尬,但这种糟糕的初学者经历比认识到你错在哪里还要糟糕。唉,每一种语言/库的设计错误都会永久地嵌入在 Java 的发行版中。\n",
+ "\n",
+ "诺贝尔经济学奖得主约瑟夫·斯蒂格利茨(*Joseph Stiglitz*)有一套适用于这里的人生哲学,叫做“承诺升级理论”:继续犯错误的成本由别人承担,而承认错误的成本由自己承担。\n",
+ "\n",
+ "看过我此前作品的读者们应该清楚,我一般倾向于指出这些错误。Java 拥有一批狂热的粉丝。他们把语言当成是阵营而不是纯粹的编程工具。我写过 Java 书籍,所以他们兀自认为我自然也是这个“阵营”的一份子。当我指出 Java 的这些错误时,会造成两种影响:\n",
+ "\n",
+ "1. 早先许多错误“阵营”的人成为了牺牲品。最终,时隔多年后,大家都意识到这是个设计上的错误。然而错误已然成为 Java 历史的一部分了。\n",
+ "\n",
+ "2. 更重要的是,新手程序员并没有经历过“语言为何采用某种方式实现”的争议过程。特别是那些隐约察觉不对却依然说服自己“我必须要这么做”或“我只是没学明白”从而继续错下去的人。更糟糕的是,教授这些编程知识的老师们没能深入地去研究这里是否有设计上的错误,而是继续错误的解读。总之,通过了解语言设计上的错误,能让开发者们更好地理解和意识到错误的本质,从而更快地进步。\n",
+ "\n",
+ "对编程语言的设计错误理解至关重要,甚至影响程序员的开发效率。部分公司在开发过程中避免使用语言的某些功能特性。这些功能特性表面上看起来高大上,但是弄不好却可能出现意料之外的错误,影响整个开发进程。\n",
+ "\n",
+ "已知的语言设计错误会给新的一门编程语言的作者提供参考。探索一门语言能做什么是很有趣的一件事,而语言设计错误能提醒你哪些“坑”是不能再趟的。多年以来,我一直感觉 Java 的设计者们有点脱离群众。Java 的有些设计错误错的太明显,我甚至怀疑设计者们到底是为出于服务用户还是其他动机设计了这些功能。Java 语言有许多臭名昭著的设计错误,很可能这也是诱惑所在。Java 似乎并不尊重开发者。为此我很长时间内不想与 Java 有任何瓜葛。很大程度上,这也是我不想碰 Java 的原因吧。\n",
+ "\n",
+ "如今再审视 Java 8,我发现了许多变化。设计者们对于语言和用户的态度似乎发生了根本性上的改变。忽视用户投诉多年之后,Java 的许多功能和类库都已被搞砸了。\n",
+ "\n",
+ "新功能的设计与以往有很大不同。掌舵者开始重视程序员的编程经验。新功能的开发都是在努力使语言变得更好,而非仅仅停留在快速堆砌功能而不去深入研究它们的含义。甚至有些新特性的实现方式非常优雅(至少在 Java 约束下尽可能优雅)。\n",
+ "\n",
+ "我猜测可能是部分设计者的离开让他们意识到了这点。说真的,我没想到会有这些变化!因为这些原因,写这本书的体验要比以往好很多。Java 8 包含了一系列基础和重要的改进。遗憾的是,为了严格地“向后兼容”,我们不大可能看到戏剧性的变化,当然我希望我是错的。尽管如此,我很赞赏那些敢于自我颠覆,并为 Java 设定更好路线的人。第一次,对于自己所写的部分 Java 8 代码我终于可以说“赞!”\n",
+ "\n",
+ "最后,本书所著时间似乎也还不错,因为 Java 8 引入的新功能已经强烈地影响了今后 Java 的编码方式。截止我在写这本书时,Java 9 似乎更专注于对语言底层的基础结构功能的重要更新,而非本书所关注的新编码方式。话说回来,得益于电子书出版形式的便捷,假如我发现本书有需要更新或添加的内容,我可以第一时间将新版本推送给现有读者。\n",
+ "\n",
+ "## 测试用例\n",
+ "\n",
+ "书中代码示例基于 Java 8 和 Gradle 编译构建,并且代码示例都保存在[这个自由访问的GitHub的仓库](https://github.com/BruceEckel/OnJava8-Examples) 中。我们需要内置的测试框架,以便于在每次构建系统时自动运行。否则,你将无法保证自己代码的可靠性。为了实现这一点,我创建了一个测试系统来显示和验证大多数示例的输出结果。这些输出结果我会附加在示例结尾的代码块中。有时仅显示必要的那几行或者首尾行。利用这种方式来改善读者的阅读和学习体验,同时也提供了一种验证示例正确性的方法。\n",
+ "\n",
+ "## 普及性\n",
+ "\n",
+ "Java 的普及性对于其受欢迎程度有重要意义。学习 Java 会让你更容易找到工作。相关的培训材料,课程和其他可用的学习资源也很多。对于企业来说,招聘 Java 程序员相对容易。如果你不喜欢 Java 语言,那么最好不要拿他当作你谋生的工具,因为这种生活体验并不好。作为一家公司,在技术选型前一定不要单单只考虑 Java 程序员好招。每种语言都有其适用的范围,有可能你们的业务更适用于另一种编程语言来达到事半功倍的效果。如果你真的喜欢 Java,那么欢迎你。希望这本书能丰富你的编程经验!\n",
+ "\n",
+ "## 关于安卓\n",
+ "\n",
+ "本书基于 Java 8 版本。如果你是 Andriod 程序员,请务必学习 Java 5。在《On Java 8》出版的时候,我的另一本基于 Java 5 的著作 *Thinking in Java 4th Edition*(《Java编程思想》第四版)已经可以在[www.OnJava8.com](http://www.OnJava8.com)上免费下载了。此外,还有许多其他专用于 Andriod 编程的资源。\n",
+ "\n",
+ "## 电子版权声明\n",
+ "\n",
+ "《On Java 8》仅提供电子版,并且仅通过 [www.OnJava8.com](http://www.OnJava8.com) 提供。任何未经 授权的其他来源或流传送机构都是非法的。本作品受版权保护!未经许可,请勿通过以任何方式分享或发布。你可以使用这些示例进行教学,只要不对本书非法重新出版。有关完整详细信息,请参阅示例分发中的 Copyright.txt 文件。对于视觉障碍者,电子版本有可搜索性,字体大小调整或文本到语音等诸多好处。\n",
+ "\n",
+ "任何购买这本书的读者,还需要一台计算机来运行和写作代码。另外电子版在计算机上和移动设备上的显示效果俱佳,推荐使用平板设备阅读。相比购买传统纸质版的价格,平板电脑价格都足够便宜。在床上阅读电子版比看这样一本厚厚的实体书要方便得多。起初你可能会有些不习惯,但我相信很快你就会发现它带来的优点远胜过不适。我已经走过这个阶段,Google Play 图书的浏览器阅读体验非常好,包括在 Linux 和 iOS 设备上。作为一次尝试,我决定尝试通过 Google 图书进行出版。\n",
+ "\n",
+ "**注意**:在撰写本文时,通过 Google Play 图书网络浏览器应用阅读图书虽然可以忍受,但体验还是有点差强人意,我强烈推荐读者们使用平板电脑来阅读。\n",
+ "\n",
+ "## 版本说明\n",
+ "\n",
+ "本书采用 [Pandoc](http://pandoc.org) 风格的 Markdown 编写,使用 Pandoc 生成 ePub v3 格式。\n",
+ "\n",
+ "正文字体为 Georgia,标题字体为 Verdana。 代码字体使用的 Ubuntu Mono,因为它特别紧凑,单行能容纳更多的代码。 我选择将代码内联(而不是将列表放入图像,参照其他书籍),因为我觉得这个功能很重要:让代码块能适应字体大小得改变而改变(否则,买电子版,还图什么呢?)。\n",
+ "\n",
+ "书中的提取,编译和测试代码示例的构建过程都是自动化的。所有自动化操作都是通过我在 Python 3 中编写的程序来实现的。\n",
+ "\n",
+ "## 封面设计\n",
+ "\n",
+ "《On Java 8》的封面是根据 W.P.A.(Works Progress Administration 1935年至1943年美国大萧条期间的一个巨大项目,它使数百万失业人员重新就业)的马赛克创作的。它还让我想起了《绿野仙踪》(*The Wizard of Oz*)系列丛书中的插图。 我的好朋友、设计师丹 *Daniel Will-Harris*([www.will-harris.com](http://www.will-harris.com))和我都喜欢这个形象。\n",
+ "\n",
+ "## 感谢的人\n",
+ "\n",
+ "感谢 *Domain-Driven Design*(《领域驱动设计》 )的作者 *Eric Evans* 建议书名,以及其他新闻组校对的帮助。\n",
+ "\n",
+ "感谢 *James Ward* 为我开始使用 Gradle 工具构建这本书,以及他多年来的帮助和友谊。\n",
+ "\n",
+ "感谢 *Ben Muschko* 在整理构建文件方面的工作,还有感谢 *Hans Dockter* 给 *Ben* 提供了时间。\n",
+ "\n",
+ "感谢 *Jeremy Cerise* 和 *Bill Frasure* 来到开发商务聚会预订,并随后提供了宝贵的帮助。\n",
+ "\n",
+ "感谢所有花时间和精力来科罗拉多州克雷斯特德比特(Crested Butte, Colorado)镇参加我的研讨会,开发商务聚会和其他活动的人!你们的贡献可能不容易看到,但却非常重要!\n",
+ "\n",
+ "## 献礼\n",
+ "\n",
+ "> 谨以此书献给我敬爱的父亲 E. Wayne Eckel。\n",
+ "> 1924年4月1日至2016年11月23日\n",
+ "\n",
+ "\n",
+ ""
+ ]
+ }
+ ],
+ "metadata": {},
+ "nbformat": 4,
+ "nbformat_minor": 4
+}
diff --git a/jupyter/01-What-is-an-Object.ipynb b/jupyter/01-What-is-an-Object.ipynb
new file mode 100644
index 00000000..252581ee
--- /dev/null
+++ b/jupyter/01-What-is-an-Object.ipynb
@@ -0,0 +1,422 @@
+{
+ "cells": [
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "# 第一章 对象的概念\n",
+ "\n",
+ "> “我们没有意识到惯用语言的结构有多大的力量。可以毫不夸张地说,它通过语义反应机制奴役我们。语言表现出来并在无意识中给我们留下深刻印象的结构会自动投射到我们周围的世界。” -- Alfred Korzybski (1930)\n",
+ "\n",
+ "计算机革命的起源来自机器。编程语言就像是那台机器。它不仅是我们思维放大的工具与另一种表达媒介,更像是我们思想的一部分。语言的灵感来自其他形式的表达,如写作,绘画,雕塑,动画和电影制作。编程语言就是创建应用程序的思想结构。\n",
+ "\n",
+ "面向对象编程(Object-Oriented Programming OOP)是一种编程思维方式和编码架构。本章讲述 OOP 的基本概述。如果读者对此不太理解,可先行跳过本章。等你具备一定编程基础后,请务必再回头看。只有这样你才能深刻理解面向对象编程的重要性及设计方式。\n",
+ "\n",
+ "## 抽象\n",
+ "\n",
+ "所有编程语言都提供抽象机制。从某种程度上来说,问题的复杂度直接取决于抽象的类型和质量。这里的“类型”意思是:抽象的内容是什么?汇编语言是对底层机器的轻微抽象。接着出现的“命令式”语言(如 FORTRAN,BASIC 和 C)是对汇编语言的抽象。与汇编相比,这类语言已有了长足的改进,但它们的抽象原理依然要求我们着重考虑计算机的结构,而非问题本身的结构。\n",
+ "\n",
+ "程序员必须要在机器模型(“解决方案空间”)和实际解决的问题模型(“问题空间”)之间建立起一种关联。这个过程既费精力,又脱离编程语言本身的范畴。这使得程序代码很难编写,维护代价高昂。同时还造就了一个副产业“编程方法”学科。\n",
+ "\n",
+ "为机器建模的另一个方法是为要解决的问题制作模型。对一些早期语言来说,如 LISP 和 APL,它们的做法是“从不同的角度观察世界”——“所有问题都归纳为列表”或“所有问题都归纳为算法”。PROLOG 则将所有\n",
+ "问题都归纳为决策链。对于这些语言,我们认为它们一部分是“基于约束”的编程,另一部分则是专为\n",
+ "处理图形符号设计的(后者被证明限制性太强)。每种方法都有自己特殊的用途,适合解决某一类的问题。只要超出了它们力所能及的范围,就会显得非常笨拙。\n",
+ "\n",
+ "面向对象的程序设计在此基础上跨出了一大步,程序员可利用一些工具表达“问题空间”内的元素。由于这种表达非常具有普遍性,所以不必受限于特定类型的问题。我们将问题空间中的元素以及它们在解决方案空间的表示称作“对象”(**Object**)。当然,还有一些在问题空间没有对应的对象体。通过添加新的对象类型,程序可进行灵活的调整,以便与特定的问题配合。所以当你在阅读描述解决方案的代码时,也是在阅读问题的表述。与我们以前见过的相比,这无疑是一种更加灵活、更加强大的语言抽象方法。总之,OOP 允许我们根据问题来描述问题,而不是根据运行解决方案的计算机。然而,它仍然与计算机有联系,每个对象都类似一台小计算机:它们有自己的状态并且可以进行特定的操作。这与现实世界的“对象”或者“物体”相似:它们都有自己的特征和行为。\n",
+ "\n",
+ "Smalltalk 作为第一个成功的面向对象并影响了 Java 的程序设计语言 ,*Alan Kay* 总结了其五大基本特征。通过这些特征,我们可理解“纯粹”的面向对象程序设计方法是什么样的:\n",
+ "\n",
+ "> 1. **万物皆对象**。你可以将对象想象成一种特殊的变量。它存储数据,但可以在你对其“发出请求”时执行本身的操作。理论上讲,你总是可以从要解决的问题身上抽象出概念性的组件,然后在程序中将其表示为一个对象。\n",
+ "> 2. **程序是一组对象,通过消息传递来告知彼此该做什么**。要请求调用一个对象的方法,你需要向该对象发送消息。\n",
+ "> 3. **每个对象都有自己的存储空间,可容纳其他对象**。或者说,通过封装现有对象,可制作出新型对象。所以,尽管对象的概念非常简单,但在程序中却可达到任意高的复杂程度。\n",
+ "> 4. **每个对象都有一种类型**。根据语法,每个对象都是某个“类”的一个“实例”。其中,“类”(Class)是“类型”(Type)的同义词。一个类最重要的特征就是“能将什么消息发给它?”。\n",
+ "> 5. **同一类所有对象都能接收相同的消息**。这实际是别有含义的一种说法,大家不久便能理解。由于类型为“圆”(Circle)的一个对象也属于类型为“形状”(Shape)的一个对象,所以一个圆完全能接收发送给\"形状”的消息。这意味着可让程序代码统一指挥“形状”,令其自动控制所有符合“形状”描述的对象,其中自然包括“圆”。这一特性称为对象的“可替换性”,是OOP最重要的概念之一。\n",
+ "\n",
+ "*Grady Booch* 提供了对对象更简洁的描述:一个对象具有自己的状态,行为和标识。这意味着对象有自己的内部数据(提供状态)、方法 (产生行为),并彼此区分(每个对象在内存中都有唯一的地址)。\n",
+ "\n",
+ "## 接口\n",
+ "\n",
+ "亚里士多德(*Aristotle*)大概是第一个认真研究“类型”的哲学家,他曾提出过“鱼类和鸟类”这样的概念。所有对象都是唯一的,但同时也是具有相同的特性和行为的对象所归属的类的一部分。这种思想被首次应用于第一个面向对象编程语言 Simula-67,它在程序中使用基本关键字 **class** 来引入新的类型(class 和 type 通常可互换使用,有些人对它们进行了进一步区分,他们强调 type 决定了接口,而 class 是那个接口的一种特殊实现方式)。\n",
+ "\n",
+ "Simula 是一个很好的例子。正如这个名字所暗示的,它的作用是“模拟”(Simulate)类似“银行出纳员”这样的经典问题。在这个例子里,我们有一系列出纳员、客户、帐号、交易和货币单位等许多\"对象”。每类成员(元素)都具有一些通用的特征:每个帐号都有一定的余额;每名出纳都能接收客户的存款;等等。与此同时,每个成员都有自己的状态;每个帐号都有不同的余额;每名出纳都有一个名字。所以在计算机程序中,能用独一无二的实体分别表示出纳员、客户、帐号以及交易。这个实体便是“对象”,而且每个对象都隶属一个特定的“类”,那个类具有自己的通用特征与行为。\n",
+ "\n",
+ "因此,在面向对象的程序设计中,尽管我们真正要做的是新建各种各样的数据“类型”(Type),但几乎所有面向对象的程序设计语言都采用了 `class` 关键字。当你看到 “type” 这个词的时候,请同时想到 `class`;反之亦然。\n",
+ "\n",
+ "创建好一个类后,可根据情况生成许多对象。随后,可将那些对象作为要解决问题中存在的元素进行处理。事实上,当我们进行面向对象的程序设计时,面临的最大一项挑战是:如何在“问题空间”(问题实际存在的地方)的元素与“方案空间”(对实际问题进行建模的地方,如计算机)的元素之间建立理想的“一对一”的映射关系。\n",
+ "\n",
+ "那么如何利用对象完成真正有用的工作呢?必须有一种办法能向对象发出请求,令其解决一些实际的问题,比如完成一次交易、在屏幕上画一些东西或者打开一个开关等等。每个对象仅能接受特定的请求。我们向对象发出的请求是通过它的“接口”(Interface)定义的,对象的“类型”或“类”则规定了它的接口形式。“类型”与“接口”的对应关系是面向对象程序设计的基础。\n",
+ "\n",
+ "下面让我们以电灯泡为例:\n",
+ "\n",
+ ""
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "Light lt = new Light();\n",
+ "lt.on();"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "在这个例子中,类型/类的名称是 **Light**,可向 **Light** 对象发出的请求包括打开 `on`、关闭 `off`、变得更明亮 `brighten` 或者变得更暗淡 `dim`。通过声明一个引用,如 `lt` 和 `new` 关键字,我们创建了一个 **Light** 类型的对象,再用等号将其赋给引用。\n",
+ "\n",
+ "为了向对象发送消息,我们使用句点符号 `.` 将 `lt` 和消息名称 `on` 连接起来。可以看出,使用一些预先定义好的类时,我们在程序里采用的代码是非常简单直观的。\n",
+ "\n",
+ "上图遵循 **UML**(Unified Modeling Language,统一建模语言)的格式。每个类由一个框表示,框的顶部有类型名称,框中间部分是要描述的任何数据成员,方法(属于此对象的方法,它们接收任何发送到该对象的消息)在框的底部。通常,只有类的名称和公共方法在 **UML** 设计图中显示,因此中间部分未显示,如本例所示。如果你只对类名感兴趣,则也不需要显示方法信息。\n",
+ "\n",
+ "## 服务提供\n",
+ "\n",
+ "在开发或理解程序设计时,我们可以将对象看成是“服务提供者”。你的程序本身将为用户提供服务,并且它能通过调用其他对象提供的服务来实现这一点。我们的最终目标是开发或调用工具库中已有的一些对象,提供理想的服务来解决问题。\n",
+ "\n",
+ "那么问题来了:我们该选择哪个对象来解决问题呢?例如,你正在开发一个记事本程序。*你可能会想到在屏幕输入默认的记事本对象*,一个用于检测不同类型打印机并执行打印的对象。这些对象中的某些已经有了。那对于还没有的对象,我们该设计成啥样呢?这些对象需要提供哪些服务,以及还需要调用其他哪些对象?\n",
+ "\n",
+ "我们可以将这些问题一一分解,抽象成一组服务。软件设计的基本原则是高内聚:每个组件的内部作用明确,功能紧密相关。然而经常有人将太多功能塞进一个对象中。例如:在支票打印模块中,你需要设计一个可以同时读取文本格式又能正确识别不同打印机型号的对象。正确的做法是提供三个或更多对象:一个对象检查所有排版布局的目录;一个或一组可以识别不同打印机型号的对象展示通用的打印界面;第三个对象组合上述两个服务来完成任务。这样,每个对象都提供了一组紧密的服务。在良好的面向对象设计中,每个对象功能单一且高效。这样的程序设计可以提高我们代码的复用性,同时也方便别人阅读和理解我们的代码。只有让人知道你提供什么服务,别人才能更好地将其应用到其他模块或程序中。\n",
+ "\n",
+ "## 封装\n",
+ "\n",
+ "我们可以把编程的侧重领域划分为研发和应用。应用程序员调用研发程序员构建的基础工具类来做快速开发。研发程序员开发一个工具类,该工具类仅向应用程序员公开必要的内容,并隐藏内部实现的细节。这样可以有效地避免该工具类被错误的使用和更改,从而减少程序出错的可能。彼此职责划分清晰,相互协作。当应用程序员调用研发程序员开发的工具类时,双方建立了关系。应用程序员通过使用现成的工具类组装应用程序或者构建更大的工具库。如果工具类的创建者将类的内部所有信息都公开给调用者,那么有些使用规则就不容易被遵守。因为前者无法保证后者是否会按照正确的规则来使用,甚至是改变该工具类。只有设定访问控制,才能从根本上阻止这种情况的发生。\n",
+ "\n",
+ "因此,使用访问控制的原因有以下两点:\n",
+ "\n",
+ "1. 让应用程序员不要触摸他们不应该触摸的部分。(请注意,这也是一个哲学决策。部分编程语言认为如果程序员有需要,则应该让他们访问细节部分。);\n",
+ "\n",
+ "2. 使类库的创建者(研发程序员)在不影响后者使用的情况下完善更新工具库。例如,我们开发了一个功能简单的工具类,后来发现可以通过优化代码来提高执行速度。假如工具类的接口和实现部分明确分开并受到保护,那我们就可以轻松地完成改造。\n",
+ "\n",
+ "Java 有三个显式关键字来设置类中的访问权限:`public`(公开),`private`(私有)和`protected`(受保护)。这些访问修饰符决定了谁能使用它们修饰的方法、变量或类。\n",
+ "\n",
+ " 1. `public`(公开)表示任何人都可以访问和使用该元素;\n",
+ "\n",
+ " 2. `private`(私有)除了类本身和类内部的方法,外界无法直接访问该元素。`private` 是类和调用者之间的屏障。任何试图访问私有成员的行为都会报编译时错误;\n",
+ "\n",
+ " 3. `protected`(受保护)类似于 `private`,区别是子类(下一节就会引入继承的概念)可以访问 `protected` 的成员,但不能访问 `private` 成员;\n",
+ "\n",
+ " 4. `default`(默认)如果你不使用前面的三者,默认就是 `default` 访问权限。`default` 被称为包访问,因为该权限下的资源可以被同一包(库组件)中其他类的成员访问。\n",
+ "\n",
+ "## 复用\n",
+ "\n",
+ "一个类经创建和测试后,理应是可复用的。然而很多时候,由于程序员没有足够的编程经验和远见,我们的代码复用性并不强。\n",
+ "\n",
+ "代码和设计方案的复用性是面向对象程序设计的优点之一。我们可以通过重复使用某个类的对象来达到这种复用性。同时,我们也可以将一个类的对象作为另一个类的成员变量使用。新的类可以是由任意数量和任意类型的其他对象构成。这里涉及到“组合”和“聚合”的概念:\n",
+ "\n",
+ "* **组合**(Composition)经常用来表示“拥有”关系(has-a relationship)。例如,“汽车拥有引擎”。\n",
+ "\n",
+ "* **聚合**(Aggregation)动态的**组合**。\n",
+ "\n",
+ "\n",
+ "\n",
+ "上图中实心三角形指向“ **Car** ”表示 **组合** 的关系;如果是 **聚合** 关系,可以使用空心三角形。\n",
+ "\n",
+ "(**译者注**:组合和聚合都属于关联关系的一种,只是额外具有整体-部分的意义。至于是聚合还是组合,需要根据实际的业务需求来判断。可能相同超类和子类,在不同的业务场景,关联关系会发生变化。只看代码是无法区分聚合和组合的,具体是哪一种关系,只能从语义级别来区分。聚合关系中,整件不会拥有部件的生命周期,所以整件删除时,部件不会被删除。再者,多个整件可以共享同一个部件。组合关系中,整件拥有部件的生命周期,所以整件删除时,部件一定会跟着删除。而且,多个整件不可以同时共享同一个部件。这个区别可以用来区分某个关联关系到底是组合还是聚合。两个类生命周期不同步,则是聚合关系,生命周期同步就是组合关系。)\n",
+ "\n",
+ "使用“组合”关系给我们的程序带来极大的灵活性。通常新建的类中,成员对象会使用 `private` 访问权限,这样应用程序员则无法对其直接访问。我们就可以在不影响客户代码的前提下,从容地修改那些成员。我们也可以在“运行时\"改变成员对象从而动态地改变程序的行为,这进一步增大了灵活性。下面一节要讲到的“继承”并不具备这种灵活性,因为编译器对通过继承创建的类进行了限制。\n",
+ "\n",
+ "在面向对象编程中经常重点强调“继承”。在新手程序员的印象里,或许先入为主地认为“继承应当随处可见”。沿着这种思路产生的程序设计通常拙劣又复杂。相反,在创建新类时首先要考虑“组合”,因为它更简单灵活,而且设计更加清晰。等我们有一些编程经验后,一旦需要用到继承,就会明显意识到这一点。\n",
+ "\n",
+ "## 继承\n",
+ "\n",
+ "“继承”给面向对象编程带来极大的便利。它在概念上允许我们将各式各样的数据和功能封装到一起,这样便可恰当表达“问题空间”的概念,而不用受制于必须使用底层机器语言。\n",
+ "\n",
+ "通过使用 `class` 关键字,这些概念形成了编程语言中的基本单元。遗憾的是,这么做还是有很多麻烦:在创建了一个类之后,即使另一个新类与其具有相似的功能,你还是得重新创建一个新类。但我们若能利用现成的数据类型,对其进行“克隆”,再根据情况进行添加和修改,情况就显得理想多了。“继承”正是针对这个目标而设计的。但继承并不完全等价于克隆。在继承过程中,若原始类(正式名称叫作基类、超类或父类)发生了变化,修改过的“克隆”类(正式名称叫作继承类或者子类)也会反映出这种变化。\n",
+ "\n",
+ "\n",
+ "\n",
+ "这个图中的箭头从派生类指向基类。正如你将看到的,通常有多个派生类。类型不仅仅描述一组对象的约束,它还涉及其他类型。两种类型可以具有共同的特征和行为,但是一种类型可能包含比另一种类型更多的特征,并且还可以处理更多的消息(或者以不同的方式处理它们)。继承通过基类和派生类的概念来表达这种相似性。基类包含派生自它的类型之间共享的所有特征和行为。创建基类以表示思想的核心。从基类中派生出其他类型来表示实现该核心的不同方式。\n",
+ "\n",
+ "\n",
+ "\n",
+ "例如,垃圾回收机对垃圾进行分类。基类是“垃圾”。每块垃圾都有重量、价值等特性,它们可以被切碎、熔化或分解。在此基础上,可以通过添加额外的特性(瓶子有颜色,钢罐有磁性)或行为(铝罐可以被压碎)派生出更具体的垃圾类型。此外,一些行为可以不同(纸张的价值取决于它的类型和状态)。使用继承,你将构建一个类型层次结构,来表示你试图解决的某种类型的问题。第二个例子是常见的“形状”例子,可能用于计算机辅助设计系统或游戏模拟。基类是“形状”,每个形状都有大小、颜色、位置等等。每个形状可以绘制、擦除、移动、着色等。由此,可以派生出(继承出)具体类型的形状——圆形、正方形、三角形等等——每个形状可以具有附加的特征和行为。\n",
+ "\n",
+ "\n",
+ "\n",
+ "例如,某些形状可以翻转。有些行为可能不同,比如计算形状的面积。类型层次结构体现了形状之间的相似性和差异性。以相同的术语将解决方案转换成问题是有用的,因为你不需要在问题描述和解决方案描述之间建立许多中间模型。通过使用对象,类型层次结构成为了主要模型,因此你可以直接从真实世界中对系统的描述过渡到用代码对系统进行描述。事实上,有时候,那些善于寻找复杂解决方案的人会被面向对象设计的简单性难倒。从现有类型继承创建新类型。这种新类型不仅包含现有类型的所有成员(尽管私有成员被隐藏起来并且不可访问),而且更重要的是它复制了基类的接口。也就是说,基类对象接收的所有消息也能被派生类对象接收。根据类接收的消息,我们知道类的类型,因此派生类与基类是相同的类型。\n",
+ "\n",
+ "在前面的例子中,“圆是形状”。这种通过继承的类型等价性是理解面向对象编程含义的基本门槛之一。因为基类和派生类都具有相同的基本接口,所以伴随此接口的必定有某些具体实现。也就是说,当对象接收到特定消息时,必须有可执行代码。如果继承一个类而不做其他任何事,则来自基类接口的方法直接进入派生类。这意味着派生类和基类不仅具有相同的类型,而且具有相同的行为,这么做没什么特别意义。\n",
+ "\n",
+ "有两种方法可以区分新的派生类与原始的基类。第一种方法很简单:在派生类中添加新方法。这些新方法不是基类接口的一部分。这意味着基类不能满足你的所有需求,所以你添加了更多的方法。继承的这种简单而原始的用途有时是解决问题的完美解决方案。然而,还是要仔细考虑是否在基类中也要有这些额外的方法。这种设计的发现与迭代过程在面向对象程序设计中会经常发生。\n",
+ "\n",
+ "尽管继承有时意味着你要在接口中添加新方法(尤其是在以 **extends** 关键字表示继承的 Java 中),但并非总需如此。第二种也是更重要地区分派生类和基类的方法是改变现有基类方法的行为,这被称为覆盖 (overriding)。要想覆盖一个方法,只需要在派生类中重新定义这个方法即可。\n",
+ "\n",
+ "### \"是一个\"与\"像是一个\"的关系\n",
+ "\n",
+ "对于继承可能会引发争论:继承应该只覆盖基类的方法(不应该添加基类中没有的方法)吗?如果这样的话,基类和派生类就是相同的类型了,因为它们具有相同的接口。这会造成,你可以用一个派生类对象完全替代基类对象,这叫作\"纯粹替代\",也经常被称作\"替代原则\"。在某种意义上,这是一种处理继承的理想方式。我们经常把这种基类和派生类的关系称为是一个(is-a)关系,因为可以说\"圆是一个形状\"。判断是否继承,就看在你的类之间有无这种 is-a 关系。\n",
+ "\n",
+ "有时你在派生类添加了新的接口元素,从而扩展接口。虽然新类型仍然可以替代基类,但是这种替代不完美,原因在于基类无法访问新添加的方法。这种关系称为像是一个(is-like-a)关系。新类型不但拥有旧类型的接口,而且包含其他方法,所以不能说新旧类型完全相同。\n",
+ "\n",
+ "\n",
+ "\n",
+ "以空调为例,假设房间里已经安装好了制冷设备的控制器,即你有了控制制冷设备的接口。想象一下,现在空调坏了,你重新安装了一个既制冷又制热的热力泵。热力泵就像是一个(is-like-a)空调,但它可以做更多。因为当初房间的控制系统被设计成只能控制制冷设备,所以它只能与新对象(热力泵)的制冷部分通信。新对象的接口已经扩展了,现有控制系统却只知道原来的接口,一旦看到这个设计,你就会发现,作为基类的制冷系统不够一般化,应该被重新命名为\"温度控制系统\",也应该包含制热功能,这样的话,我们就可以使用替代原则了。上图反映了在现实世界中进行设计时可能会发生的事情。\n",
+ "\n",
+ "当你看到替代原则时,很容易会认为纯粹替代是唯一可行的方式,并且使用纯粹替代的设计是很好的。但有些时候,你会发现必须得在派生(扩展)类中添加新方法(提供新的接口)。只要仔细审视,你可以很明显地区分两种设计方式的使用场合。\n",
+ "\n",
+ "## 多态\n",
+ "\n",
+ "我们在处理类的层次结构时,通常把一个对象看成是它所属的基类,而不是把它当成具体类。通过这种方式,我们可以编写出不局限于特定类型的代码。在上个“形状”的例子中,“方法”(method)操纵的是通用“形状”,而不关心它们是“圆”、“正方形”、“三角形”还是某种尚未定义的形状。所有的形状都可以被绘制、擦除和移动,因此“方法”向其中的任何代表“形状”的对象发送消息都不必担心对象如何处理信息。\n",
+ "\n",
+ "这样的代码不会受添加的新类型影响,并且添加新类型是扩展面向对象程序以处理新情况的常用方法。 例如,你可以通过通用的“形状”基类派生出新的“五角形”形状的子类,而不需要修改通用\"形状\"基类的方法。通过派生新的子类来扩展设计的这种能力是封装变化的基本方法之一。\n",
+ "\n",
+ "这种能力改善了我们的设计,且减少了软件的维护代价。如果我们把派生的对象类型统一看成是它本身的基类(“圆”当作“形状”,“自行车”当作“车”,“鸬鹚”当作“鸟”等等),编译器(compiler)在编译时期就无法准确地知道什么“形状”被擦除,哪一种“车”在行驶,或者是哪种“鸟”在飞行。这就是关键所在:当程序接收这种消息时,程序员并不想知道哪段代码会被执行。“绘图”的方法可以平等地应用到每种可能的“形状”上,形状会依据自身的具体类型执行恰当的代码。\n",
+ "\n",
+ "如果不需要知道执行了哪部分代码,那我们就能添加一个新的不同执行方式的子类而不需要更改调用它的方法。那么编译器在不确定该执行哪部分代码时是怎么做的呢?举个例子,下图的 **BirdController** 对象和通用 **Bird** 对象中,**BirdController** 不知道 **Bird** 的确切类型却还能一起工作。从 **BirdController** 的角度来看,这是很方便的,因为它不需要编写特别的代码来确定 **Bird** 对象的确切类型或行为。那么,在调用 **move()** 方法时是如何保证发生正确的行为(鹅走路、飞或游泳、企鹅走路或游泳)的呢?\n",
+ "\n",
+ "\n",
+ "\n",
+ "这个问题的答案,是面向对象程序设计的妙诀:在传统意义上,编译器不能进行函数调用。由非 OOP 编译器产生的函数调用会引起所谓的**早期绑定**,这个术语你可能从未听说过,不会想过其他的函数调用方式。这意味着编译器生成对特定函数名的调用,该调用会被解析为将执行的代码的绝对地址。\n",
+ "\n",
+ "通过继承,程序直到运行时才能确定代码的地址,因此发送消息给对象时,还需要其他一些方案。为了解决这个问题,面向对象语言使用**后期绑定**的概念。当向对象发送信息时,被调用的代码直到运行时才确定。编译器确保方法存在,并对参数和返回值执行类型检查,但是它不知道要执行的确切代码。\n",
+ "\n",
+ "为了执行后期绑定,Java 使用一个特殊的代码位来代替绝对调用。这段代码使用对象中存储的信息来计算方法主体的地址(此过程在多态性章节中有详细介绍)。因此,每个对象的行为根据特定代码位的内容而不同。当你向对象发送消息时,对象知道该如何处理这条消息。在某些语言中,必须显式地授予方法后期绑定属性的灵活性。例如,C++ 使用 **virtual** 关键字。在这些语言中,默认情况下方法不是动态绑定的。在 Java 中,动态绑定是默认行为,不需要额外的关键字来实现多态性。\n",
+ "\n",
+ "为了演示多态性,我们编写了一段代码,它忽略了类型的具体细节,只与基类对话。该代码与具体类型信息分离,因此更易于编写和理解。而且,如果通过继承添加了一个新类型(例如,一个六边形),那么代码对于新类型的 Shape 就像对现有类型一样有效。因此,该程序是可扩展的。\n",
+ "\n",
+ "代码示例:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "void doSomething(Shape shape) {\n",
+ " shape.erase();\n",
+ " // ...\n",
+ " shape.draw();\n",
+ "}"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "此方法与任何 **Shape** 对话,因此它与所绘制和擦除的对象的具体类型无关。如果程序的其他部分使用 `doSomething()` 方法:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ " Circle circle = new Circle();\n",
+ " Triangle triangle = new Triangle();\n",
+ " Line line = new Line();\n",
+ " doSomething(circle);\n",
+ " doSomething(triangle);\n",
+ " doSomething(line);\n"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "可以看到无论传入的“形状”是什么,程序都正确的执行了。\n",
+ "\n",
+ "\n",
+ "\n",
+ "这是一个非常令人惊奇的编程技巧。分析下面这行代码:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ " doSomething(circle);"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "当预期接收 **Shape** 的方法被传入了 **Circle**,会发生什么。由于 **Circle** 也是一种 **Shape**,所\n",
+ "以 `doSomething(circle)` 能正确地执行。也就是说,`doSomething()` 能接收任意发送给 **Shape** 的消息。这是完全安全和合乎逻辑的事情。\n",
+ "\n",
+ "这种把子类当成其基类来处理的过程叫做“向上转型”(**upcasting**)。在面向对象的编程里,经常利用这种方法来给程序解耦。再看下面的 `doSomething()` 代码示例:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ " shape.erase();\n",
+ " // ...\n",
+ " shape.draw();"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "我们可以看到程序并未这样表达:“如果你是一个 Circle ,就这样做;如果你是一个 Square,就那样做...”。若那样编写代码,就需检查 Shape 所有可能的类型,如圆、矩形等等。这显然是非常麻烦的,而且每次添加了一种新的 Shape 类型后,都要相应地进行修改。在这里,我们只需说:“你是一种几何形状,我知道你能删掉 `erase()` 和绘制 `draw()`,你自己去做吧,注意细节。”\n",
+ "\n",
+ "尽管我们没作出任何特殊指示,程序的操作也是完全正确和恰当的。我们知道,为 Circle 调用`draw()` 时执行的代码与为一个 Square 或 Line 调用 `draw()` 时执行的代码是不同的。但在将 `draw()` 信息发给一个匿名 Shape 时,根据 Shape 句柄当时连接的实际类型,会相应地采取正确的操作。这非常神奇,因为当 Java 编译器为 `doSomething()` 编译代码时,它并不知道自己要操作的准确类型是什么。\n",
+ "\n",
+ "尽管我们确实可以保证最终会为 Shape 调用 `erase()` 和 `draw()`,但并不能确定特定的 Circle,Square 或者 Line 调用什么。最后,程序执行的操作却依然是正确的,这是怎么做到的呢?\n",
+ "\n",
+ "发送消息给对象时,如果程序不知道接收的具体类型是什么,但最终执行是正确的,这就是对象的“多态性”(Polymorphism)。面向对象的程序设计语言是通过“动态绑定”的方式来实现对象的多态性的。编译器和运行时系统会负责对所有细节的控制;我们只需知道要做什么,以及如何利用多态性来更好地设计程序。\n",
+ "\n",
+ "## 单继承结构\n",
+ "\n",
+ "自从 C++ 引入以来,一个 OOP 问题变得尤为突出:是否所有的类都应该默认从一个基类继承呢?这个答案在 Java 中是肯定的(实际上,除 C++ 以外的几乎所有OOP语言中也是这样)。在 Java 中,这个最终基类的名字就是 `Object`。\n",
+ "\n",
+ "Java 的单继承结构有很多好处。由于所有对象都具有一个公共接口,因此它们最终都属于同一个基类。相反的,对于 C++ 所使用的多继承的方案则是不保证所有的对象都属于同一个基类。从向后兼容的角度看,多继承的方案更符合 C 的模型,而且受限较少。\n",
+ "\n",
+ "对于完全面向对象编程,我们必须要构建自己的层次结构,以提供与其他 OOP 语言同样的便利。我们经常会使用到新的类库和不兼容的接口。为了整合它们而花费大气力(有可能还要用上多继承)以获得 C++ 样的“灵活性”值得吗?如果从零开始,Java 这样的替代方案会是更好的选择。\n",
+ "\n",
+ "另外,单继承的结构使得垃圾收集器的实现更为容易。这也是 Java 在 C++ 基础上的根本改进之一。\n",
+ "\n",
+ "由于运行期的类型信息会存在于所有对象中,所以我们永远不会遇到判断不了对象类型的情况。这对于系统级操作尤其重要,例如[异常处理](#异常处理)。同时,这也让我们的编程具有更大的灵活性。\n",
+ "\n",
+ "## 集合\n",
+ "\n",
+ "通常,我们并不知道解决某个具体问题需要的对象数量和持续时间,以及对象的存储方式。那么我们如何知悉程序在运行时需要分配的内存空间呢?\n",
+ "\n",
+ "在面向对象的设计中,问题的解决方案有些过于轻率:创建一个新类型的对象来引用、容纳其他的对象。当然,我们也可以使用多数编程语言都支持的“数组”(array)。在 Java 中“集合”(Collection)的使用率更高。(也可称之为“容器”,但“集合”这个称呼更通用。)\n",
+ "\n",
+ "“集合”这种类型的对象可以存储任意类型、数量的其他对象。它能根据需要自动扩容,我们不用关心过程是如何实现的。\n",
+ "\n",
+ "还好,一般优秀的 OOP 语言都会将“集合”作为其基础包。在 C++ 中,“集合”是其标准库的一部分,通常被称为 STL(Standard Template Library,标准模板库)。SmallTalk 有一套非常完整的集合库。同样,Java 的标准库中也提供许多现成的集合类。\n",
+ "\n",
+ "在一些库中,一两个泛型集合就能满足我们所有的需求了,而在其他一些类库(Java)中,不同类型的集合对应不同的需求:常见的有 List,常用于保存序列;Map,也称为关联数组,常用于将对象与其他对象关联;Set,只能保存非重复的值;其他还包括如队列(Queue)、树(Tree)、栈(Stack)、堆(Heap)等等。从设计的角度来看,我们真正想要的是一个能够解决某个问题的集合。如果一种集合就满足所有需求,那么我们就不需要剩下的了。之所以选择集合有以下两个原因:\n",
+ "\n",
+ "1. 集合可以提供不同类型的接口和外部行为。堆栈、队列的应用场景和集合、列表不同,它们中的一种提供的解决方案可能比其他灵活得多。\n",
+ "\n",
+ "2. 不同的集合对某些操作有不同的效率。例如,List 的两种基本类型:ArrayList 和 LinkedList。虽然两者具有相同接口和外部行为,但是在某些操作中它们的效率差别很大。在 ArrayList 中随机查找元素是很高效的,而 LinkedList 随机查找效率低下。反之,在 LinkedList 中插入元素的效率要比在 ArrayList 中高。由于底层数据结构的不同,每种集合类型在执行相同的操作时会表现出效率上的差异。\n",
+ "\n",
+ "我们可以一开始使用 LinkedList 构建程序,在优化系统性能时改用 ArrayList。通过对 List 接口的抽象,我们可以很容易地将 LinkedList 改为 ArrayList。\n",
+ "\n",
+ "在 Java 5 泛型出来之前,集合中保存的是通用类型 `Object`。Java 单继承的结构意味着所有元素都基于 `Object` 类,所以在集合中可以保存任何类型的数据,易于重用。要使用这样的集合,我们先要往集合添加元素。由于 Java 5 版本前的集合只保存 `Object`,当我们往集合中添加元素时,元素便向上转型成了 `Object`,从而丢失自己原有的类型特性。这时我们再从集合中取出该元素时,元素的类型变成了 `Object`。那么我们该怎么将其转回原先具体的类型呢?这里,我们使用了强制类型转换将其转为更具体的类型,这个过程称为对象的“向下转型”。通过“向上转型”,我们知道“圆形”也是一种“形状”,这个过程是安全的。可是我们不能从“Object”看出其就是“圆形”或“形状”,所以除非我们能确定元素的具体类型信息,否则“向下转型”就是不安全的。也不能说这样的错误就是完全危险的,因为一旦我们转化了错误的类型,程序就会运行出错,抛出“运行时异常”(RuntimeException)。(后面的章节会提到) 无论如何,我们要寻找一种在取出集合元素时确定其具体类型的方法。另外,每次取出元素都要做额外的“向下转型”对程序和程序员都是一种开销。以某种方式创建集合,以确认保存元素的具体类型,减少集合元素“向下转型”的开销和可能出现的错误难道不好吗?这种解决方案就是:参数化类型机制(Parameterized Type Mechanism)。\n",
+ "\n",
+ "参数化类型机制可以使得编译器能够自动识别某个 `class` 的具体类型并正确地执行。举个例子,对集合的参数化类型机制可以让集合仅接受“形状”这种类型的元素,并以“形状”类型取出元素。Java 5 版本支持了参数化类型机制,称之为“泛型”(Generic)。泛型是 Java 5 的主要特性之一。你可以按以下方式向 ArrayList 中添加 Shape(形状):"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ " ArrayList shapes = new ArrayList<>();"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "泛型的应用,让 Java 的许多标准库和组件都发生了改变。在本书的代码示例中,你也会经常看到泛型的身影。\n",
+ "\n",
+ "## 对象创建与生命周期\n",
+ "\n",
+ "我们在使用对象时要注意的一个关键问题就是对象的创建和销毁方式。每个对象的生存都需要资源,尤其是内存。为了资源的重复利用,当对象不再被使用时,我们应该及时释放资源,清理内存。\n",
+ "\n",
+ "在简单的编程场景下,对象的清理并不是问题。我们创建对象,按需使用,最后销毁它。然而,情况往往要比这更复杂:\n",
+ "\n",
+ "假设,我们正在为机场设计一个空中交通管制的系统(该例也适用于仓库货柜管理、影带出租或者宠物寄养仓库系统)。第一步比较简单:创建一个用来保存飞机的集合,每当有飞机进入交通管制区域时,我们就创建一个“飞机”对象并将其加入到集合中,等到飞机离开时将其从这个集合中清除。与此同时,我们还需要一个记录飞机信息的系统,也许这些数据不像主要控制功能那样引人注意。比如,我们要记录所有飞机中的小型飞机的的信息(比如飞行计划)。此时,我们又创建了第二个集合来记录所有小型飞机。 每当创建一个“飞机”对象的时候,将其放入第一个集合;若它属于小型飞机,也必须同时将其放入第二个集合里。\n",
+ "\n",
+ "现在问题开始棘手了:我们怎么知道何时该清理这些对象呢?当某一个系统处理完成,而其他系统可能还没有处理完成。这样的问题在其他的场景下也可能发生。在 C++ 程序设计中,当使用完一个对象后,必须明确将其删除,这就让问题变复杂了。\n",
+ "\n",
+ "对象的数据在哪?它的生命周期是怎么被控制的? 在 C++ 设计中采用的观点是效率第一,因此它将选择权交给了程序员。为了获得最大的运行时速度,程序员可以在编写程序时,通过将对象放在栈(Stack,有时称为自动变量或作用域变量)或静态存储区域(static storage area)中来确定内存占用和生存时间。这些区域的对象会被优先分配内存和释放。这种控制在某些情况下非常有用。\n",
+ "\n",
+ "然而相对的,我们也牺牲了程序的灵活性。因为在编写代码时,我们必须要弄清楚对象的数量、生存时间还有类型。如果我们要用它来解决一个相当普遍的问题时(如计算机辅助设计、仓库管理或空中交通管制等),限制就太大了。\n",
+ "\n",
+ "第二种方法是在堆内存(Heap)中动态地创建对象。在这种方式下,直到程序运行我们才能确定需要创建的对象数量、生存时间和类型。什么时候需要,什么时候在堆内存中创建。 因为内存的占用是动态管理的,所以在运行时,在堆内存上开辟空间所需的时间可能比在栈内存上要长(但也不一定)。在栈内存开辟和释放空间通常是一条将栈指针向下移动和一条将栈指针向上移动的汇编指令。开辟堆内存空间的时间取决于内存机制的设计。\n",
+ "\n",
+ "动态方法有这样一个合理假设:对象通常是复杂的,相比于对象创建的整体开销,寻找和释放内存空间的开销微不足道。(原文:*The dynamic approach makes the generally logical assumption that objects tend to be complicated, so the extra overhead of finding storage and releasing that storage will not have an important impact on the creation of an object.*)此外,更好的灵活性对于问题的解决至关重要。\n",
+ "\n",
+ "Java 使用动态内存分配。每次创建对象时,使用 `new` 关键字构建该对象的动态实例。这又带来另一个问题:对象的生命周期。较之堆内存,在栈内存中创建对象,编译器能够确定该对象的生命周期并自动销毁它;然而如果你在堆内存创建对象的话,编译器是不知道它的生命周期的。在 C++ 中你必须以编程方式确定何时销毁对象,否则可能导致内存泄漏。Java 的内存管理是建立在垃圾收集器上的,它能自动发现对象不再被使用并释放内存。垃圾收集器的存在带来了极大的便利,它减少了我们之前必须要跟踪的问题和编写相关代码的数量。因此,垃圾收集器提供了更高级别的保险,以防止潜在的内存泄漏问题,这个问题使得许多 C++ 项目没落。\n",
+ "\n",
+ "Java 的垃圾收集器被设计用来解决内存释放的问题(虽然这不包括对象清理的其他方面)。垃圾收集器知道对象什么时候不再被使用并且自动释放内存。结合单继承和仅可在堆中创建对象的机制,Java 的编码过程比用 C++ 要简单得多。我们所要做的决定和要克服的障碍也会少很多!\n",
+ "\n",
+ "## 异常处理\n",
+ "\n",
+ "自编程语言被发明以来,程序的错误处理一直都是个难题。因为很难设计出一个好的错误处理方案,所以许多编程语言都忽略了这个问题,把这个问题丢给了程序类库的设计者。他们提出了在许多情况下都可以工作但很容易被规避的半途而废的措施,通常只需忽略错误。多数错误处理方案的主要问题是:它们依赖程序员之间的约定俗成而不是语言层面的限制。换句话说,如果程序员赶时间或没想起来,这些方案就很容易被忘记。\n",
+ "\n",
+ "异常处理机制将程序错误直接交给编程语言甚至是操作系统。“异常”(Exception)是一个从出错点“抛出”(thrown)后能被特定类型的异常处理程序捕获(catch)的一个对象。它不会干扰程序的正常运行,仅当程序出错的时候才被执行。这让我们的编码更简单:不用再反复检查错误了。另外,异常不像方法返回的错误值和方法设置用来表示发生错误的标志位那样可以被忽略。异常的发生是不会被忽略的,它终究会在某一时刻被处理。\n",
+ "\n",
+ "最后,“异常机制”提供了一种可靠地从错误状况中恢复的方法,使得我们可以编写出更健壮的程序。有时你只要处理好抛出的异常情况并恢复程序的运行即可,无需退出。\n",
+ "\n",
+ "Java 的异常处理机制在编程语言中脱颖而出。Java 从一开始就内置了异常处理,因此你不得不使用它。这是 Java 语言唯一接受的错误报告方法。如果没有编写适当的异常处理代码,你将会收到一条编译时错误消息。这种有保障的一致性有时会让程序的错误处理变得更容易。值得注意的是,异常处理并不是面向对象的特性。尽管在面向对象的语言中异常通常由对象表示,但是在面向对象语言之前也存在异常处理。\n",
+ "\n",
+ "## 本章小结\n",
+ "\n",
+ "面向过程程序包含数据定义和函数调用。要找到程序的意图,你必须要在脑中建立一个模型,弄清函数调用和更底层的概念。这些程序令人困扰,因为它们的表示更多地面向计算机而不是我们要解决的问题,这就是我们在设计程序时需要中间表示的原因。OOP 在面向过程编程的基础上增加了许多新的概念,所以有人会认为使用 Java 来编程会比同等的面向过程编程要更复杂。在这里,我想给大家一个惊喜:通常按照 Java 规范编写的程序会比面向过程程序更容易被理解。\n",
+ "\n",
+ "你看到的是对象的概念,这些概念是站在“问题空间”的(而不是站在计算机角度的“解决方案空间”),以及发送消息给对象以指示该空间中的活动。面向对象编程的一个优点是:设计良好的 Java 程序代码更容易被人阅读理解。由于 Java 类库的复用性,通常程序要写的代码也会少得多。\n",
+ "\n",
+ "OOP 和 Java 不一定适合每个人。评估自己的需求以及与现有方案作比较是很重要的。请充分考虑后再决定是不是选择 Java。如果在可预见的未来,Java 并不能很好的满足你的特定需求,那么你应该去寻找其他替代方案(特别是,我推荐看 Python)。如果你依然选择 Java 作为你的开发语言,我希望你至少应该清楚你选择的是什么,以及为什么选择这个方向。\n",
+ "\n",
+ "\n",
+ ""
+ ]
+ }
+ ],
+ "metadata": {
+ "kernelspec": {
+ "display_name": "Java",
+ "language": "java",
+ "name": "java"
+ },
+ "language_info": {
+ "codemirror_mode": "java",
+ "file_extension": ".jshell",
+ "mimetype": "text/x-java-source",
+ "name": "Java",
+ "pygments_lexer": "java",
+ "version": "14.0.1+7"
+ }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 4
+}
diff --git a/jupyter/02-Installing-Java-and-the-Book-Examples.ipynb b/jupyter/02-Installing-Java-and-the-Book-Examples.ipynb
new file mode 100644
index 00000000..f9d35d37
--- /dev/null
+++ b/jupyter/02-Installing-Java-and-the-Book-Examples.ipynb
@@ -0,0 +1,305 @@
+{
+ "cells": [
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "[TOC]\n",
+ "\n",
+ "# 第二章 安装Java和本书用例\n",
+ "\n",
+ "现在,我们来为这次阅读之旅做些准备吧!\n",
+ "\n",
+ "在开始学习 Java 之前,你必须要先安装好 Java 和本书的源代码示例。因为考虑到可能有“专门的初学者”从本书开始学习编程,所以我会详细地教你如何使用命令行。 如果你已经有此方面的经验了,可以跳过这段安装说明。如果你对此处描述的任何术语或过程仍不清楚,还可以通过 [Google](https://google.com/) 搜索找到答案。具体的问题或困难请试着在 [StackOverflow](https://stackoverflow.com/) 上提问。或者去 [YouTube](https://youtube.com) 看有没有相关的安装说明。\n",
+ "\n",
+ "## 编辑器\n",
+ "\n",
+ "首先你需要安装一个编辑器来创建和修改本书用例里的 Java 代码。有可能你还需要使用编辑器来更改系统配置文件。\n",
+ "\n",
+ "相比一些重量级的 IDE(Integrated Development Environments,集成开发环境),如 Eclipse、NetBeans 和 IntelliJ IDEA (译者注:做项目强烈推荐IDEA),编辑器是一种更纯粹的文本编辑器。如果你已经有了一个用着顺手的 IDE,那就可以直接用了。为了方便后面的学习和统一下教学环境,我推荐大家使用 Atom 这个编辑器。大家可以在 [atom.io](https://atom.io) 上下载。\n",
+ "\n",
+ "Atom 是一个免费开源、易于安装且跨平台(支持 Window、Mac和Linux)的文本编辑器。内置支持 Java 文件。相比 IDE 的厚重,它比较轻量级,是学习本书的理想工具。Atom 包含了许多方便的编辑功能,相信你一定会爱上它!更多关于 Atom 使用的细节问题可以到它的网站上寻找。\n",
+ "\n",
+ "还有很多其他的编辑器。有一种亚文化的群体,他们热衷于争论哪个更好用!如果你找到一个你更喜欢的编辑器,换一种使用也没什么难度。重要的是,你要找一个用着舒服的。\n",
+ "\n",
+ "## Shell\n",
+ "\n",
+ "如果你之前没有接触过编程,那么有可能对 Shell(命令行窗口) 不太熟悉。shell 的历史可以追溯到早期的计算时代,当时在计算机上的操作是都通过输入命令进行的,计算机通过回显响应。所有的操作都是基于文本的。\n",
+ "\n",
+ "尽管和现在的图形用户界面相比,Shell 操作方式很原始。但是同时 shell 也为我们提供了许多有用的功能特性。在学习本书的过程中,我们会经常使用到 Shell,包括现在这部分的安装,还有运行 Java 程序。\n",
+ "\n",
+ "Mac:单击聚光灯(屏幕右上角的放大镜图标),然后键入 `terminal`。单击看起来像小电视屏幕的应用程序(你也可以单击“return”)。这就启动了你的用户下的 shell 窗口。\n",
+ "\n",
+ "windows:首先,通过目录打开 windows 资源管理器:\n",
+ "\n",
+ "- Windows 7: 单击屏幕左下角的“开始”图标,输入“explorer”后按回车键。\n",
+ "- Windows 8: 按 Windows+Q,输入 “explorer” 后按回车键。\n",
+ "- Windows 10: 按 Windows+E 打开资源管理器,导航到所需目录,单击窗口左上角的“文件“选项卡,选择“打开 Window PowerShell”启动 Shell。\n",
+ "\n",
+ "Linux: 在 home 目录打开 Shell。\n",
+ "\n",
+ "- Debian: 按 Alt+F2, 在弹出的对话框中输入“gnome-terminal”\n",
+ "- Ubuntu: 在屏幕中鼠标右击,选择 “打开终端”,或者按住 Ctrl+Alt+T\n",
+ "- Redhat: 在屏幕中鼠标右击,选择 “打开终端”\n",
+ "- Fedora: 按 Alt+F2,在弹出的对话框中输入“gnome-terminal”\n",
+ "\n",
+ "**目录**\n",
+ "\n",
+ "目录是 Shell 的基础元素之一。目录用来保存文件和其他目录。目录就好比树的分支。如果书籍是你系统上的一个目录,并且它有两个其他目录作为分支,例如数学和艺术,那么我们就可以说你有一个书籍目录,它包含数学和艺术两个子目录。注意:Windows 使用 `\\` 而不是 `/` 来分隔路径。\n",
+ "\n",
+ "**Shell基本操作**\n",
+ "\n",
+ "我在这展示的 Shell 操作和系统中大体相同。出于本书的原因,下面列举一些在 Shell 中的基本操作:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "%%bash\n",
+ "更改目录: cd <路径> \n",
+ " cd .. 移动到上级目录 \n",
+ " pushd <路径> 记住来源的同时移动到其他目录,popd 返回来源\n",
+ "\n",
+ "目录列举: ls 列举出当前目录下所有的文件和子目录名(不包含隐藏文件),\n",
+ " 可以选择使用通配符 * 来缩小搜索范围。\n",
+ " 示例(1): 列举所有以“.java”结尾的文件,输入 ls *.java (Windows: dir *.java)\n",
+ " 示例(2): 列举所有以“F”开头,“.java”结尾的文件,输入ls F*.java (Windows: dir F*.java)\n",
+ "\n",
+ "创建目录: \n",
+ " Mac/Linux 系统:mkdir \n",
+ " 示例:mkdir books \n",
+ " Windows 系统:md \n",
+ " 示例:md books\n",
+ "\n",
+ "移除文件: \n",
+ " Mac/Linux 系统:rm\n",
+ " 示例:rm somefile.java\n",
+ " Windows 系统:del \n",
+ " 示例:del somefile.java\n",
+ "\n",
+ "移除目录: \n",
+ " Mac/Linux 系统:rm -r\n",
+ " 示例:rm -r books\n",
+ " Windows 系统:deltree \n",
+ " 示例:deltree books\n",
+ "\n",
+ "重复命令: !! 重复上条命令\n",
+ " 示例:!n 重复倒数第n条命令\n",
+ "\n",
+ "命令历史: \n",
+ " Mac/Linux 系统:history\n",
+ " Windows 系统:按 F7 键\n",
+ "\n",
+ "文件解压:\n",
+ " Linux/Mac 都有命令行解压程序 unzip,你可以通过互联网为 Windows 安装命令行解压程序 unzip。\n",
+ " 图形界面下(Windows 资源管理器,Mac Finder,Linux Nautilus 或其他等效软件)右键单击该文件,\n",
+ " 在 Mac 上选择“open”,在 Linux 上选择“extract here”,或在 Windows 上选择“extract all…”。\n",
+ " 要了解关于 shell 的更多信息,请在维基百科中搜索 Windows shell,Mac/Linux用户可搜索 bash shell。\n"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## Java安装\n",
+ "\n",
+ "为了编译和运行代码示例,首先你必须安装 JDK(Java Development Kit,JAVA 软件开发工具包)。本书中采用的是 JDK 8。\n",
+ "\n",
+ "\n",
+ "**Windows**\n",
+ "\n",
+ "1. 以下为 Chocolatey 的[安装说明](https://chocolatey.org/)。\n",
+ "2. 在命令行提示符下输入下面的命令,等待片刻,结束后 Java 安装完成并自动完成环境变量设置。"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "%%bash\n",
+ " choco install jdk8"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "**Macintosh**\n",
+ "\n",
+ "Mac 系统自带的 Java 版本太老,为了确保本书的代码示例能被正确执行,你必须将它先更新到 Java 8。我们需要管理员权限来运行下面的步骤:\n",
+ "\n",
+ "1. 以下为 HomeBrew 的[安装说明](https://brew.sh/)。安装完成后执行命令 `brew update` 更新到最新版本\n",
+ "2. 在命令行下执行下面的命令来安装 Java。"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "%%bash\n",
+ " brew cask install java"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "当以上安装都完成后,如果你有需要,可以使用游客账户来运行本书中的代码示例。\n",
+ "\n",
+ "**Linux**\n",
+ "\n",
+ "* **Ubuntu/Debian**:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "%%bash\n",
+ " sudo apt-get update\n",
+ " sudo apt-get install default-jdk"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "* **Fedora/Redhat**:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "%%bash\n",
+ " su-c \"yum install java-1.8.0-openjdk\"(注:执行引号内的内容就可以安装)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## 校验安装\n",
+ "\n",
+ "打开新的命令行输入:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "%%bash\n",
+ "java -version"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "正常情况下 你应该看到以下类似信息(版本号信息可能不一样):"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "%%bash\n",
+ "java version \"1.8.0_112\"\n",
+ "Java(TM) SE Runtime Environment (build 1.8.0_112-b15)\n",
+ "Java HotSpot(TM) 64-Bit Server VM (build 25.112-b15, mixed mode)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "如果提示命令找不到或者无法被识别,请根据安装说明重试;如果还不行,尝试到 [StackOverflow](https://stackoverflow.com/search?q=installing+java) 寻找答案。\n",
+ "\n",
+ "## 安装和运行代码示例\n",
+ "\n",
+ "当 Java 安装完毕,下一步就是安装本书的代码示例了。安装步骤所有平台一致:\n",
+ "\n",
+ "1. 从 [GitHub 仓库](https://github.com/BruceEckel/OnJava8-Examples/archive/master.zip)中下载本书代码示例\n",
+ "2. 解压到你所选目录里。\n",
+ "3. 使用 Windows 资源管理器,Mac Finder,Linux 的 Nautilus 或其他等效工具浏览,在该目录下打开 Shell。\n",
+ "4. 如果你在正确的目录中,你应该看到该目录中名为 gradlew 和 gradlew.bat 的文件,以及许多其他文件和目录。目录与书中的章节相对应。\n",
+ "5. 在shell中输入下面的命令运行:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "%%bash\n",
+ " Windows 系统:\n",
+ " gradlew run\n",
+ "\n",
+ " Mac/Linux 系统:\n",
+ " ./gradlew run"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "第一次安装时 Gradle 需要安装自身和其他的相关的包,请稍等片刻。安装完成后,后续的安装将会快很多。\n",
+ "\n",
+ "**注意**: 第一次运行 gradlew 命令时必须连接互联网。\n",
+ "\n",
+ "**Gradle 基础任务**\n",
+ "\n",
+ "本书构建的大量 Gradle 任务都可以自动运行。Gradle 使用约定大于配置的方式,简单设置即可具备高可用性。本书中“一起去骑行”的某些任务不适用于此或无法执行成功。以下是你通常会使用上的 Gradle 任务列表:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "%%bash\n",
+ " 编译本书中的所有 java 文件,除了部分错误示范的\n",
+ " gradlew compileJava\n",
+ "\n",
+ " 编译并执行 java 文件(某些文件是库组件)\n",
+ " gradlew run\n",
+ "\n",
+ " 执行所有的单元测试(在本书第16章会有详细介绍)\n",
+ " gradlew test\n",
+ "\n",
+ " 编译并运行一个具体的示例程序\n",
+ " gradlew <本书章节>:<示例名称>\n",
+ " 示例:gradlew objects:HelloDate"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "\n",
+ "\n",
+ ""
+ ]
+ }
+ ],
+ "metadata": {},
+ "nbformat": 4,
+ "nbformat_minor": 4
+}
diff --git a/jupyter/03-Objects-Everywhere.ipynb b/jupyter/03-Objects-Everywhere.ipynb
new file mode 100644
index 00000000..6eabcaad
--- /dev/null
+++ b/jupyter/03-Objects-Everywhere.ipynb
@@ -0,0 +1,1386 @@
+{
+ "cells": [
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "[TOC]\n",
+ "\n",
+ "# 第三章 万物皆对象\n",
+ "\n",
+ "> 如果我们说另外一种不同的语言,我们会发觉一个不同的世界!— Ludwig Wittgenstein (1889-1951)\n",
+ "\n",
+ "相比 C++ ,Java 是一种更纯粹的面向对象编程语言。虽然它们都是混合语言,但在 Java 中,设计者们认为混合的作用并非像在 C++ 中那般重要。混合语言允许多种编程风格,这也是 C++ 支持向后兼容 C 的原因。正因为 C++ 是 C 语言的超集,所以它也同时包含了许多 C 语言不具备的特性,这使得 C++ 在某些方面过于复杂。\n",
+ "\n",
+ " Java 语言假设你只进行面向对象编程。开始学习之前,我们需要将思维置于面向对象的世界。本章你将了解到 Java 程序的基本组成,学习在 Java 中万物(几乎)皆对象的思想。\n",
+ "\n",
+ "\n",
+ "\n",
+ "## 对象操纵\n",
+ "\n",
+ "“名字代表什么?玫瑰即使不叫玫瑰,也依旧芬芳”。(引用自 莎士比亚,《罗密欧与朱丽叶》)。\n",
+ "\n",
+ "所有的编程语言都会操纵内存中的元素。有时程序员必须要有意识地直接或间接地操纵它们。在 C/C++ 中,对象的操纵是通过指针来完成的。\n",
+ "\n",
+ "Java 利用万物皆对象的思想和单一一致的语法方式来简化问题。虽万物皆可为对象,但我们所操纵的标识符实际上只是对对象的“引用” [^1]。 举例:我们可以用遥控器(引用)去操纵电视(对象)。只要拥有对象的“引用”,就可以操纵该“对象”。换句话说,我们无需直接接触电视,就可通过遥控器(引用)自由地控制电视(对象)的频道和音量。此外,没有电视,遥控器也可以单独存在。就是说,你仅仅有一个“引用”并不意味着你必然有一个与之关联的“对象”。 \n",
+ "\n",
+ "下面来创建一个 **String** 引用,用于保存单词或语句。代码示例:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ " String s;"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "这里我们只是创建了一个 **String** 对象的引用,而非对象。直接拿来使用会出现错误:因为此时你并没有给变量 `s` 赋值--指向任何对象。通常更安全的做法是:创建一个引用的同时进行初始化。代码示例:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ " String s = \"asdf\";"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "Java 语法允许我们使用带双引号的文本内容来初始化字符串。同样,其他类型的对象也有相应的初始化方式。\n",
+ "\n",
+ "\n",
+ "## 对象创建\n",
+ "\n",
+ "“引用”用来关联“对象”。在 Java 中,通常我们使用`new`操作符来创建一个新对象。`new` 关键字代表:创建一个新的对象实例。所以,我们也可以这样来表示前面的代码示例:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ " String s = new String(\"asdf\");"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "以上展示了字符串对象的创建过程,以及如何初始化生成字符串。除了 **String** 类型以外,Java 本身自带了许多现成的数据类型。除此之外,我们还可以创建自己的数据类型。事实上,这是 Java 程序设计中的一项基本行为。在本书后面的学习中将会接触到。\n",
+ "\n",
+ "\n",
+ "### 数据存储\n",
+ "\n",
+ "那么,程序在运行时是如何存储的呢?尤其是内存是怎么分配的。有5个不同的地方可以存储数据:\n",
+ "\n",
+ "1. **寄存器**(Registers)最快的存储区域,位于 CPU 内部 [^2]。然而,寄存器的数量十分有限,所以寄存器根据需求进行分配。我们对其没有直接的控制权,也无法在自己的程序里找到寄存器存在的踪迹(另一方面,C/C++ 允许开发者向编译器建议寄存器的分配)。\n",
+ "\n",
+ "2. **栈内存**(Stack)存在于常规内存 RAM(随机访问存储器,Random Access Memory)区域中,可通过栈指针获得处理器的直接支持。栈指针下移分配内存,上移释放内存,这是一种快速有效的内存分配方法,速度仅次于寄存器。创建程序时,Java 系统必须准确地知道栈内保存的所有项的生命周期。这种约束限制了程序的灵活性。因此,虽然在栈内存上存在一些 Java 数据,特别是对象引用,但 Java 对象却是保存在堆内存的。\n",
+ "\n",
+ "3. **堆内存**(Heap)这是一种通用的内存池(也在 RAM 区域),所有 Java 对象都存在于其中。与栈内存不同,编译器不需要知道对象必须在堆内存上停留多长时间。因此,用堆内存保存数据更具灵活性。创建一个对象时,只需用 `new` 命令实例化对象即可,当执行代码时,会自动在堆中进行内存分配。这种灵活性是有代价的:分配和清理堆内存要比栈内存需要更多的时间(如果可以用 Java 在栈内存上创建对象,就像在 C++ 中那样的话)。随着时间的推移,Java 的堆内存分配机制现在已经非常快,因此这不是一个值得关心的问题了。\n",
+ "\n",
+ "4. **常量存储**(Constant storage)常量值通常直接放在程序代码中,因为它们永远不会改变。如需严格保护,可考虑将它们置于只读存储器 ROM (只读存储器,Read Only Memory)中 [^3]。\n",
+ "\n",
+ "5. **非 RAM 存储**(Non-RAM storage)数据完全存在于程序之外,在程序未运行以及脱离程序控制后依然存在。两个主要的例子:(1)序列化对象:对象被转换为字节流,通常被发送到另一台机器;(2)持久化对象:对象被放置在磁盘上,即使程序终止,数据依然存在。这些存储的方式都是将对象转存于另一个介质中,并在需要时恢复成常规的、基于 RAM 的对象。Java 为轻量级持久化提供了支持。而诸如 JDBC 和 Hibernate 这些类库为使用数据库存储和检索对象信息提供了更复杂的支持。\n",
+ "\n",
+ "\n",
+ "### 基本类型的存储\n",
+ "\n",
+ "有一组类型在 Java 中使用频率很高,它们需要特殊对待,这就是 Java 的基本类型。之所以这么说,是因为它们的创建并不是通过 `new` 关键字来产生。通常 `new` 出来的对象都是保存在堆内存中的,以此方式创建小而简单的变量往往是不划算的。所以对于这些基本类型的创建方法,Java 使用了和 C/C++ 一样的策略。也就是说,不是使用 `new` 创建变量,而是使用一个“自动”变量。 这个变量直接存储\"值\",并置于栈内存中,因此更加高效。\n",
+ "\n",
+ "Java 确定了每种基本类型的内存占用大小。 这些大小不会像其他一些语言那样随着机器环境的变化而变化。这种不变性也是 Java 更具可移植性的一个原因。\n",
+ "\n",
+ "| 基本类型 | 大小 | 最小值 | 最大值 | 包装类型 |\n",
+ "| :------: | :------: | :------: | :------: | :------: |\n",
+ "| boolean | — | — | — | Boolean |\n",
+ "| char | 16 bits | Unicode 0 | Unicode 216 -1 | Character |\n",
+ "| byte | 8 bits | -128 | +127 | Byte |\n",
+ "| short | 16 bits | - 215 | + 215 -1 | Short |\n",
+ "| int | 32 bits | - 231 | + 231 -1 | Integer |\n",
+ "| long | 64 bits | - 263 | + 263 -1 | Long |\n",
+ "| float | 32 bits | IEEE754 | IEEE754 | Float |\n",
+ "| double | 64 bits |IEEE754 | IEEE754 | Double |\n",
+ "| void | — | — | — | Void |\n",
+ "\n",
+ "所有的数值类型都是有正/负符号的。布尔(boolean)类型的大小没有明确的规定,通常定义为取字面值 “true” 或 “false” 。基本类型有自己对应的包装类型,如果你希望在堆内存里表示基本类型的数据,就需要用到它们的包装类。代码示例:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "char c = 'x';\n",
+ "Character ch = new Character(c);"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "或者你也可以使用下面的形式:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "Character ch = new Character('x');"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "基本类型自动转换成包装类型(自动装箱)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "Character ch = 'x';"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "相对的,包装类型转化为基本类型(自动拆箱):"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "char c = ch;"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "个中原因将在以后的章节里解释。\n",
+ "\n",
+ "\n",
+ "### 高精度数值\n",
+ "\n",
+ "在 Java 中有两种类型的数据可用于高精度的计算。它们是 `BigInteger` 和 `BigDecimal`。尽管它们大致可以划归为“包装类型”,但是它们并没有对应的基本类型。\n",
+ "\n",
+ "这两个类包含的方法提供的操作,与对基本类型执行的操作相似。也就是说,能对 int 或 float 做的运算,在 BigInteger 和 BigDecimal 这里也同样可以,只不过必须要通过调用它们的方法来实现而非运算符。此外,由于涉及到的计算量更多,所以运算速度会慢一些。诚然,我们牺牲了速度,但换来了精度。\n",
+ "\n",
+ "BigInteger 支持任意精度的整数。可用于精确表示任意大小的整数值,同时在运算过程中不会丢失精度。\n",
+ "BigDecimal 支持任意精度的定点数字。例如,可用它进行精确的货币计算。\n",
+ "\n",
+ "关于这两个类的详细信息,请参考 JDK 官方文档。\n",
+ "\n",
+ "\n",
+ "### 数组的存储\n",
+ "\n",
+ "许多编程语言都支持数组类型。在 C 和 C++ 中使用数组是危险的,因为那些数组只是内存块。如果程序访问了内存块之外的数组或在初始化之前使用该段内存(常见编程错误),则结果是不可预测的。\n",
+ "\n",
+ "Java 的设计主要目标之一是安全性,因此许多困扰 C 和 C++ 程序员的问题不会在 Java 中再现。在 Java 中,数组使用前需要被初始化,并且不能访问数组长度以外的数据。这种范围检查,是以每个数组上少量的内存开销及运行时检查下标的额外时间为代价的,但由此换来的安全性和效率的提高是值得的。(并且 Java 经常可以优化这些操作)。\n",
+ "\n",
+ "当我们创建对象数组时,实际上是创建了一个引用数组,并且每个引用的初始值都为 **null** 。在使用该数组之前,我们必须为每个引用指定一个对象 。如果我们尝试使用为 **null** 的引用,则会在运行时报错。因此,在 Java 中就防止了数组操作的常规错误。\n",
+ "\n",
+ "我们还可创建基本类型的数组。编译器通过将该数组的内存全部置零来保证初始化。本书稍后将详细介绍数组,特别是在数组章节中。\n",
+ "\n",
+ "\n",
+ "## 代码注释\n",
+ "\n",
+ "Java 中有两种类型的注释。第一种是传统的 C 风格的注释,以 `/*` 开头,可以跨越多行,到 `*/ ` 结束。注意,许多程序员在多行注释的每一行开头添加 `*`,所以你经常会看到:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "/* 这是\n",
+ "* 跨越多行的\n",
+ "* 注释\n",
+ "*/"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "但请记住,`/*` 和 `*/` 之间的内容都是被忽略的。所以你将其改为下面这样也是没有区别的。"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "/* 这是跨越多\n",
+ "行的注释 */"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "第二种注释形式来自 C++ 。它是单行注释,以 `//` 开头并一直持续到行结束。这种注释方便且常用,因为直观简单。所以你经常看到:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "// 这是单行注释"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "\n",
+ "## 对象清理\n",
+ "\n",
+ "在一些编程语言中,管理变量的生命周期需要大量的工作。一个变量需要存活多久?如果我们想销毁它,应该什么时候去做呢?变量生命周期的混乱会导致许多 bug,本小结向你介绍 Java 是如何通过释放存储来简化这个问题的。\n",
+ "\n",
+ "\n",
+ "### 作用域\n",
+ "\n",
+ "大多数程序语言都有作用域的概念。作用域决定了在该范围内定义的变量名的可见性和生存周期。在 C、 C++ 和 Java 中,作用域是由大括号 `{}` 的位置决定的。例如:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "{\n",
+ " int x = 12;\n",
+ " // 仅 x 变量可用\n",
+ " {\n",
+ " int q = 96;\n",
+ " // x 和 q 变量皆可用\n",
+ " }\n",
+ " // 仅 x 变量可用\n",
+ " // 变量 q 不在作用域内\n",
+ "}"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "Java 的变量只有在其作用域内才可用。缩进使得 Java 代码更易于阅读。由于 Java 是一种自由格式的语言,额外的空格、制表符和回车并不会影响程序的执行结果。在 Java 中,你不能执行以下操作,即使这在 C 和 C++ 中是合法的:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "{\n",
+ " int x = 12;\n",
+ " {\n",
+ " int x = 96; // Illegal\n",
+ " }\n",
+ "}"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "在上例中, Java 编译器会在提示变量 x 已经被定义过了。因此,在 C/C++ 中将一个较大作用域的变量\"隐藏\"起来的做法,在 Java 中是不被允许的。 因为 Java 的设计者认为这样做会导致程序混乱。\n",
+ "\n",
+ "\n",
+ "### 对象作用域\n",
+ "\n",
+ "Java 对象与基本类型具有不同的生命周期。当我们使用 `new` 关键字来创建 Java 对象时,它的生命周期将会超出作用域。因此,下面这段代码示例:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "{\n",
+ " String s = new String(\"a string\");\n",
+ "} \n",
+ "// 作用域终点"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "上例中,引用 s 在作用域终点就结束了。但是,引用 s 指向的字符串对象依然还在占用内存。在这段代码中,我们无法在这个作用域之后访问这个对象,因为唯一对它的引用 s 已超出了作用域的范围。在后面的章节中,我们还会学习怎么在编程中传递和复制对象的引用。\n",
+ "\n",
+ "只要你需要,`new` 出来的对象就会一直存活下去。 相比在 C++ 编码中操作内存可能会出现的诸多问题,这些困扰在 Java 中都不复存在了。在 C++ 中你不仅要确保对象的内存在你操作的范围内存在,还必须在使用完它们之后,将其销毁。\n",
+ "\n",
+ "那么问题来了:我们在 Java 中并没有主动清理这些对象,那么它是如何避免 C++ 中出现的内存被填满从而阻塞程序的问题呢?答案是:Java 的垃圾收集器会检查所有 `new` 出来的对象并判断哪些不再可达,继而释放那些被占用的内存,供其他新的对象使用。也就是说,我们不必担心内存回收的问题了。你只需简单创建对象即可。当其不再被需要时,能自行被垃圾收集器释放。垃圾回收机制有效防止了因程序员忘记释放内存而造成的“内存泄漏”问题。\n",
+ "\n",
+ "\n",
+ "## 类的创建\n",
+ "\n",
+ "### 类型\n",
+ "\n",
+ "如果一切都是对象,那么是什么决定了某一类对象的外观和行为呢?换句话说,是什么确定了对象的类型?你可能很自然地想到 `type` 关键字。但是,事实上大多数面向对象的语言都使用 `class` 关键字类来描述一种新的对象。 通常在 `class` 关键字的后面的紧跟类的的名称。如下代码示例:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "class ATypeName {\n",
+ " // 这里是类的内部\n",
+ "}"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "在上例中,我们引入了一个新的类型,尽管这个类里只有一行注释。但是我们一样可以通过 `new` 关键字来创建一个这种类型的对象。如下:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "ATypeName a = new ATypeName();"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "到现在为止,我们还不能用这个对象来做什么事(即不能向它发送任何有意义的消息),除非我们在这个类里定义一些方法。\n",
+ "\n",
+ "\n",
+ "### 字段\n",
+ "\n",
+ "当我们创建好一个类之后,我们可以往类里存放两种类型的元素:方法(method)和字段(field)。类的字段可以是基本类型,也可以是引用类型。如果类的字段是对某个对象的引用,那么必须要初始化该引用将其关联到一个实际的对象上(通过之前介绍的创建对象的方法)。每个对象都有用来存储其字段的空间。通常,字段不在对象间共享。下面是一个具有某些字段的类的代码示例:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "class DataOnly {\n",
+ " int i;\n",
+ " double d;\n",
+ " boolean b;\n",
+ "}"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "这个类除了存储数据之外什么也不能做。但是,我们仍然可以通过下面的代码来创建它的一个对象:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ " DataOnly data = new DataOnly();"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "我们必须通过这个对象的引用来指定字段值。格式:对象名称.方法名称或字段名称。代码示例:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ " data.i = 47;\n",
+ " data.d = 1.1;\n",
+ " data.b = false;"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "如果你想修改对象内部包含的另一个对象的数据,可以通过这样的格式修改。代码示例:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ " myPlane.leftTank.capacity = 100;"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "你可以用这种方式嵌套许多对象(尽管这样的设计会带来混乱)。\n",
+ "\n",
+ "\n",
+ "### 基本类型默认值\n",
+ "\n",
+ "如果类的成员变量(字段)是基本类型,那么在类初始化时,这些类型将会被赋予一个初始值。\n",
+ "\n",
+ "| 基本类型 | 初始值 |\n",
+ "| :-----: |:-----: |\n",
+ "| boolean | false |\n",
+ "| char | \\u0000 (null) |\n",
+ "| byte | (byte) 0 |\n",
+ "| short |(short) 0 |\n",
+ "| int | 0 |\n",
+ "| long | 0L |\n",
+ "| float | 0.0f |\n",
+ "| double | 0.0d |\n",
+ "\n",
+ "这些默认值仅在 Java 初始化类的时候才会被赋予。这种方式确保了基本类型的字段始终能被初始化(在 C++ 中不会),从而减少了 bug 的来源。但是,这些初始值对于程序来说并不一定是合法或者正确的。 所以,为了安全,我们最好始终显式地初始化变量。\n",
+ "\n",
+ "这种默认值的赋予并不适用于局部变量 —— 那些不属于类的字段的变量。 因此,若在方法中定义的基本类型数据,如下:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ " int x;"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "这里的变量 x 不会自动初始化为0,因而在使用变量 x 之前,程序员有责任主动地为其赋值(和 C 、C++ 一致)。如果我们忘记了这一步, Java 将会提示我们“编译时错误,该变量可能尚未被初始化”。 这一点做的比 C++ 更好,在后者中,编译器只是提示警告,而在 Java 中则直接报错。\n",
+ "\n",
+ "\n",
+ "### 方法使用\n",
+ "\n",
+ "在许多语言(如 C 和 C++)中,使用术语 *函数* (function) 用来命名子程序。在 Java 中,我们使用术语 *方法*(method)来表示“做某事的方式”。\n",
+ "\n",
+ "在 Java 中,方法决定对象能接收哪些消息。方法的基本组成部分包括名称、参数、返回类型、方法体。格式如:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ " [返回类型] [方法名](/*参数列表*/){\n",
+ " // 方法体\n",
+ " }"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "#### 返回类型\n",
+ "\n",
+ "方法的返回类型表明了当你调用它时会返回的结果类型。参数列表则显示了可被传递到方法内部的参数类型及名称。方法名和参数列表统称为**方法签名**(signature of the method)。签名作为方法的唯一标识。\n",
+ "\n",
+ "Java 中的方法只能作为类的一部分创建。它只能被对象所调用 [^4],并且该对象必须有权限来执行调用。若对象调用错误的方法,则程序将在编译时报错。\n",
+ "\n",
+ "我们可以像下面这样调用一个对象的方法:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "[对象引用].[方法名](参数1, 参数2, 参数3);"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "若方法不带参数,例如一个对象引用 `a` 的方法 `f` 不带参数并返回 **int** 型结果,我们可以如下表示:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "int x = a.f();"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "上例中方法 `f` 的返回值类型必须和变量 `x` 的类型兼容 。调用方法的行为有时被称为向对象发送消息。面向对象编程可以总结为:向对象发送消息。\n",
+ "\n",
+ "\n",
+ "#### 参数列表\n",
+ "\n",
+ "方法参数列表指定了传递给方法的信息。正如你可能猜到的,这些信息就像 Java 中的其他所有信息 ,以对象的形式传递。参数列表必须指定每个对象的类型和名称。同样,我们并没有直接处理对象,而是在传递对象引用 [^5] 。但是引用的类型必须是正确的。如果方法需要 String 参数,则必须传入 String,否则编译器将报错。"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "int storage(String s) {\n",
+ " return s.length() * 2;\n",
+ "}"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "此方法计算并返回某个字符串所占的字节数。参数 `s` 的类型为 **String** 。将 s 传递给 `storage()` 后,我们可以把它看作和任何其他对象一样,可以向它发送消息。在这里,我们调用 `length()` 方法,它是一个 String 方法,返回字符串中的字符数。字符串中每个字符的大小为 16 位或 2 个字节。你还看到了 **return** 关键字,它执行两项操作。首先,它意味着“方法执行结束”。其次,如果方法有返回值,那么该值就紧跟 **return** 语句之后。这里,返回值是通过计算"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "s.length() * 2"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "产生的。在方法中,我们可以返回任何类型的数据。如果我们不想方法返回数据,则可以通过给方法标识 `void` 来表明这是一个无需返回值的方法。 代码示例:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "boolean flag() { \n",
+ " return true; \n",
+ "}\n",
+ "\n",
+ "double naturalLogBase() { \n",
+ " return 2.718; \n",
+ "}\n",
+ "\n",
+ "void nothing() {\n",
+ " return;\n",
+ "}\n",
+ "\n",
+ "void nothing2() {\n",
+ "\n",
+ "}"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "当返回类型为 **void** 时, **return** 关键字仅用于退出方法,因此在方法结束处的 **return** 可被省略。我们可以随时从方法中返回,但若方法返回类型为非 `void`,则编译器会强制我们返回相应类型的值。\n",
+ "\n",
+ "上面的描述可能会让你感觉程序只不过是一堆包含各种方法的对象,在这些方法中,将对象作为参数并发送消息给其他对象。大部分情况下确实如此。但在下一章的运算符中我们将会学习如何在方法中做出决策来完成更底层、详细的工作。对于本章,知道如何发送消息就够了。\n",
+ "\n",
+ "\n",
+ "## 程序编写\n",
+ "\n",
+ "在看到第一个 Java 程序之前,我们还必须理解其他几个问题。\n",
+ "\n",
+ "### 命名可见性\n",
+ "\n",
+ "命名控制在任何一门编程语言中都是一个问题。如果你在两个模块中使用相同的命名,那么如何区分这两个名称,并防止两个名称发生“冲突”呢?在 C 语言编程中这是很具有挑战性的,因为程序通常是一个无法管理的名称海洋。C++ 将函数嵌套在类中,所以它们不会和嵌套在其他类中的函数名冲突。然而,C++ 还是允许全局数据和全局函数,因此仍有可能发生冲突。为了解决这个问题,C++ 使用附加的关键字引入了*命名空间*。\n",
+ "\n",
+ "Java 采取了一种新的方法避免了以上这些问题:为一个类库生成一个明确的名称,Java 创建者希望我们反向使用自己的网络域名,因为域名通常是唯一的。因此我的域名是 MindviewInc.com,所以我将我的 foibles 类库命名为 com.mindviewinc.utility.foibles。反转域名后,`.` 用来代表子目录的划分。\n",
+ "\n",
+ "在 Java 1.0 和 Java 1.1 中,域扩展名 com、 edu、 org 和 net 等按惯例大写,因此类库中会出现这样类似的名称:Com.mindviewinc.utility.foibles。然而,在 Java 2 的开发过程中,他们发现这会导致问题,所以现在整个包名都是小写的。此机制意味着所有文件都自动存在于自己的命名空间中,文件中的每个类都具有唯一标识符。这样,Java 语言可以防止名称冲突。\n",
+ "\n",
+ "使用反向 URL 是一种新的命名空间方法,在此之前尚未有其他语言这么做过。Java 中有许多这些“创造性”地解决问题的方法。正如你想象,如果我们未经测试就添加一个功能并用于生产,那么在将来发现该功能的问题再想纠正,通常为时已晚(有些错误太严重了就得从语言中删除新功能。)\n",
+ "\n",
+ "使用反向 URL 将命名空间与文件路径相关联不会导致BUG,但它却给源代码管理带来麻烦。例如在 `com.mindviewinc.utility.foibles` 这样的目录结构中,我们创建了 `com` 和 `mindviewinc` 空目录。它们存在的唯一目的就是用来表示这个反向的 URL。\n",
+ "\n",
+ "这种方式似乎为我们在编写 Java 程序中的某个问题打开了大门。空目录填充了深层次结构,它们不仅用于表示反向 URL,还用于捕获其他信息。这些长路径基本上用于存储有关目录中的内容的数据。如果你希望以最初设计的方式使用目录,这种方法可以从“令人沮丧”到“令人抓狂”,对于生产级的 Java 代码,你必须使用专门为此设计的 IDE 来管理代码。例如 NetBeans,Eclipse 或 IntelliJ IDEA。实际上,这些 IDE 都为我们管理和创建深层次空目录结构。\n",
+ "\n",
+ "对于这本书中的例子,我不想让深层次结构给你的学习带来额外的麻烦,这实际上需要你在开始之前学习熟悉一种重量级的 IDE。所以,我们的每个章节的示例都位于一个浅的子目录中,以章节标题为名。这导致我偶尔会与遵循深层次方法的工具发生冲突。\n",
+ "\n",
+ "\n",
+ "### 使用其他组件\n",
+ "\n",
+ "无论何时在程序中使用预先定义好的类,编译器都必须找到该类。最简单的情况下,该类存在于被调用的源代码文件中。此时我们使用该类 —— 即使该类在文件的后面才会被定义(Java 消除了所谓的“前向引用”问题)。而如果一个类位于其他文件中,又会怎样呢?你可能认为编译器应该足够智能去找到它,但这样是有问题的。想象一下,假如你要使用某个类,但目录中存在多个同名的类(可能用途不同)。或者更糟糕的是,假设你正在编写程序,在构建过程中,你想将某个新类添加到类库中,但却与已有的类名称冲突。\n",
+ "\n",
+ "要解决此问题,你必须通过使用 **import** 关键字来告诉 Java 编译器具体要使用的类。**import** 指示编译器导入一个包,也就是一个类库(在其他语言中,一个库不仅包含类,还可能包括函数和数据,但请记住 Java 中的所有代码都必须写在类里)。大多数时候,我们都在使用 Java 标准库中的组件。有了这些构件,你就不必写一长串的反转域名。例如:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "import java.util.ArrayList;"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "上例可以告诉编译器使用位于标准库 **util** 下的 ArrayList 类。但是,**util** 中包含许多类,我们可以使用通配符 `*` 来导入其中部分类,而无需显式得逐一声明这些类。代码示例:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "import java.util.*;"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "本书中的示例很小,为简单起见,我们通常会使用 `.*` 形式略过导入。然而,许多教程书籍都会要求程序员逐一导入每个类。 \n",
+ "\n",
+ "\n",
+ "### static关键字\n",
+ "\n",
+ "类是对象的外观及行为方式的描述。通常只有在使用 `new` 创建那个类的对象后,数据存储空间才被分配,对象的方法才能供外界调用。这种方式在两种情况下是不足的。\n",
+ "\n",
+ "1. 有时你只想为特定字段(注:也称为属性、域)分配一个共享存储空间,而不去考虑究竟要创建多少对象,甚至根本就不创建对象。\n",
+ "\n",
+ "2. 创建一个与此类的任何对象无关的方法。也就是说,即使没有创建对象,也能调用该方法。\n",
+ "\n",
+ "**static** 关键字(从 C++ 采用)就符合上述两点要求。当我们说某个事物是静态时,就意味着该字段或方法不依赖于任何特定的对象实例 。 即使我们从未创建过该类的对象,也可以调用其静态方法或访问其静态字段。相反,对于普通的非静态字段和方法,我们必须要先创建一个对象并使用该对象来访问字段或方法,因为非静态字段和方法必须与特定对象关联 [^6] 。\n",
+ "\n",
+ "一些面向对象的语言使用类数据(class data)和类方法(class method),表示静态数据和方法只是作为类,而不是类的某个特定对象而存在的。有时 Java 文献也使用这些术语。\n",
+ "\n",
+ "我们可以在类的字段或方法前添加 `static` 关键字来表示这是一个静态字段或静态方法。 代码示例:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "class StaticTest {\n",
+ " static int i = 47;\n",
+ "}"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "现在,即使你创建了两个 `StaticTest` 对象,但是静态变量 `i` 仍只占一份存储空间。两个对象都会共享相同的变量 `i`。 代码示例:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "StaticTest st1 = new StaticTest();\n",
+ "StaticTest st2 = new StaticTest();"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "`st1.i` 和 `st2.i` 指向同一块存储空间,因此它们的值都是 47。引用静态变量有两种方法。在前面的示例中,我们通过一个对象来定位它,例如 `st2.i`。我们也可以通过类名直接引用它,这种方式对于非静态成员则不可行:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "StaticTest.i++;"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "`++` 运算符将会使变量结果 + 1。此时 `st1.i` 和 `st2.i` 的值都变成了 48。\n",
+ "\n",
+ "使用类名直接引用静态变量是首选方法,因为它强调了变量的静态属性。类似的逻辑也适用于静态方法。我们可以通过对象引用静态方法,就像使用任何方法一样,也可以通过特殊的语法方式 `Classname.method()` 来直接调用静态字段或方法 [^7]。 代码示例:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "class Incrementable {\n",
+ " static void increment() { \n",
+ " StaticTest.i++; \n",
+ " }\n",
+ "}"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "上例中,`Incrementable` 的 `increment()` 方法通过 `++` 运算符将静态数据 `i` 加 1。我们依然可以先实例化对象再调用该方法。 代码示例:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "Incrementable sf = new Incrementable();\n",
+ "sf.increment();"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "当然了,首选的方法是直接通过类来调用它。代码示例:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "Incrementable.increment();"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "相比非静态的对象,`static` 属性改变了数据创建的方式。同样,当 `static` 关键字修饰方法时,它允许我们无需创建对象就可以直接通过类的引用来调用该方法。正如我们所知,`static` 关键字的这些特性对于应用程序入口点的 `main()` 方法尤为重要。\n",
+ "\n",
+ "\n",
+ "## 小试牛刀\n",
+ "\n",
+ "最后,我们开始编写第一个完整的程序。我们使用 Java 标准库中的 **Date** 类来展示一个字符串和日期。"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "\n",
+ "// objects/HelloDate.java\n",
+ "import java.util.*;\n",
+ "\n",
+ "public class HelloDate {\n",
+ " public static void main(String[] args) {\n",
+ " System.out.println(\"Hello, it's: \");\n",
+ " System.out.println(new Date());\n",
+ " }\n",
+ "}\n"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "在本书中,所有代码示例的第一行都是注释行,其中包含文件的路径信息(比如本章的目录名是 **objects**),后跟文件名。我的工具可以根据这些信息自动提取和测试书籍的代码,你也可以通过参考第一行注释轻松地在 Github 库中找到对应的代码示例。\n",
+ "\n",
+ "如果你想在代码中使用一些额外的类库,那么就必须在程序文件的开始处使用 **import** 关键字来导入它们。之所以说是额外的,因为有一些类库已经默认自动导入到每个文件里了。例如:`java.lang` 包。\n",
+ "\n",
+ "现在打开你的浏览器在 [Oracle](https://www.oracle.com/) 上查看文档。如果你还没有从 [Oracle](https://www.oracle.com/) 网站上下载 JDK 文档,那现在就去 [^8] 。查看包列表,你会看到 Java 附带的所有不同的类库。\n",
+ "\n",
+ "选择 `java.lang`,你会看到该库中所有类的列表。由于 `java.lang` 隐式包含在每个 Java 代码文件中,因此这些类是自动可用的。`java.lang` 类库中没有 **Date** 类,所以我们必须导入其他的类库(即 Date 所在的类库)。如果你不清楚某个类所在的类库或者想查看类库中所有的类,那么可以在 Java 文档中选择 “Tree” 查看。\n",
+ "\n",
+ "现在,我们可以找到 Java 附带的每个类。使用浏览器的“查找”功能查找 **Date**,搜索结果中将会列出 **java.util.Date**,我们就知道了 **Date** 在 **util** 库中,所以必须导入 **java.util.*** 才能使用 **Date**。\n",
+ "\n",
+ "如果你在文档中选择 **java.lang**,然后选择 **System**,你会看到 **System** 类中有几个字段,如果你选择了 **out**,你会发现它是一个静态的 **PrintStream** 对象。 所以,即使我们不使用 **new** 创建, **out** 对象就已经存在并可以使用。 **out** 对象可以执行的操作取决于它的类型: **PrintStream** ,其在文档中是一个超链接,如果单击该链接,我们将可以看到 **PrintStream** 对应的方法列表(更多详情,将在本书后面介绍)。 现在我们重点说的是 **println()** 这个方法。 它的作用是 “将信息输出到控制台,并以换行符结束”。既然如此,我们可以这样编码来输出信息到控制台。 代码示例:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "System.out.println(\"A String of things\");"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "每个 java 源文件中允许有多个类。同时,源文件的名称必须要和其中一个类名相同,否则编译器将会报错。每个独立的程序应该包含一个 `main()` 方法作为程序运行的入口。其方法签名和返回类型如下。代码示例:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "public static void main(String[] args) {\n",
+ "}"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "关键字 **public** 表示方法可以被外界访问到。( 更多详情将在 **隐藏实现** 章节讲到)\n",
+ "**main()** 方法的参数是一个 字符串(**String**) 数组。 参数 **args** 并没有在当前的程序中使用到,但是 Java 编译器强制要求必须要有, 这是因为它们被用于接收从命令行输入的参数。\n",
+ "\n",
+ "下面我们来看一段有趣的代码:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "System.out.println(new Date());"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "上面的示例中,我们创建了一个日期(**Date**)类型的对象并将其转化为字符串类型,输出到控制台中。 一旦这一行语句执行完毕,我们就不再需要该日期对象了。这时,Java 垃圾回收器就可以将其占用的内存回收,我们无需去主动清除它们。\n",
+ "\n",
+ "查看 JDK 文档时,我们可以看到在 **System** 类下还有很多其他有用的方法( Java 的牛逼之处还在于,它拥有一个庞大的标准库资源)。代码示例:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "// objects/ShowProperties.java\n",
+ "public class ShowProperties {\n",
+ " public static void main(String[] args) {\n",
+ " System.getProperties().list(System.out);\n",
+ " System.out.println(System.getProperty(\"user.name\"));\n",
+ " System.out.println(System.getProperty(\"java.library.path\"));\n",
+ " }\n",
+ "}"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "输出结果(前20行):"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "text"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "java.runtime.name=Java(TM) SE Runtime Environment\n",
+ "sun.boot.library.path=C:\\Program\n",
+ "Files\\Java\\jdk1.8.0_112\\jr...\n",
+ "java.vm.version=25.112-b15\n",
+ "java.vm.vendor=Oracle Corporation\n",
+ "java.vendor.url=http://java.oracle.com/\n",
+ "path.separator=;\n",
+ "java.vm.name=Java HotSpot(TM) 64-Bit Server VM\n",
+ "file.encoding.pkg=sun.io\n",
+ "user.script=\n",
+ "user.country=US\n",
+ "sun.java.launcher=SUN_STANDARD\n",
+ "sun.os.patch.level=\n",
+ "java.vm.specification.name=Java Virtual Machine\n",
+ "Specification\n",
+ "user.dir=C:\\Users\\Bruce\\Documents\\GitHub\\on-ja...\n",
+ "java.runtime.version=1.8.0_112-b15\n",
+ "java.awt.graphicsenv=sun.awt.Win32GraphicsEnvironment\n",
+ "java.endorsed.dirs=C:\\Program\n",
+ "Files\\Java\\jdk1.8.0_112\\jr...\n",
+ "os.arch=amd64\n",
+ "java.io.tmpdir=C:\\Users\\Bruce\\AppData\\Local\\Temp\\"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "`main()` 方法中的第一行会输出所有的系统字段,也就是环境信息。 **list()** 方法将结果发送给它的参数 **System.out**。在本书的后面,我们还会接触到将结果输出到其他地方,例如文件中。另外,我们还可以请求特定的字段。该例中我们使用到了 **user.name** 和 **java.library.path**。 \n",
+ "\n",
+ "\n",
+ "### 编译和运行\n",
+ "\n",
+ "要编译和运行本书中的代码示例,首先必须具有 Java 编程环境。 第二章的示例中描述了安装过程。如果你遵循这些说明,那么你将会在不受 Oracle 的限制的条件下用到 Java 开发工具包(JDK)。如果你使用其他开发系统,请查看该系统的文档以确定如何编译和运行程序。 第二章还介绍了如何安装本书的示例。 \n",
+ "\n",
+ "移动到子目录 **objects** 下并键入:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "%%bash\n",
+ "javac HelloDate.java"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "此命令不应产生任何响应。如果我们收到任何类型的错误消息,则表示未正确安装 JDK,那就得检查这些问题。\n",
+ "\n",
+ "若执行不报错的话,此时可以键入:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "java HelloDate"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "我们将会得到正确的日期输出。这是我们编译和运行本书中每个程序(包含 `main()` 方法)的过程 [^9]。此外,本书的源代码在根目录中也有一个名为 **build.gradle** 的文件,其中包含用于自动构建,测试和运行本书文件的 **Gradle** 配置。当你第一次运行 `gradlew` 命令时,**Gradle** 将自动安装(前提是已安装Java)。\n",
+ "\n",
+ "\n",
+ "## 编码风格\n",
+ "\n",
+ "Java 编程语言编码规范(Code Conventions for the Java Programming Language)[^10] 要求类名的首字母大写。 如果类名是由多个单词构成的,则每个单词的首字母都应大写(不采用下划线来分隔)例如:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "class AllTheColorsOfTheRainbow {\n",
+ " // ...\n",
+ "}"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "有时称这种命名风格叫“驼峰命名法”。对于几乎所有其他方法,字段(成员变量)和对象引用名都采用驼峰命名的方式,但是它们的首字母不需要大写。代码示例:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "class AllTheColorsOfTheRainbow {\n",
+ " int anIntegerRepresentingColors;\n",
+ " void changeTheHueOfTheColor(int newHue) {\n",
+ " // ...\n",
+ " }\n",
+ " // ...\n",
+ "}\n"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "在 Oracle 的官方类库中,花括号的位置同样遵循和本书中上述示例相同的规范。\n",
+ "\n",
+ "## 本章小结\n",
+ "\n",
+ "本章向你展示了简单的 Java 程序编写以及该语言相关的基本概念。到目前为止,我们的示例都只是些简单的顺序执行。在接下来的两章里,我们将会接触到 Java 的一些基本操作符,以及如何去控制程序执行的流程。\n",
+ "\n",
+ "[^1]: 这里可能有争议。有人说这是一个指针,但这假定了一个潜在的实现。此外,Java 引用的语法更类似于 C++ 引用而非指针。在 《Thinking in Java》 的第 1 版中,我发明了一个新术语叫“句柄”(handle),因为 C++ 引用和Java 引用有一些重要的区别。作为一个从 C++ 的过来人,我不想混淆 Java 可能的最大受众 —— C++ 程序员。在《Thinking in Java》的第 2 版中,我认为“引用”(reference)是更常用的术语,从 C++ 转过来的人除了引用的术语之外,还有很多东西需要处理,所以他们不妨双脚都跳进去。但是,也有些人甚至不同意“引用”。在某书中我读到一个观点:Java 支持引用传递的说法是完全错误的,因为 Java 对象标识符(根据该作者)实际上是“对象引用”(object references),并且一切都是值传递。所以你不是通过引用传递,而是“通过值传递对象引用。人们可以质疑我的这种解释的准确性,但我认为我的方法简化了对概念的理解而又没对语言造成伤害(嗯,语言专家可能会说我骗你,但我会说我只是对此进行了适当的抽象。)\n",
+ "\n",
+ "[^2]: 大多数微处理器芯片都有额外的高速缓冲存储器,但这是按照传统存储器而不是寄存器。\n",
+ "\n",
+ "[^3]: 一个例子是字符串常量池。所有文字字符串和字符串值常量表达式都会自动放入特殊的静态存储中。\n",
+ "\n",
+ "[^4]: 静态方法,我们很快就能接触到,它可以在没有对象的情况下直接被类调用。\n",
+ "\n",
+ "[^5]: 通常除了前面提到的“特殊”数据类型 boolean、 char、 byte、 short、 int、 long、 float 和 double。通常来说,传递对象就意味者传递对象的引用。\n",
+ "\n",
+ "[^6]: 静态方法在使用之前不需要创建对象,因此它们不能直接调用非静态的成员或方法(因为非静态成员和方法必须要先实例化为对象才可以被使用)。\n",
+ "\n",
+ "[^7]: 在某些情况下,它还为编译器提供了更好的优化可能。\n",
+ "\n",
+ "[^8]: 请注意,此文档未包含在 JDK 中;你必须单独下载才能获得它。\n",
+ "\n",
+ "[^9]: 对于本书中编译和运行命令行的每个程序,你可能还需要设置 CLASSPATH 。\n",
+ "\n",
+ "[^10]: 为了保持本书的代码排版紧凑,我并没完全遵守规范,但我尽量会做到符合 Java 标准。\n",
+ "\n",
+ "\n",
+ ""
+ ]
+ }
+ ],
+ "metadata": {},
+ "nbformat": 4,
+ "nbformat_minor": 4
+}
diff --git a/jupyter/04-Operators.ipynb b/jupyter/04-Operators.ipynb
new file mode 100644
index 00000000..32004d06
--- /dev/null
+++ b/jupyter/04-Operators.ipynb
@@ -0,0 +1,2247 @@
+{
+ "cells": [
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "[TOC]\n",
+ "\n",
+ "\n",
+ "# 第四章 运算符\n",
+ "\n",
+ ">运算符操纵数据。\n",
+ "\n",
+ "Java 是从 C++ 的基础上做了一些改进和简化发展而成的。对于 C/C++ 程序员来说,Java 的运算符并不陌生。如果你已了解 C 或 C++,大可以跳过本章和下一章,直接阅读 Java 与 C/C++ 不同的地方。\n",
+ "\n",
+ "如果理解这两章的内容对你来说还有点困难,那么我推荐你先了解下 《Thinking in C》 再继续后面的学习。 这本书现在可以在 [www.OnJava8.com](http://www.OnJava8.com]) 上免费下载。它的内容包含音频讲座、幻灯片、练习和解答,专门用于帮助你快速掌握学习 Java 所需的基础知识。\n",
+ "\n",
+ "\n",
+ "## 开始使用\n",
+ "\n",
+ "运算符接受一个或多个参数并生成新值。这个参数与普通方法调用的形式不同,但效果是相同的。加法 `+`、减法 `-`、乘法 `*`、除法 `/` 以及赋值 `=` 在任何编程语言中的工作方式都是类似的。所有运算符都能根据自己的运算对象生成一个值。除此以外,一些运算符可改变运算对象的值,这叫作“副作用”(**Side Effect**)。运算符最常见的用途就是修改自己的运算对象,从而产生副作用。但要注意生成的值亦可由没有副作用的运算符生成。\n",
+ "\n",
+ "几乎所有运算符都只能操作基本类型(Primitives)。唯一的例外是 `=`、`==` 和 `!=`,它们能操作所有对象(这也是令人混淆的一个地方)。除此以外,**String** 类支持 `+` 和 `+=`。\n",
+ "\n",
+ "\n",
+ "## 优先级\n",
+ "\n",
+ "运算符的优先级决定了存在多个运算符时一个表达式各部分的运算顺序。Java 对运算顺序作出了特别的规定。其中,最简单的规则就是乘法和除法在加法和减法之前完成。程序员经常都会忘记其他优先级规则,所以应该用括号明确规定运算顺序。代码示例:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 9,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "a = 5\n",
+ "b = 1\n"
+ ]
+ }
+ ],
+ "source": [
+ "// operators/Precedence.java\n",
+ "public class Precedence {\n",
+ " \n",
+ " public static void main(String[] args) {\n",
+ " int x = 1, y = 2, z = 3;\n",
+ " int a = x + y - 2/2 + z; // [1]\n",
+ " int b = x + (y - 2)/(2 + z); // [2]\n",
+ " System.out.println(\"a = \" + a);\n",
+ " System.out.println(\"b = \" + b);\n",
+ " }\n",
+ "}\n",
+ "\n",
+ "Precedence.main(new String [0]);"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "这些语句看起来大致相同,但从输出中我们可以看出它们具有非常不同的含义,具体取决于括号的使用。\n",
+ "\n",
+ "我们注意到,在 `System.out.println()` 语句中使用了 `+` 运算符。 但是在这里 `+` 代表的意思是字符串连接符。编译器会将 `+` 连接的非字符串尝试转换为字符串。上例中的输出结果说明了 a 和 b 都已经被转化成了字符串。\n",
+ "\n",
+ "\n",
+ "## 赋值\n",
+ "\n",
+ "运算符的赋值是由符号 `=` 完成的。它代表着获取 `=` 右边的值并赋给左边的变量。右边可以是任何常量、变量或者可产生一个返回值的表达式。但左边必须是一个明确的、已命名的变量。也就是说,必须要有一个物理的空间来存放右边的值。举个例子来说,可将一个常数赋给一个变量(A = 4),但不可将任何东西赋给一个常数(比如不能 4 = A)。\n",
+ "\n",
+ "**基本类型的赋值都是直接的,而不像对象,赋予的只是其内存的引用。**\n",
+ "\n",
+ "举个例子,a = b ,如果 b 是基本类型,那么赋值操作会将 b 的值复制一份给变量 a, 此后若 a 的值发生改变是不会影响到 b 的。作为一名程序员,这应该成为我们的常识。\n",
+ "\n",
+ "如果是为对象赋值,那么结果就不一样了。对一个对象进行操作时,我们实际上操作的是它的引用。所以我们将右边的对象赋予给左边时,赋予的只是该对象的引用。此时,两者指向的堆中的对象还是同一个。代码示例:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 10,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java "
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "1: t1.level: 9, t2.level: 47\n",
+ "2: t1.level: 47, t2.level: 47\n",
+ "3: t1.level: 27, t2.level: 27\n"
+ ]
+ }
+ ],
+ "source": [
+ "// operators/Assignment.java\n",
+ "// Assignment with objects is a bit tricky\n",
+ "class Tank {\n",
+ " int level;\n",
+ "}\n",
+ "\n",
+ "public class Assignment {\n",
+ "\n",
+ " public static void main(String[] args) {\n",
+ " Tank t1 = new Tank();\n",
+ " Tank t2 = new Tank();\n",
+ " t1.level = 9;\n",
+ " t2.level = 47;\n",
+ " System.out.println(\"1: t1.level: \" + t1.level +\n",
+ " \", t2.level: \" + t2.level);\n",
+ " t1 = t2;\n",
+ " System.out.println(\"2: t1.level: \" + t1.level +\n",
+ " \", t2.level: \" + t2.level);\n",
+ " t1.level = 27;\n",
+ " System.out.println(\"3: t1.level: \" + t1.level +\n",
+ " \", t2.level: \" + t2.level);\n",
+ " }\n",
+ "}\n",
+ "\n",
+ "Assignment.main(new String [0]);"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "输出结果:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "1: t1.level: 9, t2.level: 47\n",
+ "2: t1.level: 47, t2.level: 47\n",
+ "3: t1.level: 27, t2.level: 27"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "这是一个简单的 `Tank` 类,在 `main()` 方法创建了两个实例对象。 两个对象的 `level` 属性分别被赋予不同的值。 然后,t2 的值被赋予给 t1。在许多编程语言里,预期的结果是 t1 和 t2 的值会一直相对独立。但是,在 Java 中,由于赋予的只是对象的引用,改变 t1 也就改变了 t2。 这是因为 t1 和 t2 此时指向的是堆中同一个对象。(t1 原始对象的引用在 t2 赋值给其时丢失,它引用的对象会在垃圾回收时被清理)。\n",
+ "\n",
+ "这种现象通常称为别名(aliasing),这是 Java 处理对象的一种基本方式。但是假若你不想出现这里的别名引起混淆的话,你可以这么做。代码示例:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "t1.level = t2.level;"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "较之前的做法,这样做保留了两个单独的对象,而不是丢弃一个并将 t1 和 t2 绑定到同一个对象。但是这样的操作有点违背 Java 的设计原则。对象的赋值是个需要重视的环节,否则你可能收获意外的“惊喜”。\n",
+ "\n",
+ " \n",
+ "### 方法调用中的别名现象\n",
+ "\n",
+ "当我们把对象传递给方法时,会发生别名现象。"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 12,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "1: x.c: a\n",
+ "2: x.c: z\n"
+ ]
+ }
+ ],
+ "source": [
+ "// operators/PassObject.java\n",
+ "// 正在传递的对象可能不是你之前使用的\n",
+ "class Letter {\n",
+ " char c;\n",
+ "}\n",
+ "\n",
+ "public class PassObject {\n",
+ " static void f(Letter y) {\n",
+ " y.c = 'z';\n",
+ " }\n",
+ " \n",
+ " public static void main(String[] args) {\n",
+ " Letter x = new Letter();\n",
+ " x.c = 'a';\n",
+ " System.out.println(\"1: x.c: \" + x.c);\n",
+ " f(x);\n",
+ " System.out.println(\"2: x.c: \" + x.c);\n",
+ " }\n",
+ "}\n",
+ "\n",
+ "PassObject.main(new String[0])"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "输出结果:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "1: x.c: a\n",
+ "2: x.c: z"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "在许多编程语言中,方法 `f()` 似乎会在内部复制其参数 **Letter y**。但是一旦传递了一个引用,那么实际上 `y.c ='z';` 是在方法 `f()` 之外改变对象。别名现象以及其解决方案是个复杂的问题,在附录中有包含:[对象传递和返回](./Appendix-Passing-and-Returning-Objects.md)。意识到这一点,我们可以警惕类似的陷阱。\n",
+ "\n",
+ "\n",
+ "## 算术运算符\n",
+ "\n",
+ "Java 的基本算术运算符与其他大多编程语言是相同的。其中包括加号 `+`、减号 `-`、除号 `/`、乘号 `*` 以及取模 `%`(从整数除法中获得余数)。整数除法会直接砍掉小数,而不是进位。\n",
+ "\n",
+ "Java 也用一种与 C++ 相同的简写形式同时进行运算和赋值操作,由运算符后跟等号表示,并且与语言中的所有运算符一致(只要有意义)。 可用 x += 4 来表示:将 x 的值加上4的结果再赋值给 x。更多代码示例:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "// operators/MathOps.java\n",
+ "// The mathematical operators\n",
+ "import java.util.*;\n",
+ "\n",
+ "public class MathOps {\n",
+ " public static void main(String[] args) {\n",
+ " // Create a seeded random number generator:\n",
+ " Random rand = new Random(47);\n",
+ " int i, j, k;\n",
+ " // Choose value from 1 to 100:\n",
+ " j = rand.nextInt(100) + 1;\n",
+ " System.out.println(\"j : \" + j);\n",
+ " k = rand.nextInt(100) + 1;\n",
+ " System.out.println(\"k : \" + k);\n",
+ " i = j + k;\n",
+ " System.out.println(\"j + k : \" + i);\n",
+ " i = j - k;\n",
+ " System.out.println(\"j - k : \" + i);\n",
+ " i = k / j;\n",
+ " System.out.println(\"k / j : \" + i);\n",
+ " i = k * j;\n",
+ " System.out.println(\"k * j : \" + i);\n",
+ " i = k % j;\n",
+ " System.out.println(\"k % j : \" + i);\n",
+ " j %= k;\n",
+ " System.out.println(\"j %= k : \" + j);\n",
+ " // 浮点运算测试\n",
+ " float u, v, w; // Applies to doubles, too\n",
+ " v = rand.nextFloat();\n",
+ " System.out.println(\"v : \" + v);\n",
+ " w = rand.nextFloat();\n",
+ " System.out.println(\"w : \" + w);\n",
+ " u = v + w;\n",
+ " System.out.println(\"v + w : \" + u);\n",
+ " u = v - w;\n",
+ " System.out.println(\"v - w : \" + u);\n",
+ " u = v * w;\n",
+ " System.out.println(\"v * w : \" + u);\n",
+ " u = v / w;\n",
+ " System.out.println(\"v / w : \" + u);\n",
+ " // 下面的操作同样适用于 char, \n",
+ " // byte, short, int, long, and double:\n",
+ " u += v;\n",
+ " System.out.println(\"u += v : \" + u);\n",
+ " u -= v;\n",
+ " System.out.println(\"u -= v : \" + u);\n",
+ " u *= v;\n",
+ " System.out.println(\"u *= v : \" + u);\n",
+ " u /= v;\n",
+ " System.out.println(\"u /= v : \" + u); \n",
+ " }\n",
+ "}\n"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "输出结果:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "j : 59\n",
+ "k : 56\n",
+ "j + k : 115\n",
+ "j - k : 3\n",
+ "k / j : 0\n",
+ "k * j : 3304\n",
+ "k % j : 56\n",
+ "j %= k : 3\n",
+ "v : 0.5309454\n",
+ "w : 0.0534122\n",
+ "v + w : 0.5843576\n",
+ "v - w : 0.47753322\n",
+ "v * w : 0.028358962\n",
+ "v / w : 9.940527\n",
+ "u += v : 10.471473\n",
+ "u -= v : 9.940527\n",
+ "u *= v : 5.2778773\n",
+ "u /= v : 9.940527"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "为了生成随机数字,程序首先创建一个 **Random** 对象。不带参数的 **Random** 对象会利用当前的时间用作随机数生成器的“种子”(seed),从而为程序的每次执行生成不同的输出。在本书的示例中,重要的是每个示例末尾的输出尽可能一致,以便可以使用外部工具进行验证。所以我们通过在创建 **Random** 对象时提供种子(随机数生成器的初始化值,其始终为特定种子值产生相同的序列),让程序每次执行都生成相同的随机数,如此以来输出结果就是可验证的 [^1]。 若需要生成随机值,可删除代码示例中的种子参数。该对象通过调用方法 `nextInt()` 和 `nextFloat()`(还可以调用 `nextLong()` 或 `nextDouble()`),使用 **Random** 对象生成许多不同类型的随机数。`nextInt()` 的参数设置生成的数字的上限,下限为零,为了避免零除的可能性,结果偏移1。\n",
+ "\n",
+ "\n",
+ "### 一元加减运算符\n",
+ "\n",
+ "一元加 `+` 减 `-` 运算符的操作和二元是相同的。编译器可自动识别使用何种方式解析运算:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "x = -a;"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "上例的代码表意清晰,编译器可正确识别。下面再看一个示例:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "x = a * -b;"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "虽然编译器可以正确的识别,但是程序员可能会迷惑。为了避免混淆,推荐下面的写法:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "x = a * (-b);"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "一元减号可以得到数据的负值。一元加号的作用相反,不过它唯一能影响的就是把较小的数值类型自动转换为 **int** 类型。\n",
+ "\n",
+ "\n",
+ "## 递增和递减\n",
+ "\n",
+ "和 C 语言类似,Java 提供了许多快捷运算方式。快捷运算可使代码可读性,可写性都更强。其中包括递增 `++` 和递减 `--`,意为“增加或减少一个单位”。举个例子来说,假设 a 是一个 **int** 类型的值,则表达式 `++a` 就等价于 `a = a + 1`。 递增和递减运算符不仅可以修改变量,还可以生成变量的值。\n",
+ "\n",
+ "每种类型的运算符,都有两个版本可供选用;通常将其称为“前缀”和“后缀”。“前递增”表示 `++` 运算符位于变量或表达式的前面;而“后递增”表示 `++` 运算符位于变量的后面。类似地,“前递减”意味着 `--` 运算符位于变量的前面;而“后递减”意味着 `--` 运算符位于变量的后面。对于前递增和前递减(如 `++a` 或 `--a`),会先执行递增/减运算,再返回值。而对于后递增和后递减(如 `a++` 或 `a--`),会先返回值,再执行递增/减运算。代码示例:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "// operators/AutoInc.java\n",
+ "// 演示 ++ 和 -- 运算符\n",
+ "public class AutoInc {\n",
+ " public static void main(String[] args) {\n",
+ " int i = 1;\n",
+ " System.out.println(\"i: \" + i);\n",
+ " System.out.println(\"++i: \" + ++i); // 前递增\n",
+ " System.out.println(\"i++: \" + i++); // 后递增\n",
+ " System.out.println(\"i: \" + i);\n",
+ " System.out.println(\"--i: \" + --i); // 前递减\n",
+ " System.out.println(\"i--: \" + i--); // 后递减\n",
+ " System.out.println(\"i: \" + i);\n",
+ " }\n",
+ "}"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "输出结果:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "i: 1\n",
+ "++i: 2\n",
+ "i++: 2\n",
+ "i: 3\n",
+ "--i: 2\n",
+ "i--: 2\n",
+ "i: 1"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "对于前缀形式,我们将在执行递增/减操作后获取值;使用后缀形式,我们将在执行递增/减操作之前获取值。它们是唯一具有“副作用”的运算符(除那些涉及赋值的以外) —— 它们修改了操作数的值。\n",
+ "\n",
+ "C++ 名称来自于递增运算符,暗示着“比 C 更进一步”。在早期的 Java 演讲中,*Bill Joy*(Java 作者之一)说“**Java = C++ --**”(C++ 减减),意味着 Java 在 C++ 的基础上减少了许多不必要的东西,因此语言更简单。随着进一步地学习,我们会发现 Java 的确有许多地方相对 C++ 来说更简便,但是在其他方面,难度并不会比 C++ 小多少。\n",
+ "\n",
+ "\n",
+ "## 关系运算符\n",
+ "\n",
+ "关系运算符会通过产生一个布尔(**boolean**)结果来表示操作数之间的关系。如果关系为真,则结果为 **true**,如果关系为假,则结果为 **false**。关系运算符包括小于 `<`,大于 `>`,小于或等于 `<=`,大于或等于 `>=`,等于 `==` 和不等于 `!=`。`==` 和 `!=` 可用于所有基本类型,但其他运算符不能用于基本类型 **boolean**,因为布尔值只能表示 **true** 或 **false**,所以比较它们之间的“大于”或“小于”没有意义。\n",
+ "\n",
+ "\n",
+ "### 测试对象等价\n",
+ "\n",
+ "关系运算符 `==` 和 `!=` 同样适用于所有对象之间的比较运算,但它们比较的内容却经常困扰 Java 的初学者。下面是代码示例:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "// operators/Equivalence.java\n",
+ "public class Equivalence {\n",
+ " public static void main(String[] args) {\n",
+ " Integer n1 = 47;\n",
+ " Integer n2 = 47;\n",
+ " System.out.println(n1 == n2);\n",
+ " System.out.println(n1 != n2);\n",
+ " }\n",
+ "}"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "输出结果:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "true\n",
+ "false"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "表达式 `System.out.println(n1 == n2)` 将会输出比较的结果。因为两个 **Integer** 对象相同,所以先输出 **true**,再输出 **false**。但是,尽管对象的内容一样,对象的引用却不一样。`==` 和 `!=` 比较的是对象引用,所以输出实际上应该是先输出 **false**,再输出 **true**(译者注:如果你把 47 改成 128,那么打印的结果就是这样,因为 Integer 内部维护着一个 IntegerCache 的缓存,默认缓存范围是 [-128, 127],所以 [-128, 127] 之间的值用 `==` 和 `!=` 比较也能能到正确的结果,但是不推荐用关系运算符比较,具体见 JDK 中的 Integer 类源码)。\n",
+ "\n",
+ "那么怎么比较两个对象的内容是否相同呢?你必须使用所有对象(不包括基本类型)中都存在的 `equals()` 方法,下面是如何使用 `equals()` 方法的示例:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "// operators/EqualsMethod.java\n",
+ "public class EqualsMethod {\n",
+ " public static void main(String[] args) {\n",
+ " Integer n1 = 47;\n",
+ " Integer n2 = 47;\n",
+ " System.out.println(n1.equals(n2));\n",
+ " }\n",
+ "}"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "输出结果:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "true"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "上例的结果看起来是我们所期望的。但其实事情并非那么简单。下面我们来创建自己的类:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "// operators/EqualsMethod2.java\n",
+ "// 默认的 equals() 方法没有比较内容\n",
+ "class Value {\n",
+ " int i;\n",
+ "}\n",
+ "\n",
+ "public class EqualsMethod2 {\n",
+ " public static void main(String[] args) {\n",
+ " Value v1 = new Value();\n",
+ " Value v2 = new Value();\n",
+ " v1.i = v2.i = 100;\n",
+ " System.out.println(v1.equals(v2));\n",
+ " }\n",
+ "}"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "输出结果:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "false"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "上例的结果再次令人困惑:结果是 **false**。原因: `equals()` 的默认行为是比较对象的引用而非具体内容。因此,除非你在新类中覆写 `equals()` 方法,否则我们将获取不到想要的结果。不幸的是,在学习 [复用](./08-Reuse.md)(**Reuse**) 章节后我们才能接触到“覆写”(**Override**),并且直到 [附录:集合主题](./Appendix-Collection-Topics.md),才能知道定义 `equals()` 方法的正确方式,但是现在明白 `equals()` 行为方式也可能为你节省一些时间。\n",
+ "\n",
+ "大多数 Java 库类通过覆写 `equals()` 方法比较对象的内容而不是其引用。\n",
+ "\n",
+ "\n",
+ "## 逻辑运算符\n",
+ "\n",
+ "每个逻辑运算符 `&&` (**AND**)、`||`(**OR**)和 `!`(**非**)根据参数的逻辑关系生成布尔值 `true` 或 `false`。下面的代码示例使用了关系运算符和逻辑运算符:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "// operators/Bool.java\n",
+ "// 关系运算符和逻辑运算符\n",
+ "import java.util.*;\n",
+ "public class Bool {\n",
+ " public static void main(String[] args) {\n",
+ " Random rand = new Random(47);\n",
+ " int i = rand.nextInt(100);\n",
+ " int j = rand.nextInt(100);\n",
+ " System.out.println(\"i = \" + i);\n",
+ " System.out.println(\"j = \" + j);\n",
+ " System.out.println(\"i > j is \" + (i > j));\n",
+ " System.out.println(\"i < j is \" + (i < j));\n",
+ " System.out.println(\"i >= j is \" + (i >= j));\n",
+ " System.out.println(\"i <= j is \" + (i <= j));\n",
+ " System.out.println(\"i == j is \" + (i == j));\n",
+ " System.out.println(\"i != j is \" + (i != j));\n",
+ " // 将 int 作为布尔处理不是合法的 Java 写法\n",
+ " //- System.out.println(\"i && j is \" + (i && j));\n",
+ " //- System.out.println(\"i || j is \" + (i || j));\n",
+ " //- System.out.println(\"!i is \" + !i);\n",
+ " System.out.println(\"(i < 10) && (j < 10) is \"\n",
+ " + ((i < 10) && (j < 10)) );\n",
+ " System.out.println(\"(i < 10) || (j < 10) is \"\n",
+ " + ((i < 10) || (j < 10)) );\n",
+ " }\n",
+ "}"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "输出结果:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "i = 58\n",
+ "j = 55\n",
+ "i > j is true\n",
+ "i < j is false\n",
+ "i >= j is true\n",
+ "i <= j is false\n",
+ "i == j is false\n",
+ "i != j is true\n",
+ "(i < 10) && (j < 10) is false\n",
+ "(i < 10) || (j < 10) is false"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "在 Java 逻辑运算中,我们不能像 C/C++ 那样使用非布尔值, 而仅能使用 **AND**、 **OR**、 **NOT**。上面的例子中,我们将使用非布尔值的表达式注释掉了(你可以看到表达式前面是 //-)。但是,后续的表达式使用关系比较生成布尔值,然后对结果使用了逻辑运算。请注意,如果在预期为 **String** 类型的位置使用 **boolean** 类型的值,则结果会自动转为适当的文本格式(即 \"true\" 或 \"false\" 字符串)。\n",
+ "\n",
+ "我们可以将前一个程序中 **int** 的定义替换为除 **boolean** 之外的任何其他基本数据类型。但请注意,**float** 类型的数值比较非常严格,只要两个数字的最小位不同则两个数仍然不相等;只要数字最小位是大于 0 的,那么它就不等于 0。\n",
+ "\n",
+ "\n",
+ "### 短路\n",
+ "\n",
+ "逻辑运算符支持一种称为“短路”(short-circuiting)的现象。整个表达式会在运算到可以明确结果时就停止并返回结果,这意味着该逻辑表达式的后半部分不会被执行到。代码示例:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "// operators / ShortCircuit.java \n",
+ "// 逻辑运算符的短路行为\n",
+ "public class ShortCircuit {\n",
+ "\n",
+ " static boolean test1(int val) {\n",
+ " System.out.println(\"test1(\" + val + \")\");\n",
+ " System.out.println(\"result: \" + (val < 1));\n",
+ " return val < 1;\n",
+ " }\n",
+ "\n",
+ " static boolean test2(int val) {\n",
+ " System.out.println(\"test2(\" + val + \")\");\n",
+ " System.out.println(\"result: \" + (val < 2));\n",
+ " return val < 2;\n",
+ " }\n",
+ "\n",
+ " static boolean test3(int val) {\n",
+ " System.out.println(\"test3(\" + val + \")\");\n",
+ " System.out.println(\"result: \" + (val < 3));\n",
+ " return val < 3;\n",
+ " }\n",
+ "\n",
+ " public static void main(String[] args) {\n",
+ " boolean b = test1(0) && test2(2) && test3(2);\n",
+ " System.out.println(\"expression is \" + b);\n",
+ " }\n",
+ "}"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "输出结果:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "test1(0)\n",
+ "result: true\n",
+ "test2(2)\n",
+ "result: false\n",
+ "expression is false"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "每个测试都对参数执行比较并返回 `true` 或 `false`。同时控制台也会在方法执行时打印他们的执行状态。 下面的表达式:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "test1(0)&& test2(2)&& test3(2)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "可能你的预期是程序会执行 3 个 **test** 方法并返回。我们来分析一下:第一个方法的结果返回 `true`,因此表达式会继续走下去。紧接着,第二个方法的返回结果是 `false`。这就代表这整个表达式的结果肯定为 `false`,所以就没有必要再判断剩下的表达式部分了。\n",
+ "\n",
+ "所以,运用“短路”可以节省部分不必要的运算,从而提高程序潜在的性能。\n",
+ "\n",
+ "\n",
+ "## 字面值常量\n",
+ "\n",
+ "通常,当我们向程序中插入一个字面值常量(**Literal**)时,编译器会确切地识别它的类型。当类型不明确时,必须辅以字面值常量关联来帮助编译器识别。代码示例:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "// operators/Literals.java\n",
+ "public class Literals {\n",
+ " public static void main(String[] args) {\n",
+ " int i1 = 0x2f; // 16进制 (小写)\n",
+ " System.out.println(\n",
+ " \"i1: \" + Integer.toBinaryString(i1));\n",
+ " int i2 = 0X2F; // 16进制 (大写)\n",
+ " System.out.println(\n",
+ " \"i2: \" + Integer.toBinaryString(i2));\n",
+ " int i3 = 0177; // 8进制 (前导0)\n",
+ " System.out.println(\n",
+ " \"i3: \" + Integer.toBinaryString(i3));\n",
+ " char c = 0xffff; // 最大 char 型16进制值\n",
+ " System.out.println(\n",
+ " \"c: \" + Integer.toBinaryString(c));\n",
+ " byte b = 0x7f; // 最大 byte 型16进制值 10101111;\n",
+ " System.out.println(\n",
+ " \"b: \" + Integer.toBinaryString(b));\n",
+ " short s = 0x7fff; // 最大 short 型16进制值\n",
+ " System.out.println(\n",
+ " \"s: \" + Integer.toBinaryString(s));\n",
+ " long n1 = 200L; // long 型后缀\n",
+ " long n2 = 200l; // long 型后缀 (容易与数值1混淆)\n",
+ " long n3 = 200;\n",
+ " \n",
+ " // Java 7 二进制字面值常量:\n",
+ " byte blb = (byte)0b00110101;\n",
+ " System.out.println(\n",
+ " \"blb: \" + Integer.toBinaryString(blb));\n",
+ " short bls = (short)0B0010111110101111;\n",
+ " System.out.println(\n",
+ " \"bls: \" + Integer.toBinaryString(bls));\n",
+ " int bli = 0b00101111101011111010111110101111;\n",
+ " System.out.println(\n",
+ " \"bli: \" + Integer.toBinaryString(bli));\n",
+ " long bll = 0b00101111101011111010111110101111;\n",
+ " System.out.println(\n",
+ " \"bll: \" + Long.toBinaryString(bll));\n",
+ " float f1 = 1;\n",
+ " float f2 = 1F; // float 型后缀\n",
+ " float f3 = 1f; // float 型后缀\n",
+ " double d1 = 1d; // double 型后缀\n",
+ " double d2 = 1D; // double 型后缀\n",
+ " // (long 型的字面值同样适用于十六进制和8进制 )\n",
+ " }\n",
+ "}"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "输出结果:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "i1: 101111\n",
+ "i2: 101111\n",
+ "i3: 1111111\n",
+ "c: 1111111111111111\n",
+ "b: 1111111\n",
+ "s: 111111111111111\n",
+ "blb: 110101\n",
+ "bls: 10111110101111\n",
+ "bli: 101111101011111010111110101111\n",
+ "bll: 101111101011111010111110101111"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "在文本值的后面添加字符可以让编译器识别该文本值的类型。对于 **Long** 型数值,结尾使用大写 `L` 或小写 `l` 皆可(不推荐使用 `l`,因为容易与阿拉伯数值 1 混淆)。大写 `F` 或小写 `f` 表示 **float** 浮点数。大写 `D` 或小写 `d` 表示 **double** 双精度。\n",
+ "\n",
+ "十六进制(以 16 为基数),适用于所有整型数据类型,由前导 `0x` 或 `0X` 表示,后跟 0-9 或 a-f (大写或小写)。如果我们在初始化某个类型的数值时,赋值超出其范围,那么编译器会报错(不管值的数字形式如何)。在上例的代码中,**char**、**byte** 和 **short** 的值已经是最大了。如果超过这些值,编译器将自动转型为 **int**,并且提示我们需要声明强制转换(强制转换将在本章后面定义),意味着我们已越过该类型的范围界限。\n",
+ "\n",
+ "八进制(以 8 为基数)由 0~7 之间的数字和前导零 `0` 表示。\n",
+ "\n",
+ "Java 7 引入了二进制的字面值常量,由前导 `0b` 或 `0B` 表示,它可以初始化所有的整数类型。\n",
+ "\n",
+ "使用整型数值类型时,显示其二进制形式会很有用。在 Long 型和 Integer 型中这很容易实现,调用其静态的 `toBinaryString()` 方法即可。 但是请注意,若将较小的类型传递给 **Integer.**`tobinarystring()` 时,类型将自动转换为 **int**。\n",
+ "\n",
+ "\n",
+ "### 下划线\n",
+ "\n",
+ "Java 7 中有一个深思熟虑的补充:我们可以在数字字面量中包含下划线 `_`,以使结果更清晰。这对于大数值的分组特别有用。代码示例:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "// operators/Underscores.java\n",
+ "public class Underscores {\n",
+ " public static void main(String[] args) {\n",
+ " double d = 341_435_936.445_667;\n",
+ " System.out.println(d);\n",
+ " int bin = 0b0010_1111_1010_1111_1010_1111_1010_1111;\n",
+ " System.out.println(Integer.toBinaryString(bin));\n",
+ " System.out.printf(\"%x%n\", bin); // [1]\n",
+ " long hex = 0x7f_e9_b7_aa;\n",
+ " System.out.printf(\"%x%n\", hex);\n",
+ " }\n",
+ "}"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "输出结果:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "3.41435936445667E8\n",
+ "101111101011111010111110101111\n",
+ "2fafafaf\n",
+ "7fe9b7aa"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "下面是合理使用的规则:\n",
+ "\n",
+ "1. 仅限单 `_`,不能多条相连。\n",
+ "2. 数值开头和结尾不允许出现 `_`。\n",
+ "3. `F`、`D` 和 `L`的前后禁止出现 `_`。\n",
+ "4. 二进制前导 `b` 和 十六进制 `x` 前后禁止出现 `_`。\n",
+ "\n",
+ "[1] 注意 `%n`的使用。熟悉 C 风格的程序员可能习惯于看到 `\\n` 来表示换行符。问题在于它给你的是一个“Unix风格”的换行符。此外,如果我们使用的是 Windows,则必须指定 `\\r\\n`。这种差异的包袱应该由编程语言来解决。这就是 Java 用 `%n` 实现的可以忽略平台间差异而生成适当的换行符,但只有当你使用 `System.out.printf()` 或 `System.out.format()` 时。对于 `System.out.println()`,我们仍然必须使用 `\\n`;如果你使用 `%n`,`println()` 只会输出 `%n` 而不是换行符。\n",
+ "\n",
+ "\n",
+ "### 指数计数法\n",
+ "\n",
+ "指数总是采用一种我认为很不直观的记号方法:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "// operators/Exponents.java\n",
+ "// \"e\" 表示 10 的几次幂\n",
+ "public class Exponents {\n",
+ " public static void main(String[] args) {\n",
+ " // 大写 E 和小写 e 的效果相同:\n",
+ " float expFloat = 1.39e-43f;\n",
+ " expFloat = 1.39E-43f;\n",
+ " System.out.println(expFloat);\n",
+ " double expDouble = 47e47d; // 'd' 是可选的\n",
+ " double expDouble2 = 47e47; // 自动转换为 double\n",
+ " System.out.println(expDouble);\n",
+ " }\n",
+ "}"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "输出结果:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "1.39E-43\n",
+ "4.7E48"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "在科学与工程学领域,**e** 代表自然对数的基数,约等于 2.718 (Java 里用一种更精确的 **double** 值 **Math.E** 来表示自然对数)。指数表达式 \"1.39 x e-43\",意味着 “1.39 × 2.718 的 -43 次方”。然而,自 FORTRAN 语言发明后,人们自然而然地觉得e 代表 “10 的几次幂”。这种做法显得颇为古怪,因为 FORTRAN 最初是为科学与工程领域设计的。\n",
+ "\n",
+ "理所当然,它的设计者应对这样的混淆概念持谨慎态度 [^2]。但不管怎样,这种特别的表达方法在 C,C++ 以及现在的 Java 中顽固地保留下来了。所以倘若习惯 e 作为自然对数的基数使用,那么在 Java 中看到类似“1.39e-43f”这样的表达式时,请转换你的思维,从程序设计的角度思考它;它真正的含义是 “1.39 × 10 的 -43 次方”。\n",
+ "\n",
+ "注意如果编译器能够正确地识别类型,就不必使用后缀字符。对于下述语句:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "long n3 = 200;"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "它并不存在含糊不清的地方,所以 200 后面的 L 大可省去。然而,对于下述语句:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "float f4 = 1e-43f; //10 的幂数"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "编译器通常会将指数作为 **double** 类型来处理,所以假若没有这个后缀字符 `f`,编译器就会报错,提示我们应该将 **double** 型转换成 **float** 型。\n",
+ "\n",
+ "\n",
+ "## 位运算符\n",
+ "\n",
+ "位运算符允许我们操作一个整型数字中的单个二进制位。位运算符会对两个整数对应的位执行布尔代数,从而产生结果。\n",
+ "\n",
+ "位运算源自 C 语言的底层操作。我们经常要直接操纵硬件,频繁设置硬件寄存器内的二进制位。Java 的设计初衷是电视机顶盒嵌入式开发,所以这种底层的操作仍被保留了下来。但是,你可能不会使用太多位运算。\n",
+ "\n",
+ "若两个输入位都是 1,则按位“与运算符” `&` 运算后结果是 1,否则结果是 0。若两个输入位里至少有一个是 1,则按位“或运算符” `|` 运算后结果是 1;只有在两个输入位都是 0 的情况下,运算结果才是 0。若两个输入位的某一个是 1,另一个不是 1,那么按位“异或运算符” `^` 运算后结果才是 1。按位“非运算符” `~` 属于一元运算符;它只对一个自变量进行操作(其他所有运算符都是二元运算符)。按位非运算后结果与输入位相反。例如输入 0,则输出 1;输入 1,则输出 0。\n",
+ "\n",
+ "位运算符和逻辑运算符都使用了同样的字符,只不过数量不同。位短,所以位运算符只有一个字符。位运算符可与等号 `=` 联合使用以接收结果及赋值:`&=`,`|=` 和 `^=` 都是合法的(由于 `~` 是一元运算符,所以不可与 `=` 联合使用)。\n",
+ "\n",
+ "我们将 **Boolean** 类型被视为“单位值”(one-bit value),所以它多少有些独特的地方。我们可以对 boolean 型变量执行与、或、异或运算,但不能执行非运算(大概是为了避免与逻辑“非”混淆)。对于布尔值,位运算符具有与逻辑运算符相同的效果,只是它们不会中途“短路”。此外,针对布尔值进行的位运算为我们新增了一个“异或”逻辑运算符,它并未包括在逻辑运算符的列表中。在移位表达式中,禁止使用布尔值,原因将在下面解释。\n",
+ "\n",
+ "\n",
+ "## 移位运算符\n",
+ "\n",
+ "移位运算符面向的运算对象也是二进制的“位”。它们只能用于处理整数类型(基本类型的一种)。左移位运算符 `<<` 能将其左边的运算对象向左移动右侧指定的位数(在低位补 0)。右移位运算符 `>>` 则相反。右移位运算符有“正”、“负”值:若值为正,则在高位插入 0;若值为负,则在高位插入 1。Java 也添加了一种“不分正负”的右移位运算符(>>>),它使用了“零扩展”(zero extension):无论正负,都在高位插入 0。这一运算符是 C/C++ 没有的。\n",
+ "\n",
+ "如果移动 **char**、**byte** 或 **short**,则会在移动发生之前将其提升为 **int**,结果为 **int**。仅使用右值(rvalue)的 5 个低阶位。这可以防止我们移动超过 **int** 范围的位数。若对一个 **long** 值进行处理,最后得到的结果也是 **long**。\n",
+ "\n",
+ "移位可以与等号 `<<=` 或 `>>=` 或 `>>>=` 组合使用。左值被替换为其移位运算后的值。但是,问题来了,当无符号右移与赋值相结合时,若将其与 **byte** 或 **short** 一起使用的话,则结果错误。取而代之的是,它们被提升为 **int** 型并右移,但在重新赋值时被截断。在这种情况下,结果为 -1。下面是代码示例:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "// operators/URShift.java\n",
+ "// 测试无符号右移\n",
+ "public class URShift {\n",
+ " public static void main(String[] args) {\n",
+ " int i = -1;\n",
+ " System.out.println(Integer.toBinaryString(i));\n",
+ " i >>>= 10;\n",
+ " System.out.println(Integer.toBinaryString(i));\n",
+ " long l = -1;\n",
+ " System.out.println(Long.toBinaryString(l));\n",
+ " l >>>= 10;\n",
+ " System.out.println(Long.toBinaryString(l));\n",
+ " short s = -1;\n",
+ " System.out.println(Integer.toBinaryString(s));\n",
+ " s >>>= 10;\n",
+ " System.out.println(Integer.toBinaryString(s));\n",
+ " byte b = -1;\n",
+ " System.out.println(Integer.toBinaryString(b));\n",
+ " b >>>= 10;\n",
+ " System.out.println(Integer.toBinaryString(b));\n",
+ " b = -1;\n",
+ " System.out.println(Integer.toBinaryString(b));\n",
+ " System.out.println(Integer.toBinaryString(b>>>10));\n",
+ " }\n",
+ "}"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "输出结果:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "11111111111111111111111111111111\n",
+ "1111111111111111111111\n",
+ "1111111111111111111111111111111111111111111111111111111111111111\n",
+ "111111111111111111111111111111111111111111111111111111\n",
+ "11111111111111111111111111111111\n",
+ "11111111111111111111111111111111\n",
+ "11111111111111111111111111111111\n",
+ "11111111111111111111111111111111\n",
+ "11111111111111111111111111111111\n",
+ "1111111111111111111111"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "在上例中,结果并未重新赋值给变量 **b** ,而是直接打印出来,因此一切正常。下面是一个涉及所有位运算符的代码示例:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "// operators/BitManipulation.java\n",
+ "// 使用位运算符\n",
+ "import java.util.*;\n",
+ "public class BitManipulation {\n",
+ " public static void main(String[] args) {\n",
+ " Random rand = new Random(47);\n",
+ " int i = rand.nextInt();\n",
+ " int j = rand.nextInt();\n",
+ " printBinaryInt(\"-1\", -1);\n",
+ " printBinaryInt(\"+1\", +1);\n",
+ " int maxpos = 2147483647;\n",
+ " printBinaryInt(\"maxpos\", maxpos);\n",
+ " int maxneg = -2147483648;\n",
+ " printBinaryInt(\"maxneg\", maxneg);\n",
+ " printBinaryInt(\"i\", i);\n",
+ " printBinaryInt(\"~i\", ~i);\n",
+ " printBinaryInt(\"-i\", -i);\n",
+ " printBinaryInt(\"j\", j);\n",
+ " printBinaryInt(\"i & j\", i & j);\n",
+ " printBinaryInt(\"i | j\", i | j);\n",
+ " printBinaryInt(\"i ^ j\", i ^ j);\n",
+ " printBinaryInt(\"i << 5\", i << 5);\n",
+ " printBinaryInt(\"i >> 5\", i >> 5);\n",
+ " printBinaryInt(\"(~i) >> 5\", (~i) >> 5);\n",
+ " printBinaryInt(\"i >>> 5\", i >>> 5);\n",
+ " printBinaryInt(\"(~i) >>> 5\", (~i) >>> 5);\n",
+ " long l = rand.nextLong();\n",
+ " long m = rand.nextLong();\n",
+ " printBinaryLong(\"-1L\", -1L);\n",
+ " printBinaryLong(\"+1L\", +1L);\n",
+ " long ll = 9223372036854775807L;\n",
+ " printBinaryLong(\"maxpos\", ll);\n",
+ " long lln = -9223372036854775808L;\n",
+ " printBinaryLong(\"maxneg\", lln);\n",
+ " printBinaryLong(\"l\", l);\n",
+ " printBinaryLong(\"~l\", ~l);\n",
+ " printBinaryLong(\"-l\", -l);\n",
+ " printBinaryLong(\"m\", m);\n",
+ " printBinaryLong(\"l & m\", l & m);\n",
+ " printBinaryLong(\"l | m\", l | m);\n",
+ " printBinaryLong(\"l ^ m\", l ^ m);\n",
+ " printBinaryLong(\"l << 5\", l << 5);\n",
+ " printBinaryLong(\"l >> 5\", l >> 5);\n",
+ " printBinaryLong(\"(~l) >> 5\", (~l) >> 5);\n",
+ " printBinaryLong(\"l >>> 5\", l >>> 5);\n",
+ " printBinaryLong(\"(~l) >>> 5\", (~l) >>> 5);\n",
+ " }\n",
+ "\n",
+ " static void printBinaryInt(String s, int i) {\n",
+ " System.out.println(\n",
+ " s + \", int: \" + i + \", binary:\\n \" +\n",
+ " Integer.toBinaryString(i));\n",
+ " }\n",
+ "\n",
+ " static void printBinaryLong(String s, long l) {\n",
+ " System.out.println(\n",
+ " s + \", long: \" + l + \", binary:\\n \" +\n",
+ " Long.toBinaryString(l));\n",
+ " }\n",
+ "}"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "输出结果(前 32 行):"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "-1, int: -1, binary:\n",
+ "11111111111111111111111111111111\n",
+ "+1, int: 1, binary:\n",
+ "1\n",
+ "maxpos, int: 2147483647, binary:\n",
+ "1111111111111111111111111111111\n",
+ "maxneg, int: -2147483648, binary:\n",
+ "10000000000000000000000000000000\n",
+ "i, int: -1172028779, binary:\n",
+ "10111010001001000100001010010101\n",
+ "~i, int: 1172028778, binary:\n",
+ " 1000101110110111011110101101010\n",
+ "-i, int: 1172028779, binary:\n",
+ "1000101110110111011110101101011\n",
+ "j, int: 1717241110, binary:\n",
+ "1100110010110110000010100010110\n",
+ "i & j, int: 570425364, binary:\n",
+ "100010000000000000000000010100\n",
+ "i | j, int: -25213033, binary:\n",
+ "11111110011111110100011110010111\n",
+ "i ^ j, int: -595638397, binary:\n",
+ "11011100011111110100011110000011\n",
+ "i << 5, int: 1149784736, binary:\n",
+ "1000100100010000101001010100000\n",
+ "i >> 5, int: -36625900, binary:\n",
+ "11111101110100010010001000010100\n",
+ "(~i) >> 5, int: 36625899, binary:\n",
+ "10001011101101110111101011\n",
+ "i >>> 5, int: 97591828, binary:\n",
+ "101110100010010001000010100\n",
+ "(~i) >>> 5, int: 36625899, binary:\n",
+ "10001011101101110111101011\n",
+ " ..."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "结尾的两个方法 `printBinaryInt()` 和 `printBinaryLong()` 分别操作一个 **int** 和 **long** 值,并转换为二进制格式输出,同时附有简要的文字说明。除了演示 **int** 和 **long** 的所有位运算符的效果之外,本示例还显示 **int** 和 **long** 的最小值、最大值、+1 和 -1 值,以便我们了解它们的形式。注意高位代表符号:0 表示正,1 表示负。上面显示了 **int** 部分的输出。以上数字的二进制表示形式是带符号的补码(2's complement)。\n",
+ "\n",
+ "\n",
+ "## 三元运算符\n",
+ "\n",
+ "三元运算符,也称为条件运算符。这种运算符比较罕见,因为它有三个运算对象。但它确实属于运算符的一种,因为它最终也会生成一个值。这与本章后一节要讲述的普通 **if-else** 语句是不同的。下面是它的表达式格式:\n",
+ "\n",
+ "**布尔表达式 ? 值 1 : 值 2**\n",
+ "\n",
+ "若表达式计算为 **true**,则返回结果 **值 1** ;如果表达式的计算为 **false**,则返回结果 **值 2**。\n",
+ "\n",
+ "当然,也可以换用普通的 **if-else** 语句(在后面介绍),但三元运算符更加简洁。作为三元运算符的创造者, C 自诩为一门简练的语言。三元运算符的引入多半就是为了高效编程,但假若我们打算频繁使用它的话,还是先多作一些思量: 它易于产生可读性差的代码。与 **if-else** 不同的是,三元运算符是有返回结果的。请看下面的代码示例:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "// operators/TernaryIfElse.java\n",
+ "public class TernaryIfElse {\n",
+ " \n",
+ "static int ternary(int i) {\n",
+ " return i < 10 ? i * 100 : i * 10;\n",
+ "}\n",
+ "\n",
+ "static int standardIfElse(int i) {\n",
+ " if(i < 10)\n",
+ " return i * 100;\n",
+ " else\n",
+ " return i * 10;\n",
+ "}\n",
+ "\n",
+ " public static void main(String[] args) {\n",
+ " System.out.println(ternary(9));\n",
+ " System.out.println(ternary(10));\n",
+ " System.out.println(standardIfElse(9));\n",
+ " System.out.println(standardIfElse(10));\n",
+ " }\n",
+ "}"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "输出结果:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "900\n",
+ "100\n",
+ "900\n",
+ "100"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "可以看出,`ternary()` 中的代码更简短。然而,**standardIfElse()** 中的代码更易理解且不要求更多的录入。所以我们在挑选三元运算符时,请务必权衡一下利弊。\n",
+ "\n",
+ "\n",
+ "## 字符串运算符\n",
+ "\n",
+ "这个运算符在 Java 里有一项特殊用途:连接字符串。这点已在前面展示过了。尽管与 `+` 的传统意义不符,但如此使用也还是比较自然的。这一功能看起来还不错,于是在 C++ 里引入了“运算符重载”机制,以便 C++ 程序员为几乎所有运算符增加特殊的含义。但遗憾得是,与 C++ 的一些限制结合以后,它变得复杂。这要求程序员在设计自己的类时必须对此有周全的考虑。虽然在 Java 中实现运算符重载机制并非难事(如 C# 所展示的,它具有简单的运算符重载),但因该特性过于复杂,因此 Java 并未实现它。\n",
+ "\n",
+ "我们注意到运用 `String +` 时有一些有趣的现象。若表达式以一个 **String** 类型开头(编译器会自动将双引号 `\"\"` 标注的的字符序列转换为字符串),那么后续所有运算对象都必须是字符串。代码示例:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "// operators/StringOperators.java\n",
+ "public class StringOperators {\n",
+ " public static void main(String[] args) {\n",
+ " int x = 0, y = 1, z = 2;\n",
+ " String s = \"x, y, z \";\n",
+ " System.out.println(s + x + y + z);\n",
+ " // 将 x 转换为字符串\n",
+ " System.out.println(x + \" \" + s);\n",
+ " s += \"(summed) = \"; \n",
+ " // 级联操作\n",
+ " System.out.println(s + (x + y + z));\n",
+ " // Integer.toString()方法的简写:\n",
+ " System.out.println(\"\" + x);\n",
+ " }\n",
+ "}"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "输出结果:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "x, y, z 012\n",
+ "0 x, y, z\n",
+ "x, y, z (summed) = 3\n",
+ "0"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "**注意**:上例中第 1 输出语句的执行结果是 `012` 而并非 `3`,这是因为编译器将其分别转换为其字符串形式然后与字符串变量 **s** 连接。在第 2 条输出语句中,编译器将开头的变量转换为了字符串,由此可以看出,这种转换与数据的位置无关,只要当中有一条数据是字符串类型,其他非字符串数据都将被转换为字符串形式并连接。最后一条输出语句,我们可以看出 `+=` 运算符可以拼接其右侧的字符串连接结果并重赋值给自身变量 `s`。括号 `()` 可以控制表达式的计算顺序,以便在显示 **int** 之前对其进行实际求和。\n",
+ "\n",
+ "请注意主方法中的最后一个例子:我们经常会看到一个空字符串 `\"\"` 跟着一个基本类型的数据。这样可以隐式地将其转换为字符串,以代替繁琐的显式调用方法(如这里可以使用 **Integer.toString()**)。\n",
+ "\n",
+ "\n",
+ "## 常见陷阱\n",
+ "\n",
+ "使用运算符时很容易犯的一个错误是,在还没搞清楚表达式的计算方式时就试图忽略括号 `()`。在 Java 中也一样。 在 C++ 中你甚至可能犯这样极端的错误.代码示例:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "while(x = y) {\n",
+ "// ...\n",
+ "}"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "显然,程序员原意是测试等价性 `==`,而非赋值 `=`。若变量 **y** 非 0 的话,在 C/C++ 中,这样的赋值操作总会返回 `true`。于是,上面的代码示例将会无限循环。而在 Java 中,这样的表达式结果并不会转化为一个布尔值。 而编译器会试图把这个 **int** 型数据转换为预期应接收的布尔类型。最后,我们将会在试图运行前收到编译期错误。因此,Java 天生避免了这种陷阱发生的可能。\n",
+ "\n",
+ "唯一有种情况例外:当变量 `x` 和 `y` 都是布尔值,例如 `x=y` 是一个逻辑表达式。除此之外,之前的那个例子,很大可能是错误。\n",
+ "\n",
+ "在 C/C++ 里,类似的一个问题还有使用按位“与” `&` 和“或” `|` 运算,而非逻辑“与” `&&` 和“或” `||`。就象 `=` 和 `==` 一样,键入一个字符当然要比键入两个简单。在 Java 中,编译器同样可防止这一点,因为它不允许我们强行使用另一种并不符的类型。\n",
+ "\n",
+ "\n",
+ "## 类型转换\n",
+ "\n",
+ "“类型转换”(Casting)的作用是“与一个模型匹配”。在适当的时候,Java 会将一种数据类型自动转换成另一种。例如,假设我们为 **float** 变量赋值一个整数值,计算机会将 **int** 自动转换成 **float**。我们可以在程序未自动转换时显式、强制地使此类型发生转换。\n",
+ "\n",
+ "要执行强制转换,需要将所需的数据类型放在任何值左侧的括号内,如下所示:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "// operators/Casting.java\n",
+ "public class Casting {\n",
+ " public static void main(String[] args) {\n",
+ " int i = 200;\n",
+ " long lng = (long)i;\n",
+ " lng = i; // 没有必要的类型提升\n",
+ " long lng2 = (long)200;\n",
+ " lng2 = 200;\n",
+ " // 类型收缩\n",
+ " i = (int)lng2; // Cast required\n",
+ " }\n",
+ "}"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "诚然,你可以这样地去转换一个数值类型的变量。但是上例这种做法是多余的:因为编译器会在必要时自动提升 **int** 型数据为 **long** 型。\n",
+ "\n",
+ "当然,为了程序逻辑清晰或提醒自己留意,我们也可以显式地类型转换。在其他情况下,类型转换型只有在代码编译时才显出其重要性。在 C/C++ 中,类型转换有时会让人头痛。在 Java 里,类型转换则是一种比较安全的操作。但是,若将数据类型进行“向下转换”(**Narrowing Conversion**)的操作(将容量较大的数据类型转换成容量较小的类型),可能会发生信息丢失的危险。此时,编译器会强迫我们进行转型,好比在提醒我们:该操作可能危险,若你坚持让我这么做,那么对不起,请明确需要转换的类型。 对于“向上转换”(**Widening conversion**),则不必进行显式的类型转换,因为较大类型的数据肯定能容纳较小类型的数据,不会造成任何信息的丢失。\n",
+ "\n",
+ "除了布尔类型的数据,Java 允许任何基本类型的数据转换为另一种基本类型的数据。此外,类是不能进行类型转换的。为了将一个类转换为另一个类型,需要使用特殊的方法(后面将会学习到如何在父子类之间进行向上/向下转型,例如,“橡树”可以转换为“树”,反之亦然。而对于“岩石”是无法转换为“树”的)。\n",
+ "\n",
+ "\n",
+ "### 截断和舍入\n",
+ "\n",
+ "在执行“向下转换”时,必须注意数据的截断和舍入问题。若从浮点值转换为整型值,Java 会做什么呢?例如:浮点数 29.7 被转换为整型值,结果会是 29 还是 30 呢?下面是代码示例:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "// operators/CastingNumbers.java\n",
+ "// 尝试转换 float 和 double 型数据为整型数据\n",
+ "public class CastingNumbers {\n",
+ " public static void main(String[] args) {\n",
+ " double above = 0.7, below = 0.4;\n",
+ " float fabove = 0.7f, fbelow = 0.4f;\n",
+ " System.out.println(\"(int)above: \" + (int)above);\n",
+ " System.out.println(\"(int)below: \" + (int)below);\n",
+ " System.out.println(\"(int)fabove: \" + (int)fabove);\n",
+ " System.out.println(\"(int)fbelow: \" + (int)fbelow);\n",
+ " }\n",
+ "}"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "输出结果:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "(int)above: 0\n",
+ "(int)below: 0\n",
+ "(int)fabove: 0\n",
+ "(int)fbelow: 0"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "因此,答案是,从 **float** 和 **double** 转换为整数值时,小数位将被截断。若你想对结果进行四舍五入,可以使用 `java.lang.Math` 的 ` round()` 方法:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "// operators/RoundingNumbers.java\n",
+ "// float 和 double 类型数据的四舍五入\n",
+ "public class RoundingNumbers {\n",
+ " public static void main(String[] args) {\n",
+ " double above = 0.7, below = 0.4;\n",
+ " float fabove = 0.7f, fbelow = 0.4f;\n",
+ " System.out.println(\n",
+ " \"Math.round(above): \" + Math.round(above));\n",
+ " System.out.println(\n",
+ " \"Math.round(below): \" + Math.round(below));\n",
+ " System.out.println(\n",
+ " \"Math.round(fabove): \" + Math.round(fabove));\n",
+ " System.out.println(\n",
+ " \"Math.round(fbelow): \" + Math.round(fbelow));\n",
+ " }\n",
+ "}"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "输出结果:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "Math.round(above): 1\n",
+ "Math.round(below): 0\n",
+ "Math.round(fabove): 1\n",
+ "Math.round(fbelow): 0"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "因为 `round()` 方法是 `java.lang` 的一部分,所以我们无需通过 `import` 就可以使用。\n",
+ "\n",
+ "\n",
+ "### 类型提升\n",
+ "\n",
+ "你会发现,如果我们对小于 **int** 的基本数据类型(即 **char**、**byte** 或 **short**)执行任何算术或按位操作,这些值会在执行操作之前类型提升为 **int**,并且结果值的类型为 **int**。若想重新使用较小的类型,必须使用强制转换(由于重新分配回一个较小的类型,结果可能会丢失精度)。通常,表达式中最大的数据类型是决定表达式结果的数据类型。**float** 型和 **double** 型相乘,结果是 **double** 型的;**int** 和 **long** 相加,结果是 **long** 型。\n",
+ "\n",
+ "\n",
+ "## Java没有sizeof\n",
+ "\n",
+ "在 C/C++ 中,经常需要用到 `sizeof()` 方法来获取数据项被分配的字节大小。C/C++ 中使用 `sizeof()` 最有说服力的原因是为了移植性,不同数据在不同机器上可能有不同的大小,所以在进行大小敏感的运算时,程序员必须对这些类型有多大做到心中有数。例如,一台计算机可用 32 位来保存整数,而另一台只用 16 位保存。显然,在第一台机器中,程序可保存更大的值。所以,移植是令 C/C++ 程序员颇为头痛的一个问题。\n",
+ "\n",
+ "Java 不需要 ` sizeof()` 方法来满足这种需求,因为所有类型的大小在不同平台上是相同的。我们不必考虑这个层次的移植问题 —— Java 本身就是一种“与平台无关”的语言。\n",
+ "\n",
+ "\n",
+ "## 运算符总结\n",
+ "\n",
+ "上述示例分别向我们展示了哪些基本类型能被用于特定的运算符。基本上,下面的代码示例是对上述所有示例的重复,只不过概括了所有的基本类型。这个文件能被正确地编译,因为我已经把编译不通过的那部分用注释 `//` 过滤了。代码示例:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "// operators/AllOps.java\n",
+ "// 测试所有基本类型的运算符操作\n",
+ "// 看看哪些是能被 Java 编译器接受的\n",
+ "public class AllOps {\n",
+ " // 布尔值的接收测试:\n",
+ " void f(boolean b) {}\n",
+ " void boolTest(boolean x, boolean y) {\n",
+ " // 算数运算符:\n",
+ " //- x = x * y;\n",
+ " //- x = x / y;\n",
+ " //- x = x % y;\n",
+ " //- x = x + y;\n",
+ " //- x = x - y;\n",
+ " //- x++;\n",
+ " //- x--;\n",
+ " //- x = +y;\n",
+ " //- x = -y;\n",
+ " // 关系运算符和逻辑运算符:\n",
+ " //- f(x > y);\n",
+ " //- f(x >= y);\n",
+ " //- f(x < y);\n",
+ " //- f(x <= y);\n",
+ " f(x == y);\n",
+ " f(x != y);\n",
+ " f(!y);\n",
+ " x = x && y;\n",
+ " x = x || y;\n",
+ " // 按位运算符:\n",
+ " //- x = ~y;\n",
+ " x = x & y;\n",
+ " x = x | y;\n",
+ " x = x ^ y;\n",
+ " //- x = x << 1;\n",
+ " //- x = x >> 1;\n",
+ " //- x = x >>> 1;\n",
+ " // 联合赋值:\n",
+ " //- x += y;\n",
+ " //- x -= y;\n",
+ " //- x *= y;\n",
+ " //- x /= y;\n",
+ " //- x %= y;\n",
+ " //- x <<= 1;\n",
+ " //- x >>= 1;\n",
+ " //- x >>>= 1;\n",
+ " x &= y;\n",
+ " x ^= y;\n",
+ " x |= y;\n",
+ " // 类型转换:\n",
+ " //- char c = (char)x;\n",
+ " //- byte b = (byte)x;\n",
+ " //- short s = (short)x;\n",
+ " //- int i = (int)x;\n",
+ " //- long l = (long)x;\n",
+ " //- float f = (float)x;\n",
+ " //- double d = (double)x;\n",
+ " }\n",
+ "\n",
+ " void charTest(char x, char y) {\n",
+ " // 算数运算符:\n",
+ " x = (char)(x * y);\n",
+ " x = (char)(x / y);\n",
+ " x = (char)(x % y);\n",
+ " x = (char)(x + y);\n",
+ " x = (char)(x - y);\n",
+ " x++;\n",
+ " x--;\n",
+ " x = (char) + y;\n",
+ " x = (char) - y;\n",
+ " // 关系和逻辑运算符:\n",
+ " f(x > y);\n",
+ " f(x >= y);\n",
+ " f(x < y);\n",
+ " f(x <= y);\n",
+ " f(x == y);\n",
+ " f(x != y);\n",
+ " //- f(!x);\n",
+ " //- f(x && y);\n",
+ " //- f(x || y);\n",
+ " // 按位运算符:\n",
+ " x= (char)~y;\n",
+ " x = (char)(x & y);\n",
+ " x = (char)(x | y);\n",
+ " x = (char)(x ^ y);\n",
+ " x = (char)(x << 1);\n",
+ " x = (char)(x >> 1);\n",
+ " x = (char)(x >>> 1);\n",
+ " // 联合赋值:\n",
+ " x += y;\n",
+ " x -= y;\n",
+ " x *= y;\n",
+ " x /= y;\n",
+ " x %= y;\n",
+ " x <<= 1;\n",
+ " x >>= 1;\n",
+ " x >>>= 1;\n",
+ " x &= y;\n",
+ " x ^= y;\n",
+ " x |= y;\n",
+ " // 类型转换\n",
+ " //- boolean bl = (boolean)x;\n",
+ " byte b = (byte)x;\n",
+ " short s = (short)x;\n",
+ " int i = (int)x;\n",
+ " long l = (long)x;\n",
+ " float f = (float)x;\n",
+ " double d = (double)x;\n",
+ " }\n",
+ "\n",
+ " void byteTest(byte x, byte y) {\n",
+ " // 算数运算符:\n",
+ " x = (byte)(x* y);\n",
+ " x = (byte)(x / y);\n",
+ " x = (byte)(x % y);\n",
+ " x = (byte)(x + y);\n",
+ " x = (byte)(x - y);\n",
+ " x++;\n",
+ " x--;\n",
+ " x = (byte) + y;\n",
+ " x = (byte) - y;\n",
+ " // 关系和逻辑运算符:\n",
+ " f(x > y);\n",
+ " f(x >= y);\n",
+ " f(x < y);\n",
+ " f(x <= y);\n",
+ " f(x == y);\n",
+ " f(x != y);\n",
+ " //- f(!x);\n",
+ " //- f(x && y);\n",
+ " //- f(x || y);\n",
+ " //按位运算符:\n",
+ " x = (byte)~y;\n",
+ " x = (byte)(x & y);\n",
+ " x = (byte)(x | y);\n",
+ " x = (byte)(x ^ y);\n",
+ " x = (byte)(x << 1);\n",
+ " x = (byte)(x >> 1);\n",
+ " x = (byte)(x >>> 1);\n",
+ " // 联合赋值:\n",
+ " x += y;\n",
+ " x -= y;\n",
+ " x *= y;\n",
+ " x /= y;\n",
+ " x %= y;\n",
+ " x <<= 1;\n",
+ " x >>= 1;\n",
+ " x >>>= 1;\n",
+ " x &= y;\n",
+ " x ^= y;\n",
+ " x |= y;\n",
+ " // 类型转换:\n",
+ " //- boolean bl = (boolean)x;\n",
+ " char c = (char)x;\n",
+ " short s = (short)x;\n",
+ " int i = (int)x;\n",
+ " long l = (long)x;\n",
+ " float f = (float)x;\n",
+ " double d = (double)x;\n",
+ " }\n",
+ "\n",
+ " void shortTest(short x, short y) {\n",
+ " // 算术运算符:\n",
+ " x = (short)(x * y);\n",
+ " x = (short)(x / y);\n",
+ " x = (short)(x % y);\n",
+ " x = (short)(x + y);\n",
+ " x = (short)(x - y);\n",
+ " x++;\n",
+ " x--;\n",
+ " x = (short) + y;\n",
+ " x = (short) - y;\n",
+ " // 关系和逻辑运算符:\n",
+ " f(x > y);\n",
+ " f(x >= y);\n",
+ " f(x < y);\n",
+ " f(x <= y);\n",
+ " f(x == y);\n",
+ " f(x != y);\n",
+ " //- f(!x);\n",
+ " //- f(x && y);\n",
+ " //- f(x || y);\n",
+ " // 按位运算符:\n",
+ " x = (short) ~ y;\n",
+ " x = (short)(x & y);\n",
+ " x = (short)(x | y);\n",
+ " x = (short)(x ^ y);\n",
+ " x = (short)(x << 1);\n",
+ " x = (short)(x >> 1);\n",
+ " x = (short)(x >>> 1);\n",
+ " // Compound assignment:\n",
+ " x += y;\n",
+ " x -= y;\n",
+ " x *= y;\n",
+ " x /= y;\n",
+ " x %= y;\n",
+ " x <<= 1;\n",
+ " x >>= 1;\n",
+ " x >>>= 1;\n",
+ " x &= y;\n",
+ " x ^= y;\n",
+ " x |= y;\n",
+ " // 类型转换:\n",
+ " //- boolean bl = (boolean)x;\n",
+ " char c = (char)x;\n",
+ " byte b = (byte)x;\n",
+ " int i = (int)x;\n",
+ " long l = (long)x;\n",
+ " float f = (float)x;\n",
+ " double d = (double)x;\n",
+ " }\n",
+ "\n",
+ " void intTest(int x, int y) {\n",
+ " // 算术运算符:\n",
+ " x = x * y;\n",
+ " x = x / y;\n",
+ " x = x % y;\n",
+ " x = x + y;\n",
+ " x = x - y;\n",
+ " x++;\n",
+ " x--;\n",
+ " x = +y;\n",
+ " x = -y;\n",
+ " // 关系和逻辑运算符:\n",
+ " f(x > y);\n",
+ " f(x >= y);\n",
+ " f(x < y);\n",
+ " f(x <= y);\n",
+ " f(x == y);\n",
+ " f(x != y);\n",
+ " //- f(!x);\n",
+ " //- f(x && y);\n",
+ " //- f(x || y);\n",
+ " // 按位运算符:\n",
+ " x = ~y;\n",
+ " x = x & y;\n",
+ " x = x | y;\n",
+ " x = x ^ y;\n",
+ " x = x << 1;\n",
+ " x = x >> 1;\n",
+ " x = x >>> 1;\n",
+ " // 联合赋值:\n",
+ " x += y;\n",
+ " x -= y;\n",
+ " x *= y;\n",
+ " x /= y;\n",
+ " x %= y;\n",
+ " x <<= 1;\n",
+ " x >>= 1;\n",
+ " x >>>= 1;\n",
+ " x &= y;\n",
+ " x ^= y;\n",
+ " x |= y;\n",
+ " // 类型转换:\n",
+ " //- boolean bl = (boolean)x;\n",
+ " char c = (char)x;\n",
+ " byte b = (byte)x;\n",
+ " short s = (short)x;\n",
+ " long l = (long)x;\n",
+ " float f = (float)x;\n",
+ " double d = (double)x;\n",
+ " }\n",
+ "\n",
+ " void longTest(long x, long y) {\n",
+ " // 算数运算符:\n",
+ " x = x * y;\n",
+ " x = x / y;\n",
+ " x = x % y;\n",
+ " x = x + y;\n",
+ " x = x - y;\n",
+ " x++;\n",
+ " x--;\n",
+ " x = +y;\n",
+ " x = -y;\n",
+ " // 关系和逻辑运算符:\n",
+ " f(x > y);\n",
+ " f(x >= y);\n",
+ " f(x < y);\n",
+ " f(x <= y);\n",
+ " f(x == y);\n",
+ " f(x != y);\n",
+ " //- f(!x);\n",
+ " //- f(x && y);\n",
+ " //- f(x || y);\n",
+ " // 按位运算符:\n",
+ " x = ~y;\n",
+ " x = x & y;\n",
+ " x = x | y;\n",
+ " x = x ^ y;\n",
+ " x = x << 1;\n",
+ " x = x >> 1;\n",
+ " x = x >>> 1;\n",
+ " // 联合赋值:\n",
+ " x += y;\n",
+ " x -= y;\n",
+ " x *= y;\n",
+ " x /= y;\n",
+ " x %= y;\n",
+ " x <<= 1;\n",
+ " x >>= 1;\n",
+ " x >>>= 1;\n",
+ " x &= y;\n",
+ " x ^= y;\n",
+ " x |= y;\n",
+ " // 类型转换:\n",
+ " //- boolean bl = (boolean)x;\n",
+ " char c = (char)x;\n",
+ " byte b = (byte)x;\n",
+ " short s = (short)x;\n",
+ " int i = (int)x;\n",
+ " float f = (float)x;\n",
+ " double d = (double)x;\n",
+ " }\n",
+ "\n",
+ " void floatTest(float x, float y) {\n",
+ " // 算数运算符:\n",
+ " x = x * y;\n",
+ " x = x / y;\n",
+ " x = x % y;\n",
+ " x = x + y;\n",
+ " x = x - y;\n",
+ " x++;\n",
+ " x--;\n",
+ " x = +y;\n",
+ " x = -y;\n",
+ " // 关系和逻辑运算符:\n",
+ " f(x > y);\n",
+ " f(x >= y);\n",
+ " f(x < y);\n",
+ " f(x <= y);\n",
+ " f(x == y);\n",
+ " f(x != y);\n",
+ " //- f(!x);\n",
+ " //- f(x && y);\n",
+ " //- f(x || y);\n",
+ " // 按位运算符:\n",
+ " //- x = ~y;\n",
+ " //- x = x & y;\n",
+ " //- x = x | y;\n",
+ " //- x = x ^ y;\n",
+ " //- x = x << 1;\n",
+ " //- x = x >> 1;\n",
+ " //- x = x >>> 1;\n",
+ " // 联合赋值:\n",
+ " x += y;\n",
+ " x -= y;\n",
+ " x *= y;\n",
+ " x /= y;\n",
+ " x %= y;\n",
+ " //- x <<= 1;\n",
+ " //- x >>= 1;\n",
+ " //- x >>>= 1;\n",
+ " //- x &= y;\n",
+ " //- x ^= y;\n",
+ " //- x |= y;\n",
+ " // 类型转换:\n",
+ " //- boolean bl = (boolean)x;\n",
+ " char c = (char)x;\n",
+ " byte b = (byte)x;\n",
+ " short s = (short)x;\n",
+ " int i = (int)x;\n",
+ " long l = (long)x;\n",
+ " double d = (double)x;\n",
+ " }\n",
+ "\n",
+ " void doubleTest(double x, double y) {\n",
+ " // 算术运算符:\n",
+ " x = x * y;\n",
+ " x = x / y;\n",
+ " x = x % y;\n",
+ " x = x + y;\n",
+ " x = x - y;\n",
+ " x++;\n",
+ " x--;\n",
+ " x = +y;\n",
+ " x = -y;\n",
+ " // 关系和逻辑运算符:\n",
+ " f(x > y);\n",
+ " f(x >= y);\n",
+ " f(x < y);\n",
+ " f(x <= y);\n",
+ " f(x == y);\n",
+ " f(x != y);\n",
+ " //- f(!x);\n",
+ " //- f(x && y);\n",
+ " //- f(x || y);\n",
+ " // 按位运算符:\n",
+ " //- x = ~y;\n",
+ " //- x = x & y;\n",
+ " //- x = x | y;\n",
+ " //- x = x ^ y;\n",
+ " //- x = x << 1;\n",
+ " //- x = x >> 1;\n",
+ " //- x = x >>> 1;\n",
+ " // 联合赋值:\n",
+ " x += y;\n",
+ " x -= y;\n",
+ " x *= y;\n",
+ " x /= y;\n",
+ " x %= y;\n",
+ " //- x <<= 1;\n",
+ " //- x >>= 1;\n",
+ " //- x >>>= 1;\n",
+ " //- x &= y;\n",
+ " //- x ^= y;\n",
+ " //- x |= y;\n",
+ " // 类型转换:\n",
+ " //- boolean bl = (boolean)x;\n",
+ " char c = (char)x;\n",
+ " byte b = (byte)x;\n",
+ " short s = (short)x;\n",
+ " int i = (int)x;\n",
+ " long l = (long)x;\n",
+ " float f = (float)x;\n",
+ " }\n",
+ "}"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "**注意** :**boolean** 类型的运算是受限的。你能为其赋值 `true` 或 `false`,也可测试它的值是否是 `true` 或 `false`。但你不能对其作加减等其他运算。\n",
+ "\n",
+ "在 **char**,**byte** 和 **short** 类型中,我们可以看到算术运算符的“类型转换”效果。我们必须要显式强制类型转换才能将结果重新赋值为原始类型。对于 **int** 类型的运算则不用转换,因为默认就是 **int** 型。虽然我们不用再停下来思考这一切是否安全,但是两个大的 int 型整数相乘时,结果有可能超出 **int** 型的范围,这种情况下结果会发生溢出。下面的代码示例:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "// operators/Overflow.java\n",
+ "// 厉害了!内存溢出\n",
+ "public class Overflow {\n",
+ " public static void main(String[] args) {\n",
+ " int big = Integer.MAX_VALUE;\n",
+ " System.out.println(\"big = \" + big);\n",
+ " int bigger = big * 4;\n",
+ " System.out.println(\"bigger = \" + bigger);\n",
+ " }\n",
+ "}"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "输出结果:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "text"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "big = 2147483647\n",
+ "bigger = -4"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "编译器没有报错或警告,运行时一切看起来都无异常。诚然,Java 是优秀的,但是还不足够优秀。\n",
+ "\n",
+ "对于 **char**,**byte** 或者 **short**,混合赋值并不需要类型转换。即使为它们执行转型操作,也会获得与直接算术运算相同的结果。另外,省略类型转换可以使代码显得更加简练。总之,除 **boolean** 以外,其他任何两种基本类型间都可进行类型转换。当我们进行向下转换类型时,需要注意结果的范围是否溢出,否则我们就很可能在不知不觉中丢失精度。\n",
+ "\n",
+ "\n",
+ "## 本章小结\n",
+ "\n",
+ "如果你已接触过一门 C 语法风格编程语言,那么你在学习 Java 的运算符时实际上没有任何曲线。如果你觉得有难度,那么我推荐你要先去 www.OnJava8.com 观看 《Thinking in C》 的视频教程来补充一些前置知识储备。\n",
+ "\n",
+ "[^1]: 我在 *Pomona College* 大学读过两年本科,在那里 47 被称之为“魔法数字”(*magic number*),详见 [维基百科](https://en.wikipedia.org/wiki/47_(number)) 。\n",
+ "\n",
+ "[^2]: *John Kirkham* 说过:“自 1960 年我开始在 IBM 1620 上开始编程起,至 1970 年之间,FORTRAN 一直都是一种全大写的编程语言。这可能是因为许多早期的输入设备都是旧的电传打字机,使用了 5 位波特码,没有小写字母的功能。指数符号中的 e 也总是大写的,并且从未与自然对数底数 e 混淆,自然对数底数 e 总是小写的。 e 简单地代表指数,通常 10 是基数。那时,八进制也被程序员广泛使用。虽然我从未见过它的用法,但如果我看到一个指数符号的八进制数,我会认为它是以 8 为基数的。我记得第一次看到指数使用小写字母 e 是在 20 世纪 70 年代末,我也发现它令人困惑。这个问题出现的时候,小写字母悄悄进入了 Fortran。如果你真的想使用自然对数底,我们实际上有一些函数要使用,但是它们都是大写的。”\n",
+ "\n",
+ "\n",
+ ""
+ ]
+ }
+ ],
+ "metadata": {
+ "kernelspec": {
+ "display_name": "Java",
+ "language": "java",
+ "name": "java"
+ },
+ "language_info": {
+ "codemirror_mode": "java",
+ "file_extension": ".jshell",
+ "mimetype": "text/x-java-source",
+ "name": "Java",
+ "pygments_lexer": "java",
+ "version": "14.0.1+7"
+ }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 4
+}
diff --git a/jupyter/05-Control-Flow.ipynb b/jupyter/05-Control-Flow.ipynb
new file mode 100644
index 00000000..7a0f129a
--- /dev/null
+++ b/jupyter/05-Control-Flow.ipynb
@@ -0,0 +1,1436 @@
+{
+ "cells": [
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "[TOC]\n",
+ "\n",
+ "# 第五章 控制流\n",
+ "\n",
+ "> 程序必须在执行过程中控制它的世界并做出选择。 在 Java 中,你需要执行控制语句来做出选择。\n",
+ "\n",
+ "Java 使用了 C 的所有执行控制语句,因此对于熟悉 C/C++ 编程的人来说,这部分内容轻车熟路。大多数面向过程编程语言都有共通的某种控制语句。在 Java 中,涉及的关键字包括 **if-else,while,do-while,for,return,break** 和选择语句 **switch**。 Java 并不支持备受诟病的 **goto**(尽管它在某些特殊场景中依然是最行之有效的方法)。 尽管如此,在 Java 中我们仍旧可以进行类似的逻辑跳转,但较之典型的 **goto** 用法限制更多。\n",
+ "\n",
+ "\n",
+ "## true和false\n",
+ "\n",
+ "所有的条件语句都利用条件表达式的“真”或“假”来决定执行路径。举例:\n",
+ "`a == b`。它利用了条件表达式 `==` 来比较 `a` 与 `b` 的值是否相等。 该表达式返回 `true` 或 `false`。代码示例:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "// control/TrueFalse.java\n",
+ "public class TrueFalse {\n",
+ "\tpublic static void main(String[] args) {\n",
+ "\t\tSystem.out.println(1 == 1);\n",
+ "\t\tSystem.out.println(1 == 2);\n",
+ "\t}\n",
+ "}"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "输出结果:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "true false "
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "通过上一章的学习,我们知道任何关系运算符都可以产生条件语句。 **注意**:在 Java 中使用数值作为布尔值是非法的,即便这种操作在 C/C++ 中是被允许的(在这些语言中,“真”为非零,而“假”是零)。如果想在布尔测试中使用一个非布尔值,那么首先需要使用条件表达式来产生 **boolean** 类型的结果,例如 `if(a != 0)`。\n",
+ "\n",
+ "## if-else\n",
+ "\n",
+ "**if-else** 语句是控制程序执行流程最基本的形式。 其中 `else` 是可选的,因此可以有两种形式的 `if`。代码示例:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "if(Boolean-expression) \n",
+ "\t“statement” "
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "或"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "if(Boolean-expression) \n",
+ "\t“statement”\n",
+ "else\n",
+ " “statement”"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "布尔表达式(Boolean-expression)必须生成 **boolean** 类型的结果,执行语句 `statement` 既可以是以分号 `;` 结尾的一条简单语句,也可以是包含在大括号 `{}` 内的的复合语句 —— 封闭在大括号内的一组简单语句。 凡本书中提及“statement”一词,皆表示类似的执行语句。\n",
+ "\n",
+ "下面是一个有关 **if-else** 语句的例子。`test()` 方法可以告知你两个数值之间的大小关系。代码示例:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "// control/IfElse.java\n",
+ "public class IfElse {\n",
+ " static int result = 0;\n",
+ " static void test(int testval, int target) {\n",
+ " if(testval > target)\n",
+ " result = +1;\n",
+ " else if(testval < target) // [1]\n",
+ " result = -1;\n",
+ " else\n",
+ " result = 0; // Match\n",
+ " }\n",
+ "\n",
+ " public static void main(String[] args) {\n",
+ " test(10, 5);\n",
+ " System.out.println(result);\n",
+ " test(5, 10);\n",
+ " System.out.println(result);\n",
+ " test(5, 5);\n",
+ " System.out.println(result);\n",
+ " }\n",
+ "}"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "输出结果:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "1\n",
+ "-1\n",
+ "0"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "**注解**:`else if` 并非新关键字,它仅是 `else` 后紧跟的一条新 `if` 语句。\n",
+ "\n",
+ "Java 和 C/C++ 同属“自由格式”的编程语言,但通常我们会在 Java 控制流程语句中采用首部缩进的规范,以便代码更具可读性。\n",
+ "\n",
+ "\n",
+ "## 迭代语句\n",
+ "\n",
+ "**while**,**do-while** 和 **for** 用来控制循环语句(有时也称迭代语句)。只有控制循环的布尔表达式计算结果为 `false`,循环语句才会停止。 \n",
+ "\n",
+ "\n",
+ "### while\n",
+ "\n",
+ "**while** 循环的形式是:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "while(Boolean-expression) \n",
+ " statement"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "执行语句会在每一次循环前,判断布尔表达式返回值是否为 `true`。下例可产生随机数,直到满足特定条件。代码示例:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "// control/WhileTest.java\n",
+ "// 演示 while 循环\n",
+ "public class WhileTest {\n",
+ " static boolean condition() {\n",
+ " boolean result = Math.random() < 0.99;\n",
+ " System.out.print(result + \", \");\n",
+ " return result;\n",
+ " }\n",
+ " public static void main(String[] args) {\n",
+ " while(condition())\n",
+ " System.out.println(\"Inside 'while'\");\n",
+ " System.out.println(\"Exited 'while'\");\n",
+ " }\n",
+ "}"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "输出结果:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "true, Inside 'while'\n",
+ "true, Inside 'while'\n",
+ "true, Inside 'while'\n",
+ "true, Inside 'while'\n",
+ "true, Inside 'while'\n",
+ "...________...________...________...________...\n",
+ "true, Inside 'while'\n",
+ "true, Inside 'while'\n",
+ "true, Inside 'while'\n",
+ "true, Inside 'while'\n",
+ "false, Exited 'while'"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "`condition()` 方法使用到了 **Math** 库的**静态**方法 `random()`。该方法的作用是产生 0 和 1 之间 (包括 0,但不包括 1) 的一个 **double** 值。\n",
+ "\n",
+ "**result** 的值是通过比较运算符 `<` 产生的 **boolean** 类型的结果。当控制台输出 **boolean** 型值时,会自动将其转换为对应的文字形式 `true` 或 `false`。此处 `while` 条件表达式代表:“仅在 `condition()` 返回 `false` 时停止循环”。\n",
+ "\n",
+ "\n",
+ "### do-while\n",
+ "\n",
+ "**do-while** 的格式如下:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "do \n",
+ "\tstatement\n",
+ "while(Boolean-expression);"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "**while** 和 **do-while** 之间的唯一区别是:即使条件表达式返回结果为 `false`, **do-while** 语句也至少会执行一次。 在 **while** 循环体中,如布尔表达式首次返回的结果就为 `false`,那么循环体内的语句不会被执行。实际应用中,**while** 形式比 **do-while** 更为常用。\n",
+ "\n",
+ "\n",
+ "### for\n",
+ "\n",
+ "**for** 循环可能是最常用的迭代形式。 该循环在第一次迭代之前执行初始化。随后,它会执行布尔表达式,并在每次迭代结束时,进行某种形式的步进。**for** 循环的形式是:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "for(initialization; Boolean-expression; step)\n",
+ " statement"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "初始化 (initialization) 表达式、布尔表达式 (Boolean-expression) ,或者步进 (step) 运算,都可以为空。每次迭代之前都会判断布尔表达式的结果是否成立。一旦计算结果为 `false`,则跳出 **for** 循环体并继续执行后面代码。 每次循环结束时,都会执行一次步进。\n",
+ "\n",
+ "**for** 循环通常用于“计数”任务。代码示例:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "// control/ListCharacters.java\n",
+ "\n",
+ "public class ListCharacters {\n",
+ " public static void main(String[] args) {\n",
+ " for(char c = 0; c < 128; c++)\n",
+ " if(Character.isLowerCase(c))\n",
+ " System.out.println(\"value: \" + (int)c +\n",
+ " \" character: \" + c);\n",
+ " }\n",
+ "}"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "输出结果(前 10 行):"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "value: 97 character: a\n",
+ "value: 98 character: b\n",
+ "value: 99 character: c\n",
+ "value: 100 character: d\n",
+ "value: 101 character: e\n",
+ "value: 102 character: f\n",
+ "value: 103 character: g\n",
+ "value: 104 character: h\n",
+ "value: 105 character: i\n",
+ "value: 106 character: j\n",
+ " ..."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "**注意**:变量 **c** 是在 **for** 循环执行时才被定义的,并不是在主方法的开头。**c** 的作用域范围仅在 **for** 循环体内。\n",
+ "\n",
+ "传统的面向过程语言如 C 需要先在代码块(block)前定义好所有变量才能够使用。这样编译器才能在创建块时,为这些变量分配内存空间。在 Java 和 C++ 中,我们可以在整个块使用变量声明,并且可以在需要时才定义变量。 这种自然的编码风格使我们的代码更容易被人理解 [^1]。\n",
+ "\n",
+ "上例使用了 **java.lang.Character** 包装类,该类不仅包含了基本类型 `char` 的值,还封装了一些有用的方法。例如这里就用到了静态方法 `isLowerCase()` 来判断字符是否为小写。\n",
+ "\n",
+ "\n",
+ "\n",
+ "#### 逗号操作符\n",
+ "\n",
+ "在 Java 中逗号运算符(这里并非指我们平常用于分隔定义和方法参数的逗号分隔符)仅有一种用法:在 **for** 循环的初始化和步进控制中定义多个变量。我们可以使用逗号分隔多个语句,并按顺序计算这些语句。**注意**:要求定义的变量类型相同。代码示例:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "// control/CommaOperator.java\n",
+ "\n",
+ "public class CommaOperator {\n",
+ " public static void main(String[] args) {\n",
+ " for(int i = 1, j = i + 10; i < 5; i++, j = i * 2) {\n",
+ " System.out.println(\"i = \" + i + \" j = \" + j);\n",
+ " }\n",
+ " }\n",
+ "}"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "输出结果:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "i = 1 j = 11\n",
+ "i = 2 j = 4\n",
+ "i = 3 j = 6\n",
+ "i = 4 j = 8"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "上例中 **int** 类型声明包含了 `i` 和 `j`。实际上,在初始化部分我们可以定义任意数量的同类型变量。**注意**:在 Java 中,仅允许 **for** 循环在控制表达式中定义变量。 我们不能将此方法与其他的循环语句和选择语句中一起使用。同时,我们可以看到:无论在初始化还是在步进部分,语句都是顺序执行的。\n",
+ "\n",
+ "## for-in 语法 \n",
+ "\n",
+ "Java 5 引入了更为简洁的“增强版 **for** 循环”语法来操纵数组和集合。(更多细节,可参考 [数组](./21-Arrays.md) 和 [集合](./12-Collections.md) 章节内容)。大部分文档也称其为 **for-each** 语法,但因为了不与 Java 8 新添的 `forEach()` 产生混淆,因此我称之为 **for-in** 循环。 (Python 已有类似的先例,如:**for x in sequence**)。**注意**:你可能会在其他地方看到不同叫法。\n",
+ "\n",
+ "**for-in** 无需你去创建 **int** 变量和步进来控制循环计数。 下面我们来遍历获取 **float** 数组中的元素。代码示例:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "// control/ForInFloat.java\n",
+ "\n",
+ "import java.util.*;\n",
+ "\n",
+ "public class ForInFloat {\n",
+ " public static void main(String[] args) {\n",
+ " Random rand = new Random(47);\n",
+ " float[] f = new float[10];\n",
+ " for(int i = 0; i < 10; i++)\n",
+ " f[i] = rand.nextFloat();\n",
+ " for(float x : f)\n",
+ " System.out.println(x);\n",
+ " }\n",
+ "}"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "输出结果:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "0.72711575\n",
+ "0.39982635\n",
+ "0.5309454\n",
+ "0.0534122\n",
+ "0.16020656\n",
+ "0.57799757\n",
+ "0.18847865\n",
+ "0.4170137\n",
+ "0.51660204\n",
+ "0.73734957"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "上例中我们展示了传统 **for** 循环的用法。接下来再来看下 **for-in** 的用法。代码示例:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "for(float x : f) {"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "这条语句定义了一个 **float** 类型的变量 `x`,继而将每一个 `f` 的元素赋值给它。\n",
+ "\n",
+ "任何一个返回数组的方法都可以使用 **for-in** 循环语法来遍历元素。例如 **String** 类有一个方法 `toCharArray()`,返回值类型为 **char** 数组,我们可以很容易地在 **for-in** 循环中遍历它。代码示例:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "// control/ForInString.java\n",
+ "\n",
+ "public class ForInString {\n",
+ " public static void main(String[] args) {\n",
+ " for(char c: \"An African Swallow\".toCharArray())\n",
+ " System.out.print(c + \" \");\n",
+ " }\n",
+ "}"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "输出结果:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "A n A f r i c a n S w a l l o w"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "很快我们能在 [集合](./12-Collections.md) 章节里学习到,**for-in** 循环适用于任何可迭代(*iterable*)的 对象。\n",
+ "\n",
+ "通常,**for** 循环语句都会在一个整型数值序列中步进。代码示例:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "for(int i = 0; i < 100; i++)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "正因如此,除非先创建一个 **int** 数组,否则我们无法使用 **for-in** 循环来操作。为简化测试过程,我已在 `onjava` 包中封装了 **Range** 类,利用其 `range()` 方法可自动生成恰当的数组。\n",
+ "\n",
+ "在 [封装](./07-Implementation-Hiding.md)(Implementation Hiding)这一章里我们介绍了静态导入(static import),无需了解细节就可以直接使用。 有关静态导入的语法,可以在 **import** 语句中看到:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "// control/ForInInt.java\n",
+ "\n",
+ "import static onjava.Range.*;\n",
+ "\n",
+ "public class ForInInt {\n",
+ " public static void main(String[] args) {\n",
+ " for(int i : range(10)) // 0..9\n",
+ " System.out.print(i + \" \");\n",
+ " System.out.println();\n",
+ " for(int i : range(5, 10)) // 5..9\n",
+ " System.out.print(i + \" \");\n",
+ " System.out.println();\n",
+ " for(int i : range(5, 20, 3)) // 5..20 step 3\n",
+ " System.out.print(i + \" \");\n",
+ " System.out.println();\n",
+ " for(int i : range(20, 5, -3)) // Count down\n",
+ " System.out.print(i + \" \");\n",
+ " System.out.println();\n",
+ " }\n",
+ "}"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "输出结果:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "0 1 2 3 4 5 6 7 8 9\n",
+ "5 6 7 8 9\n",
+ "5 8 11 14 17\n",
+ "20 17 14 11 8"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "`range()` 方法已被 [重载](./06-Housekeeping.md#方法重载)(重载:同名方法,参数列表或类型不同)。上例中 `range()` 方法有多种重载形式:第一种产生从 0 至范围上限(不包含)的值;第二种产生参数一至参数二(不包含)范围内的整数值;第三种形式有一个步进值,因此它每次的增量为该值;第四种 `range()` 表明还可以递减。`range()` 无参方法是该生成器最简单的版本。有关内容会在本书稍后介绍。\n",
+ "\n",
+ "`range()` 的使用提高了代码可读性,让 **for-in** 循环在本书中适应更多的代码示例场景。\n",
+ "\n",
+ "请注意,`System.out.print()` 不会输出换行符,所以我们可以分段输出同一行。\n",
+ "\n",
+ "*for-in* 语法可以节省我们编写代码的时间。 更重要的是,它提高了代码可读性以及更好地描述代码意图(获取数组的每个元素)而不是详细说明这操作细节(创建索引,并用它来选择数组元素) 本书推荐使用 *for-in* 语法。\n",
+ "\n",
+ "## return\n",
+ "\n",
+ "在 Java 中有几个关键字代表无条件分支,这意味无需任何测试即可发生。这些关键字包括 **return**,**break**,**continue** 和跳转到带标签语句的方法,类似于其他语言中的 **goto**。\n",
+ "\n",
+ "**return** 关键字有两方面的作用:1.指定一个方法返回值 (在方法返回类型非 **void** 的情况下);2.退出当前方法,并返回作用 1 中值。我们可以利用 `return` 的这些特点来改写上例 `IfElse.java` 文件中的 `test()` 方法。代码示例:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "// control/TestWithReturn.java\n",
+ "\n",
+ "public class TestWithReturn {\n",
+ " static int test(int testval, int target) {\n",
+ " if(testval > target)\n",
+ " return +1;\n",
+ " if(testval < target)\n",
+ " return -1;\n",
+ " return 0; // Match\n",
+ " }\n",
+ "\n",
+ " public static void main(String[] args) {\n",
+ " System.out.println(test(10, 5));\n",
+ " System.out.println(test(5, 10));\n",
+ " System.out.println(test(5, 5));\n",
+ " }\n",
+ "}"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "输出结果:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "1\n",
+ "-1\n",
+ "0"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "这里不需要 `else`,因为该方法执行到 `return` 就结束了。\n",
+ "\n",
+ "如果在方法签名中定义了返回值类型为 **void**,那么在代码执行结束时会有一个隐式的 **return**。 也就是说我们不用在总是在方法中显式地包含 **return** 语句。 **注意**:如果你的方法声明的返回值类型为非 **void** 类型,那么则必须确保每个代码路径都返回一个值。\n",
+ "\n",
+ "## break 和 continue\n",
+ "\n",
+ "在任何迭代语句的主体内,都可以使用 **break** 和 **continue** 来控制循环的流程。 其中,**break** 表示跳出当前循环体。而 **continue** 表示停止本次循环,开始下一次循环。\n",
+ "\n",
+ "下例向大家展示 **break** 和 **continue** 在 **for**、**while** 循环中的使用。代码示例:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "// control/BreakAndContinue.java\n",
+ "// Break 和 continue 关键字\n",
+ "\n",
+ "import static onjava.Range.*;\n",
+ "\n",
+ "public class BreakAndContinue {\n",
+ " public static void main(String[] args) {\n",
+ " for(int i = 0; i < 100; i++) { // [1]\n",
+ " if(i == 74) break; // 跳出循环\n",
+ " if(i % 9 != 0) continue; // 下一次循环\n",
+ " System.out.print(i + \" \");\n",
+ " }\n",
+ " System.out.println();\n",
+ " // 使用 for-in 循环:\n",
+ " for(int i : range(100)) { // [2]\n",
+ " if(i == 74) break; // 跳出循环\n",
+ " if(i % 9 != 0) continue; // 下一次循环\n",
+ " System.out.print(i + \" \");\n",
+ " }\n",
+ " System.out.println();\n",
+ " int i = 0;\n",
+ " // \"无限循环\":\n",
+ " while(true) { // [3]\n",
+ " i++;\n",
+ " int j = i * 27;\n",
+ " if(j == 1269) break; // 跳出循环\n",
+ " if(i % 10 != 0) continue; // 循环顶部\n",
+ " System.out.print(i + \" \");\n",
+ " }\n",
+ " }\n",
+ "}"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "输出结果:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "0 9 18 27 36 45 54 63 72\n",
+ "0 9 18 27 36 45 54 63 72\n",
+ "10 20 30 40"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "**[1]** 在这个 **for** 循环中,`i` 的值永远不会达到 100,因为一旦 `i` 等于 74,**break** 语句就会中断循环。通常,只有在不知道中断条件何时满足时,才需要 **break**。因为 `i` 不能被 9 整除,**continue** 语句就会使循环从头开始。这使 **i** 递增)。如果能够整除,则将值显示出来。\n",
+ " **[2]** 使用 **for-in** 语法,结果相同。\n",
+ " **[3]** 无限 **while** 循环。循环内的 **break** 语句可中止循环。**注意**,**continue** 语句可将控制权移回循环的顶部,而不会执行 **continue** 之后的任何操作。 因此,只有当 `i` 的值可被 10 整除时才会输出。在输出中,显示值 0,因为 `0%9` 产生 0。还有一种无限循环的形式: `for(;;)`。 在编译器看来,它与 `while(true)` 无异,使用哪种完全取决于你的编程品味。\n",
+ "\n",
+ "\n",
+ "## 臭名昭著的 goto\n",
+ "\n",
+ "[**goto** 关键字](https://en.wikipedia.org/wiki/Goto) 很早就在程序设计语言中出现。事实上,**goto** 起源于[汇编](https://en.wikipedia.org/wiki/Assembly_language)(assembly language)语言中的程序控制:“若条件 A 成立,则跳到这里;否则跳到那里”。如果你读过由编译器编译后的代码,你会发现在其程序控制中充斥了大量的跳转。较之汇编产生的代码直接运行在硬件 CPU 中,Java 也会产生自己的“汇编代码”(字节码),只不过它是运行在 Java 虚拟机里的(Java Virtual Machine)。\n",
+ "\n",
+ "一个源码级别跳转的 **goto**,为何招致名誉扫地呢?若程序总是从一处跳转到另一处,还有什么办法能识别代码的控制流程呢?随着 *Edsger Dijkstra*发表著名的 “Goto 有害” 论(*Goto considered harmful*)以后,**goto** 便从此失宠。甚至有人建议将它从关键字中剔除。\n",
+ "\n",
+ "正如上述提及的经典情况,我们不应走向两个极端。问题不在 **goto**,而在于过度使用 **goto**。在极少数情况下,**goto** 实际上是控制流程的最佳方式。\n",
+ "\n",
+ "尽管 **goto** 仍是 Java 的一个保留字,但其并未被正式启用。可以说, Java 中并不支持 **goto**。然而,在 **break** 和 **continue** 这两个关键字的身上,我们仍能看出一些 **goto** 的影子。它们并不属于一次跳转,而是中断循环语句的一种方法。之所以把它们纳入 **goto** 问题中一起讨论,是由于它们使用了相同的机制:标签。\n",
+ "\n",
+ "“标签”是后面跟一个冒号的标识符。代码示例:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "label1:"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "对 Java 来说,唯一用到标签的地方是在循环语句之前。进一步说,它实际需要紧靠在循环语句的前方 —— 在标签和循环之间置入任何语句都是不明智的。而在循环之前设置标签的唯一理由是:我们希望在其中嵌套另一个循环或者一个开关。这是由于 **break** 和 **continue** 关键字通常只中断当前循环,但若搭配标签一起使用,它们就会中断并跳转到标签所在的地方开始执行。代码示例:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "label1:\n",
+ "outer-iteration { \n",
+ " inner-iteration {\n",
+ " // ...\n",
+ " break; // [1] \n",
+ " // ...\n",
+ " continue; // [2] \n",
+ " // ...\n",
+ " continue label1; // [3] \n",
+ " // ...\n",
+ " break label1; // [4] \n",
+ " } \n",
+ "}"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "**[1]** **break** 中断内部循环,并在外部循环结束。\n",
+ "**[2]** **continue** 移回内部循环的起始处。但在条件 3 中,**continue label1** 却同时中断内部循环以及外部循环,并移至 **label1** 处。\n",
+ "**[3]** 随后,它实际是继续循环,但却从外部循环开始。\n",
+ "**[4]** **break label1** 也会中断所有循环,并回到 **label1** 处,但并不重新进入循环。也就是说,它实际是完全中止了两个循环。\n",
+ "\n",
+ "下面是 **for** 循环的一个例子:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "// control/LabeledFor.java\n",
+ "// 搭配“标签 break”的 for 循环中使用 break 和 continue\n",
+ "\n",
+ "public class LabeledFor {\n",
+ " public static void main(String[] args) {\n",
+ " int i = 0;\n",
+ " outer: // 此处不允许存在执行语句\n",
+ " for(; true ;) { // 无限循环\n",
+ " inner: // 此处不允许存在执行语句\n",
+ " for(; i < 10; i++) {\n",
+ " System.out.println(\"i = \" + i);\n",
+ " if(i == 2) {\n",
+ " System.out.println(\"continue\");\n",
+ " continue;\n",
+ " }\n",
+ " if(i == 3) {\n",
+ " System.out.println(\"break\");\n",
+ " i++; // 否则 i 永远无法获得自增 \n",
+ " // 获得自增 \n",
+ " break;\n",
+ " }\n",
+ " if(i == 7) {\n",
+ " System.out.println(\"continue outer\");\n",
+ " i++; // 否则 i 永远无法获得自增 \n",
+ " // 获得自增 \n",
+ " continue outer;\n",
+ " }\n",
+ " if(i == 8) {\n",
+ " System.out.println(\"break outer\");\n",
+ " break outer;\n",
+ " }\n",
+ " for(int k = 0; k < 5; k++) {\n",
+ " if(k == 3) {\n",
+ " System.out.println(\"continue inner\");\n",
+ " continue inner;\n",
+ " }\n",
+ " }\n",
+ " }\n",
+ " }\n",
+ " // 在此处无法 break 或 continue 标签\n",
+ " }\n",
+ "}"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "输出结果:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "i = 0\n",
+ "continue inner\n",
+ "i = 1\n",
+ "continue inner\n",
+ "i = 2\n",
+ "continue\n",
+ "i = 3\n",
+ "break\n",
+ "i = 4\n",
+ "continue inner\n",
+ "i = 5\n",
+ "continue inner\n",
+ "i = 6\n",
+ "continue inner\n",
+ "i = 7\n",
+ "continue outer\n",
+ "i = 8\n",
+ "break outer"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "注意 **break** 会中断 **for** 循环,而且在抵达 **for** 循环的末尾之前,递增表达式不会执行。由于 **break** 跳过了递增表达式,所以递增会在 `i==3` 的情况下直接执行。在 `i==7` 的情况下,`continue outer` 语句也会到达循环顶部,而且也会跳过递增,所以它也是直接递增的。\n",
+ "\n",
+ "如果没有 **break outer** 语句,就没有办法在一个内部循环里找到出外部循环的路径。这是由于 **break** 本身只能中断最内层的循环(对于 **continue** 同样如此)。 当然,若想在中断循环的同时退出方法,简单地用一个 **return** 即可。\n",
+ "\n",
+ "下面这个例子向大家展示了带标签的 **break** 以及 **continue** 语句在 **while** 循环中的用法:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "// control/LabeledWhile.java\n",
+ "// 带标签的 break 和 conitue 在 while 循环中的使用\n",
+ "\n",
+ "public class LabeledWhile {\n",
+ " public static void main(String[] args) {\n",
+ " int i = 0;\n",
+ " outer:\n",
+ " while(true) {\n",
+ " System.out.println(\"Outer while loop\");\n",
+ " while(true) {\n",
+ " i++;\n",
+ " System.out.println(\"i = \" + i);\n",
+ " if(i == 1) {\n",
+ " System.out.println(\"continue\");\n",
+ " continue;\n",
+ " }\n",
+ " if(i == 3) {\n",
+ " System.out.println(\"continue outer\");\n",
+ " continue outer;\n",
+ " }\n",
+ " if(i == 5) {\n",
+ " System.out.println(\"break\");\n",
+ " break;\n",
+ " }\n",
+ " if(i == 7) {\n",
+ " System.out.println(\"break outer\");\n",
+ " break outer;\n",
+ " }\n",
+ " }\n",
+ " }\n",
+ " }\n",
+ "}"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "输出结果:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "Outer while loop\n",
+ "i = 1\n",
+ "continue\n",
+ "i = 2\n",
+ "i = 3\n",
+ "continue outer\n",
+ "Outer while loop\n",
+ "i = 4\n",
+ "i = 5\n",
+ "break\n",
+ "Outer while loop\n",
+ "i = 6\n",
+ "i = 7\n",
+ "break outer"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "同样的规则亦适用于 **while**:\n",
+ "\n",
+ "1. 简单的一个 **continue** 会退回最内层循环的开头(顶部),并继续执行。\n",
+ "\n",
+ "2. 带有标签的 **continue** 会到达标签的位置,并重新进入紧接在那个标签后面的循环。\n",
+ "\n",
+ "3. **break** 会中断当前循环,并移离当前标签的末尾。\n",
+ "\n",
+ "4. 带标签的 **break** 会中断当前循环,并移离由那个标签指示的循环的末尾。\n",
+ "\n",
+ "大家要记住的重点是:在 Java 里需要使用标签的唯一理由就是因为有循环嵌套存在,而且想从多层嵌套中 **break** 或 **continue**。\n",
+ "\n",
+ "**break** 和 **continue** 标签在编码中的使用频率相对较低 (此前的语言中很少使用或没有先例),所以我们很少在代码里看到它们。\n",
+ "\n",
+ "在 *Dijkstra* 的 **“Goto 有害”** 论文中,他最反对的就是标签,而非 **goto**。他观察到 BUG 的数量似乎随着程序中标签的数量而增加[^2]。标签和 **goto** 使得程序难以分析。但是,Java 标签不会造成这方面的问题,因为它们的应用场景受到限制,无法用于以临时方式传输控制。由此也引出了一个有趣的情形:对语言能力的限制,反而使它这项特性更加有价值。\n",
+ "\n",
+ "\n",
+ "## switch\n",
+ "\n",
+ "**switch** 有时也被划归为一种选择语句。根据整数表达式的值,**switch** 语句可以从一系列代码中选出一段去执行。它的格式如下:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "switch(integral-selector) {\n",
+ "\tcase integral-value1 : statement; break;\n",
+ "\tcase integral-value2 : statement;\tbreak;\n",
+ "\tcase integral-value3 : statement;\tbreak;\n",
+ "\tcase integral-value4 : statement;\tbreak;\n",
+ "\tcase integral-value5 : statement;\tbreak;\n",
+ "\t// ...\n",
+ "\tdefault: statement;\n",
+ "}"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "其中,**integral-selector** (整数选择因子)是一个能够产生整数值的表达式,**switch** 能够将这个表达式的结果与每个 **integral-value** (整数值)相比较。若发现相符的,就执行对应的语句(简单或复合语句,其中并不需要括号)。若没有发现相符的,就执行 **default** 语句。\n",
+ "\n",
+ "在上面的定义中,大家会注意到每个 **case** 均以一个 **break** 结尾。这样可使执行流程跳转至 **switch** 主体的末尾。这是构建 **switch** 语句的一种传统方式,但 **break** 是可选的。若省略 **break,** 会继续执行后面的 **case** 语句的代码,直到遇到一个 **break** 为止。通常我们不想出现这种情况,但对有经验的程序员来说,也许能够善加利用。注意最后的 **default** 语句没有 **break**,因为执行流程已到了 **break** 的跳转目的地。当然,如果考虑到编程风格方面的原因,完全可以在 **default** 语句的末尾放置一个 **break**,尽管它并没有任何实际的作用。\n",
+ "\n",
+ "**switch** 语句是一种实现多路选择的干净利落的一种方式(比如从一系列执行路径中挑选一个)。但它要求使用一个选择因子,并且必须是 **int** 或 **char** 那样的整数值。例如,假若将一个字串或者浮点数作为选择因子使用,那么它们在 switch 语句里是不会工作的。对于非整数类型(Java 7 以上版本中的 String 型除外),则必须使用一系列 **if** 语句。 在[下一章的结尾](./06-Housekeeping.md#枚举类型) 中,我们将会了解到**枚举类型**被用来搭配 **switch** 工作,并优雅地解决了这种限制。\n",
+ "\n",
+ "下面这个例子可随机生成字母,并判断它们是元音还是辅音字母:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "// control/VowelsAndConsonants.java\n",
+ "\n",
+ "// switch 执行语句的演示\n",
+ "import java.util.*;\n",
+ "\n",
+ "public class VowelsAndConsonants {\n",
+ " public static void main(String[] args) {\n",
+ " Random rand = new Random(47);\n",
+ " for(int i = 0; i < 100; i++) {\n",
+ " int c = rand.nextInt(26) + 'a';\n",
+ " System.out.print((char)c + \", \" + c + \": \");\n",
+ " switch(c) {\n",
+ " case 'a':\n",
+ " case 'e':\n",
+ " case 'i':\n",
+ " case 'o':\n",
+ " case 'u': System.out.println(\"vowel\");\n",
+ " break;\n",
+ " case 'y':\n",
+ " case 'w': System.out.println(\"Sometimes vowel\");\n",
+ " break;\n",
+ " default: System.out.println(\"consonant\");\n",
+ " }\n",
+ " }\n",
+ " }\n",
+ "}"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "输出结果:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "y, 121: Sometimes vowel\n",
+ "n, 110: consonant\n",
+ "z, 122: consonant\n",
+ "b, 98: consonant\n",
+ "r, 114: consonant\n",
+ "n, 110: consonant\n",
+ "y, 121: Sometimes vowel\n",
+ "g, 103: consonant\n",
+ "c, 99: consonant\n",
+ "f, 102: consonant\n",
+ "o, 111: vowel\n",
+ "w, 119: Sometimes vowel\n",
+ "z, 122: consonant\n",
+ " ..."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "由于 `Random.nextInt(26)` 会产生 0 到 25 之间的一个值,所以在其上加上一个偏移量 `a`,即可产生小写字母。在 **case** 语句中,使用单引号引起的字符也会产生用于比较的整数值。\n",
+ "\n",
+ "请注意 **case** 语句能够堆叠在一起,为一段代码形成多重匹配,即只要符合多种条件中的一种,就执行那段特别的代码。这时也应该注意将 **break** 语句置于特定 **case** 的末尾,否则控制流程会继续往下执行,处理后面的 **case**。在下面的语句中:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "int c = rand.nextInt(26) + 'a';"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "此处 `Random.nextInt()` 将产生 0~25 之间的一个随机 **int** 值,它将被加到 `a` 上。这表示 `a` 将自动被转换为 **int** 以执行加法。为了把 `c` 当作字符打印,必须将其转型为 **char**;否则,将会输出整数。\n",
+ "\n",
+ "\n",
+ "\n",
+ "## switch 字符串\n",
+ "\n",
+ "Java 7 增加了在字符串上 **switch** 的用法。 下例展示了从一组 **String** 中选择可能值的传统方法,以及新式方法:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "// control/StringSwitch.java\n",
+ "\n",
+ "public class StringSwitch {\n",
+ " public static void main(String[] args) {\n",
+ " String color = \"red\";\n",
+ " // 老的方式: 使用 if-then 判断\n",
+ " if(\"red\".equals(color)) {\n",
+ " System.out.println(\"RED\");\n",
+ " } else if(\"green\".equals(color)) {\n",
+ " System.out.println(\"GREEN\");\n",
+ " } else if(\"blue\".equals(color)) {\n",
+ " System.out.println(\"BLUE\");\n",
+ " } else if(\"yellow\".equals(color)) {\n",
+ " System.out.println(\"YELLOW\");\n",
+ " } else {\n",
+ " System.out.println(\"Unknown\");\n",
+ " }\n",
+ " // 新的方法: 字符串搭配 switch\n",
+ " switch(color) {\n",
+ " case \"red\":\n",
+ " System.out.println(\"RED\");\n",
+ " break;\n",
+ " case \"green\":\n",
+ " System.out.println(\"GREEN\");\n",
+ " break;\n",
+ " case \"blue\":\n",
+ " System.out.println(\"BLUE\");\n",
+ " break;\n",
+ " case \"yellow\":\n",
+ " System.out.println(\"YELLOW\");\n",
+ " break;\n",
+ " default:\n",
+ " System.out.println(\"Unknown\");\n",
+ " break;\n",
+ " }\n",
+ " }\n",
+ "}"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "输出结果:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "RED\n",
+ "RED"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "一旦理解了 **switch**,你会明白这其实就是一个逻辑扩展的语法糖。新的编码方式能使得结果更清晰,更易于理解和维护。\n",
+ "\n",
+ "作为 **switch** 字符串的第二个例子,我们重新访问 `Math.random()`。 它是否产生从 0 到 1 的值,包括还是不包括值 1 呢?在数学术语中,它属于 (0,1)、 [0,1)、(0,1] 、[0,1] 中的哪种呢?(方括号表示“包括”,而括号表示“不包括”)\n",
+ "\n",
+ "下面是一个可能提供答案的测试程序。 所有命令行参数都作为 **String** 对象传递,因此我们可以 **switch** 参数来决定要做什么。 那么问题来了:如果用户不提供参数 ,索引到 `args` 的数组就会导致程序失败。 解决这个问题,我们需要预先检查数组的长度,若长度为 0,则使用**空字符串** `\"\"` 替代;否则,选择 `args` 数组中的第一个元素:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "// control/RandomBounds.java\n",
+ "\n",
+ "// Math.random() 会产生 0.0 和 1.0 吗?\n",
+ "// {java RandomBounds lower}\n",
+ "import onjava.*;\n",
+ "\n",
+ "public class RandomBounds {\n",
+ " public static void main(String[] args) {\n",
+ " new TimedAbort(3);\n",
+ " switch(args.length == 0 ? \"\" : args[0]) {\n",
+ " case \"lower\":\n",
+ " while(Math.random() != 0.0)\n",
+ " ; // 保持重试\n",
+ " System.out.println(\"Produced 0.0!\");\n",
+ " break;\n",
+ " case \"upper\":\n",
+ " while(Math.random() != 1.0)\n",
+ " ; // 保持重试\n",
+ " System.out.println(\"Produced 1.0!\");\n",
+ " break;\n",
+ " default:\n",
+ " System.out.println(\"Usage:\");\n",
+ " System.out.println(\"\\tRandomBounds lower\");\n",
+ " System.out.println(\"\\tRandomBounds upper\");\n",
+ " System.exit(1);\n",
+ " }\n",
+ " }\n",
+ "}"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "要运行该程序,请键入以下任一命令:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "java RandomBounds lower \n",
+ "// 或者\n",
+ "java RandomBounds upper"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "使用 `onjava` 包中的 **TimedAbort** 类可使程序在三秒后中止。从结果来看,似乎 `Math.random()` 产生的随机值里不包含 0.0 或 1.0。 这就是该测试容易混淆的地方:若要考虑 0 至 1 之间所有不同 **double** 数值的可能性,那么这个测试的耗费的时间可能超出一个人的寿命了。 这里我们直接给出正确的结果:`Math.random()` 的结果集范围包含 0.0 ,不包含 1.0。 在数学术语中,可用 [0,1)来表示。由此可知,我们必须小心分析实验并了解它们的局限性。\n",
+ "\n",
+ "\n",
+ "## 本章小结\n",
+ "\n",
+ "本章总结了我们对大多数编程语言中出现的基本特性的探索:计算,运算符优先级,类型转换,选择和迭代。 现在让我们准备好,开始步入面向对象和函数式编程的世界吧。 下一章的内容涵盖了 Java 编程中的重要问题:对象的[初始化和清理](./06-Housekeeping.md)。紧接着,还会介绍[封装](./07-Implementation-Hiding.md)(implementation hiding)的核心概念。\n",
+ "\n",
+ "\n",
+ "[^1]: 在早期的语言中,许多决策都是基于让编译器设计者的体验更好。 但在现代语言设计中,许多决策都是为了提高语言使用者的体验,尽管有时会有妥协 —— 这通常会让语言设计者后悔。\n",
+ "\n",
+ "[^2]: **注意**,此处观点似乎难以让人信服,很可能只是一个因认知偏差而造成的[因果关系谬误](https://en.wikipedia.org/wiki/Correlation_does_not_imply_causation) 的例子。\n",
+ "\n",
+ "\n",
+ "\n",
+ ""
+ ]
+ }
+ ],
+ "metadata": {},
+ "nbformat": 4,
+ "nbformat_minor": 4
+}
diff --git a/jupyter/06-Housekeeping.ipynb b/jupyter/06-Housekeeping.ipynb
new file mode 100644
index 00000000..c64222a9
--- /dev/null
+++ b/jupyter/06-Housekeeping.ipynb
@@ -0,0 +1,3097 @@
+{
+ "cells": [
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "[TOC]\n",
+ "\n",
+ "\n",
+ "\n",
+ "# 第六章 初始化和清理\n",
+ "\n",
+ "\"不安全\"的编程是造成编程代价昂贵的罪魁祸首之一。有两个安全性问题:初始化和清理。C 语言中很多的 bug 都是因为程序员忘记初始化导致的。尤其是很多类库的使用者不知道如何初始化类库组件,甚至他们必须得去初始化。清理则是另一个特殊的问题,因为当你使用一个元素做完事后就不会去关心这个元素,所以你很容易忘记清理它。这样就造成了元素使用的资源滞留不会被回收,直到程序消耗完所有的资源(特别是内存)。\n",
+ "\n",
+ "C++ 引入了构造器的概念,这是一个特殊的方法,每创建一个对象,这个方法就会被自动调用。Java 采用了构造器的概念,另外还使用了垃圾收集器(Garbage Collector, GC)去自动回收不再被使用的对象所占的资源。这一章将讨论初始化和清理的问题,以及在 Java 中对它们的支持。\n",
+ "\n",
+ "\n",
+ "\n",
+ "## 利用构造器保证初始化\n",
+ "\n",
+ "你可能想为每个类创建一个 `initialize()` 方法,该方法名暗示着在使用类之前需要先调用它。不幸的是,用户必须得记得去调用它。在 Java 中,类的设计者通过构造器保证每个对象的初始化。如果一个类有构造器,那么 Java 会在用户使用对象之前(即对象刚创建完成)自动调用对象的构造器方法,从而保证初始化。下个挑战是如何命名构造器方法。存在两个问题:第一个是任何命名都可能与类中其他已有元素的命名冲突;第二个是编译器必须始终知道构造器方法名称,从而调用它。C++ 的解决方法看起来是最简单且最符合逻辑的,所以 Java 中使用了同样的方式:构造器名称与类名相同。在初始化过程中自动调用构造器方法是有意义的。\n",
+ "\n",
+ "以下示例是包含了一个构造器的类:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "// housekeeping/SimpleConstructor.java\n",
+ "// Demonstration of a simple constructor\n",
+ "\n",
+ "class Rock {\n",
+ " Rock() { // 这是一个构造器\n",
+ " System.out.print(\"Rock \");\n",
+ " }\n",
+ "}\n",
+ "\n",
+ "public class SimpleConstructor {\n",
+ " public static void main(String[] args) {\n",
+ " for (int i = 0; i < 10; i++) {\n",
+ " new Rock();\n",
+ " }\n",
+ " }\n",
+ "}"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "输出:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "Rock Rock Rock Rock Rock Rock Rock Rock Rock Rock "
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "现在,当创建一个对象时:`new Rock()` ,内存被分配,构造器被调用。构造器保证了对象在你使用它之前进行了正确的初始化。\n",
+ "\n",
+ "有一点需要注意,构造器方法名与类名相同,不需要符合首字母小写的编程风格。在 C++ 中,无参构造器被称为默认构造器,这个术语在 Java 出现之前使用了很多年。但是,出于一些原因,Java 设计者们决定使用无参构造器这个名称,我(作者)认为这种叫法笨拙而且没有必要,所以我打算继续使用默认构造器。Java 8 引入了 **default** 关键字修饰方法,所以算了,我还是用无参构造器的叫法吧。\n",
+ "\n",
+ "跟其他方法一样,构造器方法也可以传入参数来定义如何创建一个对象。之前的例子稍作修改,使得构造器接收一个参数:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "// housekeeping/SimpleConstructor2.java\n",
+ "// Constructors can have arguments\n",
+ "\n",
+ "class Rock2 {\n",
+ " Rock2(int i) {\n",
+ " System.out.print(\"Rock \" + i + \" \");\n",
+ " }\n",
+ "}\n",
+ "\n",
+ "public class SimpleConstructor2 {\n",
+ " public static void main(String[] args) {\n",
+ " for (int i = 0; i < 8; i++) {\n",
+ " new Rock2(i);\n",
+ " }\n",
+ " }\n",
+ "}"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "输出:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "Rock 0 Rock 1 Rock 2 Rock 3 Rock 4 Rock 5 Rock 6 Rock 7"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "如果类 **Tree** 有一个构造方法,只接收一个参数用来表示树的高度,那么你可以像下面这样创建一棵树:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "Tree t = new Tree(12); // 12-foot 树"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "如果 **Tree(int)** 是唯一的构造器,那么编译器就不允许你以其他任何方式创建 **Tree** 类型的对象。\n",
+ "\n",
+ "构造器消除了一类重要的问题,使得代码更易读。例如,在上面的代码块中,你看不到对 `initialize()` 方法的显式调用,而从概念上来看,`initialize()` 方法应该与对象的创建分离。在 Java 中,对象的创建与初始化是统一的概念,二者不可分割。\n",
+ "\n",
+ "构造器没有返回值,它是一种特殊的方法。但它和返回类型为 `void` 的普通方法不同,普通方法可以返回空值,你还能选择让它返回别的类型;而构造器没有返回值,却同时也没有给你选择的余地(`new` 表达式虽然返回了刚创建的对象的引用,但构造器本身却没有返回任何值)。如果它有返回值,并且你也可以自己选择让它返回什么,那么编译器就还得知道接下来该怎么处理那个返回值(这个返回值没有接收者)。\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "## 方法重载\n",
+ "\n",
+ "任何编程语言中都具备的一项重要特性就是命名。当你创建一个对象时,就会给此对象分配的内存空间命名。方法是行为的命名。你通过名字指代所有的对象,属性和方法。良好命名的系统易于理解和修改。就好比写散文——目的是与读者沟通。\n",
+ "\n",
+ "将人类语言细微的差别映射到编程语言中会产生一个问题。通常,相同的词可以表达多种不同的含义——它们被\"重载\"了。特别是当含义的差别很小时,这会更加有用。你会说\"清洗衬衫\"、\"清洗车\"和\"清洗狗\"。而如果硬要这么说就会显得很愚蠢:\"以洗衬衫的方式洗衬衫\"、\"以洗车的方式洗车\"和\"以洗狗的方式洗狗\",因为听众根本不需要区分行为的动作。大多数人类语言都具有\"冗余\"性,所以即使漏掉几个词,你也能明白含义。你不需要对每个概念都使用不同的词汇——可以从上下文推断出含义。\n",
+ "\n",
+ "大多数编程语言(尤其是 C 语言)要求为每个方法(在这些语言中经常称为函数)提供一个独一无二的标识符。所以,你不能有一个 `print()` 函数既能打印整型,也能打印浮点型——每个函数名都必须不同。\n",
+ "\n",
+ "在 Java (C++) 中,还有一个因素也促使了必须使用方法重载:构造器。因为构造器方法名肯定是与类名相同,所以一个类中只会有一个构造器名。那么你怎么通过不同的方式创建一个对象呢?例如,你想创建一个类,这个类的初始化方式有两种:一种是标准化方式,另一种是从文件中读取信息的方式。你需要两个构造器:无参构造器和有一个 **String** 类型参数的构造器,该参数传入文件名。两个构造器具有相同的名字——与类名相同。因此,方法重载是必要的,它允许方法具有相同的方法名但接收的参数不同。尽管方法重载对于构造器是重要的,但是也可以对任何方法很方便地进行重载。\n",
+ "\n",
+ "下例展示了如何重载构造器和方法:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "// housekeeping/Overloading.java\n",
+ "// Both constructor and ordinary method overloading\n",
+ "\n",
+ "class Tree {\n",
+ " int height;\n",
+ " Tree() {\n",
+ " System.out.println(\"Planting a seedling\");\n",
+ " height = 0;\n",
+ " }\n",
+ " Tree(int initialHeight) {\n",
+ " height = initialHeight;\n",
+ " System.out.println(\"Creating new Tree that is \" + height + \" feet tall\");\n",
+ " }\n",
+ " void info() {\n",
+ " System.out.println(\"Tree is \" + height + \" feet tall\");\n",
+ " }\n",
+ " void info(String s) {\n",
+ " System.out.println(s + \": Tree is \" + height + \" feet tall\");\n",
+ " }\n",
+ "}\n",
+ "public class Overloading {\n",
+ " public static void main(String[] args) {\n",
+ " for (int i = 0; i < 5; i++) {\n",
+ " Tree t = new Tree(i);\n",
+ " t.info();\n",
+ " t.info(\"overloaded method\");\n",
+ " }\n",
+ " new Tree(); \n",
+ " }\n",
+ "}"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "输出:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "Creating new Tree that is 0 feet tall\n",
+ "Tree is 0 feet tall\n",
+ "overloaded method: Tree is 0 feet tall\n",
+ "Creating new Tree that is 1 feet tall\n",
+ "Tree is 1 feet tall\n",
+ "overloaded method: Tree is 1 feet tall\n",
+ "Creating new Tree that is 2 feet tall\n",
+ "Tree is 2 feet tall\n",
+ "overloaded method: Tree is 2 feet tall\n",
+ "Creating new Tree that is 3 feet tall\n",
+ "Tree is 3 feet tall\n",
+ "overloaded method: Tree is 3 feet tall\n",
+ "Creating new Tree that is 4 feet tall\n",
+ "Tree is 4 feet tall\n",
+ "overloaded method: Tree is 4 feet tall\n",
+ "Planting a seedling"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "一个 **Tree** 对象既可以是一颗树苗,使用无参构造器创建,也可以是一颗在温室中已长大的树,已经有一定高度,这时候,就需要使用有参构造器创建。\n",
+ "\n",
+ "你也许想以多种方式调用 `info()` 方法。比如,如果你想打印额外的消息,就可以使用 `info(String)` 方法。如果你无话可说,就可以使用 `info()` 方法。用两个命名定义完全相同的概念看起来很奇怪,而使用方法重载,你就可以使用一个命名来定义一个概念。\n",
+ "\n",
+ "### 区分重载方法\n",
+ "\n",
+ "如果两个方法命名相同,Java是怎么知道你调用的是哪个呢?有一条简单的规则:每个被重载的方法必须有独一无二的参数列表。你稍微思考下,就会很明了了,除了通过参数列表的不同来区分两个相同命名的方法,其他也没什么方式了。你甚至可以根据参数列表中的参数顺序来区分不同的方法,尽管这会造成代码难以维护。例如:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "// housekeeping/OverloadingOrder.java\n",
+ "// Overloading based on the order of the arguments\n",
+ "\n",
+ "public class OverloadingOrder {\n",
+ " static void f(String s, int i) {\n",
+ " System.out.println(\"String: \" + s + \", int: \" + i);\n",
+ " }\n",
+ "\n",
+ " static void f(int i, String s) {\n",
+ " System.out.println(\"int: \" + i + \", String: \" + s);\n",
+ " }\n",
+ "\n",
+ " public static void main(String[] args) {\n",
+ " f(\"String first\", 1);\n",
+ " f(99, \"Int first\");\n",
+ " }\n",
+ "}"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "输出:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "String: String first, int: 1\n",
+ "int: 99, String: Int first"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "两个 `f()` 方法具有相同的参数,但是参数顺序不同,根据这个就可以区分它们。\n",
+ "\n",
+ "### 重载与基本类型\n",
+ "\n",
+ "基本类型可以自动从较小的类型转型为较大的类型。当这与重载结合时,这会令人有点困惑,下面是一个这样的例子:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "// housekeeping/PrimitiveOverloading.java\n",
+ "// Promotion of primitives and overloading\n",
+ "\n",
+ "public class PrimitiveOverloading {\n",
+ " void f1(char x) {\n",
+ " System.out.print(\"f1(char)\");\n",
+ " }\n",
+ " void f1(byte x) {\n",
+ " System.out.print(\"f1(byte)\");\n",
+ " }\n",
+ " void f1(short x) {\n",
+ " System.out.print(\"f1(short)\");\n",
+ " }\n",
+ " void f1(int x) {\n",
+ " System.out.print(\"f1(int)\");\n",
+ " }\n",
+ " void f1(long x) {\n",
+ " System.out.print(\"f1(long)\");\n",
+ " }\n",
+ " void f1(float x) {\n",
+ " System.out.print(\"f1(float)\");\n",
+ " }\n",
+ " void f1(double x) {\n",
+ " System.out.print(\"f1(double)\");\n",
+ " }\n",
+ " void f2(byte x) {\n",
+ " System.out.print(\"f2(byte)\");\n",
+ " }\n",
+ " void f2(short x) {\n",
+ " System.out.print(\"f2(short)\");\n",
+ " }\n",
+ " void f2(int x) {\n",
+ " System.out.print(\"f2(int)\");\n",
+ " }\n",
+ " void f2(long x) {\n",
+ " System.out.print(\"f2(long)\");\n",
+ " }\n",
+ " void f2(float x) {\n",
+ " System.out.print(\"f2(float)\");\n",
+ " }\n",
+ " void f2(double x) {\n",
+ " System.out.print(\"f2(double)\");\n",
+ " }\n",
+ " void f3(short x) {\n",
+ " System.out.print(\"f3(short)\");\n",
+ " }\n",
+ " void f3(int x) {\n",
+ " System.out.print(\"f3(int)\");\n",
+ " }\n",
+ " void f3(long x) {\n",
+ " System.out.print(\"f3(long)\");\n",
+ " }\n",
+ " void f3(float x) {\n",
+ " System.out.print(\"f3(float)\");\n",
+ " }\n",
+ " void f3(double x) {\n",
+ " System.out.print(\"f3(double)\");\n",
+ " }\n",
+ " void f4(int x) {\n",
+ " System.out.print(\"f4(int)\");\n",
+ " }\n",
+ " void f4(long x) {\n",
+ " System.out.print(\"f4(long)\");\n",
+ " }\n",
+ " void f4(float x) {\n",
+ " System.out.print(\"f4(float)\");\n",
+ " }\n",
+ " void f4(double x) {\n",
+ " System.out.print(\"f4(double)\");\n",
+ " }\n",
+ " void f5(long x) {\n",
+ " System.out.print(\"f5(long)\");\n",
+ " }\n",
+ " void f5(float x) {\n",
+ " System.out.print(\"f5(float)\");\n",
+ " }\n",
+ " void f5(double x) {\n",
+ " System.out.print(\"f5(double)\");\n",
+ " }\n",
+ " void f6(float x) {\n",
+ " System.out.print(\"f6(float)\");\n",
+ " }\n",
+ " void f6(double x) {\n",
+ " System.out.print(\"f6(double)\");\n",
+ " }\n",
+ " void f7(double x) {\n",
+ " System.out.print(\"f7(double)\");\n",
+ " }\n",
+ " void testConstVal() {\n",
+ " System.out.print(\"5: \");\n",
+ " f1(5);f2(5);f3(5);f4(5);f5(5);f6(5);f7(5);\n",
+ " System.out.println();\n",
+ " }\n",
+ " void testChar() {\n",
+ " char x = 'x';\n",
+ " System.out.print(\"char: \");\n",
+ " f1(x);f2(x);f3(x);f4(x);f5(x);f6(x);f7(x);\n",
+ " System.out.println();\n",
+ " }\n",
+ " void testByte() {\n",
+ " byte x = 0;\n",
+ " System.out.print(\"byte: \");\n",
+ " f1(x);f2(x);f3(x);f4(x);f5(x);f6(x);f7(x);\n",
+ " System.out.println();\n",
+ " }\n",
+ " void testShort() {\n",
+ " short x = 0;\n",
+ " System.out.print(\"short: \");\n",
+ " f1(x);f2(x);f3(x);f4(x);f5(x);f6(x);f7(x);\n",
+ " System.out.println();\n",
+ " }\n",
+ " void testInt() {\n",
+ " int x = 0;\n",
+ " System.out.print(\"int: \");\n",
+ " f1(x);f2(x);f3(x);f4(x);f5(x);f6(x);f7(x);\n",
+ " System.out.println();\n",
+ " }\n",
+ " void testLong() {\n",
+ " long x = 0;\n",
+ " System.out.print(\"long: \");\n",
+ " f1(x);f2(x);f3(x);f4(x);f5(x);f6(x);f7(x);\n",
+ " System.out.println();\n",
+ " }\n",
+ " void testFloat() {\n",
+ " float x = 0;\n",
+ " System.out.print(\"float: \");\n",
+ " f1(x);f2(x);f3(x);f4(x);f5(x);f6(x);f7(x);\n",
+ " System.out.println();\n",
+ " }\n",
+ " void testDouble() {\n",
+ " double x = 0;\n",
+ " System.out.print(\"double: \");\n",
+ " f1(x);f2(x);f3(x);f4(x);f5(x);f6(x);f7(x);\n",
+ " System.out.println();\n",
+ " }\n",
+ "\n",
+ " public static void main(String[] args) {\n",
+ " PrimitiveOverloading p = new PrimitiveOverloading();\n",
+ " p.testConstVal();\n",
+ " p.testChar();\n",
+ " p.testByte();\n",
+ " p.testShort();\n",
+ " p.testInt();\n",
+ " p.testLong();\n",
+ " p.testFloat();\n",
+ " p.testDouble();\n",
+ " }\n",
+ "}"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "输出:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "5: f1(int)f2(int)f3(int)f4(int)f5(long)f6(float)f7(double)\n",
+ "char: f1(char)f2(int)f3(int)f4(int)f5(long)f6(float)f7(double)\n",
+ "byte: f1(byte)f2(byte)f3(short)f4(int)f5(long)f6(float)f7(double)\n",
+ "short: f1(short)f2(short)f3(short)f4(int)f5(long)f6(float)f7(double)\n",
+ "int: f1(int)f2(int)f3(int)f4(int)f5(long)f6(float)f7(double)\n",
+ "long: f1(long)f2(long)f3(long)f4(long)f5(long)f6(float)f7(double)\n",
+ "float: f1(float)f2(float)f3(float)f4(float)f5(float)f6(float)f7(double)\n",
+ "double: f1(double)f2(double)f3(double)f4(double)f5(double)f6(double)f7(double)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "如果传入的参数类型大于方法期望接收的参数类型,你必须首先做下转换,如果你不做的话,编译器就会报错。\n",
+ "\n",
+ "### 返回值的重载\n",
+ "\n",
+ "经常会有人困惑,\"为什么只能通过类名和参数列表,不能通过方法的返回值区分方法呢?\"。例如以下两个方法,它们有相同的命名和参数,但是很容易区分:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "void f(){}\n",
+ "int f() {return 1;}"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "有些情况下,编译器很容易就可以从上下文准确推断出该调用哪个方法,如 `int x = f()`。\n",
+ "\n",
+ "但是,你可以调用一个方法且忽略返回值。这叫做调用一个函数的副作用,因为你不在乎返回值,只是想利用方法做些事。所以如果你直接调用 `f()`,Java 编译器就不知道你想调用哪个方法,阅读者也不明所以。因为这个原因,所以你不能根据返回值类型区分重载的方法。为了支持新特性,Java 8 在一些具体情形下提高了猜测的准确度,但是通常来说并不起作用。\n",
+ "\n",
+ "\n",
+ "\n",
+ "## 无参构造器\n",
+ "\n",
+ "如前文所说,一个无参构造器就是不接收参数的构造器,用来创建一个\"默认的对象\"。如果你创建一个类,类中没有构造器,那么编译器就会自动为你创建一个无参构造器。例如:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "// housekeeping/DefaultConstructor.java\n",
+ "class Bird {}\n",
+ "public class DefaultConstructor {\n",
+ " public static void main(String[] args) {\n",
+ " Bird bird = new Bird(); // 默认的\n",
+ " }\n",
+ "}"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "表达式 `new Bird()` 创建了一个新对象,调用了无参构造器,尽管在 **Bird** 类中并没有显式的定义无参构造器。试想如果没有构造器,我们如何创建一个对象呢。但是,一旦你显式地定义了构造器(无论有参还是无参),编译器就不会自动为你创建无参构造器。如下:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "// housekeeping/NoSynthesis.java\n",
+ "class Bird2 {\n",
+ " Bird2(int i) {}\n",
+ " Bird2(double d) {}\n",
+ "}\n",
+ "public class NoSynthesis {\n",
+ " public static void main(String[] args) {\n",
+ " //- Bird2 b = new Bird2(); // No default\n",
+ " Bird2 b2 = new Bird2(1);\n",
+ " Bird2 b3 = new Bird2(1.0);\n",
+ " }\n",
+ "}"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "如果你调用了 `new Bird2()` ,编译器会提示找不到匹配的构造器。当类中没有构造器时,编译器会说\"你一定需要构造器,那么让我为你创建一个吧\"。但是如果类中有构造器,编译器会说\"你已经写了构造器了,所以肯定知道你在做什么,如果你没有创建默认构造器,说明你本来就不需要\"。\n",
+ "\n",
+ "\n",
+ "\n",
+ "## this关键字\n",
+ "\n",
+ "对于两个相同类型的对象 **a** 和 **b**,你可能在想如何调用这两个对象的 `peel()` 方法:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "// housekeeping/BananaPeel.java\n",
+ "\n",
+ "class Banana {\n",
+ " void peel(int i) {\n",
+ " /*...*/\n",
+ " }\n",
+ "}\n",
+ "public class BananaPeel {\n",
+ " public static void main(String[] args) {\n",
+ " Banana a = new Banana(), b = new Banana();\n",
+ " a.peel(1);\n",
+ " b.peel(2);\n",
+ " }\n",
+ "}"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "如果只有一个方法 `peel()` ,那么怎么知道调用的是对象 **a** 的 `peel()`方法还是对象 **b** 的 `peel()` 方法呢?编译器做了一些底层工作,所以你可以像这样编写代码。`peel()` 方法中第一个参数隐密地传入了一个指向操作对象的\n",
+ "\n",
+ "引用。因此,上述例子中的方法调用像下面这样:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "Banana.peel(a, 1)\n",
+ "Banana.peel(b, 1)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "这是在内部实现的,你不可以直接这么编写代码,编译器不会接受,但能说明到底发生了什么。假设现在在方法内部,你想获得对当前对象的引用。但是,对象引用是被秘密地传达给编译器——并不在参数列表中。方便的是,有一个关键字: **this** 。**this** 关键字只能在非静态方法内部使用。当你调用一个对象的方法时,**this** 生成了一个对象引用。你可以像对待其他引用一样对待这个引用。如果你在一个类的方法里调用其他该类中的方法,不要使用 **this**,直接调用即可,**this** 自动地应用于其他方法上了。因此你可以像这样:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "// housekeeping/Apricot.java\n",
+ "\n",
+ "public class Apricot {\n",
+ " void pick() {\n",
+ " /* ... */\n",
+ " }\n",
+ "\n",
+ " void pit() {\n",
+ " pick();\n",
+ " /* ... */\n",
+ " }\n",
+ "}"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "在 `pit()` 方法中,你可以使用 `this.pick()`,但是没有必要。编译器自动为你做了这些。**this** 关键字只用在一些必须显式使用当前对象引用的特殊场合。例如,用在 **return** 语句中返回对当前对象的引用。"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "// housekeeping/Leaf.java\n",
+ "// Simple use of the \"this\" keyword\n",
+ "\n",
+ "public class Leaf {\n",
+ "\n",
+ " int i = 0;\n",
+ "\n",
+ " Leaf increment() {\n",
+ " i++;\n",
+ " return this;\n",
+ " }\n",
+ "\n",
+ " void print() {\n",
+ " System.out.println(\"i = \" + i);\n",
+ " }\n",
+ "\n",
+ " public static void main(String[] args) {\n",
+ " Leaf x = new Leaf();\n",
+ " x.increment().increment().increment().print();\n",
+ " }\n",
+ "}"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "输出:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "i = 3"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "因为 `increment()` 通过 **this** 关键字返回当前对象的引用,因此在相同的对象上可以轻易地执行多次操作。\n",
+ "\n",
+ "**this** 关键字在向其他方法传递当前对象时也很有用:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "// housekeeping/PassingThis.java\n",
+ "\n",
+ "class Person {\n",
+ " public void eat(Apple apple) {\n",
+ " Apple peeled = apple.getPeeled();\n",
+ " System.out.println(\"Yummy\");\n",
+ " }\n",
+ "}\n",
+ "\n",
+ "public class Peeler {\n",
+ " static Apple peel(Apple apple) {\n",
+ " // ... remove peel\n",
+ " return apple; // Peeled\n",
+ " }\n",
+ "}\n",
+ "\n",
+ "public class Apple {\n",
+ " Apple getPeeled() {\n",
+ " return Peeler.peel(this);\n",
+ " }\n",
+ "}\n",
+ "\n",
+ "public class PassingThis {\n",
+ " public static void main(String[] args) {\n",
+ " new Person().eat(new Apple());\n",
+ " }\n",
+ "}"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "输出:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "Yummy"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "**Apple** 因为某些原因(比如说工具类中的方法在多个类中重复出现,你不想代码重复),必须调用一个外部工具方法 `Peeler.peel()` 做一些行为。必须使用 **this** 才能将自身传递给外部方法。\n",
+ "\n",
+ "### 在构造器中调用构造器\n",
+ "\n",
+ "当你在一个类中写了多个构造器,有时你想在一个构造器中调用另一个构造器来避免代码重复。你通过 **this** 关键字实现这样的调用。\n",
+ "\n",
+ "通常当你说 **this**,意味着\"这个对象\"或\"当前对象\",它本身生成对当前对象的引用。在一个构造器中,当你给 **this** 一个参数列表时,它是另一层意思。它通过最直接的方式显式地调用匹配参数列表的构造器:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "// housekeeping/Flower.java\n",
+ "// Calling constructors with \"this\"\n",
+ "\n",
+ "public class Flower {\n",
+ " int petalCount = 0;\n",
+ " String s = \"initial value\";\n",
+ "\n",
+ " Flower(int petals) {\n",
+ " petalCount = petals;\n",
+ " System.out.println(\"Constructor w/ int arg only, petalCount = \" + petalCount);\n",
+ " }\n",
+ "\n",
+ " Flower(String ss) {\n",
+ " System.out.println(\"Constructor w/ string arg only, s = \" + ss);\n",
+ " s = ss;\n",
+ " }\n",
+ "\n",
+ " Flower(String s, int petals) {\n",
+ " this(petals);\n",
+ " //- this(s); // Can't call two!\n",
+ " this.s = s; // Another use of \"this\"\n",
+ " System.out.println(\"String & int args\");\n",
+ " }\n",
+ "\n",
+ " Flower() {\n",
+ " this(\"hi\", 47);\n",
+ " System.out.println(\"no-arg constructor\");\n",
+ " }\n",
+ "\n",
+ " void printPetalCount() {\n",
+ " //- this(11); // Not inside constructor!\n",
+ " System.out.println(\"petalCount = \" + petalCount + \" s = \" + s);\n",
+ " }\n",
+ "\n",
+ " public static void main(String[] args) {\n",
+ " Flower x = new Flower();\n",
+ " x.printPetalCount();\n",
+ " }\n",
+ "}"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "输出:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "Constructor w/ int arg only, petalCount = 47\n",
+ "String & int args\n",
+ "no-arg constructor\n",
+ "petalCount = 47 s = hi"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "从构造器 `Flower(String s, int petals)` 可以看出,其中只能通过 **this** 调用一次构造器。另外,必须首先调用构造器,否则编译器会报错。这个例子同样展示了 **this** 的另一个用法。参数列表中的变量名 **s** 和成员变量名 **s** 相同,会引起混淆。你可以通过 `this.s` 表明你指的是成员变量 **s**,从而避免重复。你经常会在 Java 代码中看到这种用法,同时本书中也会多次出现这种写法。在 `printPetalCount()` 方法中,编译器不允许你在一个构造器之外的方法里调用构造器。\n",
+ "\n",
+ "### static 的含义\n",
+ "\n",
+ "记住了 **this** 关键字的内容,你会对 **static** 修饰的方法有更加深入的理解:**static** 方法中不会存在 **this**。你不能在静态方法中调用非静态方法(反之可以)。静态方法是为类而创建的,不需要任何对象。事实上,这就是静态方法的主要目的,静态方法看起来就像全局方法一样,但是 Java 中不允许全局方法,一个类中的静态方法可以被其他的静态方法和静态属性访问。一些人认为静态方法不是面向对象的,因为它们的确具有全局方法的语义。使用静态方法,因为不存在 **this**,所以你没有向一个对象发送消息。的确,如果你发现代码中出现了大量的 **static** 方法,就该重新考虑自己的设计了。然而,**static** 的概念很实用,许多时候都要用到它。至于它是否真的\"面向对象\",就留给理论家去讨论吧。\n",
+ "\n",
+ "\n",
+ "\n",
+ "## 垃圾回收器\n",
+ "\n",
+ "程序员都了解初始化的重要性,但通常会忽略清理的重要性。毕竟,谁会去清理一个 **int** 呢?但是使用完一个对象就不管它并非总是安全的。Java 中有垃圾回收器回收无用对象占用的内存。但现在考虑一种特殊情况:你创建的对象不是通过 **new** 来分配内存的,而垃圾回收器只知道如何释放用 **new** 创建的对象的内存,所以它不知道如何回收不是 **new** 分配的内存。为了处理这种情况,Java 允许在类中定义一个名为 `finalize()` 的方法。\n",
+ "\n",
+ "它的工作原理\"假定\"是这样的:当垃圾回收器准备回收对象的内存时,首先会调用其 `finalize()` 方法,并在下一轮的垃圾回收动作发生时,才会真正回收对象占用的内存。所以如果你打算使用 `finalize()` ,就能在垃圾回收时做一些重要的清理工作。`finalize()` 是一个潜在的编程陷阱,因为一些程序员(尤其是 C++ 程序员)会一开始把它误认为是 C++ 中的析构函数(C++ 在销毁对象时会调用这个函数)。所以有必要明确区分一下:在 C++ 中,对象总是被销毁的(在一个 bug-free 的程序中),而在 Java 中,对象并非总是被垃圾回收,或者换句话说:\n",
+ "\n",
+ "1. 对象可能不被垃圾回收。\n",
+ "2. 垃圾回收不等同于析构。\n",
+ "\n",
+ "这意味着在你不再需要某个对象之前,如果必须执行某些动作,你得自己去做。Java 没有析构器或类似的概念,所以你必须得自己创建一个普通的方法完成这项清理工作。例如,对象在创建的过程中会将自己绘制到屏幕上。如果不是明确地从屏幕上将其擦除,它可能永远得不到清理。如果在 `finalize()` 方法中加入某种擦除功能,那么当垃圾回收发生时,`finalize()` 方法被调用(不保证一定会发生),图像就会被擦除,要是\"垃圾回收\"没有发生,图像则仍会保留下来。\n",
+ "\n",
+ "也许你会发现,只要程序没有濒临内存用完的那一刻,对象占用的空间就总也得不到释放。如果程序执行结束,而垃圾回收器一直没有释放你创建的任何对象的内存,则当程序退出时,那些资源会全部交还给操作系统。这个策略是恰当的,因为垃圾回收本身也有开销,要是不使用它,那就不用支付这部分开销了。\n",
+ "\n",
+ "### `finalize()` 的用途\n",
+ "\n",
+ "如果你不能将 `finalize()` 作为通用的清理方法,那么这个方法有什么用呢?\n",
+ "\n",
+ "这引入了要记住的第3点:\n",
+ "\n",
+ "3. 垃圾回收只与内存有关。\n",
+ "\n",
+ "也就是说,使用垃圾回收的唯一原因就是为了回收程序不再使用的内存。所以对于与垃圾回收有关的任何行为来说(尤其是 `finalize()` 方法),它们也必须同内存及其回收有关。\n",
+ "\n",
+ "但这是否意味着如果对象中包括其他对象,`finalize()` 方法就应该明确释放那些对象呢?不是,无论对象是如何创建的,垃圾回收器都会负责释放对象所占用的所有内存。这就将对 `finalize()` 的需求限制到一种特殊情况,即通过某种创建对象方式之外的方式为对象分配了存储空间。不过,你可能会想,Java 中万物皆对象,这种情况怎么可能发生?\n",
+ "\n",
+ "看起来之所以有 `finalize()` 方法,是因为在分配内存时可能采用了类似 C 语言中的做法,而非 Java 中的通常做法。这种情况主要发生在使用\"本地方法\"的情况下,本地方法是一种用 Java 语言调用非 Java 语言代码的形式(关于本地方法的讨论,见本书电子版第2版的附录B)。本地方法目前只支持 C 和 C++,但是它们可以调用其他语言写的代码,所以实际上可以调用任何代码。在非 Java 代码中,也许会调用 C 的 `malloc()` 函数系列来分配存储空间,而且除非调用 `free()` 函数,不然存储空间永远得不到释放,造成内存泄露。但是,`free()` 是 C 和 C++ 中的函数,所以你需要在 `finalize()` 方法里用本地方法调用它。\n",
+ "\n",
+ "读到这里,你可能明白了不会过多使用 `finalize()` 方法。对,它确实不是进行普通的清理工作的合适场所。那么,普通的清理工作在哪里执行呢?\n",
+ "\n",
+ "### 你必须实施清理\n",
+ "\n",
+ "要清理一个对象,用户必须在需要清理的时候调用执行清理动作的方法。这听上去相当直接,但却与 C++ 中的\"析构函数\"的概念稍有抵触。在 C++ 中,所有对象都会被销毁,或者说应该被销毁。如果在 C++ 中创建了一个局部对象(在栈上创建,在 Java 中不行),此时的销毁动作发生在以\"右花括号\"为边界的、此对象作用域的末尾处。如果对象是用 **new** 创建的(类似于 Java 中),那么当程序员调用 C++ 的 **delete** 操作符时(Java 中不存在),就会调用相应的析构函数。如果程序员忘记调用 **delete**,那么永远不会调用析构函数,这样就会导致内存泄露,对象的其他部分也不会得到清理。这种 bug 很难跟踪,也是让 C++ 程序员转向 Java 的一个主要因素。相反,在 Java 中,没有用于释放对象的 **delete**,因为垃圾回收器会帮助你释放存储空间。甚至可以肤浅地认为,正是由于垃圾回收的存在,使得 Java 没有析构函数。然而,随着学习的深入,你会明白垃圾回收器的存在并不能完全替代析构函数(而且绝对不能直接调用 `finalize()`,所以这也不是一种解决方案)。如果希望进行除释放存储空间之外的清理工作,还是得明确调用某个恰当的 Java 方法:这就等同于使用析构函数了,只是没有它方便。\n",
+ "\n",
+ "记住,无论是\"垃圾回收\"还是\"终结\",都不保证一定会发生。如果 Java 虚拟机(JVM)并未面临内存耗尽的情形,它可能不会浪费时间执行垃圾回收以恢复内存。\n",
+ "\n",
+ "### 终结条件\n",
+ "\n",
+ "通常,不能指望 `finalize()` ,你必须创建其他的\"清理\"方法,并明确地调用它们。所以看起来,`finalize()` 只对大部分程序员很难用到的一些晦涩内存清理里有用了。但是,`finalize()` 还有一个有趣的用法,它不依赖于每次都要对 `finalize()` 进行调用,这就是对象终结条件的验证。\n",
+ "\n",
+ "当对某个对象不感兴趣时——也就是它将被清理了,这个对象应该处于某种状态,这种状态下它占用的内存可以被安全地释放掉。例如,如果对象代表了一个打开的文件,在对象被垃圾回收之前程序员应该关闭这个文件。只要对象中存在没有被适当清理的部分,程序就存在很隐晦的 bug。`finalize()` 可以用来最终发现这个情况,尽管它并不总是被调用。如果某次 `finalize()` 的动作使得 bug 被发现,那么就可以据此找出问题所在——这才是人们真正关心的。以下是个简单的例子,示范了 `finalize()` 的可能使用方式:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "// housekeeping/TerminationCondition.java\n",
+ "// Using finalize() to detect a object that\n",
+ "// hasn't been properly cleaned up\n",
+ "\n",
+ "import onjava.*;\n",
+ "\n",
+ "class Book {\n",
+ " boolean checkedOut = false;\n",
+ "\n",
+ " Book(boolean checkOut) {\n",
+ " checkedOut = checkOut;\n",
+ " }\n",
+ "\n",
+ " void checkIn() {\n",
+ " checkedOut = false;\n",
+ " }\n",
+ "\n",
+ " @Override\n",
+ " protected void finalize() throws Throwable {\n",
+ " if (checkedOut) {\n",
+ " System.out.println(\"Error: checked out\");\n",
+ " }\n",
+ " // Normally, you'll also do this:\n",
+ " // super.finalize(); // Call the base-class version\n",
+ " }\n",
+ "}\n",
+ "\n",
+ "public class TerminationCondition {\n",
+ "\n",
+ " public static void main(String[] args) {\n",
+ " Book novel = new Book(true);\n",
+ " // Proper cleanup:\n",
+ " novel.checkIn();\n",
+ " // Drop the reference, forget to clean up:\n",
+ " new Book(true);\n",
+ " // Force garbage collection & finalization:\n",
+ " System.gc();\n",
+ " new Nap(1); // One second delay\n",
+ " }\n",
+ "\n",
+ "}"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "输出:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "Error: checked out"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "本例的终结条件是:所有的 **Book** 对象在被垃圾回收之前必须被登记。但在 `main()` 方法中,有一本书没有登记。要是没有 `finalize()` 方法来验证终结条件,将会很难发现这个 bug。\n",
+ "\n",
+ "你可能注意到使用了 `@Override`。`@` 意味着这是一个注解,注解是关于代码的额外信息。在这里,该注解告诉编译器这不是偶然地重定义在每个对象中都存在的 `finalize()` 方法——程序员知道自己在做什么。编译器确保你没有拼错方法名,而且确保那个方法存在于基类中。注解也是对读者的提醒,`@Override` 在 Java 5 引入,在 Java 7 中改善,本书通篇会出现。\n",
+ "\n",
+ "注意,`System.gc()` 用于强制进行终结动作。但是即使不这么做,只要重复地执行程序(假设程序将分配大量的存储空间而导致垃圾回收动作的执行),最终也能找出错误的 **Book** 对象。\n",
+ "\n",
+ "你应该总是假设基类版本的 `finalize()` 也要做一些重要的事情,使用 **super** 调用它,就像在 `Book.finalize()` 中看到的那样。本例中,它被注释掉了,因为它需要进行异常处理,而我们到现在还没有涉及到。\n",
+ "\n",
+ "### 垃圾回收器如何工作\n",
+ "\n",
+ "如果你以前用过的语言,在堆上分配对象的代价十分高昂,你可能自然会觉得 Java 中所有对象(基本类型除外)在堆上分配的方式也十分高昂。然而,垃圾回收器能很明显地提高对象的创建速度。这听起来很奇怪——存储空间的释放影响了存储空间的分配,但这确实是某些 Java 虚拟机的工作方式。这也意味着,Java 从堆空间分配的速度可以和其他语言在栈上分配空间的速度相媲美。\n",
+ "\n",
+ "例如,你可以把 C++ 里的堆想象成一个院子,里面每个对象都负责管理自己的地盘。一段时间后,对象可能被销毁,但地盘必须复用。在某些 Java 虚拟机中,堆的实现截然不同:它更像一个传送带,每分配一个新对象,它就向前移动一格。这意味着对象存储空间的分配速度特别快。Java 的\"堆指针\"只是简单地移动到尚未分配的区域,所以它的效率与 C++ 在栈上分配空间的效率相当。当然实际过程中,在簿记工作方面还有少量额外开销,但是这部分开销比不上查找可用空间开销大。\n",
+ "\n",
+ "你可能意识到了,Java 中的堆并非完全像传送带那样工作。要是那样的话,势必会导致频繁的内存页面调度——将其移进移出硬盘,因此会显得需要拥有比实际需要更多的内存。页面调度会显著影响性能。最终,在创建了足够多的对象后,内存资源被耗尽。其中的秘密在于垃圾回收器的介入。当它工作时,一边回收内存,一边使堆中的对象紧凑排列,这样\"堆指针\"就可以很容易地移动到更靠近传送带的开始处,也就尽量避免了页面错误。垃圾回收器通过重新排列对象,实现了一种高速的、有无限空间可分配的堆模型。\n",
+ "\n",
+ "要想理解 Java 中的垃圾回收,先了解其他系统中的垃圾回收机制将会很有帮助。一种简单但速度很慢的垃圾回收机制叫做*引用计数*。每个对象中含有一个引用计数器,每当有引用指向该对象时,引用计数加 1。当引用离开作用域或被置为 **null** 时,引用计数减 1。因此,管理引用计数是一个开销不大但是在程序的整个生命周期频繁发生的负担。垃圾回收器会遍历含有全部对象的列表,当发现某个对象的引用计数为 0 时,就释放其占用的空间(但是,引用计数模式经常会在计数为 0 时立即释放对象)。这个机制存在一个缺点:如果对象之间存在循环引用,那么它们的引用计数都不为 0,就会出现应该被回收但无法被回收的情况。对垃圾回收器而言,定位这样的循环引用所需的工作量极大。引用计数常用来说明垃圾回收的工作方式,但似乎从未被应用于任何一种 Java 虚拟机实现中。\n",
+ "\n",
+ "在更快的策略中,垃圾回收器并非基于引用计数。它们依据的是:对于任意\"活\"的对象,一定能最终追溯到其存活在栈或静态存储区中的引用。这个引用链条可能会穿过数个对象层次,由此,如果从栈或静态存储区出发,遍历所有的引用,你将会发现所有\"活\"的对象。对于发现的每个引用,必须追踪它所引用的对象,然后是该对象包含的所有引用,如此反复进行,直到访问完\"根源于栈或静态存储区的引用\"所形成的整个网络。你所访问过的对象一定是\"活\"的。注意,这解决了对象间循环引用的问题,这些对象不会被发现,因此也就被自动回收了。\n",
+ "\n",
+ "在这种方式下,Java 虚拟机采用了一种*自适应*的垃圾回收技术。至于如何处理找到的存活对象,取决于不同的 Java 虚拟机实现。其中有一种做法叫做停止-复制(stop-and-copy)。顾名思义,这需要先暂停程序的运行(不属于后台回收模式),然后将所有存活的对象从当前堆复制到另一个堆,没有复制的就是需要被垃圾回收的。另外,当对象被复制到新堆时,它们是一个挨着一个紧凑排列,然后就可以按照前面描述的那样简单、直接地分配新空间了。\n",
+ "\n",
+ "当对象从一处复制到另一处,所有指向它的引用都必须修正。位于栈或静态存储区的引用可以直接被修正,但可能还有其他指向这些对象的引用,它们在遍历的过程中才能被找到(可以想象成一个表格,将旧地址映射到新地址)。\n",
+ "\n",
+ "这种所谓的\"复制回收器\"效率低下主要因为两个原因。其一:得有两个堆,然后在这两个分离的堆之间来回折腾,得维护比实际需要多一倍的空间。某些 Java 虚拟机对此问题的处理方式是,按需从堆中分配几块较大的内存,复制动作发生在这些大块内存之间。\n",
+ "\n",
+ "其二在于复制本身。一旦程序进入稳定状态之后,可能只会产生少量垃圾,甚至没有垃圾。尽管如此,复制回收器仍然会将所有内存从一处复制到另一处,这很浪费。为了避免这种状况,一些 Java 虚拟机会进行检查:要是没有新垃圾产生,就会转换到另一种模式(即\"自适应\")。这种模式称为标记-清扫(mark-and-sweep),Sun 公司早期版本的 Java 虚拟机一直使用这种技术。对一般用途而言,\"标记-清扫\"方式速度相当慢,但是当你知道程序只会产生少量垃圾甚至不产生垃圾时,它的速度就很快了。\n",
+ "\n",
+ "\"标记-清扫\"所依据的思路仍然是从栈和静态存储区出发,遍历所有的引用,找出所有存活的对象。但是,每当找到一个存活对象,就给对象设一个标记,并不回收它。只有当标记过程完成后,清理动作才开始。在清理过程中,没有标记的对象将被释放,不会发生任何复制动作。\"标记-清扫\"后剩下的堆空间是不连续的,垃圾回收器要是希望得到连续空间的话,就需要重新整理剩下的对象。\n",
+ "\n",
+ "\"停止-复制\"指的是这种垃圾回收动作不是在后台进行的;相反,垃圾回收动作发生的同时,程序将会暂停。在 Oracle 公司的文档中会发现,许多参考文献将垃圾回收视为低优先级的后台进程,但是早期版本的 Java 虚拟机并不是这么实现垃圾回收器的。当可用内存较低时,垃圾回收器会暂停程序。同样,\"标记-清扫\"工作也必须在程序暂停的情况下才能进行。\n",
+ "\n",
+ "如前文所述,这里讨论的 Java 虚拟机中,内存分配以较大的\"块\"为单位。如果对象较大,它会占用单独的块。严格来说,\"停止-复制\"要求在释放旧对象之前,必须先将所有存活对象从旧堆复制到新堆,这导致了大量的内存复制行为。有了块,垃圾回收器就可以把对象复制到废弃的块。每个块都有年代数来记录自己是否存活。通常,如果块在某处被引用,其年代数加 1,垃圾回收器会对上次回收动作之后新分配的块进行整理。这对处理大量短命的临时对象很有帮助。垃圾回收器会定期进行完整的清理动作——大型对象仍然不会复制(只是年代数会增加),含有小型对象的那些块则被复制并整理。Java 虚拟机会监视,如果所有对象都很稳定,垃圾回收的效率降低的话,就切换到\"标记-清扫\"方式。同样,Java 虚拟机会跟踪\"标记-清扫\"的效果,如果堆空间出现很多碎片,就会切换回\"停止-复制\"方式。这就是\"自适应\"的由来,你可以给它个啰嗦的称呼:\"自适应的、分代的、停止-复制、标记-清扫\"式的垃圾回收器。\n",
+ "\n",
+ "Java 虚拟机中有许多附加技术用来提升速度。尤其是与加载器操作有关的,被称为\"即时\"(Just-In-Time, JIT)编译器的技术。这种技术可以把程序全部或部分翻译成本地机器码,所以不需要 JVM 来进行翻译,因此运行得更快。当需要装载某个类(通常是创建该类的第一个对象)时,编译器会先找到其 **.class** 文件,然后将该类的字节码装入内存。你可以让即时编译器编译所有代码,但这种做法有两个缺点:一是这种加载动作贯穿整个程序生命周期内,累加起来需要花更多时间;二是会增加可执行代码的长度(字节码要比即时编译器展开后的本地机器码小很多),这会导致页面调度,从而一定降低程序速度。另一种做法称为*惰性评估*,意味着即时编译器只有在必要的时候才编译代码。这样,从未被执行的代码也许就压根不会被 JIT 编译。新版 JDK 中的 Java HotSpot 技术就采用了类似的做法,代码每被执行一次就优化一些,所以执行的次数越多,它的速度就越快。\n",
+ "\n",
+ "\n",
+ "\n",
+ "## 成员初始化\n",
+ "\n",
+ "Java 尽量保证所有变量在使用前都能得到恰当的初始化。对于方法的局部变量,这种保证会以编译时错误的方式呈现,所以如果写成:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "void f() {\n",
+ " int i;\n",
+ " i++;\n",
+ "}"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "你会得到一条错误信息,告诉你 **i** 可能尚未初始化。编译器可以为 **i** 赋一个默认值,但是未初始化的局部变量更有可能是程序员的疏忽,所以采用默认值反而会掩盖这种失误。强制程序员提供一个初始值,往往能帮助找出程序里的 bug。\n",
+ "\n",
+ "要是类的成员变量是基本类型,情况就会变得有些不同。正如在\"万物皆对象\"一章中所看到的,类的每个基本类型数据成员保证都会有一个初始值。下面的程序可以验证这类情况,并显示它们的值:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "// housekeeping/InitialValues.java\n",
+ "// Shows default initial values\n",
+ "\n",
+ "public class InitialValues {\n",
+ " boolean t;\n",
+ " char c;\n",
+ " byte b;\n",
+ " short s;\n",
+ " int i;\n",
+ " long l;\n",
+ " float f;\n",
+ " double d;\n",
+ " InitialValues reference;\n",
+ "\n",
+ " void printInitialValues() {\n",
+ " System.out.println(\"Data type Initial value\");\n",
+ " System.out.println(\"boolean \" + t);\n",
+ " System.out.println(\"char[\" + c + \"]\");\n",
+ " System.out.println(\"byte \" + b);\n",
+ " System.out.println(\"short \" + s);\n",
+ " System.out.println(\"int \" + i);\n",
+ " System.out.println(\"long \" + l);\n",
+ " System.out.println(\"float \" + f);\n",
+ " System.out.println(\"double \" + d);\n",
+ " System.out.println(\"reference \" + reference);\n",
+ " }\n",
+ "\n",
+ " public static void main(String[] args) {\n",
+ " new InitialValues().printInitialValues();\n",
+ " }\n",
+ "}"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "输出:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "Java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "Data type Initial value\n",
+ "boolean false\n",
+ "char[NUL]\n",
+ "byte 0\n",
+ "short 0\n",
+ "int 0\n",
+ "long 0\n",
+ "float 0.0\n",
+ "double 0.0\n",
+ "reference null"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "可见尽管数据成员的初值没有给出,但它们确实有初值(char 值为 0,所以显示为空白)。所以这样至少不会出现\"未初始化变量\"的风险了。\n",
+ "\n",
+ "在类里定义一个对象引用时,如果不将其初始化,那么引用就会被赋值为 **null**。\n",
+ "\n",
+ "### 指定初始化\n",
+ "\n",
+ "怎么给一个变量赋初值呢?一种很直接的方法是在定义类成员变量的地方为其赋值。以下代码修改了 InitialValues 类成员变量的定义,直接提供了初值:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "// housekeeping/InitialValues2.java\n",
+ "// Providing explicit initial values\n",
+ "\n",
+ "public class InitialValues2 {\n",
+ " boolean bool = true;\n",
+ " char ch = 'x';\n",
+ " byte b = 47;\n",
+ " short s = 0xff;\n",
+ " int i = 999;\n",
+ " long lng = 1;\n",
+ " float f = 3.14f;\n",
+ " double d = 3.14159;\n",
+ "}"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "你也可以用同样的方式初始化非基本类型的对象。如果 **Depth** 是一个类,那么可以像下面这样创建一个对象并初始化它:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "// housekeeping/Measurement.java\n",
+ "\n",
+ "class Depth {}\n",
+ "\n",
+ "public class Measurement {\n",
+ " Depth d = new Depth();\n",
+ " // ...\n",
+ "}"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "如果没有为 **d** 赋予初值就尝试使用它,就会出现运行时错误,告诉你产生了一个异常(详细见\"异常\"章节)。\n",
+ "\n",
+ "你也可以通过调用某个方法来提供初值:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "// housekeeping/MethodInit.java\n",
+ "\n",
+ "public class MethodInit {\n",
+ " int i = f();\n",
+ " \n",
+ " int f() {\n",
+ " return 11;\n",
+ " }\n",
+ " \n",
+ "}"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "这个方法可以带有参数,但这些参数不能是未初始化的类成员变量。因此,可以这么写:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "// housekeeping/MethodInit2.java\n",
+ "\n",
+ "public class MethodInit2 {\n",
+ " int i = f();\n",
+ " int j = g(i);\n",
+ " \n",
+ " int f() {\n",
+ " return 11;\n",
+ " }\n",
+ " \n",
+ " int g(int n) {\n",
+ " return n * 10;\n",
+ " }\n",
+ "}"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "但是你不能这么写:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "// housekeeping/MethodInit3.java\n",
+ "\n",
+ "public class MethodInit3 {\n",
+ " //- int j = g(i); // Illegal forward reference\n",
+ " int i = f();\n",
+ "\n",
+ " int f() {\n",
+ " return 11;\n",
+ " }\n",
+ "\n",
+ " int g(int n) {\n",
+ " return n * 10;\n",
+ " }\n",
+ "}"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "显然,上述程序的正确性取决于初始化的顺序,而与其编译方式无关。所以,编译器恰当地对\"向前引用\"发出了警告。\n",
+ "\n",
+ "这种初始化方式简单直观,但有个限制:类 **InitialValues** 的每个对象都有相同的初值,有时这的确是我们需要的,但有时却需要更大的灵活性。\n",
+ "\n",
+ "\n",
+ "\n",
+ "## 构造器初始化\n",
+ "\n",
+ "可以用构造器进行初始化,这种方式给了你更大的灵活性,因为你可以在运行时调用方法进行初始化。但是,这无法阻止自动初始化的进行,他会在构造器被调用之前发生。因此,如果使用如下代码:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "// housekeeping/Counter.java\n",
+ "\n",
+ "public class Counter {\n",
+ " int i;\n",
+ " \n",
+ " Counter() {\n",
+ " i = 7;\n",
+ " }\n",
+ " // ...\n",
+ "}"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "**i** 首先会被初始化为 **0**,然后变为 **7**。对于所有的基本类型和引用,包括在定义时已明确指定初值的变量,这种情况都是成立的。因此,编译器不会强制你一定要在构造器的某个地方或在使用它们之前初始化元素——初始化早已得到了保证。, \n",
+ "\n",
+ "### 初始化的顺序\n",
+ "\n",
+ "在类中变量定义的顺序决定了它们初始化的顺序。即使变量定义散布在方法定义之间,它们仍会在任何方法(包括构造器)被调用之前得到初始化。例如:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "// housekeeping/OrderOfInitialization.java\n",
+ "// Demonstrates initialization order\n",
+ "// When the constructor is called to create a\n",
+ "// Window object, you'll see a message:\n",
+ "\n",
+ "class Window {\n",
+ " Window(int marker) {\n",
+ " System.out.println(\"Window(\" + marker + \")\");\n",
+ " }\n",
+ "}\n",
+ "\n",
+ "class House {\n",
+ " Window w1 = new Window(1); // Before constructor\n",
+ "\n",
+ " House() {\n",
+ " // Show that we're in the constructor:\n",
+ " System.out.println(\"House()\");\n",
+ " w3 = new Window(33); // Reinitialize w3\n",
+ " }\n",
+ "\n",
+ " Window w2 = new Window(2); // After constructor\n",
+ "\n",
+ " void f() {\n",
+ " System.out.println(\"f()\");\n",
+ " }\n",
+ "\n",
+ " Window w3 = new Window(3); // At end\n",
+ "}\n",
+ "\n",
+ "public class OrderOfInitialization {\n",
+ " public static void main(String[] args) {\n",
+ " House h = new House();\n",
+ " h.f(); // Shows that construction is done\n",
+ " }\n",
+ "}"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "输出:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "Window(1)\n",
+ "Window(2)\n",
+ "Window(3)\n",
+ "House()\n",
+ "Window(33)\n",
+ "f()"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "在 **House** 类中,故意把几个 **Window** 对象的定义散布在各处,以证明它们全都会在调用构造器或其他方法之前得到初始化。此外,**w3** 在构造器中被再次赋值。\n",
+ "\n",
+ "由输出可见,引用 **w3** 被初始化了两次:一次在调用构造器前,一次在构造器调用期间(第一次引用的对象将被丢弃,并作为垃圾回收)。这乍一看可能觉得效率不高,但保证了正确的初始化。试想,如果定义了一个重载构造器,在其中没有初始化 **w3**,同时在定义 **w3** 时没有赋予初值,那会产生怎样的后果呢?\n",
+ "\n",
+ "### 静态数据的初始化\n",
+ "\n",
+ "无论创建多少个对象,静态数据都只占用一份存储区域。**static** 关键字不能应用于局部变量,所以只能作用于属性(字段、域)。如果一个字段是静态的基本类型,你没有初始化它,那么它就会获得基本类型的标准初值。如果它是对象引用,那么它的默认初值就是 **null**。\n",
+ "\n",
+ "如果在定义时进行初始化,那么静态变量看起来就跟非静态变量一样。\n",
+ "\n",
+ "下面例子显示了静态存储区是何时初始化的:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "// housekeeping/StaticInitialization.java\n",
+ "// Specifying initial values in a class definition\n",
+ "\n",
+ "class Bowl {\n",
+ " Bowl(int marker) {\n",
+ " System.out.println(\"Bowl(\" + marker + \")\");\n",
+ " }\n",
+ " \n",
+ " void f1(int marker) {\n",
+ " System.out.println(\"f1(\" + marker + \")\");\n",
+ " }\n",
+ "}\n",
+ "\n",
+ "class Table {\n",
+ " static Bowl bowl1 = new Bowl(1);\n",
+ " \n",
+ " Table() {\n",
+ " System.out.println(\"Table()\");\n",
+ " bowl2.f1(1);\n",
+ " }\n",
+ " \n",
+ " void f2(int marker) {\n",
+ " System.out.println(\"f2(\" + marker + \")\");\n",
+ " }\n",
+ " \n",
+ " static Bowl bowl2 = new Bowl(2);\n",
+ "}\n",
+ "\n",
+ "class Cupboard {\n",
+ " Bowl bowl3 = new Bowl(3);\n",
+ " static Bowl bowl4 = new Bowl(4);\n",
+ " \n",
+ " Cupboard() {\n",
+ " System.out.println(\"Cupboard()\");\n",
+ " bowl4.f1(2);\n",
+ " }\n",
+ " \n",
+ " void f3(int marker) {\n",
+ " System.out.println(\"f3(\" + marker + \")\");\n",
+ " }\n",
+ " \n",
+ " static Bowl bowl5 = new Bowl(5);\n",
+ "}\n",
+ "\n",
+ "public class StaticInitialization {\n",
+ " public static void main(String[] args) {\n",
+ " System.out.println(\"main creating new Cupboard()\");\n",
+ " new Cupboard();\n",
+ " System.out.println(\"main creating new Cupboard()\");\n",
+ " new Cupboard();\n",
+ " table.f2(1);\n",
+ " cupboard.f3(1);\n",
+ " }\n",
+ " \n",
+ " static Table table = new Table();\n",
+ " static Cupboard cupboard = new Cupboard();\n",
+ "}"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "输出:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "Bowl(1)\n",
+ "Bowl(2)\n",
+ "Table()\n",
+ "f1(1)\n",
+ "Bowl(4)\n",
+ "Bowl(5)\n",
+ "Bowl(3)\n",
+ "Cupboard()\n",
+ "f1(2)\n",
+ "main creating new Cupboard()\n",
+ "Bowl(3)\n",
+ "Cupboard()\n",
+ "f1(2)\n",
+ "main creating new Cupboard()\n",
+ "Bowl(3)\n",
+ "Cupboard()\n",
+ "f1(2)\n",
+ "f2(1)\n",
+ "f3(1)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "**Bowl** 类展示类的创建,而 **Table** 和 **Cupboard** 在它们的类定义中包含 **Bowl** 类型的静态数据成员。注意,在静态数据成员定义之前,**Cupboard** 类中先定义了一个 **Bowl** 类型的非静态成员 **b3**。\n",
+ "\n",
+ "由输出可见,静态初始化只有在必要时刻才会进行。如果不创建 **Table** 对象,也不引用 **Table.bowl1** 或 **Table.bowl2**,那么静态的 **Bowl** 类对象 **bowl1** 和 **bowl2** 永远不会被创建。只有在第一个 Table 对象被创建(或被访问)时,它们才会被初始化。此后,静态对象不会再次被初始化。\n",
+ "\n",
+ "初始化的顺序先是静态对象(如果它们之前没有被初始化的话),然后是非静态对象,从输出中可以看出。要执行 `main()` 方法,必须加载 **StaticInitialization** 类,它的静态属性 **table** 和 **cupboard** 随后被初始化,这会导致它们对应的类也被加载,而由于它们都包含静态的 **Bowl** 对象,所以 **Bowl** 类也会被加载。因此,在这个特殊的程序中,所有的类都会在 `main()` 方法之前被加载。实际情况通常并非如此,因为在典型的程序中,不会像本例中所示的那样,将所有事物通过 **static** 联系起来。\n",
+ "\n",
+ "概括一下创建对象的过程,假设有个名为 **Dog** 的类:\n",
+ "\n",
+ "1. 即使没有显式地使用 **static** 关键字,构造器实际上也是静态方法。所以,当首次创建 **Dog** 类型的对象或是首次访问 **Dog** 类的静态方法或属性时,Java 解释器必须在类路径中查找,以定位 **Dog.class**。\n",
+ "2. 当加载完 **Dog.class** 后(后面会学到,这将创建一个 **Class** 对象),有关静态初始化的所有动作都会执行。因此,静态初始化只会在首次加载 **Class** 对象时初始化一次。\n",
+ "3. 当用 `new Dog()` 创建对象时,首先会在堆上为 **Dog** 对象分配足够的存储空间。\n",
+ "4. 分配的存储空间首先会被清零,即会将 **Dog** 对象中的所有基本类型数据设置为默认值(数字会被置为 0,布尔型和字符型也相同),引用被置为 **null**。\n",
+ "5. 执行所有出现在字段定义处的初始化动作。\n",
+ "6. 执行构造器。你将会在\"复用\"这一章看到,这可能会牵涉到很多动作,尤其当涉及继承的时候。\n",
+ "\n",
+ "### 显式的静态初始化\n",
+ "\n",
+ "你可以将一组静态初始化动作放在类里面一个特殊的\"静态子句\"(有时叫做静态块)中。像下面这样:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "// housekeeping/Spoon.java\n",
+ "\n",
+ "public class Spoon {\n",
+ " static int i;\n",
+ " \n",
+ " static {\n",
+ " i = 47;\n",
+ " }\n",
+ "}"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "这看起来像个方法,但实际上它只是一段跟在 **static** 关键字后面的代码块。与其他静态初始化动作一样,这段代码仅执行一次:当首次创建这个类的对象或首次访问这个类的静态成员(甚至不需要创建该类的对象)时。例如:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "// housekeeping/ExplicitStatic.java\n",
+ "// Explicit static initialization with \"static\" clause\n",
+ "\n",
+ "class Cup {\n",
+ " Cup(int marker) {\n",
+ " System.out.println(\"Cup(\" + marker + \")\");\n",
+ " }\n",
+ " \n",
+ " void f(int marker) {\n",
+ " System.out.println(\"f(\" + marker + \")\");\n",
+ " }\n",
+ "}\n",
+ "\n",
+ "class Cups {\n",
+ " static Cup cup1;\n",
+ " static Cup cup2;\n",
+ " \n",
+ " static {\n",
+ " cup1 = new Cup(1);\n",
+ " cup2 = new Cup(2);\n",
+ " }\n",
+ " \n",
+ " Cups() {\n",
+ " System.out.println(\"Cups()\");\n",
+ " }\n",
+ "}\n",
+ "\n",
+ "public class ExplicitStatic {\n",
+ " public static void main(String[] args) {\n",
+ " System.out.println(\"Inside main()\");\n",
+ " Cups.cup1.f(99); // [1]\n",
+ " }\n",
+ " \n",
+ " // static Cups cups1 = new Cups(); // [2]\n",
+ " // static Cups cups2 = new Cups(); // [2]\n",
+ "}"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "输出:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "Inside main\n",
+ "Cup(1)\n",
+ "Cup(2)\n",
+ "f(99)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "无论是通过标为 [1] 的行访问静态的 **cup1** 对象,还是把标为 [1] 的行去掉,让它去运行标为 [2] 的那行代码(去掉 [2] 的注释),**Cups** 的静态初始化动作都会执行。如果同时注释 [1] 和 [2] 处,那么 **Cups** 的静态初始化就不会进行。此外,把标为 [2] 处的注释都去掉还是只去掉一个,静态初始化只会执行一次。\n",
+ "\n",
+ "### 非静态实例初始化\n",
+ "\n",
+ "Java 提供了被称为*实例初始化*的类似语法,用来初始化每个对象的非静态变量,例如:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "// housekeeping/Mugs.java\n",
+ "// Instance initialization\n",
+ "\n",
+ "class Mug {\n",
+ " Mug(int marker) {\n",
+ " System.out.println(\"Mug(\" + marker + \")\");\n",
+ " }\n",
+ "}\n",
+ "\n",
+ "public class Mugs {\n",
+ " Mug mug1;\n",
+ " Mug mug2;\n",
+ " { // [1]\n",
+ " mug1 = new Mug(1);\n",
+ " mug2 = new Mug(2);\n",
+ " System.out.println(\"mug1 & mug2 initialized\");\n",
+ " }\n",
+ " \n",
+ " Mugs() {\n",
+ " System.out.println(\"Mugs()\");\n",
+ " }\n",
+ " \n",
+ " Mugs(int i) {\n",
+ " System.out.println(\"Mugs(int)\");\n",
+ " }\n",
+ " \n",
+ " public static void main(String[] args) {\n",
+ " System.out.println(\"Inside main()\");\n",
+ " new Mugs();\n",
+ " System.out.println(\"new Mugs() completed\");\n",
+ " new Mugs(1);\n",
+ " System.out.println(\"new Mugs(1) completed\");\n",
+ " }\n",
+ "}"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "输出:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "Inside main\n",
+ "Mug(1)\n",
+ "Mug(2)\n",
+ "mug1 & mug2 initialized\n",
+ "Mugs()\n",
+ "new Mugs() completed\n",
+ "Mug(1)\n",
+ "Mug(2)\n",
+ "mug1 & mug2 initialized\n",
+ "Mugs(int)\n",
+ "new Mugs(1) completed"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "看起来它很像静态代码块,只不过少了 **static** 关键字。这种语法对于支持\"匿名内部类\"(参见\"内部类\"一章)的初始化是必须的,但是你也可以使用它保证某些操作一定会发生,而不管哪个构造器被调用。从输出看出,实例初始化子句是在两个构造器之前执行的。\n",
+ "\n",
+ "\n",
+ "\n",
+ "## 数组初始化\n",
+ "\n",
+ "数组是相同类型的、用一个标识符名称封装到一起的一个对象序列或基本类型数据序列。数组是通过方括号下标操作符 [] 来定义和使用的。要定义一个数组引用,只需要在类型名加上方括号:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "int[] a1;"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "方括号也可放在标识符的后面,两者的含义是一样的:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "int a1[];"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "这种格式符合 C 和 C++ 程序员的习惯。不过前一种格式或许更合理,毕竟它表明类型是\"一个 **int** 型数组\"。本书中采用这种格式。\n",
+ "\n",
+ "编译器不允许指定数组的大小。这又把我们带回有关\"引用\"的问题上。你所拥有的只是对数组的一个引用(你已经为该引用分配了足够的存储空间),但是还没有给数组对象本身分配任何空间。为了给数组创建相应的存储空间,必须写初始化表达式。对于数组,初始化动作可以出现在代码的任何地方,但是也可以使用一种特殊的初始化表达式,它必须在创建数组的地方出现。这种特殊的初始化是由一对花括号括起来的值组成。这种情况下,存储空间的分配(相当于使用 **new**) 将由编译器负责。例如:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "int[] a1 = {1, 2, 3, 4, 5};"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "那么为什么在还没有数组的时候定义一个数组引用呢?"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "int[] a2;"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "在 Java 中可以将一个数组赋值给另一个数组,所以可以这样:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "a2 = a1;"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "其实真正做的只是复制了一个引用,就像下面演示的这样:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "// housekeeping/ArraysOfPrimitives.java\n",
+ "\n",
+ "public class ArraysOfPrimitives {\n",
+ " public static void main(String[] args) {\n",
+ " int[] a1 = {1, 2, 3, 4, 5};\n",
+ " int[] a2;\n",
+ " a2 = a1;\n",
+ " for (int i = 0; i < a2.length; i++) {\n",
+ " a2[i] += 1;\n",
+ " }\n",
+ " for (int i = 0; i < a1.length; i++) {\n",
+ " System.out.println(\"a1[\" + i + \"] = \" + a1[i]);\n",
+ " }\n",
+ " }\n",
+ "}"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "输出:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "a1[0] = 2;\n",
+ "a1[1] = 3;\n",
+ "a1[2] = 4;\n",
+ "a1[3] = 5;\n",
+ "a1[4] = 6;"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "**a1** 初始化了,但是 **a2** 没有;这里,**a2** 在后面被赋给另一个数组。由于 **a1** 和 **a2** 是相同数组的别名,因此通过 **a2** 所做的修改在 **a1** 中也能看到。\n",
+ "\n",
+ "所有的数组(无论是对象数组还是基本类型数组)都有一个固定成员 **length**,告诉你这个数组有多少个元素,你不能对其修改。与 C 和 C++ 类似,Java 数组计数也是从 0 开始的,所能使用的最大下标数是 **length - 1**。超过这个边界,C 和 C++ 会默认接受,允许你访问所有内存,许多声名狼藉的 bug 都是由此而生。但是 Java 在你访问超出这个边界时,会报运行时错误(异常),从而避免此类问题。\n",
+ "\n",
+ "### 动态数组创建\n",
+ "\n",
+ "如果在编写程序时,不确定数组中需要多少个元素,那么该怎么办呢?你可以直接使用 **new** 在数组中创建元素。下面例子中,尽管创建的是基本类型数组,**new** 仍然可以工作(不能用 **new** 创建单个的基本类型数组):"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "// housekeeping/ArrayNew.java\n",
+ "// Creating arrays with new\n",
+ "import java.util.*;\n",
+ "\n",
+ "public class ArrayNew {\n",
+ " public static void main(String[] args) {\n",
+ " int[] a;\n",
+ " Random rand = new Random(47);\n",
+ " a = new int[rand.nextInt(20)];\n",
+ " System.out.println(\"length of a = \" + a.length);\n",
+ " System.out.println(Arrays.toString(a));\n",
+ " } \n",
+ "}"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "输出:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "length of a = 18\n",
+ "[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "数组的大小是通过 `Random.nextInt()` 随机确定的,这个方法会返回 0 到输入参数之间的一个值。 由于随机性,很明显数组的创建确实是在运行时进行的。此外,程序输出表明,数组元素中的基本数据类型值会自动初始化为空值(对于数字和字符是 0;对于布尔型是 **false**)。`Arrays.toString()` 是 **java.util** 标准类库中的方法,会产生一维数组的可打印版本。\n",
+ "\n",
+ "本例中,数组也可以在定义的同时进行初始化:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "int[] a = new int[rand.nextInt(20)];"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "如果可能的话,应该尽量这么做。\n",
+ "\n",
+ "如果你创建了一个非基本类型的数组,那么你创建的是一个引用数组。以整型的包装类型 **Integer** 为例,它是一个类而非基本类型:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "// housekeeping/ArrayClassObj.java\n",
+ "// Creating an array of nonprimitive objects\n",
+ "\n",
+ "import java.util.*;\n",
+ "\n",
+ "public class ArrayClassObj {\n",
+ " public static void main(String[] args) {\n",
+ " Random rand = new Random(47);\n",
+ " Integer[] a = new Integer[rand.nextInt(20)];\n",
+ " System.out.println(\"length of a = \" + a.length);\n",
+ " for (int i = 0; i < a.length; i++) {\n",
+ " a[i] = rand.nextInt(500); // Autoboxing\n",
+ " }\n",
+ " System.out.println(Arrays.toString(a));\n",
+ " }\n",
+ "}"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "输出:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "length of a = 18\n",
+ "[55, 193, 361, 461, 429, 368, 200, 22, 207, 288, 128, 51, 89, 309, 278, 498, 361, 20]"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "这里,即使使用 new 创建数组之后:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "Integer[] a = new Integer[rand.nextInt(20)];\t"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "它只是一个引用数组,直到通过创建新的 **Integer** 对象(通过自动装箱),并把对象赋值给引用,初始化才算结束:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "a[i] = rand.nextInt(500);"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "如果忘记了创建对象,但试图使用数组中的空引用,就会在运行时产生异常。\n",
+ "\n",
+ "也可以用花括号括起来的列表来初始化数组,有两种形式:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "// housekeeping/ArrayInit.java\n",
+ "// Array initialization\n",
+ "import java.util.*;\n",
+ "\n",
+ "public class ArrayInit {\n",
+ " public static void main(String[] args) {\n",
+ " Integer[] a = {\n",
+ " 1, 2,\n",
+ " 3, // Autoboxing\n",
+ " };\n",
+ " Integer[] b = new Integer[] {\n",
+ " 1, 2,\n",
+ " 3, // Autoboxing\n",
+ " };\n",
+ " System.out.println(Arrays.toString(a));\n",
+ " System.out.println(Arrays.toString(b));\n",
+ "\n",
+ " }\n",
+ "}"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "输出:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "[1, 2, 3]\n",
+ "[1, 2, 3]"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "在这两种形式中,初始化列表的最后一个逗号是可选的(这一特性使维护长列表变得更容易)。\n",
+ "\n",
+ "尽管第一种形式很有用,但是它更加受限,因为它只能用于数组定义处。第二种和第三种形式可以用在任何地方,甚至用在方法的内部。例如,你创建了一个 **String** 数组,将其传递给另一个类的 `main()` 方法,如下:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "// housekeeping/DynamicArray.java\n",
+ "// Array initialization\n",
+ "\n",
+ "public class DynamicArray {\n",
+ " public static void main(String[] args) {\n",
+ " Other.main(new String[] {\"fiddle\", \"de\", \"dum\"});\n",
+ " }\n",
+ "}\n",
+ "\n",
+ "class Other {\n",
+ " public static void main(String[] args) {\n",
+ " for (String s: args) {\n",
+ " System.out.print(s + \" \");\n",
+ " }\n",
+ " }\n",
+ "}"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "输出:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "fiddle de dum "
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "`Other.main()` 的参数是在调用处创建的,因此你甚至可以在方法调用处提供可替换的参数。\n",
+ "\n",
+ "### 可变参数列表\n",
+ "\n",
+ "你可以以一种类似 C 语言中的可变参数列表(C 通常把它称为\"varargs\")来创建和调用方法。这可以应用在参数个数或类型未知的场合。由于所有的类都最后继承于 **Object** 类(随着本书的进展,你会对此有更深的认识),所以你可以创建一个以 Object 数组为参数的方法,并像下面这样调用:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "// housekeeping/VarArgs.java\n",
+ "// Using array syntax to create variable argument lists\n",
+ "\n",
+ "class A {}\n",
+ "\n",
+ "public class VarArgs {\n",
+ " static void printArray(Object[] args) {\n",
+ " for (Object obj: args) {\n",
+ " System.out.print(obj + \" \");\n",
+ " }\n",
+ " System.out.println();\n",
+ " }\n",
+ " \n",
+ " public static void main(String[] args) {\n",
+ " printArray(new Object[] {47, (float) 3.14, 11.11});\n",
+ " printArray(new Object[] {\"one\", \"two\", \"three\"});\n",
+ " printArray(new Object[] {new A(), new A(), new A()});\n",
+ " }\n",
+ "}"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "输出:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "47 3.14 11.11 \n",
+ "one two three \n",
+ "A@15db9742 A@6d06d69c A@7852e922"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "`printArray()` 的参数是 **Object** 数组,使用 for-in 语法遍历和打印数组的每一项。标准 Java 库能输出有意义的内容,但这里创建的是类的对象,打印出的内容是类名,后面跟着一个 **@** 符号以及多个十六进制数字。因而,默认行为(如果没有定义 `toString()` 方法的话,后面会讲这个方法)就是打印类名和对象的地址。\n",
+ "\n",
+ "你可能看到像上面这样编写的 Java 5 之前的代码,它们可以产生可变的参数列表。在 Java 5 中,这种期盼已久的特性终于添加了进来,就像在 `printArray()` 中看到的那样:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "// housekeeping/NewVarArgs.java\n",
+ "// Using array syntax to create variable argument lists\n",
+ "\n",
+ "public class NewVarArgs {\n",
+ " static void printArray(Object... args) {\n",
+ " for (Object obj: args) {\n",
+ " System.out.print(obj + \" \");\n",
+ " }\n",
+ " System.out.println();\n",
+ " }\n",
+ " \n",
+ " public static void main(String[] args) {\n",
+ " // Can take individual elements:\n",
+ " printArray(47, (float) 3.14, 11.11);\n",
+ " printArray(47, 3.14F, 11.11);\n",
+ " printArray(\"one\", \"two\", \"three\");\n",
+ " printArray(new A(), new A(), new A());\n",
+ " // Or an array:\n",
+ " printArray((Object[]) new Integer[] {1, 2, 3, 4});\n",
+ " printArray(); // Empty list is OK\n",
+ " }\n",
+ "}"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "输出:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "47 3.14 11.11 \n",
+ "47 3.14 11.11 \n",
+ "one two three \n",
+ "A@15db9742 A@6d06d69c A@7852e922 \n",
+ "1 2 3 4 "
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "有了可变参数,你就再也不用显式地编写数组语法了,当你指定参数时,编译器实际上会为你填充数组。你获取的仍然是一个数组,这就是为什么 `printArray()` 可以使用 for-in 迭代数组的原因。但是,这不仅仅只是从元素列表到数组的自动转换。注意程序的倒数第二行,一个 **Integer** 数组(通过自动装箱创建)被转型为一个 **Object** 数组(为了移除编译器的警告),并且传递给了 `printArray()`。显然,编译器会发现这是一个数组,不会执行转换。因此,如果你有一组事物,可以把它们当作列表传递,而如果你已经有了一个数组,该方法会把它们当作可变参数列表来接受。\n",
+ "\n",
+ "程序的最后一行表明,可变参数的个数可以为 0。当具有可选的尾随参数时,这一特性会有帮助:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "// housekeeping/OptionalTrailingArguments.java\n",
+ "\n",
+ "public class OptionalTrailingArguments {\n",
+ " static void f(int required, String... trailing) {\n",
+ " System.out.print(\"required: \" + required + \" \");\n",
+ " for (String s: trailing) {\n",
+ " System.out.print(s + \" \");\n",
+ " }\n",
+ " System.out.println();\n",
+ " }\n",
+ " \n",
+ " public static void main(String[] args) {\n",
+ " f(1, \"one\");\n",
+ " f(2, \"two\", \"three\");\n",
+ " f(0);\n",
+ " }\n",
+ "}"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "输出:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "required: 1 one \n",
+ "required: 2 two three \n",
+ "required: 0 "
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "这段程序展示了如何使用除了 **Object** 类之外类型的可变参数列表。这里,所有的可变参数都是 **String** 对象。可变参数列表中可以使用任何类型的参数,包括基本类型。下面例子展示了可变参数列表变为数组的情形,并且如果列表中没有任何元素,那么转变为大小为 0 的数组:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "// housekeeping/VarargType.java\n",
+ "\n",
+ "public class VarargType {\n",
+ " static void f(Character... args) {\n",
+ " System.out.print(args.getClass());\n",
+ " System.out.println(\" length \" + args.length);\n",
+ " }\n",
+ " \n",
+ " static void g(int... args) {\n",
+ " System.out.print(args.getClass());\n",
+ " System.out.println(\" length \" + args.length)\n",
+ " }\n",
+ " \n",
+ " public static void main(String[] args) {\n",
+ " f('a');\n",
+ " f();\n",
+ " g(1);\n",
+ " g();\n",
+ " System.out.println(\"int[]: \"+ new int[0].getClass());\n",
+ " }\n",
+ "}"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "输出:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "class [Ljava.lang.Character; length 1\n",
+ "class [Ljava.lang.Character; length 0\n",
+ "class [I length 1\n",
+ "class [I length 0\n",
+ "int[]: class [I"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "`getClass()` 方法属于 Object 类,将在\"类型信息\"一章中全面介绍。它会产生对象的类,并在打印该类时,看到表示该类类型的编码字符串。前导的 **[** 代表这是一个后面紧随的类型的数组,**I** 表示基本类型 **int**;为了进行双重检查,我在最后一行创建了一个 **int** 数组,打印了其类型。这样也验证了使用可变参数列表不依赖于自动装箱,而使用的是基本类型。\n",
+ "\n",
+ "然而,可变参数列表与自动装箱可以和谐共处,如下:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "// housekeeping/AutoboxingVarargs.java\n",
+ "\n",
+ "public class AutoboxingVarargs {\n",
+ " public static void f(Integer... args) {\n",
+ " for (Integer i: args) {\n",
+ " System.out.print(i + \" \");\n",
+ " }\n",
+ " System.out.println();\n",
+ " }\n",
+ " \n",
+ " public static void main(String[] args) {\n",
+ " f(1, 2);\n",
+ " f(4, 5, 6, 7, 8, 9);\n",
+ " f(10, 11, 12);\n",
+ " \n",
+ " }\n",
+ "}"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "输出:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "1 2\n",
+ "4 5 6 7 8 9\n",
+ "10 11 12"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "注意吗,你可以在单个参数列表中将类型混合在一起,自动装箱机制会有选择地把 **int** 类型的参数提升为 **Integer**。\n",
+ "\n",
+ "可变参数列表使得方法重载更加复杂了,尽管乍看之下似乎足够安全:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "// housekeeping/OverloadingVarargs.java\n",
+ "\n",
+ "public class OverloadingVarargs {\n",
+ " static void f(Character... args) {\n",
+ " System.out.print(\"first\");\n",
+ " for (Character c: args) {\n",
+ " System.out.print(\" \" + c);\n",
+ " }\n",
+ " System.out.println();\n",
+ " }\n",
+ " \n",
+ " static void f(Integer... args) {\n",
+ " System.out.print(\"second\");\n",
+ " for (Integer i: args) {\n",
+ " System.out.print(\" \" + i);\n",
+ " }\n",
+ " System.out.println();\n",
+ " }\n",
+ " \n",
+ " static void f(Long... args) {\n",
+ " System.out.println(\"third\");\n",
+ " }\n",
+ " \n",
+ " public static void main(String[] args) {\n",
+ " f('a', 'b', 'c');\n",
+ " f(1);\n",
+ " f(2, 1);\n",
+ " f(0);\n",
+ " f(0L);\n",
+ " //- f(); // Won's compile -- ambiguous\n",
+ " }\n",
+ "}"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "输出:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "first a b c\n",
+ "second 1\n",
+ "second 2 1\n",
+ "second 0\n",
+ "third"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "在每种情况下,编译器都会使用自动装箱来匹配重载的方法,然后调用最明确匹配的方法。\n",
+ "\n",
+ "但是如果调用不含参数的 `f()`,编译器就无法知道应该调用哪个方法了。尽管这个错误可以弄清楚,但是它可能会使客户端程序员感到意外。\n",
+ "\n",
+ "你可能会通过在某个方法中增加一个非可变参数解决这个问题:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "// housekeeping/OverloadingVarargs2.java\n",
+ "// {WillNotCompile}\n",
+ "\n",
+ "public class OverloadingVarargs2 {\n",
+ " static void f(float i, Character... args) {\n",
+ " System.out.println(\"first\");\n",
+ " }\n",
+ " \n",
+ " static void f(Character... args) {\n",
+ " System.out.println(\"second\");\n",
+ " }\n",
+ " \n",
+ " public static void main(String[] args) {\n",
+ " f(1, 'a');\n",
+ " f('a', 'b');\n",
+ " }\n",
+ "}"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "**{WillNotCompile}** 注释把该文件排除在了本书的 Gradle 构建之外。如果你手动编译它,会得到下面的错误信息:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "OverloadingVarargs2.java:14:error:reference to f is ambiguous f('a', 'b');\n",
+ "\\^\n",
+ "both method f(float, Character...) in OverloadingVarargs2 and method f(Character...) in OverloadingVarargs2 match 1 error"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "如果你给这两个方法都添加一个非可变参数,就可以解决问题了:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "// housekeeping/OverloadingVarargs3\n",
+ "\n",
+ "public class OverloadingVarargs3 {\n",
+ " static void f(float i, Character... args) {\n",
+ " System.out.println(\"first\");\n",
+ " }\n",
+ " \n",
+ " static void f(char c, Character... args) {\n",
+ " System.out.println(\"second\");\n",
+ " }\n",
+ " \n",
+ " public static void main(String[] args) {\n",
+ " f(1, 'a');\n",
+ " f('a', 'b');\n",
+ " }\n",
+ "}"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "输出:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "first\n",
+ "second"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "你应该总是在重载方法的一个版本上使用可变参数列表,或者压根不用它。\n",
+ "\n",
+ "\n",
+ "\n",
+ "## 枚举类型\n",
+ "\n",
+ "Java 5 中添加了一个看似很小的特性 **enum** 关键字,它使得我们在需要群组并使用枚举类型集时,可以很方便地处理。以前,你需要创建一个整数常量集,但是这些值并不会将自身限制在这个常量集的范围内,因此使用它们更有风险,而且更难使用。枚举类型属于非常普遍的需求,C、C++ 和其他许多语言都已经拥有它了。在 Java 5 之前,Java 程序员必须了解许多细节并格外仔细地去达成 **enum** 的效果。现在 Java 也有了 **enum**,并且它的功能比 C/C++ 中的完备得多。下面是个简单的例子:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "// housekeeping/Spiciness.java\n",
+ "\n",
+ "public enum Spiciness {\n",
+ " NOT, MILD, MEDIUM, HOT, FLAMING\n",
+ "}"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "这里创建了一个名为 **Spiciness** 的枚举类型,它有5个值。由于枚举类型的实例是常量,因此按照命名惯例,它们都用大写字母表示(如果名称中含有多个单词,使用下划线分隔)。\n",
+ "\n",
+ "要使用 **enum**,需要创建一个该类型的引用,然后将其赋值给某个实例:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "// housekeeping/SimpleEnumUse.java\n",
+ "\n",
+ "public class SimpleEnumUse {\n",
+ " public static void main(String[] args) {\n",
+ " Spiciness howHot = Spiciness.MEDIUM;\n",
+ " System.out.println(howHot);\n",
+ " }\n",
+ "}"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "输出:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "MEDIUM"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "在你创建 **enum** 时,编译器会自动添加一些有用的特性。例如,它会创建 `toString()` 方法,以便你方便地显示某个 **enum** 实例的名称,这从上面例子中的输出可以看出。编译器还会创建 `ordinal()` 方法表示某个特定 **enum** 常量的声明顺序,`static values()` 方法按照 enum 常量的声明顺序,生成这些常量值构成的数组:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "// housekeeping/EnumOrder.java\n",
+ "\n",
+ "public class EnumOrder {\n",
+ " public static void main(String[] args) {\n",
+ " for (Spiciness s: Spiciness.values()) {\n",
+ " System.out.println(s + \", ordinal \" + s.ordinal());\n",
+ " }\n",
+ " }\n",
+ "}"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "输出:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "NOT, ordinal 0\n",
+ "MILD, ordinal 1\n",
+ "MEDIUM, ordinal 2\n",
+ "HOT, ordinal 3\n",
+ "FLAMING, ordinal 4"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "尽管 **enum** 看起来像是一种新的数据类型,但是这个关键字只是在生成 **enum** 的类时,产生了某些编译器行为,因此在很大程度上你可以将 **enum** 当作其他任何类。事实上,**enum** 确实是类,并且具有自己的方法。\n",
+ "\n",
+ "**enum** 有一个很实用的特性,就是在 **switch** 语句中使用:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "// housekeeping/Burrito.java\n",
+ "\n",
+ "public class Burrito {\n",
+ " Spiciness degree;\n",
+ " \n",
+ " public Burrito(Spiciness degree) {\n",
+ " this.degree = degree;\n",
+ " }\n",
+ " \n",
+ " public void describe() {\n",
+ " System.out.print(\"This burrito is \");\n",
+ " switch(degree) {\n",
+ " case NOT:\n",
+ " System.out.println(\"not spicy at all.\");\n",
+ " break;\n",
+ " case MILD:\n",
+ " case MEDIUM:\n",
+ " System.out.println(\"a little hot.\");\n",
+ " break;\n",
+ " case HOT:\n",
+ " case FLAMING:\n",
+ " default:\n",
+ " System.out.println(\"maybe too hot\");\n",
+ " }\n",
+ " }\n",
+ " \n",
+ " public static void main(String[] args) {\n",
+ " Burrito plain = new Burrito(Spiciness.NOT),\n",
+ " greenChile = new Burrito(Spiciness.MEDIUM),\n",
+ " jalapeno = new Burrito(Spiciness.HOT);\n",
+ " plain.describe();\n",
+ " greenChile.describe();\n",
+ " jalapeno.describe();\n",
+ " }\n",
+ "}"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "输出:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "This burrito is not spicy at all.\n",
+ "This burrito is a little hot.\n",
+ "This burrito is maybe too hot."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "由于 **switch** 是在有限的可能值集合中选择,因此它与 **enum** 是绝佳的组合。注意,enum 的名称是如何能够倍加清楚地表明程序的目的的。\n",
+ "\n",
+ "通常,你可以将 **enum** 用作另一种创建数据类型的方式,然后使用所得到的类型。这正是关键所在,所以你不用过多地考虑它们。在 **enum** 被引入之前,你必须花费大量的精力去创建一个等同的枚举类型,并是安全可用的。\n",
+ "\n",
+ "这些介绍对于你理解和使用基本的 **enum** 已经足够了,我们会在\"枚举\"一章中进行更深入的探讨。\n",
+ "\n",
+ "\n",
+ "\n",
+ "## 本章小结\n",
+ "\n",
+ "构造器,这种看起来精巧的初始化机制,应该给了你很强的暗示:初始化在编程语言中的重要地位。C++ 的发明者 Bjarne Stroustrup 在设计 C++ 期间,在针对 C 语言的生产效率进行的最初调查中发现,错误的初始化会导致大量编程错误。这些错误很难被发现,同样,不合理的清理也会如此。因为构造器能保证进行正确的初始化和清理(没有正确的构造器调用,编译器就不允许创建对象),所以你就有了完全的控制和安全。\n",
+ "\n",
+ "在 C++ 中,析构器很重要,因为用 **new** 创建的对象必须被明确地销毁。在 Java 中,垃圾回收器会自动地释放所有对象的内存,所以很多时候类似的清理方法就不太需要了(但是当要用到的时候,你得自己动手)。在不需要类似析构器行为的时候,Java 的垃圾回收器极大地简化了编程,并加强了内存管理上的安全性。一些垃圾回收器甚至能清理其他资源,如图形和文件句柄。然而,垃圾回收器确实增加了运行时开销,由于 Java 解释器从一开始就很慢,所以这种开销到底造成多大的影响很难看出来。随着时间的推移,Java 在性能方面提升了很多,但是速度问题仍然是它涉足某些特定编程领域的障碍。\n",
+ "\n",
+ "由于要保证所有对象被创建,实际上构造器比这里讨论得更加复杂。特别是当通过*组合*或*继承*创建新类的时候,这种保证仍然成立,并且需要一些额外的语法来支持。在后面的章节中,你会学习组合,继承以及它们如何影响构造器。\n",
+ "\n",
+ "\n",
+ "\n",
+ ""
+ ]
+ }
+ ],
+ "metadata": {},
+ "nbformat": 4,
+ "nbformat_minor": 4
+}
diff --git a/jupyter/07-Implementation-Hiding.ipynb b/jupyter/07-Implementation-Hiding.ipynb
new file mode 100644
index 00000000..ad55bfa3
--- /dev/null
+++ b/jupyter/07-Implementation-Hiding.ipynb
@@ -0,0 +1,1325 @@
+{
+ "cells": [
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "[TOC]\n",
+ "\n",
+ "\n",
+ "# 第七章 封装\n",
+ "\n",
+ "> *访问控制(Access control)*(或者*隐藏实现(implementation hiding)*)与“最初的实现不恰当”有关。\n",
+ "\n",
+ "所有优秀的作者——包括那些编写软件的人——都知道一件好的作品都是经过反复打磨才变得优秀的。如果你把一段代码置于某个位置一段时间,过一会重新来看,你可能发现更好的实现方式。这是*重构*(refactoring)的原动力之一,重构就是重写可工作的代码,使之更加可读,易懂,因而更易维护。\n",
+ "\n",
+ "但是,在修改和完善代码的愿望下,也存在巨大的压力。通常,一些用户(*客户端程序员(client programmers)*)希望你的代码在某些方面保持不变。所以你想修改代码,但他们希望代码保持不变。由此引出了面向对象设计中的一个基本问题:“如何区分变动的事物和不变的事物”。\n",
+ "\n",
+ "这个问题对于类库(library)而言尤其重要。类库的使用者必须依赖他们所使用的那部分类库,并且知道如果使用了类库的新版本,不需要改写代码。另一方面,类库的开发者必须有修改和改进类库的自由,并保证客户代码不会受这些改动影响。\n",
+ "\n",
+ "这可以通过约定解决。例如,类库开发者必须同意在修改类库中的一个类时,不会移除已有的方法,因为那样将会破坏客户端程序员的代码。与之相反的情况更加复杂。在有成员属性的情况下,类库开发者如何知道哪些属性被客户端程序员使用?这同样会发生在那些只为实现类库类而创建的方法上,它们也不是设计成可供客户端程序员调用的。如果类库开发者想删除旧的实现,添加新的实现,结果会怎样呢?任何这些成员的改动都可能破环客户端程序员的代码。因此类库开发者会被束缚,不能修改任何事物。\n",
+ "\n",
+ "为了解决这一问题,Java 提供了*访问修饰符*(access specifier)供类库开发者指明哪些对于客户端程序员是可用的,哪些是不可用的。访问控制权限的等级,从“最大权限”到“最小权限”依次是:**public**,**protected**,*包访问权限(package access)*(没有关键字)和 **private**。根据上一段的内容,你可能会想,作为一名类库设计者,你会尽可能将一切都设为 **private**,仅向客户端程序员暴露你愿意他们使用的方法。这就是你通常所做的,尽管这与那些使用其他语言(尤其是 C)编程以及习惯了不受限制地访问任何东西的人们的直觉相违背。\n",
+ "\n",
+ "然而,类库组件的概念和对类库组件访问的控制仍然不完善。其中仍然存在问题就是如何将类库组件捆绑到一个内聚的类库单元中。Java 中通过 **package** 关键字加以控制,类在相同包下还是在不同包下,会影响访问修饰符。所以在这章开始,你将会学习如何将类库组件置于同一个包下,之后你就能明白访问修饰符的全部含义。\n",
+ "\n",
+ "\n",
+ "\n",
+ "## 包的概念\n",
+ "\n",
+ "包内包含一组类,它们被组织在一个单独的*命名空间*(namespace)下。\n",
+ "\n",
+ "例如,标准 Java 发布中有一个工具库,它被组织在 **java.util** 命名空间下。**java.util** 中含有一个类,叫做 **ArrayList**。使用 **ArrayList** 的一种方式是用其全名 **java.util.ArrayList**。"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "// hiding/FullQualification.java\n",
+ "\n",
+ "public class FullQualification {\n",
+ " public static void main(String[] args) {\n",
+ " java.util.ArrayList list = new java.util.ArrayList();\n",
+ " }\n",
+ "}"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "这种方式使得程序冗长乏味,因此你可以换一种方式,使用 **import** 关键字。如果需要导入某个类,就需要在 **import** 语句中声明:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "// hiding/SingleImport.java\n",
+ "import java.util.ArrayList;\n",
+ "\n",
+ "public class SingleImport {\n",
+ " public static void main(String[] args) {\n",
+ " ArrayList list = new ArrayList();\n",
+ " }\n",
+ "}"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "现在你就可以不加限定词,直接使用 **ArrayList** 了。但是对于 **java.util** 包下的其他类,你还是不能用。要导入其中所有的类,只需使用 **\\*** ,就像本书中其他示例那样:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "import java.util.*"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "之所以使用导入,是为了提供一种管理命名空间的机制。所有类名之间都是相互隔离的。类 **A** 中的方法 `f()` 不会与类 **B** 中具有相同签名的方法 `f()` 冲突。但是如果类名冲突呢?假设你创建了一个 **Stack** 类,打算安装在一台已经有别人所写的 **Stack** 类的机器上,该怎么办呢?这种类名的潜在冲突,正是我们需要在 Java 中对命名空间进行完全控制的原因。为了解决冲突,我们为每个类创建一个唯一标识符组合。\n",
+ "\n",
+ "到目前为止的大部分示例都只存在单个文件,并为本地使用的,所以尚未受到包名的干扰。但是,这些示例其实已经位于包中了,叫做“未命名”包或*默认包*(default package)。这当然是一种选择,为了简单起见,本书其余部分会尽可能采用这种方式。但是,如果你打算为相同机器上的其他 Java 程序创建友好的类库或程序时,就必须仔细考虑以防类名冲突。\n",
+ "\n",
+ "一个 Java 源代码文件称为一个*编译单元(compilation unit)*(有时也称*翻译单元(translation unit)*)。每个编译单元的文件名后缀必须是 **.java**。在编译单元中可以有一个 **public** 类,它的类名必须与文件名相同(包括大小写,但不包括后缀名 **.java**)。每个编译单元中只能有一个 **public** 类,否则编译器不接受。如果这个编译单元中还有其他类,那么在包之外是无法访问到这些类的,因为它们不是 **public** 类,此时它们为主 **public** 类提供“支持”类 。\n",
+ "\n",
+ "### 代码组织\n",
+ "\n",
+ "当编译一个 **.java** 文件时,**.java** 文件的每个类都会有一个输出文件。每个输出的文件名和 **.java** 文件中每个类的类名相同,只是后缀名是 **.class**。因此,在编译少量的 **.java** 文件后,会得到大量的 **.class** 文件。如果你使用过编译型语言,那么你可能习惯编译后产生一个中间文件(通常称为“obj”文件),然后与使用链接器(创建可执行文件)或类库生成器(创建类库)产生的其他同类文件打包到一起的情况。这不是 Java 工作的方式。在 Java 中,可运行程序是一组 **.class** 文件,它们可以打包压缩成一个 Java 文档文件(JAR,使用 **jar** 文档生成器)。Java 解释器负责查找、加载和解释这些文件。\n",
+ "\n",
+ "类库是一组类文件。每个源文件通常都含有一个 **public** 类和任意数量的非 **public** 类,因此每个文件都有一个 **public** 组件。如果把这些组件集中在一起,就需要使用关键字 **package**。\n",
+ "\n",
+ "如果你使用了 **package** 语句,它必须是文件中除了注释之外的第一行代码。当你如下这样写:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "package hiding;"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "意味着这个编译单元是一个名为 **hiding** 类库的一部分。换句话说,你正在声明的编译单元中的 **public** 类名称位于名为 **hiding** 的保护伞下。任何人想要使用该名称,必须指明完整的类名或者使用 **import** 关键字导入 **hiding** 。(注意,Java 包名按惯例一律小写,即使中间的单词也需要小写,与驼峰命名不同)\n",
+ "\n",
+ "例如,假设文件名是 **MyClass.java** ,这意味着文件中只能有一个 **public** 类,且类名必须是 **MyClass**(大小写也与文件名相同):"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "// hiding/mypackage/MyClass.java\n",
+ "package hiding.mypackage\n",
+ "\n",
+ "public class MyClass {\n",
+ " // ...\n",
+ "}"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "现在,如果有人想使用 **MyClass** 或 **hiding.mypackage** 中的其他 **public** 类,就必须使用关键字 **import** 来使 **hiding.mypackage** 中的名称可用。还有一种选择是使用完整的名称:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "// hiding/QualifiedMyClass.java\n",
+ "\n",
+ "public class QualifiedMyClass {\n",
+ " public static void main(String[] args) {\n",
+ " hiding.mypackage.MyClass m = new hiding.mypackage.MyClass();\n",
+ " }\n",
+ "}"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "关键字 **import** 使之更简洁:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "// hiding/ImportedMyClass.java\n",
+ "import hiding.mypackage.*;\n",
+ "\n",
+ "public class ImportedMyClass {\n",
+ " public static void main(String[] args) {\n",
+ " MyClass m = new MyClass();\n",
+ " }\n",
+ "}\t"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "**package** 和 **import** 这两个关键字将单一的全局命名空间分隔开,从而避免名称冲突。\n",
+ "\n",
+ "### 创建独一无二的包名\n",
+ "\n",
+ "你可能注意到,一个包从未真正被打包成单一的文件,它可以由很多 **.class** 文件构成,因而事情就变得有点复杂了。为了避免这种情况,一种合乎逻辑的做法是将特定包下的所有 **.class** 文件都放在一个目录下。也就是说,利用操作系统的文件结构的层次性。这是 Java 解决混乱问题的一种方式;稍后你还会在我们介绍 **jar** 工具时看到另一种方式。\n",
+ "\n",
+ "将所有的文件放在一个子目录还解决了其他的两个问题:创建独一无二的包名和查找可能隐藏于目录结构某处的类。这是通过将 **.class** 文件所在的路径位置编码成 **package** 名称来实现的。按照惯例,**package** 名称是类的创建者的反顺序的 Internet 域名。如果你遵循惯例,因为 Internet 域名是独一无二的,所以你的 **package** 名称也应该是独一无二的,不会发生名称冲突。如果你没有自己的域名,你就得构造一组不大可能与他人重复的组合(比如你的姓名),来创建独一无二的 package 名称。如果你打算发布 Java 程序代码,那么花些力气去获取一个域名是值得的。\n",
+ "\n",
+ "此技巧的第二部分是把 **package** 名称分解成你机器上的一个目录,所以当 Java 解释器必须要加载一个 .class 文件时,它能定位到 **.class** 文件所在的位置。首先,它找出环境变量 **CLASSPATH**(通过操作系统设置,有时也能通过 Java 的安装程序或基于 Java 的工具设置)。**CLASSPATH** 包含一个或多个目录,用作查找 .**class** 文件的根目录。从根目录开始,Java 解释器获取包名并将每个句点替换成反斜杠,生成一个基于根目录的路径名(取决于你的操作系统,包名 **foo.bar.baz** 变成 **foo\\bar\\baz** 或 **foo/bar/baz** 或其它)。然后这个路径与 **CLASSPATH** 的不同项连接,解释器就在这些目录中查找与你所创建的类名称相关的 **.class** 文件(解释器还会查找某些涉及 Java 解释器所在位置的标准目录)。\n",
+ "\n",
+ "为了理解这点,比如说我的域名 **MindviewInc.com**,将之反转并全部改为小写后就是 **com.mindviewinc**,这将作为我创建的类的独一无二的全局名称。(com、edu、org等扩展名之前在 Java 包中都是大写,但是 Java 2 之后都统一用小写。)我决定再创建一个名为 **simple** 的类库,从而细分名称:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "package com.mindviewinc.simple;"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "这个包名可以用作下面两个文件的命名空间保护伞:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "// com/mindviewinc/simple/Vector.java\n",
+ "// Creating a package\n",
+ "package com.mindviewinc.simple;\n",
+ "\n",
+ "public class Vector {\n",
+ " public Vector() {\n",
+ " System.out.println(\"com.mindviewinc.simple.Vector\");\n",
+ " }\n",
+ "}"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "如前所述,**package** 语句必须是文件的第一行非注释代码。第二个文件看上去差不多:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "// com/mindviewinc/simple/List.java\n",
+ "// Creating a package\n",
+ "package com.mindviewinc.simple;\n",
+ "\n",
+ "public class List {\n",
+ " System.out.println(\"com.mindview.simple.List\");\n",
+ "}"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "这两个文件都位于我机器上的子目录中,如下:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "C:\\DOC\\Java\\com\\mindviewinc\\simple"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "(注意,本书的每个文件的第一行注释都指明了文件在源代码目录树中的位置——供本书的自动代码提取工具使用。)\n",
+ "\n",
+ "如果你回头看这个路径,会看到包名 **com.mindviewinc.simple**,但是路径的第一部分呢?CLASSPATH 环境变量会处理它。我机器上的环境变量部分如下:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "CLASSPATH=.;D:\\JAVA\\LIB;C:\\DOC\\Java"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "CLASSPATH 可以包含多个不同的搜索路径。\n",
+ "\n",
+ "但是在使用 JAR 文件时,有点不一样。你必须在类路径写清楚 JAR 文件的实际名称,不能仅仅是 JAR 文件所在的目录。因此,对于一个名为 **grape.jar** 的 JAR 文件,类路径应包括:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "CLASSPATH=.;D\\JAVA\\LIB;C:\\flavors\\grape.jar"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "一旦设置好类路径,下面的文件就可以放在任意目录:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "// hiding/LibTest.java\n",
+ "// Uses the library\n",
+ "import com.mindviewinc.simple.*;\n",
+ "\n",
+ "public class LibTest {\n",
+ " public static void main(String[] args) {\n",
+ " Vector v = new Vector();\n",
+ " List l = new List();\n",
+ " }\n",
+ "}"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "输出:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "com.mindviewinc.simple.Vector\n",
+ "com.mindviewinc.simple.List"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "当编译器遇到导入 **simple** 库的 **import** 语句时,它首先会在 CLASSPATH 指定的目录中查找子目录 **com/mindviewinc/simple**,然后从已编译的文件中找出名称相符者(对 **Vector** 而言是 **Vector.class**,对 **List** 而言是 **List.class**)。注意,这两个类和其中要访问的方法都必须是 **public** 修饰的。\n",
+ "\n",
+ "对于 Java 新手而言,设置 CLASSPATH 是一件麻烦的事(我最初使用时是这么觉得的),后面版本的 JDK 更加智能。你会发现当你安装好 JDK 时,即使不设置 CLASSPATH,也能够编译和运行基本的 Java 程序。但是,为了编译和运行本书的代码示例(从[https://github.com/BruceEckel/OnJava8-examples](https://github.com/BruceEckel/OnJava8-examples) 取得),你必须将本书程序代码树的基本目录加入到 CLASSPATH 中( gradlew 命令管理自身的 CLASSPATH,所以如果你想直接使用 javac 和 java,不用 Gradle 的话,就需要设置 CLASSPATH)。\n",
+ "\n",
+ "### 冲突\n",
+ "\n",
+ "如果通过 **\\*** 导入了两个包含相同名字类名的类库,会发生什么?例如,假设程序如下:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "import com.mindviewinc.simple.*;\n",
+ "import java.util.*;"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "因为 **java.util.*** 也包含了 **Vector** 类,这就存在潜在的冲突。但是只要你不写导致冲突的代码,就不会有问题——这样很好,否则就得做很多类型检查工作来防止那些根本不会出现的冲突。\n",
+ "\n",
+ "现在如果要创建一个 Vector 类,就会出现冲突:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "Vector v = new Vector();"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "这里的 **Vector** 类指的是谁呢?编译器不知道,读者也不知道。所以编译器报错,强制你明确指明。对于标准的 Java 类 **Vector**,你可以这么写:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "java.util.Vector v = new java.util.Vector();"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "这种写法完全指明了 **Vector** 类的位置(配合 CLASSPATH),那么就没有必要写 **import java.util.*** 语句,除非使用其他来自 **java.util** 中的类。\n",
+ "\n",
+ "或者,可以导入单个类以防冲突——只要不在同一个程序中使用有冲突的名字(若使用了有冲突的名字,必须明确指明全名)。\n",
+ "\n",
+ "### 定制工具库\n",
+ "\n",
+ "具备了以上知识,现在就可以创建自己的工具库来减少重复的程序代码了。\n",
+ "\n",
+ "一般来说,我会使用反转后的域名来命名要创建的工具包,比如 **com.mindviewinc.util** ,但为了简化,这里我把工具包命名为 **onjava**。\n",
+ "\n",
+ "比如,下面是“控制流”一章中使用到的 `range()` 方法,采用了 for-in 语法进行简单的遍历:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "// onjava/Range.java\n",
+ "// Array creation methods that can be used without\n",
+ "// qualifiers, using static imports:\n",
+ "package onjava;\n",
+ "\n",
+ "public class Range {\n",
+ " // Produce a sequence [0,n)\n",
+ " public static int[] range(int n) {\n",
+ " int[] result = new int[n];\n",
+ " for (int i = 0; i < n; i++) {\n",
+ " result[i] = i;\n",
+ " }\n",
+ " return result;\n",
+ " }\n",
+ " // Produce a sequence [start..end)\n",
+ " public static int[] range(int start, int end) {\n",
+ " int sz = end - start;\n",
+ " int[] result = new int[sz];\n",
+ " for (int i = 0; i < sz; i++) {\n",
+ " result[i] = start + i;\n",
+ " }\n",
+ " return result;\n",
+ " }\n",
+ " // Produce sequence [start..end) incrementing by step\n",
+ " public static int[] range(int start, int end, int step) {\n",
+ " int sz = (end - start) / step;\n",
+ " int[] result = new int[sz];\n",
+ " for (int i = 0; i < sz; i++) {\n",
+ " result[i] = start + (i * step);\n",
+ " }\n",
+ " return result;\n",
+ " }\n",
+ "}"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "这个文件的位置一定是在某个以一个 CLASSPATH 位置开始,然后接着是 **onjava** 的目录下。编译完之后,就可以在系统的任何地方使用 **import static** 语句来使用这些方法了。\n",
+ "\n",
+ "从现在开始,无论何时你创建了有用的新工具,都可以把它加入到自己的类库中。在本书中,你将会看到更多的组件加入到 **onjava** 库。\n",
+ "\n",
+ "### 使用 import 改变行为\n",
+ "\n",
+ "Java 没有 C 的*条件编译*(conditional compilation)功能,该功能使你不必更改任何程序代码而能够切换开关产生不同的行为。Java 之所以去掉此功能,可能是因为 C 在绝大多数情况下使用该功能解决跨平台问题:程序代码的不同部分要根据不同的平台来编译。而 Java 自身就是跨平台设计的,这个功能就没有必要了。\n",
+ "\n",
+ "但是,条件编译还有其他的用途。调试是一个很常见的用途,调试功能在开发过程中是开启的,在发布的产品中是禁用的。可以通过改变导入的 **package** 来实现这一目的,修改的方法是将程序中的代码从调试版改为发布版。这个技术可用于任何种类的条件代码。\n",
+ "\n",
+ "### 使用包的忠告\n",
+ "\n",
+ "当创建一个包时,包名就隐含了目录结构。这个包必须位于包名指定的目录中,该目录必须在以 CLASSPATH 开始的目录中可以查询到。 最初使用关键字 **package** 可能会有点不顺,因为除非遵守“包名对应目录路径”的规则,否则会收到很多意外的运行时错误信息如找不到特定的类,即使这个类就位于同一目录中。如果你收到类似信息,尝试把 **package** 语句注释掉,如果程序能运行的话,你就知道问题出现在哪里了。\n",
+ "\n",
+ "注意,编译过的代码通常位于与源代码的不同目录中。这是很多工程的标准,而且集成开发环境(IDE)通常会自动为我们做这些。必须保证 JVM 通过 CLASSPATH 能找到编译后的代码。\n",
+ "\n",
+ "\n",
+ "\n",
+ "## 访问权限修饰符\n",
+ "\n",
+ "Java 访问权限修饰符 **public**,**protected** 和 **private** 位于定义的类名,属性名和方法名之前。每个访问权限修饰符只能控制它所修饰的对象。\n",
+ "\n",
+ "如果不提供访问修饰符,就意味着\"包访问权限\"。所以无论如何,万物都有某种形式的访问控制权。接下来的几节中,你将学习各种类型的访问权限。\n",
+ "\n",
+ "### 包访问权限\n",
+ "\n",
+ "本章之前的所有示例要么使用 **public** 访问修饰符,要么就没使用修饰符(*默认访问权限(default access)*)。默认访问权限没有关键字,通常被称为*包访问权限(package access)*(有时也称为 **friendly**)。这意味着当前包中的所有其他类都可以访问那个成员。对于这个包之外的类,这个成员看上去是 **private** 的。由于一个编译单元(即一个文件)只能隶属于一个包,所以通过包访问权限,位于同一编译单元中的所有类彼此之间都是可访问的。\n",
+ "\n",
+ "包访问权限可以把相关类聚到一个包下,以便它们能轻易地相互访问。包里的类赋予了它们包访问权限的成员相互访问的权限,所以你\"拥有”了包内的程序代码。只能通过你所拥有的代码去访问你所拥有的其他代码,这样规定很有意义。构建包访问权限机制是将类聚集在包中的重要原因之一。在许多语言中,在文件中组织定义的方式是任意的,但是在 Java 中你被强制以一种合理的方式组织它们。另外,你可能会将不应该对当前包中的类具有访问权限的类排除在包外。\n",
+ "\n",
+ "类控制着哪些代码有权访问自己的成员。其他包中的代码不能一上来就说\"嗨,我是 **Bob** 的朋友!\",然后想看到 **Bob** 的 **protected**、包访问权限和 **private** 成员。取得对成员的访问权的唯一方式是:\n",
+ "\n",
+ "1. 使成员成为 **public**。那么无论是谁,无论在哪,都可以访问它。\n",
+ "2. 赋予成员默认包访问权限,不用加任何访问修饰符,然后将其他类放在相同的包内。这样,其他类就可以访问该成员。\n",
+ "3. 在\"复用\"这一章你将看到,继承的类既可以访问 **public** 成员,也可以访问 **protected** 成员(但不能访问 **private** 成员)。只有当两个类处于同一个包内,它才可以访问包访问权限的成员。但现在不用担心继承和 **protected**。\n",
+ "4. 提供访问器(accessor)和修改器(mutator)方法(有时也称为\"get/set\" 方法),从而读取和改变值。\n",
+ "\n",
+ "### public: 接口访问权限\n",
+ "\n",
+ "当你使用关键字 **public**,就意味着紧随 public 后声明的成员对于每个人都是可用的,尤其是使用类库的客户端程序员更是如此。假设定义了一个包含下面编译单元的 **dessert** 包:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "// hiding/dessert/Cookie.java\n",
+ "// Creates a library\n",
+ "package hiding.dessert;\n",
+ "\n",
+ "public class Cookie {\n",
+ " public Cookie() {\n",
+ " System.out.println(\"Cookie constructor\");\n",
+ " }\n",
+ " \n",
+ " void bite() {\n",
+ " System.out.println(\"bite\");\n",
+ " }\n",
+ "}"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "记住,**Cookie.java** 文件产生的类文件必须位于名为 **dessert** 的子目录中,该子目录在 **hiding** (表明本书的\"封装\"章节)下,它必须在 CLASSPATH 的几个目录之下。不要错误地认为 Java 总是会将当前目录视作查找行为的起点之一。如果你的 CLASSPATH 中没有 **.**,Java 就不会查找当前目录。\n",
+ "\n",
+ "现在,使用 **Cookie** 创建一个程序:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "// hiding/Dinner.java\n",
+ "// Uses the library\n",
+ "import hiding.dessert.*;\n",
+ "\n",
+ "public class Dinner {\n",
+ " public static void main(String[] args) {\n",
+ " Cookie x = new Cookie();\n",
+ " // -x.bite(); // Can't access\n",
+ " }\n",
+ "}"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "输出:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "Cookie constructor"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "你可以创建一个 **Cookie** 对象,因为它构造器和类都是 **public** 的。(后面会看到更多 **public** 的概念)但是,在 **Dinner.java** 中无法访问到 **Cookie** 对象中的 `bite()` 方法,因为 `bite()` 只提供了包访问权限,因而在 **dessert** 包之外无法访问,编译器禁止你使用它。\n",
+ "\n",
+ "### 默认包\n",
+ "\n",
+ "你可能惊讶地发现,以下代码尽管看上去破坏了规则,但是仍然可以编译:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "// hiding/Cake.java\n",
+ "// Accesses a class in a separate compilation unit\n",
+ "class Cake {\n",
+ " public static void main(String[] args) {\n",
+ " Pie x = new Pie();\n",
+ " x.f();\n",
+ " }\n",
+ "}"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "输出:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "Pie.f()"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "同一目录下的第二个文件:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "// hiding/Pie.java\n",
+ "// The other class\n",
+ "class Pie {\n",
+ " void f() {\n",
+ " System.out.println(\"Pie.f()\");\n",
+ " }\n",
+ "}"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "最初看上去这两个文件毫不相关,但在 **Cake** 中可以创建一个 **Pie** 对象并调用它的 `f()` 方法。(注意,你的 CLASSPATH 中一定得有 **.**,这样文件才能编译)通常会认为 **Pie** 和 `f()` 具有包访问权限,因此不能被 **Cake** 访问。它们的确具有包访问权限,这是部分正确。**Cake.java** 可以访问它们是因为它们在相同的目录中且没有给自己设定明确的包名。Java 把这样的文件看作是隶属于该目录的默认包中,因此它们为该目录中所有的其他文件都提供了包访问权限。\n",
+ "\n",
+ "### private: 你无法访问\n",
+ "\n",
+ "关键字 **private** 意味着除了包含该成员的类,其他任何类都无法访问这个成员。同一包中的其他类无法访问 **private** 成员,因此这等于说是自己隔离自己。另一方面,让许多人合作创建一个包也是有可能的。使用 **private**,你可以自由地修改那个被修饰的成员,无需担心会影响同一包下的其他类。\n",
+ "\n",
+ "默认的包访问权限通常提供了足够的隐藏措施;记住,使用类的客户端程序员无法访问包访问权限成员。这样做很好,因为默认访问权限是一种我们常用的权限(同时也是一种在忘记添加任何访问权限时自动得到的权限)。因此,通常考虑的是把哪些成员声明成 **public** 供客户端程序员使用。所以,最初不常使用关键字 **private**,因为程序没有它也可以照常工作。然而,使用 **private** 是非常重要的,尤其是在多线程环境中。(在\"并发编程\"一章中将看到)。\n",
+ "\n",
+ "以下是一个使用 **private** 的例子:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "// hiding/IceCream.java\n",
+ "// Demonstrates \"private\" keyword\n",
+ "\n",
+ "class Sundae {\n",
+ " private Sundae() {}\n",
+ " static Sundae makeASundae() {\n",
+ " return new Sundae();\n",
+ " }\n",
+ "}\n",
+ "\n",
+ "public class IceCream {\n",
+ " public static void main(String[] args) {\n",
+ " //- Sundae x = new Sundae();\n",
+ " Sundae x = Sundae.makeASundae();\n",
+ " }\n",
+ "}"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "以上展示了 **private** 的用武之地:控制如何创建对象,防止别人直接访问某个特定的构造器(或全部构造器)。例子中,你无法通过构造器创建一个 **Sundae** 对象,而必须调用 `makeASundae()` 方法创建对象。\n",
+ "\n",
+ "任何可以肯定只是该类的\"助手\"方法,都可以声明为 **private**,以确保不会在包中的其他地方误用它,也防止了你会去改变或删除它。将方法声明为 **private** 确保了你拥有这种选择权。\n",
+ "\n",
+ "对于类中的 **private** 属性也是一样。除非必须公开底层实现(这种情况很少见),否则就将属性声明为 **private**。然而,不能因为类中某个对象的引用是 **private**,就认为其他对象也无法拥有该对象的 **public** 引用(参见附录:对象传递和返回)。\n",
+ "\n",
+ "### protected: 继承访问权限\n",
+ "\n",
+ "要理解 **protected** 的访问权限,我们在内容上需要作一点跳跃。首先,在介绍本书\"复用\"章节前,你不必真正理解本节的内容。但为了内容的完整性,这里作了简要介绍,举了个使用 **protected** 的例子。\n",
+ "\n",
+ "关键字 **protected** 处理的是继承的概念,通过继承可以利用一个现有的类——我们称之为基类,然后添加新成员到现有类中而不必碰现有类。我们还可以改变类的现有成员的行为。为了从一个类中继承,需要声明新类 extends 一个现有类,像这样:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "class Foo extends Bar {}"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "类定义的其他部分看起来是一样的。\n",
+ "\n",
+ "如果你创建了一个新包,并从另一个包继承类,那么唯一能访问的就是被继承类的 **public** 成员。(如果在同一个包中继承,就可以操作所有的包访问权限的成员。)有时,基类的创建者会希望某个特定成员能被继承类访问,但不能被其他类访问。这时就需要使用 **protected**。**protected** 也提供包访问权限,也就是说,相同包内的其他类可以访问 **protected** 元素。\n",
+ "\n",
+ "回顾下先前的文件 **Cookie.java**,下面的类不能调用包访问权限的方法 `bite()`:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "// hiding/ChocolateChip.java\n",
+ "// Can't use package-access member from another package\n",
+ "import hiding.dessert.*;\n",
+ "\n",
+ "public class ChocolateChip extends Cookie {\n",
+ " public ChocolateChip() {\n",
+ " System.out.println(\"ChocolateChip constructor\");\n",
+ " } \n",
+ " \n",
+ " public void chomp() {\n",
+ " //- bite(); // Can't access bite\n",
+ " }\n",
+ " \n",
+ " public static void main(String[] args) {\n",
+ " ChocolateChip x = new ChocolateChip();\n",
+ " x.chomp();\n",
+ " }\n",
+ "}"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "输出:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "Cookie constructor\n",
+ "ChocolateChip constructor"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "如果类 **Cookie** 中存在一个方法 `bite()`,那么它的任何子类中都存在 `bite()` 方法。但是因为 `bite()` 具有包访问权限并且位于另一个包中,所以我们在这个包中无法使用它。你可以把它声明为 **public**,但这样一来每个人都能访问它,这可能也不是你想要的。如果你将 **Cookie** 改成如下这样:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "// hiding/cookie2/Cookie.java\n",
+ "package hiding.cookie2;\n",
+ "\n",
+ "public class Cookie {\n",
+ " public Cookie() {\n",
+ " System.out.println(\"Cookie constructor\");\n",
+ " }\n",
+ " \n",
+ " protected void bite() {\n",
+ " System.out.println(\"bite\");\n",
+ " }\n",
+ "}"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "这样,`bite()` 对于所有继承 **Cookie** 的类,都是可访问的:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "// hiding/ChocolateChip2.java\n",
+ "import hiding.cookie2.*;\n",
+ "\n",
+ "public class ChocolateChip2 extends Cookie {\n",
+ " public ChocoalteChip2() {\n",
+ " System.out.println(\"ChocolateChip2 constructor\");\n",
+ " }\n",
+ " \n",
+ " public void chomp() {\n",
+ " bite(); // Protected method\n",
+ " }\n",
+ " \n",
+ " public static void main(String[] args) {\n",
+ " ChocolateChip2 x = new ChocolateChip2();\n",
+ " x.chomp();\n",
+ " }\n",
+ "}"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "输出:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "Cookie constructor\n",
+ "ChocolateChip2 constructor\n",
+ "bite"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "尽管 `bite()` 也具有包访问权限,但它不是 **public** 的。\n",
+ "\n",
+ "### 包访问权限 Vs Public 构造器\n",
+ "\n",
+ "当你定义一个具有包访问权限的类时,你可以在类中定义一个 public 构造器,编译器不会报错:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "// hiding/packageaccess/PublicConstructor.java\n",
+ "package hiding.packageaccess;\n",
+ "\n",
+ "class PublicConstructor {\n",
+ " public PublicConstructor() {}\n",
+ "}"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "有一个 Checkstyle 工具,你可以运行命令 **gradlew hiding:checkstyleMain** 使用它,它会指出这种写法是虚假的,而且从技术上来说是错误的。实际上你不能从包外访问到这个 **public** 构造器:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "// hiding/CreatePackageAccessObject.java\n",
+ "// {WillNotCompile}\n",
+ "import hiding.packageaccess.*;\n",
+ "\n",
+ "public class CreatePackageAcessObject {\n",
+ " public static void main(String[] args) {\n",
+ " new PublicConstructor();\n",
+ " }\n",
+ "}"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "如果你编译下这个类,会得到编译错误信息:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "CreatePackageAccessObject.java:6:error:\n",
+ "PublicConstructor is not public in hiding.packageaccess;\n",
+ "cannot be accessed from outside package\n",
+ "new PublicConstructor();\n",
+ "^\n",
+ "1 error"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "因此,在一个具有包访问权限的类中定义一个 **public** 的构造器并不能真的使这个构造器成为 **public**,在声明的时候就应该标记为编译时错误。\n",
+ "\n",
+ "\n",
+ "\n",
+ "## 接口和实现\n",
+ "\n",
+ "访问控制通常被称为*隐藏实现*(implementation hiding)。将数据和方法包装进类中并把具体实现隐藏被称作是*封装*(encapsulation)。其结果就是一个同时带有特征和行为的数据类型。\n",
+ "\n",
+ "出于两个重要的原因,访问控制在数据类型内部划定了边界。第一个原因是确立客户端程序员可以使用和不能使用的边界。可以在结构中建立自己的内部机制而不必担心客户端程序员偶尔将内部实现作为他们可以使用的接口的一部分。\n",
+ "\n",
+ "这直接引出了第二个原因:将接口与实现分离。如果在一组程序中使用接口,而客户端程序员只能向 **public** 接口发送消息的话,那么就可以自由地修改任何不是 **public** 的事物(例如包访问权限,protected,或 private 修饰的事物),却不会破坏客户端代码。\n",
+ "\n",
+ "为了清晰起见,你可以采用一种创建类的风格:**public** 成员放在类的开头,接着是 **protected** 成员,包访问权限成员,最后是 **private** 成员。这么做的好处是类的使用者可以从头读起,首先会看到对他们而言最重要的部分(public 成员,因为可以从文件外访问它们),直到遇到非 **public** 成员时停止阅读,下面就是内部实现了:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "// hiding/OrganizedByAccess.java\n",
+ "\n",
+ "public class OrganizedByAccess {\n",
+ " public void pub1() {/* ... */}\n",
+ " public void pub2() {/* ... */}\n",
+ " public void pub3() {/* ... */}\n",
+ " private void priv1() {/* ... */}\n",
+ " private void priv2() {/* ... */}\n",
+ " private void priv3() {/* ... */}\n",
+ " private int i;\n",
+ " // ...\n",
+ "}"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "这么做只能是程序阅读起来稍微容易一些,因为实现和接口还是混合在一起。也就是说,你仍然能看到源代码——实现部分,因为它就在类中。另外,javadoc 提供的注释文档功能降低了程序代码的可读性对客户端程序员的重要性。将接口展现给类的使用者实际上是类浏览器的任务,类浏览器会展示所有可用的类,并告诉你如何使用它们(比如说哪些成员可用)。在 Java 中,JDK 文档起到了类浏览器的作用。\n",
+ "\n",
+ "\n",
+ "\n",
+ "## 类访问权限\n",
+ "\n",
+ "访问权限修饰符也可以用于确定类库中的哪些类对于类库的使用者是可用的。如果希望某个类可以被客户端程序员使用,就把关键字 **public** 作用于整个类的定义。这甚至控制着客户端程序员能否创建类的对象。\n",
+ "\n",
+ "为了控制一个类的访问权限,修饰符必须出现在关键字 **class** 之前:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "public class Widget {"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "如果你的类库名是 **hiding**,那么任何客户端程序员都可以通过如下声明访问 **Widget**:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "import hiding.Widget;"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "或者"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "import hiding.*;"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "这里有一些额外的限制:\n",
+ "\n",
+ "1. 每个编译单元(即每个文件)中只能有一个 **public** 类。这表示,每个编译单元有一个公共的接口用 **public** 类表示。该接口可以包含许多支持包访问权限的类。一旦一个编译单元中出现一个以上的 **public** 类,编译就会报错。\n",
+ "2. **public** 类的名称必须与含有该编译单元的文件名相同,包括大小写。所以对于 **Widget** 来说,文件名必须是 **Widget.java**,不能是 **widget.java** 或 **WIDGET.java**。再次强调,如果名字不匹配,编译器会报错。\n",
+ "3. 虽然不是很常见,但是编译单元内没有 **public** 类也是可能的。这时可以随意命名文件(尽管随意命名会让代码的阅读者和维护者感到困惑)。\n",
+ "\n",
+ "如果获取了一个在 **hiding** 包中的类,只用来完成 **Widget** 或 **hiding** 包下一些其他 **public** 类所要执行的任务,怎么办呢? 你不想自找麻烦为客户端程序员创建说明文档,并且你认为不久后会完全改变原有方案并将旧版本删除,替换成新版本。为了保留此灵活性,需要确保客户端程序员不依赖隐藏在 **hiding** 中的任何特定细节,那么把 **public** 关键字从类中去掉,给予它包访问权限,就可以了。\n",
+ "\n",
+ "当你创建了一个包访问权限的类,把类中的属性声明为 **private** 仍然是有意义的——应该尽可能将所有属性都声明为 **private**,但是通常把方法声明成与类(包访问权限)相同的访问权限也是合理的。一个包访问权限的类只能被用于包内,除非强制将某些方法声明为 **public**,这种情况下,编译器会告诉你。\n",
+ "\n",
+ "注意,类既不能是 **private** 的(这样除了该类自身,任何类都不能访问它),也不能是 **protected** 的。所以对于类的访问权限只有两种选择:包访问权限或者 **public**。为了防止类被外界访问,可以将所有的构造器声明为 **private**,这样只有你自己能创建对象(在类的 static 成员中):"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "// hiding/Lunch.java\n",
+ "// Demonstrates class access specifiers. Make a class\n",
+ "// effectively private with private constructors:\n",
+ "\n",
+ "class Soup1 {\n",
+ " private Soup1() {}\n",
+ " \n",
+ " public static Soup1 makeSoup() { // [1]\n",
+ " return new Soup1();\n",
+ " }\n",
+ "}\n",
+ "\n",
+ "class Soup2 {\n",
+ " private Soup2() {}\n",
+ " \n",
+ " private static Soup2 ps1 = new Soup2(); // [2]\n",
+ " \n",
+ " public static Soup2 access() {\n",
+ " return ps1;\n",
+ " }\n",
+ " \n",
+ " public void f() {}\n",
+ "}\n",
+ "// Only one public class allowed per file:\n",
+ "public class Lunch {\n",
+ " void testPrivate() {\n",
+ " // Can't do this! Private constructor:\n",
+ " //- Soup1 soup = new Soup1();\n",
+ " }\n",
+ " \n",
+ " void testStatic() {\n",
+ " Soup1 soup = Soup1.makeSoup();\n",
+ " }\n",
+ " \n",
+ " void testSingleton() {\n",
+ " Soup2.access().f();\n",
+ " }\n",
+ "}"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "可以像 [1] 那样通过 **static** 方法创建对象,也可以像 [2] 那样先创建一个静态对象,当用户需要访问它时返回对象的引用即可。\n",
+ "\n",
+ "到目前为止,大部分的方法要么返回 void,要么返回基本类型,所以 [1] 处的定义乍看之下会有点困惑。方法名(**makeSoup**)前面的 **Soup1** 表明了方法返回的类型。到目前为止,这里经常是 **void**,即不返回任何东西。然而也可以返回对象的引用,就像这里一样。这个方法返回了对 **Soup1** 类对象的引用。\n",
+ "\n",
+ "**Soup1** 和 **Soup2** 展示了如何通过将你所有的构造器声明为 **private** 的方式防止直接创建某个类的对象。记住,如果你不显式地创建构造器,编译器会自动为你创建一个无参构造器(没有参数的构造器)。如果我们编写了无参构造器,那么编译器就不会自动创建构造器了。将构造器声明为 **private**,那么谁也无法创建该类的对象了。但是现在别人该怎么使用这个类呢?上述例子给出了两个选择。在 **Soup1** 中,有一个 **static** 方法,它的作用是创建一个新的 **Soup1** 对象并返回对象的引用。如果想要在返回引用之前在 **Soup1** 上做一些额外操作,或是记录创建了多少个 **Soup1** 对象(可以用来限制数量),这种做法是有用的。\n",
+ "\n",
+ "**Soup2** 用到了所谓的*设计模式*(design pattern)。这种模式叫做*单例模式*(singleton),因为它只允许创建类的一个对象。**Soup2** 类的对象是作为 **Soup2** 的 **static** **private** 成员而创建的,所以有且只有一个,你只能通过 **public** 修饰的 `access()` 方法访问到这个对象。\n",
+ "\n",
+ "\n",
+ "\n",
+ "## 本章小结\n",
+ "\n",
+ "无论在什么样的关系中,划定一些供各成员共同遵守的界限是很重要的。当你创建了一个类库,也就与该类库的使用者产生了联系,他们是类库的客户端程序员,需要使用你的类库创建应用或更大的类库。\n",
+ "\n",
+ "没有规则,客户端程序员就可以对类的所有成员为所欲为,即使你希望他们不要操作部分成员。这种情况下,所有事物都是公开的。\n",
+ "\n",
+ "本章讨论了类库是如何通过类构建的:首先,介绍了将一组类打包到类库的方式,其次介绍了类如何控制对其成员的访问。\n",
+ "\n",
+ "据估计,用 C 语言开发项目,当代码量达到 5 万行和 10 万行时就会出现问题,因为 C 语言只有单一的命名空间,名称开始冲突造成额外的管理开销。在 Java 中,关键字 **package**,包命名模式和关键字 **import** 给了你对于名称的完全控制权,因此可以轻易地避免名称冲突的问题。\n",
+ "\n",
+ "控制成员访问权限有两个原因。第一个原因是使用户不要接触他们不该接触的部分,这部分对于类内部来说是必要的,但是不属于客户端程序员所需接口的一部分。因此将方法和属性声明为 **private** 对于客户端程序员来说是一种服务,可以让他们清楚地看到什么是重要的,什么可以忽略。这可以简化他们对类的理解。\n",
+ "\n",
+ "第二个也是最重要的原因是为了让类库设计者更改类内部的工作方式,而不用担心会影响到客户端程序员。比如最初以某种方式创建一个类,随后发现如果更改代码结构可以极大地提高运行速度。如果接口与实现被明确地隔离和保护,你可以实现这一目的,而不必强制客户端程序员重新编写代码。访问权限控制确保客户端程序员不会依赖某个类的底层实现的任何部分。\n",
+ "\n",
+ "当你具备更改底层实现的能力时,不但可以自由地改善设计,还可能会随意地犯错。无论如何细心地计划和设计,都有可能犯错。当了解到犯错是相对安全的时候,你可以更加放心地实验,更快地学会,更快地完成项目。\n",
+ "\n",
+ "类的 **public** 接口是用户真正看到的,所以在分析和设计阶段决定这部分接口是最重要的部分。尽管如此,你仍然有改变的空间。如果最初没有创建出正确的接口,可以添加更多的方法,只要你不删除那些客户端程序员已经在他们的代码中使用的东西。\n",
+ "\n",
+ "注意到访问权限控制关注的是类库创建者和外部使用者之间的关系,一种交流方式。很多情况下,事实并非如此。例如,你自己编写了所有的代码,或者在一个小组中工作,所有的东西都放在同一个包下。这些情况下,交流方式则是另外一种,此时严格地遵循访问权限规则也许不是最佳选择,默认(包)访问权限也许就足够好了。\n",
+ "\n",
+ "\n",
+ "\n",
+ ""
+ ]
+ }
+ ],
+ "metadata": {},
+ "nbformat": 4,
+ "nbformat_minor": 4
+}
diff --git a/jupyter/08-Reuse.ipynb b/jupyter/08-Reuse.ipynb
new file mode 100644
index 00000000..ea06c47b
--- /dev/null
+++ b/jupyter/08-Reuse.ipynb
@@ -0,0 +1,1661 @@
+{
+ "cells": [
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "[TOC]\n",
+ "\n",
+ "\n",
+ "\n",
+ "# 第八章 复用\n",
+ "\n",
+ "\n",
+ "> 代码复用是面向对象编程(OOP)最具魅力的原因之一。\n",
+ "\n",
+ "对于像 C 语言等面向过程语言来说,“复用”通常指的就是“复制代码”。任何语言都可通过简单复制来达到代码复用的目的,但是这样做的效果并不好。Java 围绕“类”(Class)来解决问题。我们可以直接使用别人构建或调试过的代码,而非创建新类、重新开始。\n",
+ "\n",
+ "如何在不污染源代码的前提下使用现存代码是需要技巧的。在本章里,你将学习到两种方式来达到这个目的:\n",
+ "\n",
+ "1. 第一种方式直接了当。在新类中创建现有类的对象。这种方式叫做“组合”(Composition),通过这种方式复用代码的功能,而非其形式。\n",
+ "\n",
+ "2. 第二种方式更为微妙。创建现有类类型的新类。照字面理解:采用现有类形式,又无需在编码时改动其代码,这种方式就叫做“继承”(Inheritance),编译器会做大部分的工作。**继承**是面向对象编程(OOP)的重要基础之一。更多功能相关将在[多态](./09-Polymorphism.md)(Polymorphism)章节中介绍。\n",
+ "\n",
+ "组合与继承的语法、行为上有许多相似的地方(这其实是有道理的,毕竟都是基于现有类型构建新的类型)。在本章中,你会学到这两种代码复用的方法。\n",
+ "\n",
+ "\n",
+ "\n",
+ "## 组合语法\n",
+ "\n",
+ "在前面的学习中,“组合”(Composition)已经被多次使用。你仅需要把对象的引用(object references)放置在一个新的类里,这就使用了组合。例如,假设你需要一个对象,其中内置了几个 **String** 对象,两个基本类型(primitives)的属性字段,一个其他类的对象。对于非基本类型对象,将引用直接放置在新类中,对于基本类型属性字段则仅进行声明。"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "// reuse/SprinklerSystem.java\n",
+ "// (c)2017 MindView LLC: see Copyright.txt\n",
+ "// We make no guarantees that this code is fit for any purpose.\n",
+ "// Visit http://OnJava8.com for more book information.\n",
+ "// Composition for code reuse\n",
+ "\n",
+ "class WaterSource {\n",
+ " private String s;\n",
+ " WaterSource() {\n",
+ " System.out.println(\"WaterSource()\");\n",
+ " s = \"Constructed\";\n",
+ " }\n",
+ " @Override\n",
+ " public String toString() { return s; }\n",
+ "}\n",
+ "\n",
+ "public class SprinklerSystem {\n",
+ " private String valve1, valve2, valve3, valve4;\n",
+ " private WaterSource source = new WaterSource();\n",
+ " private int i;\n",
+ " private float f;\n",
+ " @Override\n",
+ " public String toString() {\n",
+ " return\n",
+ " \"valve1 = \" + valve1 + \" \" +\n",
+ " \"valve2 = \" + valve2 + \" \" +\n",
+ " \"valve3 = \" + valve3 + \" \" +\n",
+ " \"valve4 = \" + valve4 + \"\\n\" +\n",
+ " \"i = \" + i + \" \" + \"f = \" + f + \" \" +\n",
+ " \"source = \" + source; // [1]\n",
+ " }\n",
+ " public static void main(String[] args) {\n",
+ " SprinklerSystem sprinklers = new SprinklerSystem();\n",
+ " System.out.println(sprinklers);\n",
+ " }\n",
+ "}\n",
+ "/* Output:\n",
+ "WaterSource()\n",
+ "valve1 = null valve2 = null valve3 = null valve4 = null\n",
+ "i = 0 f = 0.0 source = Constructed\n",
+ "*/\n"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "这两个类中定义的一个方法是特殊的: `toString()`。每个非基本类型对象都有一个 `toString()` 方法,在编译器需要字符串但它有对象的特殊情况下调用该方法。因此,在 [1] 中,编译器看到你试图“添加”一个 **WaterSource** 类型的字符串对象 。因为字符串只能拼接另一个字符串,所以它就先会调用 `toString()` 将 **source** 转换成一个字符串。然后,它可以拼接这两个字符串并将结果字符串传递给 `System.out.println()`。要对创建的任何类允许这种行为,只需要编写一个 **toString()** 方法。在 `toString()` 上使用 **@Override** 注释来告诉编译器,以确保正确地覆盖。**@Override** 是可选的,但它有助于验证你没有拼写错误 (或者更微妙地说,大小写字母输入错误)。类中的基本类型字段自动初始化为零,正如 **object Everywhere** 一章中所述。但是对象引用被初始化为 **null**,如果你尝试调用其任何一个方法,你将得到一个异常(一个运行时错误)。方便的是,打印 **null** 引用却不会得到异常。\n",
+ "\n",
+ "编译器不会为每个引用创建一个默认对象,这是有意义的,因为在许多情况下,这会导致不必要的开销。初始化引用有四种方法:\n",
+ "\n",
+ "1. 当对象被定义时。这意味着它们总是在调用构造函数之前初始化。\n",
+ "2. 在该类的构造函数中。\n",
+ "3. 在实际使用对象之前。这通常称为*延迟初始化*。在对象创建开销大且不需要每次都创建对象的情况下,它可以减少开销。\n",
+ "4. 使用实例初始化。\n",
+ "\n",
+ "以上四种实例创建的方法例子在这:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "// reuse/Bath.java\n",
+ "// (c)2017 MindView LLC: see Copyright.txt\n",
+ "// We make no guarantees that this code is fit for any purpose.\n",
+ "// Visit http://OnJava8.com for more book information.\n",
+ "// Constructor initialization with composition\n",
+ "\n",
+ "class Soap {\n",
+ " private String s;\n",
+ " Soap() {\n",
+ " System.out.println(\"Soap()\");\n",
+ " s = \"Constructed\";\n",
+ " }\n",
+ " @Override\n",
+ " public String toString() { return s; }\n",
+ "}\n",
+ "\n",
+ "public class Bath {\n",
+ " private String // Initializing at point of definition:\n",
+ " s1 = \"Happy\",\n",
+ " s2 = \"Happy\",\n",
+ " s3, s4;\n",
+ " private Soap castille;\n",
+ " private int i;\n",
+ " private float toy;\n",
+ " public Bath() {\n",
+ " System.out.println(\"Inside Bath()\");\n",
+ " s3 = \"Joy\";\n",
+ " toy = 3.14f;\n",
+ " castille = new Soap();\n",
+ " }\n",
+ " // Instance initialization:\n",
+ " { i = 47; }\n",
+ " @Override\n",
+ " public String toString() {\n",
+ " if(s4 == null) // Delayed initialization:\n",
+ " s4 = \"Joy\";\n",
+ " return\n",
+ " \"s1 = \" + s1 + \"\\n\" +\n",
+ " \"s2 = \" + s2 + \"\\n\" +\n",
+ " \"s3 = \" + s3 + \"\\n\" +\n",
+ " \"s4 = \" + s4 + \"\\n\" +\n",
+ " \"i = \" + i + \"\\n\" +\n",
+ " \"toy = \" + toy + \"\\n\" +\n",
+ " \"castille = \" + castille;\n",
+ " }\n",
+ " public static void main(String[] args) {\n",
+ " Bath b = new Bath();\n",
+ " System.out.println(b);\n",
+ " }\n",
+ "}\n",
+ "/* Output:\n",
+ "Inside Bath()\n",
+ "Soap()\n",
+ "s1 = Happy\n",
+ "s2 = Happy\n",
+ "s3 = Joy\n",
+ "s4 = Joy\n",
+ "i = 47\n",
+ "toy = 3.14\n",
+ "castille = Constructed\n",
+ "*/\n"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "在 **Bath** 构造函数中,有一个代码块在所有初始化发生前就已经执行了。当你不在定义处初始化时,仍然不能保证在向对象引用发送消息之前执行任何初始化——如果你试图对未初始化的引用调用方法,则未初始化的引用将产生运行时异常。\n",
+ "\n",
+ "当调用 `toString()` 时,它将赋值 s4,以便在使用字段的时候所有的属性都已被初始化。\n",
+ "\n",
+ "\n",
+ "\n",
+ "## 继承语法\n",
+ "\n",
+ "继承是所有面向对象语言的一个组成部分。事实证明,在创建类时总是要继承,因为除非显式地继承其他类,否则就隐式地继承 Java 的标准根类对象(Object)。\n",
+ "\n",
+ "组合的语法很明显,但是继承使用了一种特殊的语法。当你继承时,你说,“这个新类与那个旧类类似。你可以在类主体的左大括号前的代码中声明这一点,使用关键字 **extends** 后跟基类的名称。当你这样做时,你将自动获得基类中的所有字段和方法。这里有一个例子:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "// reuse/Detergent.java\n",
+ "// (c)2017 MindView LLC: see Copyright.txt\n",
+ "// We make no guarantees that this code is fit for any purpose.\n",
+ "// Visit http://OnJava8.com for more book information.\n",
+ "// Inheritance syntax & properties\n",
+ "\n",
+ "class Cleanser {\n",
+ " private String s = \"Cleanser\";\n",
+ " public void append(String a) { s += a; }\n",
+ " public void dilute() { append(\" dilute()\"); }\n",
+ " public void apply() { append(\" apply()\"); }\n",
+ " public void scrub() { append(\" scrub()\"); }\n",
+ " @Override\n",
+ " public String toString() { return s; }\n",
+ " public static void main(String[] args) {\n",
+ " Cleanser x = new Cleanser();\n",
+ " x.dilute(); x.apply(); x.scrub();\n",
+ " System.out.println(x);\n",
+ " }\n",
+ "}\n",
+ "\n",
+ "public class Detergent extends Cleanser {\n",
+ " // Change a method:\n",
+ " @Override\n",
+ " public void scrub() {\n",
+ " append(\" Detergent.scrub()\");\n",
+ " super.scrub(); // Call base-class version\n",
+ " }\n",
+ " // Add methods to the interface:\n",
+ " public void foam() { append(\" foam()\"); }\n",
+ " // Test the new class:\n",
+ " public static void main(String[] args) {\n",
+ " Detergent x = new Detergent();\n",
+ " x.dilute();\n",
+ " x.apply();\n",
+ " x.scrub();\n",
+ " x.foam();\n",
+ " System.out.println(x);\n",
+ " System.out.println(\"Testing base class:\");\n",
+ " Cleanser.main(args);\n",
+ " }\n",
+ "}\n",
+ "/* Output:\n",
+ "Cleanser dilute() apply() Detergent.scrub() scrub()\n",
+ "foam()\n",
+ "Testing base class:\n",
+ "Cleanser dilute() apply() scrub()\n",
+ "*/\n"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "这演示了一些特性。首先,在 **Cleanser** 的 `append()` 方法中,使用 `+=` 操作符将字符串连接到 **s**,这是 Java 设计人员“重载”来处理字符串的操作符之一 (还有 + )。\n",
+ "\n",
+ "第二,**Cleanser** 和 **Detergent** 都包含一个 `main()` 方法。你可以为每个类创建一个 `main()` ; 这允许对每个类进行简单的测试。当你完成测试时,不需要删除 `main()`; 你可以将其留在以后的测试中。即使程序中有很多类都有 `main()` 方法,惟一运行的只有在命令行上调用的 `main()`。这里,当你使用 **java Detergent** 时候,就调用了 `Detergent.main()`。但是你也可以使用 **java Cleanser** 来调用 `Cleanser.main()`,即使 **Cleanser** 不是一个公共类。即使类只具有包访问权,也可以访问 `public main()`。\n",
+ "\n",
+ "在这里,`Detergent.main()` 显式地调用 `Cleanser.main()`,从命令行传递相同的参数(当然,你可以传递任何字符串数组)。\n",
+ "\n",
+ "**Cleanser** 中的所有方法都是公开的。请记住,如果不使用任何访问修饰符,则成员默认为包访问权限,这只允许包内成员访问。因此,如果没有访问修饰符,那么包内的任何人都可以使用这些方法。例如,**Detergent** 就没有问题。但是,如果其他包中的类继承 **Cleanser**,则该类只能访问 **Cleanser** 的公共成员。因此,为了允许继承,一般规则是所有字段为私有,所有方法为公共。(受保护成员也允许派生类访问;你以后会知道的。)在特定的情况下,你必须进行调整,但这是一个有用的指南。\n",
+ "\n",
+ "**Cleanser** 的接口中有一组方法: `append()`、`dilute()`、`apply()`、`scrub()` 和 `toString()`。因为 **Detergent** 是从 **Cleanser** 派生的(通过 **extends** 关键字),所以它会在其接口中自动获取所有这些方法,即使你没有在 **Detergent** 中看到所有这些方法的显式定义。那么,可以把继承看作是复用类。如在 `scrub()` 中所见,可以使用基类中定义的方法并修改它。在这里,你可以在新类中调用基类的该方法。但是在 `scrub()` 内部,不能简单地调用 `scrub()`,因为这会产生递归调用。为了解决这个问题,Java的 **super** 关键字引用了当前类继承的“超类”(基类)。因此表达式 `super.scrub()` 调用方法 `scrub()` 的基类版本。\n",
+ "\n",
+ "继承时,你不受限于使用基类的方法。你还可以像向类添加任何方法一样向派生类添加新方法:只需定义它。方法 `foam()` 就是一个例子。`Detergent.main()` 中可以看到,对于 **Detergent** 对象,你可以调用 **Cleanser** 和 **Detergent** 中可用的所有方法 (如 `foam()` )。\n",
+ "\n",
+ "\n",
+ "\n",
+ "### 初始化基类\n",
+ "\n",
+ "现在涉及到两个类:基类和派生类。想象派生类生成的结果对象可能会让人感到困惑。从外部看,新类与基类具有相同的接口,可能还有一些额外的方法和字段。但是继承并不只是复制基类的接口。当你创建派生类的对象时,它包含基类的子对象。这个子对象与你自己创建基类的对象是一样的。只是从外部看,基类的子对象被包装在派生类的对象中。\n",
+ "\n",
+ "必须正确初始化基类子对象,而且只有一种方法可以保证这一点 : 通过调用基类构造函数在构造函数中执行初始化,该构造函数具有执行基类初始化所需的所有适当信息和特权。Java 自动在派生类构造函数中插入对基类构造函数的调用。下面的例子展示了三个层次的继承:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "// reuse/Cartoon.java\n",
+ "// (c)2017 MindView LLC: see Copyright.txt\n",
+ "// We make no guarantees that this code is fit for any purpose.\n",
+ "// Visit http://OnJava8.com for more book information.\n",
+ "// Constructor calls during inheritance\n",
+ "\n",
+ "class Art {\n",
+ " Art() {\n",
+ " System.out.println(\"Art constructor\");\n",
+ " }\n",
+ "}\n",
+ "\n",
+ "class Drawing extends Art {\n",
+ " Drawing() {\n",
+ " System.out.println(\"Drawing constructor\");\n",
+ " }\n",
+ "}\n",
+ "\n",
+ "public class Cartoon extends Drawing {\n",
+ " public Cartoon() {\n",
+ " System.out.println(\"Cartoon constructor\");\n",
+ " }\n",
+ " public static void main(String[] args) {\n",
+ " Cartoon x = new Cartoon();\n",
+ " }\n",
+ "}\n",
+ "/* Output:\n",
+ "Art constructor\n",
+ "Drawing constructor\n",
+ "Cartoon constructor\n",
+ "*/\n"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "构造从基类“向外”进行,因此基类在派生类构造函数能够访问它之前进行初始化。即使不为 **Cartoon** 创建构造函数,编译器也会为你合成一个无参数构造函数,调用基类构造函数。尝试删除 **Cartoon** 构造函数来查看这个。\n",
+ "\n",
+ "\n",
+ "\n",
+ "### 带参数的构造函数\n",
+ "\n",
+ "上面的所有例子中构造函数都是无参数的 ; 编译器很容易调用这些构造函数,因为不需要参数。如果没有无参数的基类构造函数,或者必须调用具有参数的基类构造函数,则必须使用 **super** 关键字和适当的参数列表显式地编写对基类构造函数的调用:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "// reuse/Chess.java\n",
+ "// (c)2017 MindView LLC: see Copyright.txt\n",
+ "// We make no guarantees that this code is fit for any purpose.\n",
+ "// Visit http://OnJava8.com for more book information.\n",
+ "// Inheritance, constructors and arguments\n",
+ "\n",
+ "class Game {\n",
+ " Game(int i) {\n",
+ " System.out.println(\"Game constructor\");\n",
+ " }\n",
+ "}\n",
+ "\n",
+ "class BoardGame extends Game {\n",
+ " BoardGame(int i) {\n",
+ " super(i);\n",
+ " System.out.println(\"BoardGame constructor\");\n",
+ " }\n",
+ "}\n",
+ "\n",
+ "public class Chess extends BoardGame {\n",
+ " Chess() {\n",
+ " super(11);\n",
+ " System.out.println(\"Chess constructor\");\n",
+ " }\n",
+ " public static void main(String[] args) {\n",
+ " Chess x = new Chess();\n",
+ " }\n",
+ "}\n",
+ "/* Output:\n",
+ "Game constructor\n",
+ "BoardGame constructor\n",
+ "Chess constructor\n",
+ "*/\n"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "如果没有在 **BoardGame** 构造函数中调用基类构造函数,编译器就会报错找不到 `Game()` 的构造函数。此外,对基类构造函数的调用必须是派生类构造函数中的第一个操作。(如果你写错了,编译器会提醒你。)\n",
+ "\n",
+ "\n",
+ "\n",
+ "## 委托\n",
+ "\n",
+ "Java不直接支持的第三种重用关系称为委托。这介于继承和组合之间,因为你将一个成员对象放在正在构建的类中(比如组合),但同时又在新类中公开来自成员对象的所有方法(比如继承)。例如,宇宙飞船需要一个控制模块:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "// reuse/SpaceShipControls.java\n",
+ "// (c)2017 MindView LLC: see Copyright.txt\n",
+ "// We make no guarantees that this code is fit for any purpose.\n",
+ "// Visit http://OnJava8.com for more book information.\n",
+ "\n",
+ "public class SpaceShipControls {\n",
+ " void up(int velocity) {}\n",
+ " void down(int velocity) {}\n",
+ " void left(int velocity) {}\n",
+ " void right(int velocity) {}\n",
+ " void forward(int velocity) {}\n",
+ " void back(int velocity) {}\n",
+ " void turboBoost() {}\n",
+ "}\n"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "建造宇宙飞船的一种方法是使用继承:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "// reuse/DerivedSpaceShip.java\n",
+ "// (c)2017 MindView LLC: see Copyright.txt\n",
+ "// We make no guarantees that this code is fit for any purpose.\n",
+ "// Visit http://OnJava8.com for more book information.\n",
+ "\n",
+ "public class\n",
+ "DerivedSpaceShip extends SpaceShipControls {\n",
+ " private String name;\n",
+ " public DerivedSpaceShip(String name) {\n",
+ " this.name = name;\n",
+ " }\n",
+ " @Override\n",
+ " public String toString() { return name; }\n",
+ " public static void main(String[] args) {\n",
+ " DerivedSpaceShip protector =\n",
+ " new DerivedSpaceShip(\"NSEA Protector\");\n",
+ " protector.forward(100);\n",
+ " }\n",
+ "}\n"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "然而, **DerivedSpaceShip** 并不是真正的“一种” **SpaceShipControls** ,即使你“告诉” **DerivedSpaceShip** 调用 `forward()`。更准确地说,一艘宇宙飞船包含了 **SpaceShipControls **,同时 **SpaceShipControls** 中的所有方法都暴露在宇宙飞船中。委托解决了这个难题:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "// reuse/SpaceShipDelegation.java\n",
+ "// (c)2017 MindView LLC: see Copyright.txt\n",
+ "// We make no guarantees that this code is fit for any purpose.\n",
+ "// Visit http://OnJava8.com for more book information.\n",
+ "\n",
+ "public class SpaceShipDelegation {\n",
+ " private String name;\n",
+ " private SpaceShipControls controls =\n",
+ " new SpaceShipControls();\n",
+ " public SpaceShipDelegation(String name) {\n",
+ " this.name = name;\n",
+ " }\n",
+ " // Delegated methods:\n",
+ " public void back(int velocity) {\n",
+ " controls.back(velocity);\n",
+ " }\n",
+ " public void down(int velocity) {\n",
+ " controls.down(velocity);\n",
+ " }\n",
+ " public void forward(int velocity) {\n",
+ " controls.forward(velocity);\n",
+ " }\n",
+ " public void left(int velocity) {\n",
+ " controls.left(velocity);\n",
+ " }\n",
+ " public void right(int velocity) {\n",
+ " controls.right(velocity);\n",
+ " }\n",
+ " public void turboBoost() {\n",
+ " controls.turboBoost();\n",
+ " }\n",
+ " public void up(int velocity) {\n",
+ " controls.up(velocity);\n",
+ " }\n",
+ " public static void main(String[] args) {\n",
+ " SpaceShipDelegation protector =\n",
+ " new SpaceShipDelegation(\"NSEA Protector\");\n",
+ " protector.forward(100);\n",
+ " }\n",
+ "}\n"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "方法被转发到底层 **control** 对象,因此接口与继承的接口是相同的。但是,你对委托有更多的控制,因为你可以选择只在成员对象中提供方法的子集。\n",
+ "\n",
+ "虽然Java语言不支持委托,但是开发工具常常支持。例如,上面的例子是使用 JetBrains Idea IDE 自动生成的。\n",
+ "\n",
+ "\n",
+ "\n",
+ "## 结合组合与继承\n",
+ "\n",
+ "你将经常同时使用组合和继承。下面的例子展示了使用继承和组合创建类,以及必要的构造函数初始化:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "// reuse/PlaceSetting.java\n",
+ "// (c)2017 MindView LLC: see Copyright.txt\n",
+ "// We make no guarantees that this code is fit for any purpose.\n",
+ "// Visit http://OnJava8.com for more book information.\n",
+ "// Combining composition & inheritance\n",
+ "\n",
+ "class Plate {\n",
+ " Plate(int i) {\n",
+ " System.out.println(\"Plate constructor\");\n",
+ " }\n",
+ "}\n",
+ "\n",
+ "class DinnerPlate extends Plate {\n",
+ " DinnerPlate(int i) {\n",
+ " super(i);\n",
+ " System.out.println(\"DinnerPlate constructor\");\n",
+ " }\n",
+ "}\n",
+ "\n",
+ "class Utensil {\n",
+ " Utensil(int i) {\n",
+ " System.out.println(\"Utensil constructor\");\n",
+ " }\n",
+ "}\n",
+ "\n",
+ "class Spoon extends Utensil {\n",
+ " Spoon(int i) {\n",
+ " super(i);\n",
+ " System.out.println(\"Spoon constructor\");\n",
+ " }\n",
+ "}\n",
+ "\n",
+ "class Fork extends Utensil {\n",
+ " Fork(int i) {\n",
+ " super(i);\n",
+ " System.out.println(\"Fork constructor\");\n",
+ " }\n",
+ "}\n",
+ "\n",
+ "class Knife extends Utensil {\n",
+ " Knife(int i) {\n",
+ " super(i);\n",
+ " System.out.println(\"Knife constructor\");\n",
+ " }\n",
+ "}\n",
+ "\n",
+ "// A cultural way of doing something:\n",
+ "class Custom {\n",
+ " Custom(int i) {\n",
+ " System.out.println(\"Custom constructor\");\n",
+ " }\n",
+ "}\n",
+ "\n",
+ "public class PlaceSetting extends Custom {\n",
+ " private Spoon sp;\n",
+ " private Fork frk;\n",
+ " private Knife kn;\n",
+ " private DinnerPlate pl;\n",
+ " public PlaceSetting(int i) {\n",
+ " super(i + 1);\n",
+ " sp = new Spoon(i + 2);\n",
+ " frk = new Fork(i + 3);\n",
+ " kn = new Knife(i + 4);\n",
+ " pl = new DinnerPlate(i + 5);\n",
+ " System.out.println(\"PlaceSetting constructor\");\n",
+ " }\n",
+ " public static void main(String[] args) {\n",
+ " PlaceSetting x = new PlaceSetting(9);\n",
+ " }\n",
+ "}\n",
+ "/* Output:\n",
+ "Custom constructor\n",
+ "Utensil constructor\n",
+ "Spoon constructor\n",
+ "Utensil constructor\n",
+ "Fork constructor\n",
+ "Utensil constructor\n",
+ "Knife constructor\n",
+ "Plate constructor\n",
+ "DinnerPlate constructor\n",
+ "PlaceSetting constructor\n",
+ "*/\n"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "尽管编译器强制你初始化基类,并要求你在构造函数的开头就初始化基类,但它并不监视你以确保你初始化了成员对象。注意类是如何干净地分离的。你甚至不需要方法重用代码的源代码。你最多只导入一个包。(这对于继承和组合都是正确的。)\n",
+ "\n",
+ "\n",
+ "\n",
+ "### 保证适当的清理\n",
+ "\n",
+ "Java 没有 C++ 中析构函数的概念,析构函数是在对象被销毁时自动调用的方法。原因可能是,在Java中,通常是忘掉而不是销毁对象,从而允许垃圾收集器根据需要回收内存。通常这是可以的,但是有时你的类可能在其生命周期中执行一些需要清理的活动。初始化和清理章节提到,你无法知道垃圾收集器何时会被调用,甚至它是否会被调用。因此,如果你想为类清理一些东西,必须显式地编写一个特殊的方法来完成它,并确保客户端程序员知道他们必须调用这个方法。最重要的是——正如在\"异常\"章节中描述的——你必须通过在 **finally **子句中放置此类清理来防止异常。\n",
+ "\n",
+ "请考虑一个在屏幕上绘制图片的计算机辅助设计系统的例子:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "// reuse/CADSystem.java\n",
+ "// (c)2017 MindView LLC: see Copyright.txt\n",
+ "// We make no guarantees that this code is fit for any purpose.\n",
+ "// Visit http://OnJava8.com for more book information.\n",
+ "// Ensuring proper cleanup\n",
+ "// {java reuse.CADSystem}\n",
+ "package reuse;\n",
+ "\n",
+ "class Shape {\n",
+ " Shape(int i) {\n",
+ " System.out.println(\"Shape constructor\");\n",
+ " }\n",
+ " void dispose() {\n",
+ " System.out.println(\"Shape dispose\");\n",
+ " }\n",
+ "}\n",
+ "\n",
+ "class Circle extends Shape {\n",
+ " Circle(int i) {\n",
+ " super(i);\n",
+ " System.out.println(\"Drawing Circle\");\n",
+ " }\n",
+ " @Override\n",
+ " void dispose() {\n",
+ " System.out.println(\"Erasing Circle\");\n",
+ " super.dispose();\n",
+ " }\n",
+ "}\n",
+ "\n",
+ "class Triangle extends Shape {\n",
+ " Triangle(int i) {\n",
+ " super(i);\n",
+ " System.out.println(\"Drawing Triangle\");\n",
+ " }\n",
+ " @Override\n",
+ " void dispose() {\n",
+ " System.out.println(\"Erasing Triangle\");\n",
+ " super.dispose();\n",
+ " }\n",
+ "}\n",
+ "\n",
+ "class Line extends Shape {\n",
+ " private int start, end;\n",
+ " Line(int start, int end) {\n",
+ " super(start);\n",
+ " this.start = start;\n",
+ " this.end = end;\n",
+ " System.out.println(\n",
+ " \"Drawing Line: \" + start + \", \" + end);\n",
+ " }\n",
+ " @Override\n",
+ " void dispose() {\n",
+ " System.out.println(\n",
+ " \"Erasing Line: \" + start + \", \" + end);\n",
+ " super.dispose();\n",
+ " }\n",
+ "}\n",
+ "\n",
+ "public class CADSystem extends Shape {\n",
+ " private Circle c;\n",
+ " private Triangle t;\n",
+ " private Line[] lines = new Line[3];\n",
+ " public CADSystem(int i) {\n",
+ " super(i + 1);\n",
+ " for(int j = 0; j < lines.length; j++)\n",
+ " lines[j] = new Line(j, j*j);\n",
+ " c = new Circle(1);\n",
+ " t = new Triangle(1);\n",
+ " System.out.println(\"Combined constructor\");\n",
+ " }\n",
+ " @Override\n",
+ " public void dispose() {\n",
+ " System.out.println(\"CADSystem.dispose()\");\n",
+ " // The order of cleanup is the reverse\n",
+ " // of the order of initialization:\n",
+ " t.dispose();\n",
+ " c.dispose();\n",
+ " for(int i = lines.length - 1; i >= 0; i--)\n",
+ " lines[i].dispose();\n",
+ " super.dispose();\n",
+ " }\n",
+ " public static void main(String[] args) {\n",
+ " CADSystem x = new CADSystem(47);\n",
+ " try {\n",
+ " // Code and exception handling...\n",
+ " } finally {\n",
+ " x.dispose();\n",
+ " }\n",
+ " }\n",
+ "}\n",
+ "/* Output:\n",
+ "Shape constructor\n",
+ "Shape constructor\n",
+ "Drawing Line: 0, 0\n",
+ "Shape constructor\n",
+ "Drawing Line: 1, 1\n",
+ "Shape constructor\n",
+ "Drawing Line: 2, 4\n",
+ "Shape constructor\n",
+ "Drawing Circle\n",
+ "Shape constructor\n",
+ "Drawing Triangle\n",
+ "Combined constructor\n",
+ "CADSystem.dispose()\n",
+ "Erasing Triangle\n",
+ "Shape dispose\n",
+ "Erasing Circle\n",
+ "Shape dispose\n",
+ "Erasing Line: 2, 4\n",
+ "Shape dispose\n",
+ "Erasing Line: 1, 1\n",
+ "Shape dispose\n",
+ "Erasing Line: 0, 0\n",
+ "Shape dispose\n",
+ "Shape dispose\n",
+ "*/\n"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "这个系统中的所有东西都是某种 **Shape** (它本身是一种 **Object**,因为它是从根类隐式继承的) 。除了使用 **super** 调用该方法的基类版本外,每个类还覆盖 `dispose()` 方法。特定的 **Shape** 类——**Circle**、**Triangle** 和 **Line**,都有 “draw” 构造函数,尽管在对象的生命周期中调用的任何方法都可以负责做一些需要清理的事情。每个类都有自己的 `dispose()` 方法来将非内存的内容恢复到对象存在之前的状态。\n",
+ "\n",
+ "在 `main()` 中,有两个关键字是你以前没有见过的,在\"异常\"一章之前不会详细解释: **try** 和 **finally**。**try** 关键字表示后面的块 (用花括号分隔 )是一个受保护的区域,这意味着它得到了特殊处理。其中一个特殊处理是,无论 **try** 块如何退出,在这个保护区域之后的 **finally** 子句中的代码总是被执行。(通过异常处理,可以用许多不同寻常的方式留下 **try** 块。)这里,**finally** 子句的意思是,“无论发生什么,始终调用 `x.dispose()`。”\n",
+ "\n",
+ "在清理方法 (在本例中是 `dispose()` ) 中,还必须注意基类和成员对象清理方法的调用顺序,以防一个子对象依赖于另一个子对象。首先,按与创建的相反顺序执行特定于类的所有清理工作。(一般来说,这要求基类元素仍然是可访问的。) 然后调用基类清理方法,如这所示。\n",
+ "\n",
+ "在很多情况下,清理问题不是问题;你只需要让垃圾收集器来完成这项工作。但是,当你必须执行显式清理时,就需要多做努力,更加细心,因为在垃圾收集方面没有什么可以依赖的。可能永远不会调用垃圾收集器。如果调用,它可以按照它想要的任何顺序回收对象。除了内存回收外,你不能依赖垃圾收集来做任何事情。如果希望进行清理,可以使用自己的清理方法,不要使用 `finalize()`。\n",
+ "\n",
+ "\n",
+ "\n",
+ "### 名称隐藏\n",
+ "\n",
+ "如果 Java 基类的方法名多次重载,则在派生类中重新定义该方法名不会隐藏任何基类版本。不管方法是在这个级别定义的,还是在基类中定义的,重载都会起作用:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "// reuse/Hide.java\n",
+ "// (c)2017 MindView LLC: see Copyright.txt\n",
+ "// We make no guarantees that this code is fit for any purpose.\n",
+ "// Visit http://OnJava8.com for more book information.\n",
+ "// Overloading a base-class method name in a derived\n",
+ "// class does not hide the base-class versions\n",
+ "\n",
+ "class Homer {\n",
+ " char doh(char c) {\n",
+ " System.out.println(\"doh(char)\");\n",
+ " return 'd';\n",
+ " }\n",
+ " float doh(float f) {\n",
+ " System.out.println(\"doh(float)\");\n",
+ " return 1.0f;\n",
+ " }\n",
+ "}\n",
+ "\n",
+ "class Milhouse {}\n",
+ "\n",
+ "class Bart extends Homer {\n",
+ " void doh(Milhouse m) {\n",
+ " System.out.println(\"doh(Milhouse)\");\n",
+ " }\n",
+ "}\n",
+ "\n",
+ "public class Hide {\n",
+ " public static void main(String[] args) {\n",
+ " Bart b = new Bart();\n",
+ " b.doh(1);\n",
+ " b.doh('x');\n",
+ " b.doh(1.0f);\n",
+ " b.doh(new Milhouse());\n",
+ " }\n",
+ "}\n",
+ "/* Output:\n",
+ "doh(float)\n",
+ "doh(char)\n",
+ "doh(float)\n",
+ "doh(Milhouse)\n",
+ "*/\n"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "**Homer** 的所有重载方法在 **Bart** 中都是可用的,尽管 **Bart** 引入了一种新的重载方法。在下一章中你将看到,使用与基类中完全相同的签名和返回类型覆盖相同名称的方法要常见得多。否则就会令人困惑。\n",
+ "\n",
+ "你已经看到了Java 5 **@Override **注释,它不是关键字,但是可以像使用关键字一样使用它。当你打算重写一个方法时,你可以选择添加这个注释,如果你不小心用了重载而不是重写,编译器会产生一个错误消息:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "// reuse/Lisa.java\n",
+ "// (c)2017 MindView LLC: see Copyright.txt\n",
+ "// We make no guarantees that this code is fit for any purpose.\n",
+ "// Visit http://OnJava8.com for more book information.\n",
+ "// {WillNotCompile}\n",
+ "\n",
+ "class Lisa extends Homer {\n",
+ " @Override void doh(Milhouse m) {\n",
+ " System.out.println(\"doh(Milhouse)\");\n",
+ " }\n",
+ "}\n"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "**{WillNotCompile}** 标记将该文件排除在本书的 **Gradle** 构建之外,但是如果你手工编译它,你将看到:方法不会覆盖超类中的方法, **@Override** 注释防止你意外地重载。\n",
+ "\n",
+ "\n",
+ "\n",
+ "## 组合与继承的选择\n",
+ "\n",
+ "组合和继承都允许在新类中放置子对象(组合是显式的,而继承是隐式的)。你或许想知道这二者之间的区别,以及怎样在二者间做选择。\n",
+ "\n",
+ "当你想在新类中包含一个已有类的功能时,使用组合,而非继承。也就是说,在新类中嵌入一个对象(通常是私有的),以实现其功能。新类的使用者看到的是你所定义的新类的接口,而非嵌入对象的接口。\n",
+ "\n",
+ "有时让类的用户直接访问到新类中的组合成分是有意义的。只需将成员对象声明为 **public** 即可(可以把这当作“半委托”的一种)。成员对象隐藏了具体实现,所以这是安全的。当用户知道你正在组装一组部件时,会使得接口更加容易理解。下面的 car 对象是个很好的例子:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "// reuse/Car.java\n",
+ "// Composition with public objects\n",
+ "class Engine {\n",
+ " public void start() {}\n",
+ " public void rev() {}\n",
+ " public void stop() {}\n",
+ "}\n",
+ "\n",
+ "class Wheel {\n",
+ " public void inflate(int psi) {}\n",
+ "}\n",
+ "\n",
+ "class Window {\n",
+ " public void rollup() {}\n",
+ " public void rolldown() {}\n",
+ "}\n",
+ "\n",
+ "class Door {\n",
+ " public Window window = new Window();\n",
+ " \n",
+ " public void open() {}\n",
+ " public void close() {}\n",
+ "}\n",
+ "\n",
+ "public class Car {\n",
+ " public Engine engine = new Engine();\n",
+ " public Wheel[] wheel = new Wheel[4];\n",
+ " public Door left = new Door(), right = new Door(); // 2-door\n",
+ " \n",
+ " public Car() {\n",
+ " for (int i = 0; i < 4; i++) {\n",
+ " wheel[i] = new Wheel();\n",
+ " }\n",
+ " }\n",
+ " \n",
+ " public static void main(String[] args) {\n",
+ " Car car = new Car();\n",
+ " car.left.window.rollup();\n",
+ " car.wheel[0].inflate(72);\n",
+ " }\n",
+ "}"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "因为在这个例子中 car 的组合也是问题分析的一部分(不是底层设计的部分),所以声明成员为 **public** 有助于客户端程序员理解如何使用类,且降低了类创建者面临的代码复杂度。但是,记住这是一个特例。通常来说,属性还是应该声明为 **private**。\n",
+ "\n",
+ "当使用继承时,使用一个现有类并开发出它的新版本。通常这意味着使用一个通用类,并为了某个特殊需求将其特殊化。稍微思考下,你就会发现,用一个交通工具对象来组成一部车是毫无意义的——车不包含交通工具,它就是交通工具。这种“是一个”的关系是用继承来表达的,而“有一个“的关系则用组合来表达。\n",
+ "\n",
+ "\n",
+ "\n",
+ "## protected\n",
+ "\n",
+ "即然你已经接触到继承,关键字 **protected** 就变得有意义了。在理想世界中,仅靠关键字 **private** 就足够了。在实际项目中,却经常想把一个事物尽量对外界隐藏,而允许派生类的成员访问。\n",
+ "\n",
+ "关键字 **protected** 就起这个作用。它表示“就类的用户而言,这是 **private** 的。但对于任何继承它的子类或在同一包中的类,它是可访问的。”(**protected** 也提供了包访问权限)\n",
+ "\n",
+ "尽管可以创建 **protected** 属性,但是最好的方式是将属性声明为 **private** 以一直保留更改底层实现的权利。然后通过 **protected** 控制类的继承者的访问权限。"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "// reuse/Orc.java\n",
+ "// The protected keyword\n",
+ "class Villain {\n",
+ " private String name;\n",
+ " \n",
+ " protected void set(String nm) {\n",
+ " name = nm;\n",
+ " }\n",
+ " \n",
+ " Villain(String name) {\n",
+ " this.name = name;\n",
+ " }\n",
+ " \n",
+ " @Override\n",
+ " public String toString() {\n",
+ " return \"I'm a Villain and my name is \" + name;\n",
+ " }\n",
+ "}\n",
+ "\n",
+ "public class Orc extends Villain {\n",
+ " private int orcNumber;\n",
+ " \n",
+ " public Orc(String name, int orcNumber) {\n",
+ " super(name);\n",
+ " this.orcNumber = orcNumber;\n",
+ " }\n",
+ " \n",
+ " public void change(String name, int orcNumber) {\n",
+ " set(name); // Available because it's protected\n",
+ " this.orcNumber = orcNumber;\n",
+ " }\n",
+ " \n",
+ " @Override\n",
+ " public String toString() {\n",
+ " return \"Orc \" + orcNumber + \": \" + super.toString();\n",
+ " }\n",
+ " \n",
+ " public static void main(String[] args) {\n",
+ " Orc orc = new Orc(\"Limburger\", 12);\n",
+ " System.out.println(orc);\n",
+ " orc.change(\"Bob\", 19);\n",
+ " System.out.println(orc);\n",
+ " }\n",
+ "}"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "输出:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "Orc 12: I'm a Villain and my name is Limburger\n",
+ "Orc 19: I'm a Villain and my name is Bob"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "`change()` 方法可以访问 `set()` 方法,因为 `set()` 方法是 **protected**。注意到,类 **Orc** 的 `toString()` 方法也使用了基类的版本。\n",
+ "\n",
+ "\n",
+ "\n",
+ "## 向上转型\n",
+ "\n",
+ "继承最重要的方面不是为新类提供方法。它是新类与基类的一种关系。简而言之,这种关系可以表述为“新类是已有类的一种类型”。\n",
+ "\n",
+ "这种描述并非是解释继承的一种花哨方式,这是直接由语言支持的。例如,假设有一个基类 **Instrument** 代表音乐乐器和一个派生类 **Wind**。 因为继承保证了基类的所有方法在派生类中也是可用的,所以任意发送给该基类的消息也能发送给派生类。如果 **Instrument** 有一个 `play()` 方法,那么 **Wind** 也有该方法。这意味着你可以准确地说 **Wind** 对象也是一种类型的 **Instrument**。下面例子展示了编译器是如何支持这一概念的:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "// reuse/Wind.java\n",
+ "// Inheritance & upcasting\n",
+ "class Instrument {\n",
+ " public void play() {}\n",
+ " \n",
+ " static void tune(Instrument i) {\n",
+ " // ...\n",
+ " i.play();\n",
+ " }\n",
+ "}\n",
+ "\n",
+ "// Wind objects are instruments\n",
+ "// because they have the same interface:\n",
+ "public class Wind extends Instrument {\n",
+ " public static void main(String[] args) {\n",
+ " Wind flute = new Wind();\n",
+ " Instrument.tune(flute); // Upcasting\n",
+ " }\n",
+ "}"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "`tune()` 方法接受了一个 **Instrument** 类型的引用。但是,在 **Wind** 的 `main()` 方法里,`tune()` 方法却传入了一个 **Wind** 引用。鉴于 Java 对类型检查十分严格,一个接收一种类型的方法接受了另一种类型看起来很奇怪,除非你意识到 **Wind** 对象同时也是一个 **Instrument** 对象,而且 **Instrument** 的 `tune` 方法一定会存在于 **Wind** 中。在 `tune()` 中,代码对 **Instrument** 和 所有 **Instrument** 的派生类起作用,这种把 **Wind** 引用转换为 **Instrument** 引用的行为称作*向上转型*。\n",
+ "\n",
+ "该术语是基于传统的类继承图:图最上面是根,然后向下铺展。(当然你可以以任意方式画你认为有帮助的类图。)于是,**Wind.java** 的类图是:\n",
+ "\n",
+ "\n",
+ "\n",
+ "继承图中派生类转型为基类是向上的,所以通常称作*向上转型*。因为是从一个更具体的类转化为一个更一般的类,所以向上转型永远是安全的。也就是说,派生类是基类的一个超集。它可能比基类包含更多的方法,但它必须至少具有与基类一样的方法。在向上转型期间,类接口只可能失去方法,不会增加方法。这就是为什么编译器在没有任何明确转型或其他特殊标记的情况下,仍然允许向上转型的原因。\n",
+ "\n",
+ "也可以执行与向上转型相反的向下转型,但是会有问题,对于该问题会放在下一章和“类型信息”一章进行更深入的探讨。\n",
+ "\n",
+ "### 再论组合和继承\n",
+ "\n",
+ "在面向对象编程中,创建和使用代码最有可能的方法是将数据和方法一起打包到类中,然后使用该类的对象。也可以使用已有的类通过组合来创建新类。继承其实不太常用。因此尽管在教授 OOP 的过程中我们多次强调继承,但这并不意味着要尽可能使用它。恰恰相反,尽量少使用它,除非确实使用继承是有帮助的。一种判断使用组合还是继承的最清晰的方法是问一问自己是否需要把新类向上转型为基类。如果必须向上转型,那么继承就是必要的,但如果不需要,则要进一步考虑是否该采用继承。“多态”一章提出了一个使用向上转型的最有力的理由,但是只要记住问一问“我需要向上转型吗?”,就能在这两者中作出较好的选择。\n",
+ "\n",
+ "\n",
+ "\n",
+ "## final关键字\n",
+ "\n",
+ "根据上下文环境,Java 的关键字 **final** 的含义有些微的不同,但通常它指的是“这是不能被改变的”。防止改变有两个原因:设计或效率。因为这两个原因相差很远,所以有可能误用关键字 **final**。\n",
+ "\n",
+ "以下几节讨论了可能使用 **final** 的三个地方:数据、方法和类。\n",
+ "\n",
+ "### final 数据\n",
+ "\n",
+ "许多编程语言都有某种方法告诉编译器有一块数据是恒定不变的。恒定是有用的,如:\n",
+ "\n",
+ "1. 一个永不改变的编译时常量。\n",
+ "2. 一个在运行时初始化就不会改变的值。\n",
+ "\n",
+ "对于编译时常量这种情况,编译器可以把常量带入计算中;也就是说,可以在编译时计算,减少了一些运行时的负担。在 Java 中,这类常量必须是基本类型,而且用关键字 **final** 修饰。你必须在定义常量的时候进行赋值。\n",
+ "\n",
+ "一个被 **static** 和 **final** 同时修饰的属性只会占用一段不能改变的存储空间。\n",
+ "\n",
+ "当用 **final** 修饰对象引用而非基本类型时,其含义会有一点令人困惑。对于基本类型,**final** 使数值恒定不变,而对于对象引用,**final** 使引用恒定不变。一旦引用被初始化指向了某个对象,它就不能改为指向其他对象。但是,对象本身是可以修改的,Java 没有提供将任意对象设为常量的方法。(你可以自己编写类达到使对象恒定不变的效果)这一限制同样适用数组,数组也是对象。\n",
+ "\n",
+ "下面例子展示了 **final** 属性的使用:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "// reuse/FinalData.java\n",
+ "// The effect of final on fields\n",
+ "import java.util.*;\n",
+ "\n",
+ "class Value {\n",
+ " int i; // package access\n",
+ " \n",
+ " Value(int i) {\n",
+ " this.i = i;\n",
+ " }\n",
+ "}\n",
+ "\n",
+ "public class FinalData {\n",
+ " private static Random rand = new Random(47);\n",
+ " private String id;\n",
+ " \n",
+ " public FinalData(String id) {\n",
+ " this.id = id;\n",
+ " }\n",
+ " // Can be compile-time constants:\n",
+ " private final int valueOne = 9;\n",
+ " private static final int VALUE_TWO = 99;\n",
+ " // Typical public constant:\n",
+ " public static final int VALUE_THREE = 39;\n",
+ " // Cannot be compile-time constants:\n",
+ " private final int i4 = rand.nextInt(20);\n",
+ " static final int INT_5 = rand.nextInt(20);\n",
+ " private Value v1 = new Value(11);\n",
+ " private final Value v2 = new Value(22);\n",
+ " private static final Value VAL_3 = new Value(33);\n",
+ " // Arrays:\n",
+ " private final int[] a = {1, 2, 3, 4, 5, 6};\n",
+ " \n",
+ " @Override\n",
+ " public String toString() {\n",
+ " return id + \": \" + \"i4 = \" + i4 + \", INT_5 = \" + INT_5;\n",
+ " }\n",
+ " \n",
+ " public static void main(String[] args) {\n",
+ " FinalData fd1 = new FinalData(\"fd1\");\n",
+ " //- fd1.valueOne++; // Error: can't change value\n",
+ " fd1.v2.i++; // Object isn't constant\n",
+ " fd1.v1 = new Value(9); // OK -- not final\n",
+ " for (int i = 0; i < fd1.a.length; i++) {\n",
+ " fd1.a[i]++; // Object isn't constant\n",
+ " }\n",
+ " //- fd1.v2 = new Value(0); // Error: Can't\n",
+ " //- fd1.VAL_3 = new Value(1); // change reference\n",
+ " //- fd1.a = new int[3];\n",
+ " System.out.println(fd1);\n",
+ " System.out.println(\"Creating new FinalData\");\n",
+ " FinalData fd2 = new FinalData(\"fd2\");\n",
+ " System.out.println(fd1);\n",
+ " System.out.println(fd2);\n",
+ " }\n",
+ "}"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "输出:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "fd1: i4 = 15, INT_5 = 18\n",
+ "Creating new FinalData\n",
+ "fd1: i4 = 15, INT_5 = 18\n",
+ "fd2: i4 = 13, INT_5 = 18"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "因为 **valueOne** 和 **VALUE_TWO** 都是带有编译时值的 **final** 基本类型,它们都可用作编译时常量,没有多大区别。**VALUE_THREE** 是一种更加典型的常量定义的方式:**public** 意味着可以在包外访问,**static** 强调只有一个,**final** 说明是一个常量。\n",
+ "\n",
+ "按照惯例,带有恒定初始值的 **final** **static** 基本变量(即编译时常量)命名全部使用大写,单词之间用下划线分隔。(源于 C 语言中定义常量的方式。)\n",
+ "\n",
+ "我们不能因为某数据被 **final** 修饰就认为在编译时可以知道它的值。由上例中的 **i4** 和 **INT_5** 可以看出,它们在运行时才会赋值随机数。示例部分也展示了将 **final** 值定义为 **static** 和非 **static** 的区别。此区别只有当值在运行时被初始化时才会显现,因为编译器对编译时数值一视同仁。(而且编译时数值可能因优化而消失。)当运行程序时就能看到这个区别。注意到 **fd1** 和 **fd2** 的 **i4** 值不同,但 **INT_5** 的值并没有因为创建了第二个 **FinalData** 对象而改变,这是因为它是 **static** 的,在加载时已经被初始化,并不是每次创建新对象时都初始化。\n",
+ "\n",
+ "**v1** 到 **VAL_3** 变量说明了 **final** 引用的意义。正如你在 `main()` 中所见,**v2** 是 **final** 的并不意味着你不能修改它的值。因为它是引用,所以只是说明它不能指向一个新的对象。这对于数组具有同样的意义,数组只不过是另一种引用。(我不知道有什么方法能使数组引用本身成为 **final**。)看起来,声明引用为 **final** 没有声明基本类型 **final** 有用。\n",
+ "\n",
+ "### 空白 final\n",
+ "\n",
+ "空白 final 指的是没有初始化值的 **final** 属性。编译器确保空白 final 在使用前必须被初始化。这样既能使一个类的每个对象的 **final** 属性值不同,也能保持它的不变性。"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "// reuse/BlankFinal.java\n",
+ "// \"Blank\" final fields\n",
+ "class Poppet {\n",
+ " private int i;\n",
+ " \n",
+ " Poppet(int ii) {\n",
+ " i = ii;\n",
+ " }\n",
+ "}\n",
+ "\n",
+ "public class BlankFinal {\n",
+ " private final int i = 0; // Initialized final\n",
+ " private final int j; // Blank final\n",
+ " private final Poppet p; // Blank final reference\n",
+ " // Blank finals MUST be initialized in constructor\n",
+ " public BlankFinal() {\n",
+ " j = 1; // Initialize blank final\n",
+ " p = new Poppet(1); // Init blank final reference\n",
+ " }\n",
+ " \n",
+ " public BlankFinal(int x) {\n",
+ " j = x; // Initialize blank final\n",
+ " p = new Poppet(x); // Init blank final reference\n",
+ " }\n",
+ " \n",
+ " public static void main(String[] args) {\n",
+ " new BlankFinal();\n",
+ " new BlankFinal(47);\n",
+ " }\n",
+ "}"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "你必须在定义时或在每个构造器中执行 final 变量的赋值操作。这保证了 final 属性在使用前已经被初始化过。\n",
+ "\n",
+ "### final 参数\n",
+ "\n",
+ "在参数列表中,将参数声明为 final 意味着在方法中不能改变参数指向的对象或基本变量:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "// reuse/FinalArguments.java\n",
+ "// Using \"final\" with method arguments\n",
+ "class Gizmo {\n",
+ " public void spin() {\n",
+ " \n",
+ " }\n",
+ "}\n",
+ "\n",
+ "public class FinalArguments {\n",
+ " void with(final Gizmo g) {\n",
+ " //-g = new Gizmo(); // Illegal -- g is final\n",
+ " }\n",
+ " \n",
+ " void without(Gizmo g) {\n",
+ " g = new Gizmo(); // OK -- g is not final\n",
+ " g.spin();\n",
+ " }\n",
+ " \n",
+ " //void f(final int i) { i++; } // Can't change\n",
+ " // You can only read from a final primitive\n",
+ " int g(final int i) {\n",
+ " return i + 1;\n",
+ " }\n",
+ " \n",
+ " public static void main(String[] args) {\n",
+ " FinalArguments bf = new FinalArguments();\n",
+ " bf.without(null);\n",
+ " bf.with(null);\n",
+ " }\n",
+ "}"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "方法 `f()` 和 `g()` 展示了 **final** 基本类型参数的使用情况。你只能读取而不能修改参数。这个特性主要用于传递数据给匿名内部类。这将在”内部类“章节中详解。\n",
+ "\n",
+ "### final 方法\n",
+ "\n",
+ "使用 **final** 方法的原因有两个。第一个原因是给方法上锁,防止子类通过覆写改变方法的行为。这是出于继承的考虑,确保方法的行为不会因继承而改变。\n",
+ "\n",
+ "过去建议使用 **final** 方法的第二个原因是效率。在早期的 Java 实现中,如果将一个方法指明为 **final**,就是同意编译器把对该方法的调用转化为内嵌调用。当编译器遇到 **final** 方法的调用时,就会很小心地跳过普通的插入代码以执行方法的调用机制(将参数压栈,跳至方法代码处执行,然后跳回并清理栈中的参数,最终处理返回值),而用方法体内实际代码的副本替代方法调用。这消除了方法调用的开销。但是如果一个方法很大代码膨胀,你也许就看不到内嵌带来的性能提升,因为内嵌调用带来的性能提高被花费在方法里的时间抵消了。\n",
+ "\n",
+ "在最近的 Java 版本中,虚拟机可以探测到这些情况(尤其是 *hotspot* 技术),并优化去掉这些效率反而降低的内嵌调用方法。有很长一段时间,使用 **final** 来提高效率都被阻止。你应该让编译器和 JVM 处理性能问题,只有在为了明确禁止覆写方法时才使用 **final**。\n",
+ "\n",
+ "### final 和 private\n",
+ "\n",
+ "类中所有的 **private** 方法都隐式地指定为 **final**。因为不能访问 **private** 方法,所以不能覆写它。可以给 **private** 方法添加 **final** 修饰,但是并不能给方法带来额外的含义。\n",
+ "\n",
+ "以下情况会令人困惑,当你试图覆写一个 **private** 方法(隐式是 **final** 的)时,看上去奏效,而且编译器不会给出错误信息:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "// reuse/FinalOverridingIllusion.java\n",
+ "// It only looks like you can override\n",
+ "// a private or private final method\n",
+ "class WithFinals {\n",
+ " // Identical to \"private\" alone:\n",
+ " private final void f() {\n",
+ " System.out.println(\"WithFinals.f()\");\n",
+ " }\n",
+ " // Also automatically \"final\":\n",
+ " private void g() {\n",
+ " System.out.println(\"WithFinals.g()\");\n",
+ " }\n",
+ "}\n",
+ "\n",
+ "class OverridingPrivate extends WithFinals {\n",
+ " private final void f() {\n",
+ " System.out.println(\"OverridingPrivate.f()\");\n",
+ " }\n",
+ " \n",
+ " private void g() {\n",
+ " System.out.println(\"OverridingPrivate.g()\");\n",
+ " }\n",
+ "}\n",
+ "\n",
+ "class OverridingPrivate2 extends OverridingPrivate {\n",
+ " public final void f() {\n",
+ " System.out.println(\"OverridingPrivate2.f()\");\n",
+ " } \n",
+ " \n",
+ " public void g() {\n",
+ " System.out.println(\"OverridingPrivate2.g()\");\n",
+ " }\n",
+ "}\n",
+ "\n",
+ "public class FinalOverridingIllusion {\n",
+ " public static void main(String[] args) {\n",
+ " OverridingPrivate2 op2 = new OverridingPrivate2();\n",
+ " op2.f();\n",
+ " op2.g();\n",
+ " // You can upcast:\n",
+ " OverridingPrivate op = op2;\n",
+ " // But you can't call the methods:\n",
+ " //- op.f();\n",
+ " //- op.g();\n",
+ " // Same here:\n",
+ " WithFinals wf = op2;\n",
+ " //- wf.f();\n",
+ " //- wf.g();\n",
+ " }\n",
+ "}"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "输出:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "OverridingPrivate2.f()\n",
+ "OverridingPrivate2.g()"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "\"覆写\"只发生在方法是基类的接口时。也就是说,必须能将一个对象向上转型为基类并调用相同的方法(这一点在下一章阐明)。如果一个方法是 **private** 的,它就不是基类接口的一部分。它只是隐藏在类内部的代码,且恰好有相同的命名而已。但是如果你在派生类中以相同的命名创建了 **public**,**protected** 或包访问权限的方法,这些方法与基类中的方法没有联系,你没有覆写方法,只是在创建新的方法而已。由于 **private** 方法无法触及且能有效隐藏,除了把它看作类中的一部分,其他任何事物都不需要考虑到它。\n",
+ "\n",
+ "### final 类\n",
+ "\n",
+ "当说一个类是 **final** (**final** 关键字在类定义之前),就意味着它不能被继承。之所以这么做,是因为类的设计就是永远不需要改动,或者是出于安全考虑不希望它有子类。"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "// reuse/Jurassic.java\n",
+ "// Making an entire class final\n",
+ "class SmallBrain {}\n",
+ "\n",
+ "final class Dinosaur {\n",
+ " int i = 7;\n",
+ " int j = 1;\n",
+ " SmallBrain x = new SmallBrain();\n",
+ " \n",
+ " void f() {}\n",
+ "}\n",
+ "\n",
+ "//- class Further extends Dinosaur {}\n",
+ "// error: Cannot extend final class 'Dinosaur'\n",
+ "public class Jurassic {\n",
+ " public static void main(String[] args) {\n",
+ " Dinosaur n = new Dinosaur();\n",
+ " n.f();\n",
+ " n.i = 40;\n",
+ " n.j++;\n",
+ " }\n",
+ "}"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "**final** 类的属性可以根据个人选择是或不是 **final**。这同样适用于不管类是否是 **final** 的内部 **final** 属性。然而,由于 **final** 类禁止继承,类中所有的方法都被隐式地指定为 **final**,所以没有办法覆写它们。你可以在 final 类中的方法加上 **final** 修饰符,但不会增加任何意义。\n",
+ "\n",
+ "### final 忠告\n",
+ "\n",
+ "在设计类时将一个方法指明为 final 看上去是明智的。你可能会觉得没人会覆写那个方法。有时这是对的。\n",
+ "\n",
+ "但请留意你的假设。通常来说,预见一个类如何被复用是很困难的,特别是通用类。如果将一个方法指定为 **final**,可能会防止其他程序员的项目中通过继承来复用你的类,而这仅仅是因为你没有想到它被以那种方式使用。\n",
+ "\n",
+ "Java 标准类库就是一个很好的例子。尤其是 Java 1.0/1.1 的 **Vector** 类被广泛地使用,而且从效率考虑(这近乎是个幻想),如果它的所有方法没有被指定为 **final**,可能会更加有用。很容易想到,你可能会继承并覆写这么一个基础类,但是设计者们认为这么做不合适。有两个讽刺的原因。第一,**Stack** 继承自 **Vector**,就是说 **Stack** 是个 **Vector**,但从逻辑上来说不对。尽管如此,Java 设计者们仍然这么做,在用这种方式创建 **Stack** 时,他们应该意识到了 **final** 方法过于约束。\n",
+ "\n",
+ "第二,**Vector** 中的很多重要方法,比如 `addElement()` 和 `elementAt()` 方法都是同步的。在“并发编程”一章中会看同步会导致很大的执行开销,可能会抹煞 **final** 带来的好处。这加强了程序员永远无法正确猜到优化应该发生在何处的观点。如此笨拙的设计却出现在每个人都要使用的标准库中,太糟糕了。庆幸的是,现代 Java 容器用 **ArrayList** 代替了 **Vector**,它的行为要合理得多。不幸的是,仍然有很多新代码使用旧的集合类库,其中就包括 **Vector**。\n",
+ "\n",
+ "Java 1.0/1.1 标准类库中另一个重要的类是 **Hashtable**(后来被 **HashMap** 取代),它不含任何 **final** 方法。本书中其他地方也提到,很明显不同的类是由不同的人设计的。**Hashtable** 就比 **Vector** 中的方法名简洁得多,这又是一条证据。对于类库的使用者来说,这是一个本不应该如此草率的事情。这种不规则的情况造成用户需要做更多的工作——这是对粗糙的设计和代码的又一讽刺。\n",
+ "\n",
+ "\n",
+ "\n",
+ "## 类初始化和加载\n",
+ "\n",
+ "在许多传统语言中,程序在启动时一次性全部加载。接着初始化,然后程序开始运行。必须仔细控制这些语言的初始化过程,以确保 **statics** 初始化的顺序不会造成麻烦。在 C++ 中,如果一个 **static** 期望使用另一个 **static**,而另一个 **static** 还没有初始化,就会出现问题。\n",
+ "\n",
+ "Java 中不存在这样的问题,因为它采用了一种不同的方式加载。因为 Java 中万物皆对象,所以加载活动就容易得多。记住每个类的编译代码都存在于它自己独立的文件中。该文件只有在使用程序代码时才会被加载。一般可以说“类的代码在首次使用时加载“。这通常是指创建类的第一个对象,或者是访问了类的 **static** 属性或方法。构造器也是一个 **static** 方法尽管它的 **static** 关键字是隐式的。因此,准确地说,一个类当它任意一个 **static** 成员被访问时,就会被加载。\n",
+ "\n",
+ "首次使用时就是 **static** 初始化发生时。所有的 **static** 对象和 **static** 代码块在加载时按照文本的顺序(在类中定义的顺序)依次初始化。**static** 变量只被初始化一次。\n",
+ "\n",
+ "### 继承和初始化\n",
+ "\n",
+ "了解包括继承在内的整个初始化过程是有帮助的,这样可以对所发生的一切有全局性的把握。考虑下面的例子:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "// reuse/Beetle.java\n",
+ "// The full process of initialization\n",
+ "class Insect {\n",
+ " private int i = 9;\n",
+ " protected int j;\n",
+ " \n",
+ " Insect() {\n",
+ " System.out.println(\"i = \" + i + \", j = \" + j);\n",
+ " j = 39;\n",
+ " }\n",
+ " \n",
+ " private static int x1 = printInit(\"static Insect.x1 initialized\");\n",
+ " \n",
+ " static int printInit(String s) {\n",
+ " System.out.println(s);\n",
+ " return 47;\n",
+ " }\n",
+ "}\n",
+ "\n",
+ "public class Beetle extends Insect {\n",
+ " private int k = printInit(\"Beetle.k.initialized\");\n",
+ " \n",
+ " public Beetle() {\n",
+ " System.out.println(\"k = \" + k);\n",
+ " System.out.println(\"j = \" + j);\n",
+ " }\n",
+ " \n",
+ " private static int x2 = printInit(\"static Beetle.x2 initialized\");\n",
+ " \n",
+ " public static void main(String[] args) {\n",
+ " System.out.println(\"Beetle constructor\");\n",
+ " Beetle b = new Beetle();\n",
+ " }\n",
+ "}"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "输出:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "static Insect.x1 initialized\n",
+ "static Beetle.x2 initialized\n",
+ "Beetle constructor\n",
+ "i = 9, j = 0\n",
+ "Beetle.k initialized\n",
+ "k = 47\n",
+ "j = 39"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "当执行 **java Beetle**,首先会试图访问 **Beetle** 类的 `main()` 方法(一个静态方法),加载器启动并找出 **Beetle** 类的编译代码(在名为 **Beetle.class** 的文件中)。在加载过程中,编译器注意到有一个基类,于是继续加载基类。不论是否创建了基类的对象,基类都会被加载。(可以尝试把创建基类对象的代码注释掉证明这点。)\n",
+ "\n",
+ "如果基类还存在自身的基类,那么第二个基类也将被加载,以此类推。接下来,根基类(例子中根基类是 **Insect**)的 **static** 的初始化开始执行,接着是派生类,以此类推。这点很重要,因为派生类中 **static** 的初始化可能依赖基类成员是否被正确地初始化。\n",
+ "\n",
+ "至此,必要的类都加载完毕,可以创建对象了。首先,对象中的所有基本类型变量都被置为默认值,对象引用被设为 **null** —— 这是通过将对象内存设为二进制零值一举生成的。接着会调用基类的构造器。本例中是自动调用的,但是你也可以使用 **super** 调用指定的基类构造器(在 **Beetle** 构造器中的第一步操作)。基类构造器和派生类构造器一样以相同的顺序经历相同的过程。当基类构造器完成后,实例变量按文本顺序初始化。最终,构造器的剩余部分被执行。\n",
+ "\n",
+ "\n",
+ "\n",
+ "## 本章小结\n",
+ "\n",
+ "继承和组合都是从已有类型创建新类型。组合将已有类型作为新类型底层实现的一部分,继承复用的是接口。\n",
+ "\n",
+ "使用继承时,派生类具有基类接口,因此可以向上转型为基类,这对于多态至关重要,在下一章你将看到。\n",
+ "\n",
+ "尽管在面向对象编程时极力强调继承,但在开始设计时,优先使用组合(或委托),只有当确实需要时再使用继承。组合更具灵活性。另外,通过对成员类型使用继承的技巧,可以在运行时改变成员的类型和行为。因此,可以在运行时改变组合对象的行为。\n",
+ "\n",
+ "在设计一个系统时,目标是发现或创建一系列类,每个类有特定的用途,而且既不应太大(包括太多功能难以复用),也不应太小(不添加其他功能就无法使用)。如果设计变得过于复杂,通过将现有类拆分为更小的部分而添加更多的对象,通常是有帮助的。\n",
+ "\n",
+ "当开始设计一个系统时,记住程序开发是一个增量过程,正如人类学习。它依赖实验,你可以尽可能多做分析,然而在项目开始时仍然无法知道所有的答案。如果把项目视作一个有机的,进化着的生命去培养,而不是视为像摩天大楼一样快速见效,就能获得更多的成功和更迅速的反馈。继承和组合正是可以让你执行如此实验的面向对象编程中最基本的两个工具。\n",
+ "\n",
+ "\n",
+ "\n",
+ ""
+ ]
+ }
+ ],
+ "metadata": {},
+ "nbformat": 4,
+ "nbformat_minor": 4
+}
diff --git a/jupyter/09-Polymorphism.ipynb b/jupyter/09-Polymorphism.ipynb
new file mode 100644
index 00000000..f06488fb
--- /dev/null
+++ b/jupyter/09-Polymorphism.ipynb
@@ -0,0 +1,1809 @@
+{
+ "cells": [
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "[TOC]\n",
+ "\n",
+ "\n",
+ "# 第九章 多态\n",
+ "\n",
+ "> 曾经有人请教我 “ Babbage 先生,如果输入错误的数字到机器中,会得出正确结果吗?” 我无法理解产生如此问题的概念上的困惑。 —— Charles Babbage (1791 - 1871)\n",
+ "\n",
+ "多态是面向对象编程语言中,继数据抽象和继承之外的第三个重要特性。\n",
+ "\n",
+ "多态提供了另一个维度的接口与实现分离,以解耦做什么和怎么做。多态不仅能改善代码的组织,提高代码的可读性,而且能创建有扩展性的程序——无论在最初创建项目时还是在添加新特性时都可以“生长”的程序。\n",
+ "\n",
+ "封装通过合并特征和行为来创建新的数据类型。隐藏实现通过将细节**私有化**把接口与实现分离。这种类型的组织机制对于有面向过程编程背景的人来说,更容易理解。而多态是消除类型之间的耦合。在上一章中,继承允许把一个对象视为它本身的类型或它的基类类型。这样就能把很多派生自一个基类的类型当作同一类型处理,因而一段代码就可以无差别地运行在所有不同的类型上了。多态方法调用允许一种类型表现出与相似类型的区别,只要这些类型派生自一个基类。这种区别是当你通过基类调用时,由方法的不同行为表现出来的。\n",
+ "\n",
+ "在本章中,通过一些基本、简单的例子(这些例子中只保留程序中与多态有关的行为),你将逐步学习多态(也称为*动态绑定*或*后期绑定*或*运行时绑定*)。\n",
+ "\n",
+ "\n",
+ "\n",
+ "## 向上转型回顾\n",
+ "\n",
+ "在上一章中,你看到了如何把一个对象视作它的自身类型或它的基类类型。这种把一个对象引用当作它的基类引用的做法称为向上转型,因为继承图中基类一般都位于最上方。\n",
+ "\n",
+ "同样你也在下面的音乐乐器例子中发现了问题。即然几个例子都要演奏乐符(**Note**),首先我们先在包中单独创建一个 Note 枚举类:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "// polymorphism/music/Note.java\n",
+ "// Notes to play on musical instruments\n",
+ "package polymorphism.music;\n",
+ "\n",
+ "public enum Note {\n",
+ " MIDDLE_C, C_SHARP, B_FLAT; // Etc.\n",
+ "}"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "枚举已经在”第 6 章初始化和清理“一章中介绍过了。\n",
+ "\n",
+ "这里,**Wind** 是一种 **Instrument**;因此,**Wind** 继承 **Instrument**:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "// polymorphism/music/Instrument.java\n",
+ "package polymorphism.music;\n",
+ "\n",
+ "class Instrument {\n",
+ " public void play(Note n) {\n",
+ " System.out.println(\"Instrument.play()\");\n",
+ " }\n",
+ "}\n",
+ "\n",
+ "// polymorphism/music/Wind.java\n",
+ "package polymorphism.music;\n",
+ "// Wind objects are instruments\n",
+ "// because they have the same interface:\n",
+ "public class Wind extends Instrument {\n",
+ " // Redefine interface method:\n",
+ " @Override\n",
+ " public void play(Note n) {\n",
+ " System.out.println(\"Wind.play() \" + n);\n",
+ " }\n",
+ "}"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "**Music** 的方法 `tune()` 接受一个 **Instrument** 引用,同时也接受任何派生自 **Instrument** 的类引用:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "// polymorphism/music/Music.java\n",
+ "// Inheritance & upcasting\n",
+ "// {java polymorphism.music.Music}\n",
+ "package polymorphism.music;\n",
+ "\n",
+ "public class Music {\n",
+ " public static void tune(Instrument i) {\n",
+ " // ...\n",
+ " i.play(Note.MIDDLE_C);\n",
+ " }\n",
+ " \n",
+ " public static void main(String[] args) {\n",
+ " Wind flute = new Wind();\n",
+ " tune(flute); // Upcasting\n",
+ " }\n",
+ "}"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "输出:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "Wind.play() MIDDLE_C"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "在 `main()` 中你看到了 `tune()` 方法传入了一个 **Wind** 引用,而没有做类型转换。这样做是允许的—— **Instrument** 的接口一定存在于 **Wind** 中,因此 **Wind** 继承了 **Instrument**。从 **Wind** 向上转型为 **Instrument** 可能“缩小”接口,但不会比 **Instrument** 的全部接口更少。\n",
+ "\n",
+ "### 忘掉对象类型\n",
+ "\n",
+ "**Music.java** 看起来似乎有点奇怪。为什么所有人都故意忘记掉对象类型呢?当向上转型时,就会发生这种情况,而且看起来如果 `tune()` 接受的参数是一个 **Wind** 引用会更为直观。这会带来一个重要问题:如果你那么做,就要为系统内 **Instrument** 的每种类型都编写一个新的 `tune()` 方法。假设按照这种推理,再增加 **Stringed** 和 **Brass** 这两种 **Instrument** :"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "// polymorphism/music/Music2.java\n",
+ "// Overloading instead of upcasting\n",
+ "// {java polymorphism.music.Music2}\n",
+ "package polymorphism.music;\n",
+ "\n",
+ "class Stringed extends Instrument {\n",
+ " @Override\n",
+ " public void play(Note n) {\n",
+ " System.out.println(\"Stringed.play() \" + n);\n",
+ " }\n",
+ "}\n",
+ "\n",
+ "class Brass extends Instrument {\n",
+ " @Override\n",
+ " public void play(Note n) {\n",
+ " System.out.println(\"Brass.play() \" + n);\n",
+ " }\n",
+ "}\n",
+ "\n",
+ "public class Music2 {\n",
+ " public static void tune(Wind i) {\n",
+ " i.play(Note.MIDDLE_C);\n",
+ " }\n",
+ " \n",
+ " public static void tune(Stringed i) {\n",
+ " i.play(Note.MIDDLE_C);\n",
+ " }\n",
+ " \n",
+ " public static void tune(Brass i) {\n",
+ " i.play(Note.MIDDLE_C);\n",
+ " }\n",
+ " \n",
+ " public static void main(String[] args) {\n",
+ " Wind flute = new Wind();\n",
+ " Stringed violin = new Stringed();\n",
+ " Brass frenchHorn = new Brass();\n",
+ " tune(flute); // No upcasting\n",
+ " tune(violin);\n",
+ " tune(frenchHorn);\n",
+ " }\n",
+ "}"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "输出:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "Wind.play() MIDDLE_C\n",
+ "Stringed.play() MIDDLE_C\n",
+ "Brass.play() MIDDLE_C"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "这样行得通,但是有一个主要缺点:必须为添加的每个新 **Instrument** 类编写特定的方法。这意味着开始时就需要更多的编程,而且以后如果添加类似 `tune()` 的新方法或 **Instrument** 的新类型时,还有大量的工作要做。考虑到如果你忘记重载某个方法,编译器也不会提示你,这会造成类型的整个处理过程变得难以管理。\n",
+ "\n",
+ "如果只写一个方法以基类作为参数,而不用管是哪个具体派生类,这样会变得更好吗?也就是说,如果忘掉派生类,编写的代码只与基类打交道,会不会更好呢?\n",
+ "\n",
+ "这正是多态所允许的。但是大部分拥有面向过程编程背景的程序员会对多态的运作方式感到一些困惑。\n",
+ "\n",
+ "\n",
+ "\n",
+ "## 转机\n",
+ "\n",
+ "运行程序后会看到 **Music.java** 的难点。**Wind.play()** 的输出结果正是我们期望的,然而它看起来似乎不应该得出这样的结果。观察 `tune()` 方法:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "public static void tune(Instrument i) {\n",
+ " // ...\n",
+ " i.play(Note.MIDDLE_C);\n",
+ "}"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "它接受一个 **Instrument** 引用。那么编译器是如何知道这里的 **Instrument** 引用指向的是 **Wind**,而不是 **Brass** 或 **Stringed** 呢?编译器无法得知。为了深入理解这个问题,有必要研究一下*绑定*这个主题。\n",
+ "\n",
+ "### 方法调用绑定\n",
+ "\n",
+ "将一个方法调用和一个方法主体关联起来称作*绑定*。若绑定发生在程序运行前(如果有的话,由编译器和链接器实现),叫做*前期绑定*。你可能从来没有听说这个术语,因为它是面向过程语言不需选择默认的绑定方式,例如在 C 语言中就只有*前期绑定*这一种方法调用。\n",
+ "\n",
+ "上述程序让人困惑的地方就在于前期绑定,因为编译器只知道一个 **Instrument** 引用,它无法得知究竟会调用哪个方法。\n",
+ "\n",
+ "解决方法就是*后期绑定*,意味着在运行时根据对象的类型进行绑定。后期绑定也称为*动态绑定*或*运行时绑定*。当一种语言实现了后期绑定,就必须具有某种机制在运行时能判断对象的类型,从而调用恰当的方法。也就是说,编译器仍然不知道对象的类型,但是方法调用机制能找到正确的方法体并调用。每种语言的后期绑定机制都不同,但是可以想到,对象中一定存在某种类型信息。\n",
+ "\n",
+ "Java 中除了 **static** 和 **final** 方法(**private** 方法也是隐式的 **final**)外,其他所有方法都是后期绑定。这意味着通常情况下,我们不需要判断后期绑定是否会发生——它自动发生。\n",
+ "\n",
+ "为什么将一个对象指明为 **final** ?正如前一章所述,它可以防止方法被重写。但更重要的一点可能是,它有效地”关闭了“动态绑定,或者说告诉编译器不需要对其进行动态绑定。这可以让编译器为 **final** 方法生成更高效的代码。然而,大部分情况下这样做不会对程序的整体性能带来什么改变,因此最好是为了设计使用 **final**,而不是为了提升性能而使用。\n",
+ "\n",
+ "### 产生正确的行为\n",
+ "\n",
+ "一旦当你知道 Java 中所有方法都是通过后期绑定来实现多态时,就可以编写只与基类打交道的代码,而且代码对于派生类来说都能正常地工作。或者换种说法,你向对象发送一条消息,让对象自己做正确的事。\n",
+ "\n",
+ "面向对象编程中的经典例子是形状 **Shape**。这个例子很直观,但不幸的是,它可能让初学者困惑,认为面向对象编程只适合图形化程序设计,实际上不是这样。\n",
+ "\n",
+ "形状的例子中,有一个基类称为 **Shape** ,多个不同的派生类型分别是:**Circle**,**Square**,**Triangle** 等等。这个例子之所以好用,是因为我们可以直接说“圆(Circle)是一种形状(Shape)”,这很容易理解。继承图展示了它们之间的关系:\n",
+ "\n",
+ "\n",
+ "\n",
+ "向上转型就像下面这么简单:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "Shape s = new Circle();"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "这会创建一个 **Circle** 对象,引用被赋值给 **Shape** 类型的变量 s,这看似错误(将一种类型赋值给另一种类型),然而是没问题的,因此从继承上可认为圆(Circle)就是一个形状(Shape)。因此编译器认可了赋值语句,没有报错。\n",
+ "\n",
+ "假设你调用了一个基类方法(在各个派生类中都被重写):"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "s.draw()"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "你可能再次认为 **Shape** 的 `draw()` 方法被调用,因为 s 是一个 **Shape** 引用——编译器怎么可能知道要做其他的事呢?然而,由于后期绑定(多态)被调用的是 **Circle** 的 `draw()` 方法,这是正确的。\n",
+ "\n",
+ "下面的例子稍微有些不同。首先让我们创建一个可复用的 **Shape** 类库,基类 **Shape** 为它的所有子类建立了公共接口——所有的形状都可以被绘画和擦除:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "// polymorphism/shape/Shape.java\n",
+ "package polymorphism.shape;\n",
+ "\n",
+ "public class Shape {\n",
+ " public void draw() {}\n",
+ " public void erase() {}\n",
+ "}"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "派生类通过重写这些方法为每个具体的形状提供独一无二的方法行为:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "// polymorphism/shape/Circle.java\n",
+ "package polymorphism.shape;\n",
+ "\n",
+ "public class Circle extends Shape {\n",
+ " @Override\n",
+ " public void draw() {\n",
+ " System.out.println(\"Circle.draw()\");\n",
+ " }\n",
+ " @Override\n",
+ " public void erase() {\n",
+ " System.out.println(\"Circle.erase()\");\n",
+ " }\n",
+ "}\n",
+ "\n",
+ "// polymorphism/shape/Square.java\n",
+ "package polymorphism.shape;\n",
+ "\n",
+ "public class Square extends Shape {\n",
+ " @Override\n",
+ " public void draw() {\n",
+ " System.out.println(\"Square.draw()\");\n",
+ " }\n",
+ " @Override\n",
+ " public void erase() {\n",
+ " System.out.println(\"Square.erase()\");\n",
+ " }\n",
+ " }\n",
+ "\n",
+ "// polymorphism/shape/Triangle.java\n",
+ "package polymorphism.shape;\n",
+ "\n",
+ "public class Triangle extends Shape {\n",
+ " @Override\n",
+ " public void draw() {\n",
+ " System.out.println(\"Triangle.draw()\");\n",
+ " }\n",
+ " @Override\n",
+ " public void erase() {\n",
+ " System.out.println(\"Triangle.erase()\");\n",
+ " }\n",
+ "}"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "**RandomShapes** 是一种工厂,每当我们调用 `get()` 方法时,就会产生一个指向随机创建的 **Shape** 对象的引用。注意,向上转型发生在 **return** 语句中,每条 **return** 语句取得一个指向某个 **Circle**,**Square** 或 **Triangle** 的引用, 并将其以 **Shape** 类型从 `get()` 方法发送出去。因此无论何时调用 `get()` 方法,你都无法知道具体的类型是什么,因为你总是得到一个简单的 **Shape** 引用:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "// polymorphism/shape/RandomShapes.java\n",
+ "// A \"factory\" that randomly creates shapes\n",
+ "package polymorphism.shape;\n",
+ "import java.util.*;\n",
+ "\n",
+ "public class RandomShapes {\n",
+ " private Random rand = new Random(47);\n",
+ " \n",
+ " public Shape get() {\n",
+ " switch(rand.nextInt(3)) {\n",
+ " default:\n",
+ " case 0: return new Circle();\n",
+ " case 1: return new Square();\n",
+ " case 2: return new Triangle();\n",
+ " }\n",
+ " }\n",
+ " \n",
+ " public Shape[] array(int sz) {\n",
+ " Shape[] shapes = new Shape[sz];\n",
+ " // Fill up the array with shapes:\n",
+ " for (int i = 0; i < shapes.length; i++) {\n",
+ " shapes[i] = get();\n",
+ " }\n",
+ " return shapes;\n",
+ " }\n",
+ "}"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "`array()` 方法分配并填充了 **Shape** 数组,这里使用了 for-in 表达式:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "// polymorphism/Shapes.java\n",
+ "// Polymorphism in Java\n",
+ "import polymorphism.shape.*;\n",
+ "\n",
+ "public class Shapes {\n",
+ " public static void main(String[] args) {\n",
+ " RandomShapes gen = new RandomShapes();\n",
+ " // Make polymorphic method calls:\n",
+ " for (Shape shape: gen.array(9)) {\n",
+ " shape.draw();\n",
+ " }\n",
+ " }\n",
+ "}"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "输出:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "Triangle.draw()\n",
+ "Triangle.draw()\n",
+ "Square.draw()\n",
+ "Triangle.draw()\n",
+ "Square.draw()\n",
+ "Triangle.draw()\n",
+ "Square.draw()\n",
+ "Triangle.draw()\n",
+ "Circle.draw()"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "`main()` 方法中包含了一个 **Shape** 引用组成的数组,其中每个元素通过调用 **RandomShapes** 类的 `get()` 方法生成。现在你只知道拥有一些形状,但除此之外一无所知(编译器也是如此)。然而当遍历这个数组为每个元素调用 `draw()` 方法时,从运行程序的结果中可以看到,与类型有关的特定行为奇迹般地发生了。\n",
+ "\n",
+ "随机生成形状是为了让大家理解:在编译时,编译器不需要知道任何具体信息以进行正确的调用。所有对方法 `draw()` 的调用都是通过动态绑定进行的。\n",
+ "\n",
+ "### 可扩展性\n",
+ "\n",
+ "现在让我们回头看音乐乐器的例子。由于多态机制,你可以向系统中添加任意多的新类型,而不需要修改 `tune()` 方法。在一个设计良好的面向对象程序中,许多方法将会遵循 `tune()` 的模型,只与基类接口通信。这样的程序是可扩展的,因为可以从通用的基类派生出新的数据类型,从而添加新的功能。那些操纵基类接口的方法不需要改动就可以应用于新类。\n",
+ "\n",
+ "考虑一下乐器的例子,如果在基类中添加更多的方法,并加入一些新类,将会发生什么呢:\n",
+ "\n",
+ "\n",
+ "\n",
+ "所有的新类都可以和原有类正常运行,不需要改动 `tune()` 方法。即使 `tune()` 方法单独存放在某个文件中,而且向 **Instrument** 接口中添加了新的方法,`tune()` 方法也无需再编译就能正确运行。下面是类图的实现:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "// polymorphism/music3/Music3.java\n",
+ "// An extensible program\n",
+ "// {java polymorphism.music3.Music3}\n",
+ "package polymorphism.music3;\n",
+ "import polymorphism.music.Note;\n",
+ "\n",
+ "class Instrument {\n",
+ " void play(Note n) {\n",
+ " System.out.println(\"Instrument.play() \" + n);\n",
+ " }\n",
+ " \n",
+ " String what() {\n",
+ " return \"Instrument\";\n",
+ " }\n",
+ " \n",
+ " void adjust() {\n",
+ " System.out.println(\"Adjusting Instrument\");\n",
+ " }\n",
+ "}\n",
+ "\n",
+ "class Wind extends Instrument {\n",
+ " @Override\n",
+ " void play(Note n) {\n",
+ " System.out.println(\"Wind.play() \" + n);\n",
+ " }\n",
+ " @Override\n",
+ " String what() {\n",
+ " return \"Wind\";\n",
+ " }\n",
+ " @Override\n",
+ " void adjust() {\n",
+ " System.out.println(\"Adjusting Wind\");\n",
+ " }\n",
+ "}\n",
+ "\n",
+ "class Percussion extends Instrument {\n",
+ " @Override\n",
+ " void play(Note n) {\n",
+ " System.out.println(\"Percussion.play() \" + n);\n",
+ " }\n",
+ " @Override\n",
+ " String what() {\n",
+ " return \"Percussion\";\n",
+ " }\n",
+ " @Override\n",
+ " void adjust() {\n",
+ " System.out.println(\"Adjusting Percussion\");\n",
+ " }\n",
+ "}\n",
+ "\n",
+ "class Stringed extends Instrument {\n",
+ " @Override\n",
+ " void play(Note n) {\n",
+ " System.out.println(\"Stringed.play() \" + n);\n",
+ " } \n",
+ " @Override\n",
+ " String what() {\n",
+ " return \"Stringed\";\n",
+ " }\n",
+ " @Override\n",
+ " void adjust() {\n",
+ " System.out.println(\"Adjusting Stringed\");\n",
+ " }\n",
+ "}\n",
+ "\n",
+ "class Brass extends Wind {\n",
+ " @Override\n",
+ " void play(Note n) {\n",
+ " System.out.println(\"Brass.play() \" + n);\n",
+ " }\n",
+ " @Override\n",
+ " void adjust() {\n",
+ " System.out.println(\"Adjusting Brass\");\n",
+ " }\n",
+ "}\n",
+ "\n",
+ "class Woodwind extends Wind {\n",
+ " @Override\n",
+ " void play(Note n) {\n",
+ " System.out.println(\"Woodwind.play() \" + n);\n",
+ " }\n",
+ " @Override\n",
+ " String what() {\n",
+ " return \"Woodwind\";\n",
+ " }\n",
+ "}\n",
+ "\n",
+ "public class Music3 {\n",
+ " // Doesn't care about type, so new types\n",
+ " // added to the system still work right:\n",
+ " public static void tune(Instrument i) {\n",
+ " // ...\n",
+ " i.play(Note.MIDDLE_C);\n",
+ " }\n",
+ " \n",
+ " public static void tuneAll(Instrument[] e) {\n",
+ " for (Instrument i: e) {\n",
+ " tune(i);\n",
+ " }\n",
+ " }\n",
+ " \n",
+ " public static void main(String[] args) {\n",
+ " // Upcasting during addition to the array:\n",
+ " Instrument[] orchestra = {\n",
+ " new Wind(),\n",
+ " new Percussion(),\n",
+ " new Stringed(),\n",
+ " new Brass(),\n",
+ " new Woodwind()\n",
+ " };\n",
+ " tuneAll(orchestra);\n",
+ " }\n",
+ "}"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "输出:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "Wind.play() MIDDLE_C\n",
+ "Percussion.play() MIDDLE_C\n",
+ "Stringed.play() MIDDLE_C\n",
+ "Brass.play() MIDDLE_C\n",
+ "Woodwind.play() MIDDLE_C"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "新方法 `what()` 返回一个带有类描述的 **String** 引用,`adjust()` 提供一些乐器调音的方法。\n",
+ "\n",
+ "在 `main()` 方法中,当向 **orchestra** 数组添加元素时,元素会自动向上转型为 **Instrument**。\n",
+ "\n",
+ "`tune()` 方法可以忽略周围所有代码发生的变化,仍然可以正常运行。这正是我们期待多态能提供的特性。代码中的修改不会破坏程序中其他不应受到影响的部分。换句话说,多态是一项“将改变的事物与不变的事物分离”的重要技术。\n",
+ "\n",
+ "### 陷阱:“重写”私有方法\n",
+ "\n",
+ "你可能天真地试图像下面这样做:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "// polymorphism/PrivateOverride.java\n",
+ "// Trying to override a private method\n",
+ "// {java polymorphism.PrivateOverride}\n",
+ "package polymorphism;\n",
+ "\n",
+ "public class PrivateOverride {\n",
+ " private void f() {\n",
+ " System.out.println(\"private f()\");\n",
+ " }\n",
+ " \n",
+ " public static void main(String[] args) {\n",
+ " PrivateOverride po = new Derived();\n",
+ " po.f();\n",
+ " }\n",
+ "}\n",
+ "\n",
+ "public Derived extends PrivateOverride {\n",
+ " public void f() {\n",
+ " System.out.println(\"public f()\");\n",
+ " }\n",
+ "}"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "输出:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "private f()"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "你可能期望输出是 **public f()**,然而 **private** 方法可以当作是 **final** 的,对于派生类来说是隐蔽的。因此,这里 **Derived** 的 `f()` 是一个全新的方法;因为基类版本的 `f()` 屏蔽了 **Derived** ,因此它都不算是重写方法。\n",
+ "\n",
+ "结论是只有非 **private** 方法才能被重写,但是得小心重写 **private** 方法的现象,编译器不报错,但不会按我们所预期的执行。为了清晰起见,派生类中的方法名采用与基类中 **private** 方法名不同的命名。\n",
+ "\n",
+ "如果使用了 `@Override` 注解,就能检测出问题:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "// polymorphism/PrivateOverride2.java\n",
+ "// Detecting a mistaken override using @Override\n",
+ "// {WillNotCompile}\n",
+ "package polymorphism;\n",
+ "\n",
+ "public class PrivateOverride2 {\n",
+ " private void f() {\n",
+ " System.out.println(\"private f()\");\n",
+ " }\n",
+ " \n",
+ " public static void main(String[] args) {\n",
+ " PrivateOverride2 po = new Derived2();\n",
+ " po.f();\n",
+ " }\n",
+ "}\n",
+ "\n",
+ "class Derived2 extends PrivateOverride2 {\n",
+ " @Override\n",
+ " public void f() {\n",
+ " System.out.println(\"public f()\");\n",
+ " }\n",
+ "}"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "编译器报错信息是:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "error: method does not override or\n",
+ "implement a method from a supertype"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "### 陷阱:属性与静态方法\n",
+ "\n",
+ "一旦学会了多态,就可以以多态的思维方式考虑每件事。然而,只有普通的方法调用可以是多态的。例如,如果你直接访问一个属性,该访问会在编译时解析:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "// polymorphism/FieldAccess.java\n",
+ "// Direct field access is determined at compile time\n",
+ "class Super {\n",
+ " public int field = 0;\n",
+ " \n",
+ " public int getField() {\n",
+ " return field;\n",
+ " }\n",
+ "}\n",
+ "\n",
+ "class Sub extends Super {\n",
+ " public int field = 1;\n",
+ " \n",
+ " @Override\n",
+ " public int getField() {\n",
+ " return field;\n",
+ " }\n",
+ " \n",
+ " public int getSuperField() {\n",
+ " return super.field;\n",
+ " }\n",
+ "}\n",
+ "\n",
+ "public class FieldAccess {\n",
+ " public static void main(String[] args) {\n",
+ " Super sup = new Sub(); // Upcast\n",
+ " System.out.println(\"sup.field = \" + sup.field + \n",
+ " \", sup.getField() = \" + sup.getField());\n",
+ " Sub sub = new Sub();\n",
+ " System.out.println(\"sub.field = \" + sub.field + \n",
+ " \", sub.getField() = \" + sub.getField()\n",
+ " + \", sub.getSuperField() = \" + sub.getSuperField())\n",
+ " }\n",
+ "}"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "输出:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "sup.field = 0, sup.getField() = 1\n",
+ "sub.field = 1, sub.getField() = 1, sub.getSuperField() = 0"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "当 **Sub** 对象向上转型为 **Super** 引用时,任何属性访问都被编译器解析,因此不是多态的。在这个例子中,**Super.field** 和 **Sub.field** 被分配了不同的存储空间,因此,**Sub** 实际上包含了两个称为 **field** 的属性:它自己的和来自 **Super** 的。然而,在引用 **Sub** 的 **field** 时,默认的 **field** 属性并不是 **Super** 版本的 **field** 属性。为了获取 **Super** 的 **field** 属性,需要显式地指明 **super.field**。\n",
+ "\n",
+ "尽管这看起来是个令人困惑的问题,实际上基本不会发生。首先,通常会将所有的属性都指明为 **private**,因此不能直接访问它们,只能通过方法来访问。此外,你可能也不会给基类属性和派生类属性起相同的名字,这样做会令人困惑。\n",
+ "\n",
+ "如果一个方法是静态(**static**)的,它的行为就不具有多态性:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "// polymorphism/StaticPolymorphism.java\n",
+ "// static methods are not polymorphic\n",
+ "class StaticSuper {\n",
+ " public static String staticGet() {\n",
+ " return \"Base staticGet()\";\n",
+ " }\n",
+ " \n",
+ " public String dynamicGet() {\n",
+ " return \"Base dynamicGet()\";\n",
+ " }\n",
+ "}\n",
+ "\n",
+ "class StaticSub extends StaticSuper {\n",
+ " public static String staticGet() {\n",
+ " return \"Derived staticGet()\";\n",
+ " }\n",
+ " @Override\n",
+ " public String dynamicGet() {\n",
+ " return \"Derived dynamicGet()\";\n",
+ " }\n",
+ "}\n",
+ "\n",
+ "public class StaticPolymorphism {\n",
+ " public static void main(String[] args) {\n",
+ " StaticSuper sup = new StaticSub(); // Upcast\n",
+ " System.out.println(StaticSuper.staticGet());\n",
+ " System.out.println(sup.dynamicGet());\n",
+ " }\n",
+ "}"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "输出:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "Base staticGet()\n",
+ "Derived dynamicGet()"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "静态的方法只与类关联,与单个的对象无关。\n",
+ "\n",
+ "\n",
+ "\n",
+ "## 构造器和多态\n",
+ "\n",
+ "通常,构造器不同于其他类型的方法。在涉及多态时也是如此。尽管构造器不具有多态性(事实上人们会把它看作是隐式声明的静态方法),但是理解构造器在复杂层次结构中运作多态还是非常重要的。这个理解可以帮助你避免一些不愉快的困扰。\n",
+ "\n",
+ "### 构造器调用顺序\n",
+ "\n",
+ "在“初始化和清理”和“复用”两章中已经简单地介绍过构造器的调用顺序,但那时还没有介绍多态。\n",
+ "\n",
+ "在派生类的构造过程中总会调用基类的构造器。初始化会自动按继承层次结构上移,因此每个基类的构造器都会被调用到。这么做是有意义的,因为构造器有着特殊的任务:检查对象是否被正确地构造。由于属性通常声明为 **private**,你必须假定派生类只能访问自己的成员而不能访问基类的成员。只有基类的构造器拥有恰当的知识和权限来初始化自身的元素。因此,必须得调用所有构造器;否则就不能构造完整的对象。这就是为什么编译器会强制调用每个派生类中的构造器的原因。如果在派生类的构造器主体中没有显式地调用基类构造器,编译器就会默默地调用无参构造器。如果没有无参构造器,编译器就会报错(当类中不含构造器时,编译器会自动合成一个无参构造器)。\n",
+ "\n",
+ "下面的例子展示了组合、继承和多态在构建顺序上的作用:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "// polymorphism/Sandwich.java\n",
+ "// Order of constructor calls\n",
+ "// {java polymorphism.Sandwich}\n",
+ "package polymorphism;\n",
+ "\n",
+ "class Meal {\n",
+ " Meal() {\n",
+ " System.out.println(\"Meal()\");\n",
+ " }\n",
+ "}\n",
+ "\n",
+ "class Bread {\n",
+ " Bread() {\n",
+ " System.out.println(\"Bread()\");\n",
+ " }\n",
+ "}\n",
+ "\n",
+ "class Cheese {\n",
+ " Cheese() {\n",
+ " System.out.println(\"Cheese()\");\n",
+ " }\n",
+ "}\n",
+ "\n",
+ "class Lettuce {\n",
+ " Lettuce() {\n",
+ " System.out.println(\"Lettuce()\");\n",
+ " }\n",
+ "}\n",
+ "\n",
+ "class Lunch extends Meal {\n",
+ " Lunch() {\n",
+ " System.out.println(\"Lunch()\");\n",
+ " }\n",
+ "}\n",
+ "\n",
+ "class PortableLunch extends Lunch {\n",
+ " PortableLunch() {\n",
+ " System.out.println(\"PortableLunch()\");\n",
+ " }\n",
+ "}\n",
+ "\n",
+ "public class Sandwich extends PortableLunch {\n",
+ " private Bread b = new Bread();\n",
+ " private Cheese c = new Cheese();\n",
+ " private Lettuce l = new Lettuce();\n",
+ " \n",
+ " public Sandwich() {\n",
+ " System.out.println(\"Sandwich()\");\n",
+ " }\n",
+ " \n",
+ " public static void main(String[] args) {\n",
+ " new Sandwich();\n",
+ " }\n",
+ "}"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "输出:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "Meal()\n",
+ "Lunch()\n",
+ "PortableLunch()\n",
+ "Bread()\n",
+ "Cheese()\n",
+ "Lettuce()\n",
+ "Sandwich()"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "这个例子用其他类创建了一个复杂的类。每个类都在构造器中声明自己。重要的类是 **Sandwich**,它反映了三层继承(如果算上 **Object** 的话,就是四层),包含了三个成员对象。\n",
+ "\n",
+ "从创建 **Sandwich** 对象的输出中可以看出对象的构造器调用顺序如下:\n",
+ "\n",
+ "1. 基类构造器被调用。这个步骤被递归地重复,这样一来类层次的顶级父类会被最先构造,然后是它的派生类,以此类推,直到最底层的派生类。\n",
+ "2. 按声明顺序初始化成员。\n",
+ "3. 调用派生类构造器的方法体。\n",
+ "\n",
+ "构造器的调用顺序很重要。当使用继承时,就已经知道了基类的一切,并可以访问基类中任意 **public** 和 **protected** 的成员。这意味着在派生类中可以假定所有的基类成员都是有效的。在一个标准方法中,构造动作已经发生过,对象其他部分的所有成员都已经创建好。\n",
+ "\n",
+ "在构造器中必须确保所有的成员都已经构建完。唯一能保证这点的方法就是首先调用基类的构造器。接着,在派生类的构造器中,所有你可以访问的基类成员都已经初始化。另一个在构造器中能知道所有成员都是有效的理由是:无论何时有可能的话,你应该在所有成员对象(通过组合将对象置于类中)定义处初始化它们(例如,例子中的 **b**、**c** 和 **l**)。如果遵循这条实践,就可以帮助确保所有的基类成员和当前对象的成员对象都已经初始化。\n",
+ "\n",
+ "不幸的是,这不能处理所有情况,在下一节会看到。\n",
+ "\n",
+ "### 继承和清理\n",
+ "\n",
+ "在使用组合和继承创建新类时,大部分时候你无需关心清理。子对象通常会留给垃圾收集器处理。如果你存在清理问题,那么必须用心地为新类创建一个 `dispose()` 方法(这里用的是我选择的名称,你可以使用更好的名称)。由于继承,如果有其他特殊的清理工作的话,就必须在派生类中重写 `dispose()` 方法。当重写 `dispose()` 方法时,记得调用基类的 `dispose()` 方法,否则基类的清理工作不会发生:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "// polymorphism/Frog.java\n",
+ "// Cleanup and inheritance\n",
+ "// {java polymorphism.Frog}\n",
+ "package polymorphism;\n",
+ "\n",
+ "class Characteristic {\n",
+ " private String s;\n",
+ " \n",
+ " Characteristic(String s) {\n",
+ " this.s = s;\n",
+ " System.out.println(\"Creating Characteristic \" + s);\n",
+ " }\n",
+ " \n",
+ " protected void dispose() {\n",
+ " System.out.println(\"disposing Characteristic \" + s);\n",
+ " }\n",
+ "}\n",
+ "\n",
+ "class Description {\n",
+ " private String s;\n",
+ " \n",
+ " Description(String s) {\n",
+ " this.s = s;\n",
+ " System.out.println(\"Creating Description \" + s);\n",
+ " }\n",
+ " \n",
+ " protected void dispose() {\n",
+ " System.out.println(\"disposing Description \" + s);\n",
+ " }\n",
+ "}\n",
+ "\n",
+ "class LivingCreature {\n",
+ " private Characteristic p = new Characteristic(\"is alive\");\n",
+ " private Description t = new Description(\"Basic Living Creature\");\n",
+ " \n",
+ " LivingCreature() {\n",
+ " System.out.println(\"LivingCreature()\");\n",
+ " }\n",
+ " \n",
+ " protected void dispose() {\n",
+ " System.out.println(\"LivingCreature dispose\");\n",
+ " t.dispose();\n",
+ " p.dispose();\n",
+ " }\n",
+ "}\n",
+ "\n",
+ "class Animal extends LivingCreature {\n",
+ " private Characteristic p = new Characteristic(\"has heart\");\n",
+ " private Description t = new Description(\"Animal not Vegetable\");\n",
+ " \n",
+ " Animal() {\n",
+ " System.out.println(\"Animal()\");\n",
+ " }\n",
+ " \n",
+ " @Override\n",
+ " protected void dispose() {\n",
+ " System.out.println(\"Animal dispose\");\n",
+ " t.dispose();\n",
+ " p.dispose();\n",
+ " super.dispose();\n",
+ " }\n",
+ "}\n",
+ "\n",
+ "class Amphibian extends Animal {\n",
+ " private Characteristic p = new Characteristic(\"can live in water\");\n",
+ " private Description t = new Description(\"Both water and land\");\n",
+ " \n",
+ " Amphibian() {\n",
+ " System.out.println(\"Amphibian()\");\n",
+ " }\n",
+ " \n",
+ " @Override\n",
+ " protected void dispose() {\n",
+ " System.out.println(\"Amphibian dispose\");\n",
+ " t.dispose();\n",
+ " p.dispose();\n",
+ " super.dispose();\n",
+ " }\n",
+ "}\n",
+ "\n",
+ "public class Frog extends Amphibian {\n",
+ " private Characteristic p = new Characteristic(\"Croaks\");\n",
+ " private Description t = new Description(\"Eats Bugs\");\n",
+ " \n",
+ " public Frog() {\n",
+ " System.out.println(\"Frog()\");\n",
+ " }\n",
+ " \n",
+ " @Override\n",
+ " protected void dispose() {\n",
+ " System.out.println(\"Frog dispose\");\n",
+ " t.dispose();\n",
+ " p.dispose();\n",
+ " super.dispose();\n",
+ " }\n",
+ " \n",
+ " public static void main(String[] args) {\n",
+ " Frog frog = new Frog();\n",
+ " System.out.println(\"Bye!\");\n",
+ " frog.dispose();\n",
+ " }\n",
+ "}"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "输出:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "Creating Characteristic is alive\n",
+ "Creating Description Basic Living Creature\n",
+ "LivingCreature()\n",
+ "Creating Characteristiv has heart\n",
+ "Creating Description Animal not Vegetable\n",
+ "Animal()\n",
+ "Creating Characteristic can live in water\n",
+ "Creating Description Both water and land\n",
+ "Amphibian()\n",
+ "Creating Characteristic Croaks\n",
+ "Creating Description Eats Bugs\n",
+ "Frog()\n",
+ "Bye!\n",
+ "Frog dispose\n",
+ "disposing Description Eats Bugs\n",
+ "disposing Characteristic Croaks\n",
+ "Amphibian dispose\n",
+ "disposing Description Both wanter and land\n",
+ "disposing Characteristic can live in water\n",
+ "Animal dispose\n",
+ "disposing Description Animal not Vegetable\n",
+ "disposing Characteristic has heart\n",
+ "LivingCreature dispose\n",
+ "disposing Description Basic Living Creature\n",
+ "disposing Characteristic is alive"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "层级结构中的每个类都有 **Characteristic** 和 **Description** 两个类型的成员对象,它们必须得被销毁。销毁的顺序应该与初始化的顺序相反,以防一个对象依赖另一个对象。对于属性来说,就意味着与声明的顺序相反(因为属性是按照声明顺序初始化的)。对于基类(遵循 C++ 析构函数的形式),首先进行派生类的清理工作,然后才是基类的清理。这是因为派生类的清理可能调用基类的一些方法,所以基类组件这时得存活,不能过早地被销毁。输出显示了,**Frog** 对象的所有部分都是按照创建的逆序销毁的。\n",
+ "\n",
+ "尽管通常不必进行清理工作,但万一需要时,就得谨慎小心地执行。\n",
+ "\n",
+ "**Frog** 对象拥有自己的成员对象,它创建了这些成员对象,并且知道它们能存活多久,所以它知道何时调用 `dispose()` 方法。然而,一旦某个成员对象被其它一个或多个对象共享时,问题就变得复杂了,不能只是简单地调用 `dispose()`。这里,也许就必须使用*引用计数*来跟踪仍然访问着共享对象的对象数量,如下:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "// polymorphism/ReferenceCounting.java\n",
+ "// Cleaning up shared member objects\n",
+ "class Shared {\n",
+ " private int refcount = 0;\n",
+ " private static long counter = 0;\n",
+ " private final long id = counter++;\n",
+ " \n",
+ " Shared() {\n",
+ " System.out.println(\"Creating \" + this);\n",
+ " }\n",
+ " \n",
+ " public void addRef() {\n",
+ " refcount++;\n",
+ " }\n",
+ " \n",
+ " protected void dispose() {\n",
+ " if (--refcount == 0) {\n",
+ " System.out.println(\"Disposing \" + this);\n",
+ " }\n",
+ " }\n",
+ " \n",
+ " @Override\n",
+ " public String toString() {\n",
+ " return \"Shared \" + id;\n",
+ " }\n",
+ "}\n",
+ "\n",
+ "class Composing {\n",
+ " private Shared shared;\n",
+ " private static long counter = 0;\n",
+ " private final long id = counter++;\n",
+ " \n",
+ " Composing(Shared shared) {\n",
+ " System.out.println(\"Creating \" + this);\n",
+ " this.shared = shared;\n",
+ " this.shared.addRef();\n",
+ " }\n",
+ " \n",
+ " protected void dispose() {\n",
+ " System.out.println(\"disposing \" + this);\n",
+ " shared.dispose();\n",
+ " }\n",
+ " \n",
+ " @Override\n",
+ " public String toString() {\n",
+ " return \"Composing \" + id;\n",
+ " }\n",
+ "}\n",
+ "\n",
+ "public class ReferenceCounting {\n",
+ " public static void main(String[] args) {\n",
+ " Shared shared = new Shared();\n",
+ " Composing[] composing = {\n",
+ " new Composing(shared),\n",
+ " new Composing(shared),\n",
+ " new Composing(shared),\n",
+ " new Composing(shared),\n",
+ " new Composing(shared),\n",
+ " };\n",
+ " for (Composing c: composing) {\n",
+ " c.dispose();\n",
+ " }\n",
+ " }\n",
+ "}"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "输出:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "Creating Shared 0\n",
+ "Creating Composing 0\n",
+ "Creating Composing 1\n",
+ "Creating Composing 2\n",
+ "Creating Composing 3\n",
+ "Creating Composing 4\n",
+ "disposing Composing 0\n",
+ "disposing Composing 1\n",
+ "disposing Composing 2\n",
+ "disposing Composing 3\n",
+ "disposing Composing 4\n",
+ "Disposing Shared 0"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "**static long counter** 跟踪所创建的 **Shared** 实例数量,还提供了 **id** 的值。**counter** 的类型是 **long** 而不是 **int**,以防溢出(这只是个良好实践,对于本书的所有示例,**counter** 不会溢出)。**id** 是 **final** 的,因为它的值在初始化时确定后不应该变化。\n",
+ "\n",
+ "在将一个 **shared** 对象附着在类上时,必须记住调用 `addRef()`,而 `dispose()` 方法会跟踪引用数,以确定在何时真正地执行清理工作。使用这种技巧需要加倍细心,但是如果正在共享需要被清理的对象,就没有太多选择了。\n",
+ "\n",
+ "### 构造器内部多态方法的行为\n",
+ "\n",
+ "构造器调用的层次结构带来了一个困境。如果在构造器中调用了正在构造的对象的动态绑定方法,会发生什么呢?\n",
+ "\n",
+ "在普通的方法中,动态绑定的调用是在运行时解析的,因为对象不知道它属于方法所在的类还是类的派生类。\n",
+ "\n",
+ "如果在构造器中调用了动态绑定方法,就会用到那个方法的重写定义。然而,调用的结果难以预料因为被重写的方法在对象被完全构造出来之前已经被调用,这使得一些 bug 很隐蔽,难以发现。\n",
+ "\n",
+ "从概念上讲,构造器的工作就是创建对象(这并非是平常的工作)。在构造器内部,整个对象可能只是部分形成——只知道基类对象已经初始化。如果构造器只是构造对象过程中的一个步骤,且构造的对象所属的类是从构造器所属的类派生出的,那么派生部分在当前构造器被调用时还没有初始化。然而,一个动态绑定的方法调用向外深入到继承层次结构中,它可以调用派生类的方法。如果你在构造器中这么做,就可能调用一个方法,该方法操纵的成员可能还没有初始化——这肯定会带来灾难。\n",
+ "\n",
+ "下面例子展示了这个问题:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "// polymorphism/PolyConstructors.java\n",
+ "// Constructors and polymorphism\n",
+ "// don't produce what you might expect\n",
+ "class Glyph {\n",
+ " void draw() {\n",
+ " System.out.println(\"Glyph.draw()\");\n",
+ " }\n",
+ "\n",
+ " Glyph() {\n",
+ " System.out.println(\"Glyph() before draw()\");\n",
+ " draw();\n",
+ " System.out.println(\"Glyph() after draw()\");\n",
+ " }\n",
+ "}\n",
+ "\n",
+ "class RoundGlyph extends Glyph {\n",
+ " private int radius = 1;\n",
+ "\n",
+ " RoundGlyph(int r) {\n",
+ " radius = r;\n",
+ " System.out.println(\"RoundGlyph.RoundGlyph(), radius = \" + radius);\n",
+ " }\n",
+ "\n",
+ " @Override\n",
+ " void draw() {\n",
+ " System.out.println(\"RoundGlyph.draw(), radius = \" + radius);\n",
+ " }\n",
+ "}\n",
+ "\n",
+ "public class PolyConstructors {\n",
+ " public static void main(String[] args) {\n",
+ " new RoundGlyph(5);\n",
+ " }\n",
+ "}"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "输出:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "Glyph() before draw()\n",
+ "RoundGlyph.draw(), radius = 0\n",
+ "Glyph() after draw()\n",
+ "RoundGlyph.RoundGlyph(), radius = 5"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "**Glyph** 的 `draw()` 被设计为可重写,在 **RoundGlyph** 这个方法被重写。但是 **Glyph** 的构造器里调用了这个方法,结果调用了 **RoundGlyph** 的 `draw()` 方法,这看起来正是我们的目的。输出结果表明,当 **Glyph** 构造器调用了 `draw()` 时,**radius** 的值不是默认初始值 1 而是 0。这可能会导致在屏幕上只画了一个点或干脆什么都不画,于是我们只能干瞪眼,试图找到程序不工作的原因。\n",
+ "\n",
+ "前一小节描述的初始化顺序并不十分完整,而这正是解决谜团的关键所在。初始化的实际过程是:\n",
+ "\n",
+ "1. 在所有事发生前,分配给对象的存储空间会被初始化为二进制 0。\n",
+ "2. 如前所述调用基类构造器。此时调用重写后的 `draw()` 方法(是的,在调用 **RoundGraph** 构造器之前调用),由步骤 1 可知,**radius** 的值为 0。\n",
+ "3. 按声明顺序初始化成员。\n",
+ "4. 最终调用派生类的构造器。\n",
+ "\n",
+ "这么做有个优点:所有事物至少初始化为 0(或某些特殊数据类型与 0 等价的值),而不是仅仅留作垃圾。这包括了通过组合嵌入类中的对象引用,被赋予 **null**。如果忘记初始化该引用,就会在运行时出现异常。观察输出结果,就会发现所有事物都是 0。\n",
+ "\n",
+ "另一方面,应该震惊于输出结果。逻辑方面我们已经做得非常完美,然而行为仍不可思议的错了,编译器也没有报错(C++ 在这种情况下会产生更加合理的行为)。像这样的 bug 很容易被忽略,需要花很长时间才能发现。\n",
+ "\n",
+ "因此,编写构造器有一条良好规范:做尽量少的事让对象进入良好状态。如果有可能的话,尽量不要调用类中的任何方法。在基类的构造器中能安全调用的只有基类的 **final** 方法(这也适用于可被看作是 **final** 的 **private** 方法)。这些方法不能被重写,因此不会产生意想不到的结果。你可能无法永远遵循这条规范,但应该朝着它努力。\n",
+ "\n",
+ "\n",
+ "\n",
+ "## 协变返回类型\n",
+ "\n",
+ "Java 5 中引入了协变返回类型,这表示派生类的被重写方法可以返回基类方法返回类型的派生类型:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "// polymorphism/CovariantReturn.java\n",
+ "class Grain {\n",
+ " @Override\n",
+ " public String toString() {\n",
+ " return \"Grain\";\n",
+ " }\n",
+ "}\n",
+ "\n",
+ "class Wheat extends Grain {\n",
+ " @Override\n",
+ " public String toString() {\n",
+ " return \"Wheat\";\n",
+ " }\n",
+ "}\n",
+ "\n",
+ "class Mill {\n",
+ " Grain process() {\n",
+ " return new Grain();\n",
+ " }\n",
+ "}\n",
+ "\n",
+ "class WheatMill extends Mill {\n",
+ " @Override\n",
+ " Wheat process() {\n",
+ " return new Wheat();\n",
+ " }\n",
+ "}\n",
+ "\n",
+ "public class CovariantReturn {\n",
+ " public static void main(String[] args) {\n",
+ " Mill m = new Mill();\n",
+ " Grain g = m.process();\n",
+ " System.out.println(g);\n",
+ " m = new WheatMill();\n",
+ " g = m.process();\n",
+ " System.out.println(g);\n",
+ " }\n",
+ "}"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "输出:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "Grain\n",
+ "Wheat"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "关键区别在于 Java 5 之前的版本强制要求被重写的 `process()` 方法必须返回 **Grain** 而不是 **Wheat**,即使 **Wheat** 派生自 **Grain**,因而也应该是一种合法的返回类型。协变返回类型允许返回更具体的 **Wheat** 类型。\n",
+ "\n",
+ "\n",
+ "\n",
+ "## 使用继承设计\n",
+ "\n",
+ "学习过多态之后,一切看似都可以被继承,因为多态是如此巧妙的工具。这会给设计带来负担。事实上,如果利用已有类创建新类首先选择继承的话,事情会变得莫名的复杂。\n",
+ "\n",
+ "更好的方法是首先选择组合,特别是不知道该使用哪种方法时。组合不会强制设计是继承层次结构,而且组合更加灵活,因为可以动态地选择类型(因而选择相应的行为),而继承要求必须在编译时知道确切类型。下面例子说明了这点:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "// polymorphism/Transmogrify.java\n",
+ "// Dynamically changing the behavior of an object\n",
+ "// via composition (the \"State\" design pattern)\n",
+ "class Actor {\n",
+ " public void act() {}\n",
+ "}\n",
+ "\n",
+ "class HappyActor extends Actor {\n",
+ " @Override\n",
+ " public void act() {\n",
+ " System.out.println(\"HappyActor\");\n",
+ " }\n",
+ "}\n",
+ "\n",
+ "class SadActor extends Actor {\n",
+ " @Override\n",
+ " public void act() {\n",
+ " System.out.println(\"SadActor\");\n",
+ " }\n",
+ "}\n",
+ "\n",
+ "class Stage {\n",
+ " private Actor actor = new HappyActor();\n",
+ " \n",
+ " public void change() {\n",
+ " actor = new SadActor();\n",
+ " }\n",
+ " \n",
+ " public void performPlay() {\n",
+ " actor.act();\n",
+ " }\n",
+ "}\n",
+ "\n",
+ "public class Transmogrify {\n",
+ " public static void main(String[] args) {\n",
+ " Stage stage = new Stage();\n",
+ " stage.performPlay();\n",
+ " stage.change();\n",
+ " stage.performPlay();\n",
+ " }\n",
+ "}"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "输出:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "HappyActor\n",
+ "SadActor"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "**Stage** 对象中包含了 **Actor** 引用,该引用被初始化为指向一个 **HappyActor** 对象,这意味着 `performPlay()` 会产生一个特殊行为。但是既然引用可以在运行时与其他不同的对象绑定,那么它就可以被替换成对 **SadActor** 的引用,`performPlay()` 的行为随之改变。这样你就获得了运行时的动态灵活性(这被称为状态模式)。与之相反,我们不能在运行时决定继承不同的对象,那在编译时就完全确定下来了。\n",
+ "\n",
+ "有一条通用准则:使用继承表达行为的差异,使用属性表达状态的变化。在上个例子中,两者都用到了。通过继承得到的两个不同类在 `act()` 方法中表达了不同的行为,**Stage** 通过组合使自己的状态发生变化。这里状态的改变产生了行为的改变。\n",
+ "\n",
+ "### 替代 vs 扩展\n",
+ "\n",
+ "采用“纯粹”的方式创建继承层次结构看上去是最清晰的方法。即只有基类的方法才能在派生类中被重写,就像下图这样:\n",
+ "\n",
+ "\n",
+ "\n",
+ "这被称作纯粹的“is - a\"关系,因为类的接口已经确定了它是什么。继承可以确保任何派生类都拥有基类的接口,绝对不会少。如果按图上这么做,派生类将只拥有基类的接口。\n",
+ "\n",
+ "纯粹的替代意味着派生类可以完美地替代基类,当使用它们时,完全不需要知道这些子类的信息。也就是说,基类可以接收任意发送给派生类的消息,因为它们具有完全相同的接口。只需将派生类向上转型,不要关注对象的具体类型。所有一切都可以通过多态处理。\n",
+ "\n",
+ "按这种方式思考,似乎只有纯粹的“is - a”关系才是唯一明智的做法,其他任何设计只会导致混乱且注定失败。这其实也是个陷阱。一旦按这种方式开始思考,就会转而发现继承扩展接口(遗憾的是,extends 关键字似乎怂恿我们这么做)才是解决特定问题的完美方案。这可以称为“is - like - a” 关系,因为派生类就像是基类——它有着相同的基本接口,但还具有需要额外方法实现的其他特性:\n",
+ "\n",
+ "\n",
+ "\n",
+ "虽然这是一种有用且明智的方法(依赖具体情况),但是也存在缺点。派生类中接口的扩展部分在基类中不存在(不能通过基类访问到这些扩展接口),因此一旦向上转型,就不能通过基类调用这些新方法:\n",
+ "\n",
+ "\n",
+ "\n",
+ "如果不向上转型,就不会遇到这个问题。但是通常情况下,我们需要重新查明对象的确切类型,从而能够访问该类型中的扩展方法。下一节说明如何做到这点。\n",
+ "\n",
+ "### 向下转型与运行时类型信息\n",
+ "\n",
+ "由于向上转型(在继承层次中向上移动)会丢失具体的类型信息,那么为了重新获取类型信息,就需要在继承层次中向下移动,使用*向下转型*。\n",
+ "\n",
+ "向上转型永远是安全的,因为基类不会具有比派生类更多的接口。因此,每条发送给基类接口的消息都能被接收。但是对于向下转型,你无法知道一个形状是圆,它有可能是三角形、正方形或其他一些类型。\n",
+ "\n",
+ "为了解决这个问题,必须得有某种方法确保向下转型是正确的,防止意外转型到一个错误类型,进而发送对象无法接收的消息。这么做是不安全的。\n",
+ "\n",
+ "在某些语言中(如 C++),必须执行一个特殊的操作来获得安全的向下转型,但是在 Java 中,每次转型都会被检查!所以即使只是进行一次普通的加括号形式的类型转换,在运行时这个转换仍会被检查,以确保它的确是希望的那种类型。如果不是,就会得到 ClassCastException (类转型异常)。这种在运行时检查类型的行为称作运行时类型信息。下面例子展示了 RTTI 的行为:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "// polymorphism/RTTI.java\n",
+ "// Downcasting & Runtime type information (RTTI)\n",
+ "// {ThrowsException}\n",
+ "class Useful {\n",
+ " public void f() {}\n",
+ " public void g() {}\n",
+ "}\n",
+ "\n",
+ "class MoreUseful extends Useful {\n",
+ " @Override\n",
+ " public void f() {}\n",
+ " @Override\n",
+ " public void g() {}\n",
+ " public void u() {}\n",
+ " public void v() {}\n",
+ " public void w() {}\n",
+ "}\n",
+ "\n",
+ "public class RTTI {\n",
+ " public static void main(String[] args) {\n",
+ " Useful[] x = {\n",
+ " new Useful(),\n",
+ " new MoreUseful()\n",
+ " };\n",
+ " x[0].f();\n",
+ " x[1].g();\n",
+ " // Compile time: method not found in Useful:\n",
+ " //- x[1].u();\n",
+ " ((MoreUseful) x[1]).u(); // Downcast/RTTI\n",
+ " ((MoreUseful) x[0]).u(); // Exception thrown\n",
+ " }\n",
+ "}"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "输出:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "Exception in thread \"main\"\n",
+ "java.lang.ClassCastException: Useful cannot be cast to\n",
+ "MoreUseful\n",
+ "at RTTI.main"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "正如前面类图所示,**MoreUseful** 扩展了 **Useful** 的接口。而 **MoreUseful** 也继承了 **Useful**,所以它可以向上转型为 **Useful**。在 `main()` 方法中可以看到这种情况的发生。因为两个对象都是 **Useful** 类型,所以对它们都可以调用 `f()` 和 `g()` 方法。如果试图调用 `u()` 方法(只存在于 **MoreUseful** 中),就会得到编译时错误信息。\n",
+ "\n",
+ "为了访问 **MoreUseful** 对象的扩展接口,就得尝试向下转型。如果转型为正确的类型,就转型成功。否则,就会得到 ClassCastException 异常。你不必为这个异常编写任何特殊代码,因为它指出了程序员在程序的任何地方都可能犯的错误。**{ThrowsException}** 注释标签告知本书的构建系统:在运行程序时,预期抛出一个异常。\n",
+ "\n",
+ "RTTI 不仅仅包括简单的转型。例如,它还提供了一种方法,使你可以在试图向下转型前检查所要处理的类型。“类型信息”一章中会详细阐述运行时类型信息的方方面面。\n",
+ "\n",
+ "\n",
+ "\n",
+ "## 本章小结\n",
+ "\n",
+ "多态意味着“不同的形式”。在面向对象编程中,我们持有从基类继承而来的相同接口和使用该接口的不同形式:不同版本的动态绑定方法。\n",
+ "\n",
+ "在本章中,你可以看到,如果不使用数据抽象和继承,就不可能理解甚至创建多态的例子。多态是一种不能单独看待的特性(比如像 **switch** 语句那样),它只能作为类关系全景中的一部分,与其他特性协同工作。\n",
+ "\n",
+ "为了在程序中有效地使用多态乃至面向对象的技术,就必须扩展自己的编程视野,不能只看到单一类中的成员和消息,而要看到类之间的共同特性和它们之间的关系。尽管这需要很大的努力,但是这么做是值得的。它能带来更快的程序开发、更好的代码组织、扩展性更好的程序和更易维护的代码。\n",
+ "\n",
+ "但是记住,多态可能被滥用。仔细分析代码以确保多态确实能带来好处。\n",
+ "\n",
+ "\n",
+ "\n",
+ ""
+ ]
+ }
+ ],
+ "metadata": {},
+ "nbformat": 4,
+ "nbformat_minor": 4
+}
diff --git a/jupyter/10-Interfaces.ipynb b/jupyter/10-Interfaces.ipynb
new file mode 100644
index 00000000..30453dc4
--- /dev/null
+++ b/jupyter/10-Interfaces.ipynb
@@ -0,0 +1,2640 @@
+{
+ "cells": [
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "[TOC]\n",
+ "\n",
+ "\n",
+ "# 第十章 接口\n",
+ "\n",
+ "接口和抽象类提供了一种将接口与实现分离的更加结构化的方法。\n",
+ "\n",
+ "这种机制在编程语言中不常见,例如 C++ 只对这种概念有间接的支持。而在 Java 中存在这些关键字,说明这些思想很重要,Java 为它们提供了直接支持。\n",
+ "\n",
+ "首先,我们将学习抽象类,一种介于普通类和接口之间的折中手段。尽管你的第一想法是创建接口,但是对于构建具有属性和未实现方法的类来说,抽象类也是重要且必要的工具。你不可能总是使用纯粹的接口。\n",
+ "\n",
+ "\n",
+ "\n",
+ "## 抽象类和方法\n",
+ "\n",
+ "在上一章的乐器例子中,基类 **Instrument** 中的方法往往是“哑”方法。如果调用了这些方法,就会出现一些错误。这是因为接口的目的是为它的派生类创建一个通用接口。\n",
+ "\n",
+ "在那些例子中,创建这个通用接口的唯一理由是,不同的子类可以用不同的方式表示此接口。通用接口建立了一个基本形式,以此表达所有派生类的共同部分。另一种说法把 **Instrument** 称为抽象基类,或简称抽象类。\n",
+ "\n",
+ "对于像 **Instrument** 那样的抽象类来说,它的对象几乎总是没有意义的。创建一个抽象类是为了通过通用接口操纵一系列类。因此,**Instrument** 只是表示接口,不是具体实现,所以创建一个 **Instrument** 的对象毫无意义,我们可能希望阻止用户这么做。通过让 **Instrument** 所有的方法产生错误,就可以达到这个目的,但是这么做会延迟到运行时才能得知错误信息,并且需要用户进行可靠、详尽的测试。最好能在编译时捕捉问题。\n",
+ "\n",
+ "Java 提供了一个叫做*抽象方法*的机制,这个方法是不完整的:它只有声明没有方法体。下面是抽象方法的声明语法:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "abstract void f();"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "包含抽象方法的类叫做*抽象类*。如果一个类包含一个或多个抽象方法,那么类本身也必须限定为抽象的,否则,编译器会报错。"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "// interface/Basic.java\n",
+ "abstract class Basic {\n",
+ " abstract void unimplemented();\n",
+ "}"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "如果一个抽象类是不完整的,当试图创建这个类的对象时,Java 会怎么做呢?它不会创建抽象类的对象,所以我们只会得到编译器的错误信息。这样保证了抽象类的纯粹性,我们不用担心误用它。"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "// interfaces/AttemptToUseBasic.java\n",
+ "// {WillNotCompile}\n",
+ "public class AttemptToUseBasic {\n",
+ " Basic b = new Basic();\n",
+ " // error: Basic is abstract; cannot be instantiated\n",
+ "}"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "如果创建一个继承抽象类的新类并为之创建对象,那么就必须为基类的所有抽象方法提供方法定义。如果不这么做(可以选择不做),新类仍然是一个抽象类,编译器会强制我们为新类加上 **abstract** 关键字。"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "// interfaces/Basic2.java\n",
+ "abstract class Basic2 extends Basic {\n",
+ " int f() {\n",
+ " return 111;\n",
+ " }\n",
+ " \n",
+ " abstract void g() {\n",
+ " // unimplemented() still not implemented\n",
+ " }\n",
+ "}"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "可以将一个不包含任何抽象方法的类指明为 **abstract**,在类中的抽象方法没啥意义但想阻止创建类的对象时,这么做就很有用。"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "// interfaces/AbstractWithoutAbstracts.java\n",
+ "abstract class Basic3 {\n",
+ " int f() {\n",
+ " return 111;\n",
+ " }\n",
+ " \n",
+ " // No abstract methods\n",
+ "}\n",
+ "\n",
+ "public class AbstractWithoutAbstracts {\n",
+ " // Basic b3 = new Basic3();\n",
+ " // error: Basic 3 is abstract; cannot be instantiated\n",
+ "}"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "为了创建可初始化的类,就要继承抽象类,并提供所有抽象方法的定义:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "// interfaces/Instantiable.java\n",
+ "abstract class Uninstantiable {\n",
+ " abstract void f();\n",
+ " abstract int g();\n",
+ "}\n",
+ "\n",
+ "public class Instantiable extends Uninstantiable {\n",
+ " @Override\n",
+ " void f() {\n",
+ " System.out.println(\"f()\");\n",
+ " }\n",
+ " \n",
+ " @Override\n",
+ " int g() {\n",
+ " return 22;\n",
+ " }\n",
+ " \n",
+ " public static void main(String[] args) {\n",
+ " Uninstantiable ui = new Instantiable();\n",
+ " }\n",
+ "}"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "留意 `@Override` 的使用。没有这个注解的话,如果你没有定义相同的方法名或签名,抽象机制会认为你没有实现抽象方法从而产生编译时错误。因此,你可能认为这里的 `@Override` 是多余的。但是,`@Override` 还提示了这个方法被覆写——我认为这是有用的,所以我会使用 `@Override`,即使在没有这个注解,编译器告诉我错误的时候。 \n",
+ "\n",
+ "记住,事实上的访问权限是“friendly”。你很快会看到接口自动将其方法指明为 **public**。事实上,接口只允许 **public** 方法,如果不加访问修饰符的话,接口的方法不是 **friendly** 而是 **public**。然而,抽象类允许每件事:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "// interfaces/AbstractAccess.java\n",
+ "abstract class AbstractAccess {\n",
+ " private void m1() {}\n",
+ " \n",
+ " // private abstract void m1a(); // illegal\n",
+ " \n",
+ " protected void m2() {}\n",
+ " \n",
+ " protected abstract void m2a();\n",
+ " \n",
+ " void m3() {}\n",
+ " \n",
+ " abstract void m3a();\n",
+ " \n",
+ " public void m4() {}\n",
+ " \n",
+ " public abstract void m4a();\n",
+ "}"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "**private abstract** 被禁止了是有意义的,因为你不可能在 **AbstractAccess** 的任何子类中合法地定义它。\n",
+ "\n",
+ "上一章的 **Instrument** 类可以很轻易地转换为一个抽象类。只需要部分方法是 **abstract** 即可。将一个类指明为 **abstract** 并不强制类中的所有方法必须都是抽象方法。如下图所示:\n",
+ "\n",
+ "\n",
+ "\n",
+ "下面是修改成使用抽象类和抽象方法的管弦乐器的例子:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "// interfaces/music4/Music4.java\n",
+ "// Abstract classes and methods\n",
+ "// {java interfaces.music4.Music4}\n",
+ "package interfaces.music4;\n",
+ "import polymorphism.music.Note;\n",
+ "\n",
+ "abstract class Instrument {\n",
+ " private int i; // Storage allocated for each\n",
+ " \n",
+ " public abstract void play(Note n);\n",
+ " \n",
+ " public String what() {\n",
+ " return \"Instrument\";\n",
+ " }\n",
+ " \n",
+ " public abstract void adjust();\n",
+ "}\n",
+ "\n",
+ "class Wind extends Instrument {\n",
+ " @Override\n",
+ " public void play(Note n) {\n",
+ " System.out.println(\"Wind.play() \" + n);\n",
+ " }\n",
+ " \n",
+ " @Override\n",
+ " public String what() {\n",
+ " return \"Wind\";\n",
+ " }\n",
+ " \n",
+ " @Override\n",
+ " public void adjust() {\n",
+ " System.out.println(\"Adjusting Wind\");\n",
+ " }\n",
+ "}\n",
+ "\n",
+ "class Percussion extends Instrument {\n",
+ " @Override\n",
+ " public void play(Note n) {\n",
+ " System.out.println(\"Percussion.play() \" + n);\n",
+ " }\n",
+ " \n",
+ " @Override\n",
+ " public String what() {\n",
+ " return \"Percussion\";\n",
+ " }\n",
+ " \n",
+ " @Override\n",
+ " public void adjust() {\n",
+ " System.out.println(\"Adjusting Percussion\");\n",
+ " }\n",
+ "}\n",
+ "\n",
+ "class Stringed extends Instrument {\n",
+ " @Override\n",
+ " public void play(Note n) {\n",
+ " System.out.println(\"Stringed.play() \" + n);\n",
+ " }\n",
+ " \n",
+ " @Override\n",
+ " public String what() {\n",
+ " return \"Stringed\";\n",
+ " }\n",
+ " \n",
+ " @Override\n",
+ " public void adjust() {\n",
+ " System.out.println(\"Adjusting Stringed\");\n",
+ " }\n",
+ "}\n",
+ "\n",
+ "class Brass extends Wind {\n",
+ " @Override\n",
+ " public void play(Note n) {\n",
+ " System.out.println(\"Brass.play() \" + n);\n",
+ " }\n",
+ " \n",
+ " @Override\n",
+ " public void adjust() {\n",
+ " System.out.println(\"Adjusting Brass\");\n",
+ " }\n",
+ "}\n",
+ "\n",
+ "class Woodwind extends Wind {\n",
+ " @Override\n",
+ " public void play(Note n) {\n",
+ " System.out.println(\"Woodwind.play() \" + n);\n",
+ " }\n",
+ " \n",
+ " @Override\n",
+ " public String what() {\n",
+ " return \"Woodwind\";\n",
+ " }\n",
+ "}\n",
+ "\n",
+ "public class Music4 {\n",
+ " // Doesn't care about type, so new types\n",
+ " // added to system still work right:\n",
+ " static void tune(Instrument i) {\n",
+ " // ...\n",
+ " i.play(Note.MIDDLE_C);\n",
+ " }\n",
+ " \n",
+ " static void tuneAll(Instrument[] e) {\n",
+ " for (Instrument i: e) {\n",
+ " tune(i);\n",
+ " }\n",
+ " }\n",
+ " \n",
+ " public static void main(String[] args) {\n",
+ " // Upcasting during addition to the array:\n",
+ " Instrument[] orchestra = {\n",
+ " new Wind(),\n",
+ " new Percussion(),\n",
+ " new Stringed(),\n",
+ " new Brass(),\n",
+ " new Woodwind()\n",
+ " };\n",
+ " tuneAll(orchestra);\n",
+ " }\n",
+ "}"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "输出:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "Wind.play() MIDDLE_C\n",
+ "Percussion.play() MIDDLE_C\n",
+ "Stringed.play() MIDDLE_C\n",
+ "Brass.play() MIDDLE_C\n",
+ "Woodwind.play() MIDDLE_C"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "除了 **Instrument**,基本没区别。\n",
+ "\n",
+ "创建抽象类和抽象方法是有帮助的,因为它们使得类的抽象性很明确,并能告知用户和编译器使用意图。抽象类同时也是一种有用的重构工具,使用它们使得我们很容易地将沿着继承层级结构上移公共方法。\n",
+ "\n",
+ "\n",
+ "\n",
+ "## 接口创建\n",
+ "\n",
+ "使用 **interface** 关键字创建接口。在本书中,interface 和 class 一样随处常见,除非特指关键字 **interface**,其他情况下都采用正常字体书写 interface。\n",
+ "\n",
+ "描述 Java 8 之前的接口更加容易,因为它们只允许抽象方法。像下面这样:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "// interfaces/PureInterface.java\n",
+ "// Interface only looked like this before Java 8\n",
+ "public interface PureInterface {\n",
+ " int m1(); \n",
+ " void m2();\n",
+ " double m3();\n",
+ "}"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "我们甚至不用为方法加上 **abstract** 关键字,因为方法在接口中。Java 知道这些方法不能有方法体(仍然可以为方法加上 **abstract** 关键字,但是看起来像是不明白接口,徒增难堪罢了)。\n",
+ "\n",
+ "因此,在 Java 8之前我们可以这么说:**interface** 关键字产生一个完全抽象的类,没有提供任何实现。我们只能描述类应该像什么,做什么,但不能描述怎么做,即只能决定方法名、参数列表和返回类型,但是无法确定方法体。接口只提供形式,通常来说没有实现,尽管在某些受限制的情况下可以有实现。\n",
+ "\n",
+ "一个接口表示:所有实现了该接口的类看起来都像这样。因此,任何使用某特定接口的代码都知道可以调用该接口的哪些方法,而且仅需知道这些。所以,接口被用来建立类之间的协议。(一些面向对象编程语言中,使用 protocol 关键字完成相同的功能。)\n",
+ "\n",
+ "Java 8 中接口稍微有些变化,因为 Java 8 允许接口包含默认方法和静态方法——基于某些重要原因,看到后面你会理解。接口的基本概念仍然没变,介于类型之上、实现之下。接口与抽象类最明显的区别可能就是使用上的惯用方式。接口的典型使用是代表一个类的类型或一个形容词,如 Runnable 或 Serializable,而抽象类通常是类层次结构的一部分或一件事物的类型,如 String 或 ActionHero。\n",
+ "\n",
+ "使用关键字 **interface** 而不是 **class** 来创建接口。和类一样,需要在关键字 **interface** 前加上 **public** 关键字(但只是在接口名与文件名相同的情况下),否则接口只有包访问权限,只能在接口相同的包下才能使用它。\n",
+ "\n",
+ "接口同样可以包含属性,这些属性被隐式指明为 **static** 和 **final**。\n",
+ "\n",
+ "使用 **implements** 关键字使一个类遵循某个特定接口(或一组接口),它表示:接口只是外形,现在我要说明它是如何工作的。除此之外,它看起来像继承。"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "// interfaces/ImplementingAnInterface.java\n",
+ "interface Concept { // Package access\n",
+ " void idea1();\n",
+ " void idea2();\n",
+ "}\n",
+ "\n",
+ "class Implementation implements Concept {\n",
+ " @Override\n",
+ " public void idea1() {\n",
+ " System.out.println(\"idea1\");\n",
+ " }\n",
+ " \n",
+ " @Override\n",
+ " public void idea2() {\n",
+ " System.out.println(\"idea2\");\n",
+ " }\n",
+ "}"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "你可以选择显式地声明接口中的方法为 **public**,但是即使你不这么做,它们也是 **public** 的。所以当实现一个接口时,来自接口中的方法必须被定义为 **public**。否则,它们只有包访问权限,这样在继承时,它们的可访问权限就被降低了,这是 Java 编译器所不允许的。\n",
+ "\n",
+ "### 默认方法\n",
+ "\n",
+ "Java 8 为关键字 **default** 增加了一个新的用途(之前只用于 **switch** 语句和注解中)。当在接口中使用它时,任何实现接口却没有定义方法的时候可以使用 **default** 创建的方法体。默认方法比抽象类中的方法受到更多的限制,但是非常有用,我们将在“流式编程”一章中看到。现在让我们看下如何使用:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "// interfaces/AnInterface.java\n",
+ "interface AnInterface {\n",
+ " void firstMethod();\n",
+ " void secondMethod();\n",
+ "}"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "我们可以像这样实现接口:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "// interfaces/AnImplementation.java\n",
+ "public class AnImplementation implements AnInterface {\n",
+ " public void firstMethod() {\n",
+ " System.out.println(\"firstMethod\");\n",
+ " }\n",
+ " \n",
+ " public void secondMethod() {\n",
+ " System.out.println(\"secondMethod\");\n",
+ " }\n",
+ " \n",
+ " public static void main(String[] args) {\n",
+ " AnInterface i = new AnImplementation();\n",
+ " i.firstMethod();\n",
+ " i.secondMethod();\n",
+ " }\n",
+ "}"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "输出:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "firstMethod\n",
+ "secondMethod"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "如果我们在 **AnInterface** 中增加一个新方法 `newMethod()`,而在 **AnImplementation** 中没有实现它,编译器就会报错:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "AnImplementation.java:3:error: AnImplementation is not abstract and does not override abstract method newMethod() in AnInterface\n",
+ "public class AnImplementation implements AnInterface {\n",
+ "^\n",
+ "1 error"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "如果我们使用关键字 **default** 为 `newMethod()` 方法提供默认的实现,那么所有与接口有关的代码能正常工作,不受影响,而且这些代码还可以调用新的方法 `newMethod()`:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "// interfaces/InterfaceWithDefault.java\n",
+ "interface InterfaceWithDefault {\n",
+ " void firstMethod();\n",
+ " void secondMethod();\n",
+ " \n",
+ " default void newMethod() {\n",
+ " System.out.println(\"newMethod\");\n",
+ " }\n",
+ "}"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "关键字 **default** 允许在接口中提供方法实现——在 Java 8 之前被禁止。"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "// interfaces/Implementation2.java\n",
+ "public class Implementation2 implements InterfaceWithDefault {\n",
+ " @Override\n",
+ " public void firstMethod() {\n",
+ " System.out.println(\"firstMethod\");\n",
+ " }\n",
+ " \n",
+ " @Override\n",
+ " public void secondMethod() {\n",
+ " System.out.println(\"secondMethod\")\n",
+ " }\n",
+ " \n",
+ " public static void main(String[] args) {\n",
+ " InterfaceWithDefault i = new Implementation2();\n",
+ " i.firstMethod();\n",
+ " i.secondMethod();\n",
+ " i.newMethod();\n",
+ " }\n",
+ "}"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "输出:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "firstMethod\n",
+ "secondMethod\n",
+ "newMethod"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "尽管 **Implementation2** 中未定义 `newMethod()`,但是可以使用 `newMethod()` 了。 \n",
+ "\n",
+ "增加默认方法的极具说服力的理由是它允许在不破坏已使用接口的代码的情况下,在接口中增加新的方法。默认方法有时也被称为*守卫方法*或*虚拟扩展方法*。\n",
+ "\n",
+ "### 多继承\n",
+ "\n",
+ "多继承意味着一个类可能从多个父类型中继承特征和特性。\n",
+ "\n",
+ "Java 在设计之初,C++ 的多继承机制饱受诟病。Java 过去是一种严格要求单继承的语言:只能继承自一个类(或抽象类),但可以实现任意多个接口。在 Java 8 之前,接口没有包袱——它只是方法外貌的描述。\n",
+ "\n",
+ "多年后的现在,Java 通过默认方法具有了某种多继承的特性。结合带有默认方法的接口意味着结合了多个基类中的行为。因为接口中仍然不允许存在属性(只有静态属性,不适用),所以属性仍然只会来自单个基类或抽象类,也就是说,不会存在状态的多继承。正如下面这样:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "// interfaces/MultipleInheritance.java\n",
+ "import java.util.*;\n",
+ "\n",
+ "interface One {\n",
+ " default void first() {\n",
+ " System.out.println(\"first\");\n",
+ " }\n",
+ "}\n",
+ "\n",
+ "interface Two {\n",
+ " default void second() {\n",
+ " System.out.println(\"second\");\n",
+ " }\n",
+ "}\n",
+ "\n",
+ "interface Three {\n",
+ " default void third() {\n",
+ " System.out.println(\"third\");\n",
+ " }\n",
+ "}\n",
+ "\n",
+ "class MI implements One, Two, Three {}\n",
+ "\n",
+ "public class MultipleInheritance {\n",
+ " public static void main(String[] args) {\n",
+ " MI mi = new MI();\n",
+ " mi.first();\n",
+ " mi.second();\n",
+ " mi.third();\n",
+ " }\n",
+ "}"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "输出:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "first\n",
+ "second\n",
+ "third"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "现在我们做些在 Java 8 之前不可能完成的事:结合多个源的实现。只要基类方法中的方法名和参数列表不同,就能工作得很好,否则会得到编译器错误:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "// interface/MICollision.java\n",
+ "import java.util.*;\n",
+ "\n",
+ "interface Bob1 {\n",
+ " default void bob() {\n",
+ " System.out.println(\"Bob1::bob\");\n",
+ " }\n",
+ "}\n",
+ "\n",
+ "interface Bob2 {\n",
+ " default void bob() {\n",
+ " System.out.println(\"Bob2::bob\");\n",
+ " }\n",
+ "}\n",
+ "\n",
+ "// class Bob implements Bob1, Bob2 {}\n",
+ "/* Produces:\n",
+ "error: class Bob inherits unrelated defaults\n",
+ "for bob() from types Bob1 and Bob2\n",
+ "class Bob implements Bob1, Bob2 {}\n",
+ "^\n",
+ "1 error\n",
+ "*/\n",
+ "\n",
+ "interface Sam1 {\n",
+ " default void sam() {\n",
+ " System.out.println(\"Sam1::sam\");\n",
+ " }\n",
+ "}\n",
+ "\n",
+ "interface Sam2 {\n",
+ " default void sam(int i) {\n",
+ " System.out.println(i * 2);\n",
+ " }\n",
+ "}\n",
+ "\n",
+ "// This works because the argument lists are distinct:\n",
+ "class Sam implements Sam1, Sam2 {}\n",
+ "\n",
+ "interface Max1 {\n",
+ " default void max() {\n",
+ " System.out.println(\"Max1::max\");\n",
+ " }\n",
+ "}\n",
+ "\n",
+ "interface Max2 {\n",
+ " default int max() {\n",
+ " return 47;\n",
+ " }\n",
+ "}\n",
+ "\n",
+ "// class Max implements Max1, Max2 {}\n",
+ "/* Produces:\n",
+ "error: types Max2 and Max1 are imcompatible;\n",
+ "both define max(), but with unrelated return types\n",
+ "class Max implements Max1, Max2 {}\n",
+ "^\n",
+ "1 error\n",
+ "*/"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "**Sam** 类中的两个 `sam()` 方法有相同的方法名但是签名不同——方法签名包括方法名和参数类型,编译器也是用它来区分方法。但是从 **Max** 类可看出,返回类型不是方法签名的一部分,因此不能用来区分方法。为了解决这个问题,需要覆写冲突的方法:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "// interfaces/Jim.java\n",
+ "import java.util.*;\n",
+ "\n",
+ "interface Jim1 {\n",
+ " default void jim() {\n",
+ " System.out.println(\"Jim1::jim\");\n",
+ " }\n",
+ "}\n",
+ "\n",
+ "interface Jim2 {\n",
+ " default void jim() {\n",
+ " System.out.println(\"Jim2::jim\");\n",
+ " }\n",
+ "}\n",
+ "\n",
+ "public class Jim implements Jim1, Jim2 {\n",
+ " @Override\n",
+ " public void jim() {\n",
+ " Jim2.super.jim();\n",
+ " }\n",
+ " \n",
+ " public static void main(String[] args) {\n",
+ " new Jim().jim();\n",
+ " }\n",
+ "}"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "输出:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "Jim2::jim"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "当然,你可以重定义 `jim()` 方法,但是也能像上例中那样使用 **super** 关键字选择基类实现中的一种。\n",
+ "\n",
+ "### 接口中的静态方法\n",
+ "\n",
+ "Java 8 允许在接口中添加静态方法。这么做能恰当地把工具功能置于接口中,从而操作接口,或者成为通用的工具:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "// onjava/Operations.java\n",
+ "package onjava;\n",
+ "import java.util.*;\n",
+ "\n",
+ "public interface Operations {\n",
+ " void execute();\n",
+ " \n",
+ " static void runOps(Operations... ops) {\n",
+ " for (Operations op: ops) {\n",
+ " op.execute();\n",
+ " }\n",
+ " }\n",
+ " \n",
+ " static void show(String msg) {\n",
+ " System.out.println(msg);\n",
+ " }\n",
+ "}"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "这是模版方法设计模式的一个版本(在“设计模式”一章中详细描述),`runOps()` 是一个模版方法。`runOps()` 使用可变参数列表,因而我们可以传入任意多的 **Operation** 参数并按顺序运行它们:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "// interface/Machine.java\n",
+ "import java.util.*;\n",
+ "import onjava.Operations;\n",
+ "\n",
+ "class Bing implements Operations {\n",
+ " @Override\n",
+ " public void execute() {\n",
+ " Operations.show(\"Bing\");\n",
+ " }\n",
+ "}\n",
+ "\n",
+ "class Crack implements Operations {\n",
+ " @Override\n",
+ " public void execute() {\n",
+ " Operations.show(\"Crack\");\n",
+ " }\n",
+ "}\n",
+ "\n",
+ "class Twist implements Operations {\n",
+ " @Override\n",
+ " public void execute() {\n",
+ " Operations.show(\"Twist\");\n",
+ " }\n",
+ "}\n",
+ "\n",
+ "public class Machine {\n",
+ " public static void main(String[] args) {\n",
+ " Operations.runOps(\n",
+ " \tnew Bing(), new Crack(), new Twist());\n",
+ " }\n",
+ "}"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "输出:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "Bing\n",
+ "Crack\n",
+ "Twist"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "这里展示了创建 **Operations** 的不同方式:一个外部类(Bing),一个匿名类,一个方法引用和 lambda 表达式——毫无疑问用在这里是最好的解决方法。\n",
+ "\n",
+ "这个特性是一项改善,因为它允许把静态方法放在更合适的地方。\n",
+ "\n",
+ "### Instrument 作为接口\n",
+ "\n",
+ "回顾下乐器的例子,使用接口的话:\n",
+ "\n",
+ "\n",
+ "\n",
+ "类 **Woodwind** 和 **Brass** 说明一旦实现了某个接口,那么其实现就变成一个普通类,可以按常规方式扩展它。\n",
+ "\n",
+ "接口的工作方式使得我们不需要显式声明其中的方法为 **public**,它们自动就是 **public** 的。`play()` 和 `adjust()` 使用 **default** 关键字定义实现。在 Java 8 之前,这些定义要在每个实现中重复实现,显得多余且令人烦恼:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "// interfaces/music5/Music5.java\n",
+ "// {java interfaces.music5.Music5}\n",
+ "package interfaces.music5;\n",
+ "import polymorphism.music.Note;\n",
+ "\n",
+ "interface Instrument {\n",
+ " // Compile-time constant:\n",
+ " int VALUE = 5; // static & final\n",
+ " \n",
+ " default void play(Note n) // Automatically public \n",
+ " System.out.println(this + \".play() \" + n);\n",
+ " }\n",
+ " \n",
+ " default void adjust() {\n",
+ " System.out.println(\"Adjusting \" + this);\n",
+ " }\n",
+ "}\n",
+ "\n",
+ "class Wind implements Instrument {\n",
+ " @Override\n",
+ " public String toString() {\n",
+ " return \"Wind\";\n",
+ " }\n",
+ "}\n",
+ "\n",
+ "class Percussion implements Instrument {\n",
+ " @Override\n",
+ " public String toString() {\n",
+ " return \"Percussion\";\n",
+ " }\n",
+ "}\n",
+ "\n",
+ "class Stringed implements Instrument {\n",
+ " @Override\n",
+ " public String toString() {\n",
+ " return \"Stringed\";\n",
+ " }\n",
+ "}\n",
+ "\n",
+ "class Brass extends Wind {\n",
+ " @Override\n",
+ " public String toString() {\n",
+ " return \"Brass\";\n",
+ " }\n",
+ "}\n",
+ "\n",
+ "class Woodwind extends Wind {\n",
+ " @Override\n",
+ " public String toString() {\n",
+ " return \"Woodwind\";\n",
+ " }\n",
+ "}\n",
+ "\n",
+ "public class Music5 {\n",
+ " // Doesn't care about type, so new types\n",
+ " // added to the system still work right:\n",
+ " static void tune(Instrument i) {\n",
+ " // ...\n",
+ " i.play(Note.MIDDLE_C);\n",
+ " }\n",
+ " \n",
+ " static void tuneAll(Instrument[] e) {\n",
+ " for (Instrument i: e) {\n",
+ " tune(i);\n",
+ " }\n",
+ " }\n",
+ " \n",
+ " public static void main(String[] args) {\n",
+ " // Upcasting during addition to the array:\n",
+ " Instrument[] orchestra = {\n",
+ " new Wind(),\n",
+ " new Percussion(),\n",
+ " new Stringed(),\n",
+ " new Brass(),\n",
+ " new Woodwind()\n",
+ " }\n",
+ " tuneAll(orchestra);\n",
+ " }\n",
+ "}"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "输出:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "Wind.play() MIDDLE_C\n",
+ "Percussion.play() MIDDLE_C\n",
+ "Stringed.play() MIDDLE_C\n",
+ "Brass.play() MIDDLE_C\n",
+ "Woodwind.play() MIDDLE_C"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "这个版本的例子的另一个变化是:`what()` 被修改为 `toString()` 方法,因为 `toString()` 实现的正是 `what()` 方法要实现的逻辑。因为 `toString()` 是根基类 **Object** 的方法,所以它不需要出现在接口中。\n",
+ "\n",
+ "注意到,无论是将其向上转型为称作 **Instrument** 的普通类,或称作 **Instrument** 的抽象类,还是叫作 **Instrument** 的接口,其行为都是相同的。事实上,从 `tune()` 方法上看不出来 **Instrument** 到底是一个普通类、抽象类,还是一个接口。\n",
+ "\n",
+ "\n",
+ "\n",
+ "## 抽象类和接口\n",
+ "\n",
+ "尤其是在 Java 8 引入 **default** 方法之后,选择用抽象类还是用接口变得更加令人困惑。下表做了明确的区分:\n",
+ "\n",
+ "| 特性 | 接口 | 抽象类 |\n",
+ "| :------------------: | :--------------------------------------------------------: | :--------------------------------------: |\n",
+ "| 组合 | 新类可以组合多个接口 | 只能继承单一抽象类 |\n",
+ "| 状态 | 不能包含属性(除了静态属性,不支持对象状态) | 可以包含属性,非抽象方法可能引用这些属性 |\n",
+ "| 默认方法 和 抽象方法 | 不需要在子类中实现默认方法。默认方法可以引用其他接口的方法 | 必须在子类中实现抽象方法 |\n",
+ "| 构造器 | 没有构造器 | 可以有构造器 |\n",
+ "| 可见性 | 隐式 **public** | 可以是 **protected** 或友元 |\n",
+ "\n",
+ "抽象类仍然是一个类,在创建新类时只能继承它一个。而创建类的过程中可以实现多个接口。\n",
+ "\n",
+ "有一条实际经验:尽可能地抽象。因此,更倾向使用接口而不是抽象类。只有当必要时才使用抽象类。除非必须使用,否则不要用接口和抽象类。大多数时候,普通类已经做得很好,如果不行的话,再移动到接口或抽象类中。\n",
+ "\n",
+ "\n",
+ "\n",
+ "## 完全解耦\n",
+ "\n",
+ "当方法操纵的是一个类而非接口时,它就只能作用于那个类或其子类。如果想把方法应用于那个继承层级结构之外的类,就会触霉头。接口在很大程度上放宽了这个限制,因而使用接口可以编写复用性更好的代码。\n",
+ "\n",
+ "例如有一个类 **Process** 有两个方法 `name()` 和 `process()`。`process()` 方法接受输入,修改并输出。把这个类作为基类用来创建各种不同类型的 **Processor**。下例中,**Processor** 的各个子类修改 String 对象(注意,返回类型可能是协变类型而非参数类型):"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "// interfaces/Applicator.java\n",
+ "import java.util.*;\n",
+ "\n",
+ "class Processor {\n",
+ " public String name() {\n",
+ " return getClass().getSimpleName();\n",
+ " }\n",
+ " \n",
+ " public Object process(Object input) {\n",
+ " return input;\n",
+ " }\n",
+ "}\n",
+ "\n",
+ "class Upcase extends Processor {\n",
+ " // 返回协变类型\n",
+ " @Override \n",
+ " public String process(Object input) {\n",
+ " return ((String) input).toUpperCase();\n",
+ " }\n",
+ "}\n",
+ "\n",
+ "class Downcase extends Processor {\n",
+ " @Override\n",
+ " public String process(Object input) {\n",
+ " return ((String) input).toLowerCase();\n",
+ " }\n",
+ "}\n",
+ "\n",
+ "class Splitter extends Processor {\n",
+ " @Override\n",
+ " public String process(Object input) {\n",
+ " // split() divides a String into pieces:\n",
+ " return Arrays.toString(((String) input).split(\" \"));\n",
+ " }\n",
+ "}\n",
+ "\n",
+ "public class Applicator {\n",
+ " public static void apply(Processor p, Object s) {\n",
+ " System.out.println(\"Using Processor \" + p.name());\n",
+ " System.out.println(p.process(s));\n",
+ " }\n",
+ " \n",
+ " public static void main(String[] args) {\n",
+ " String s = \"We are such stuff as dreams are made on\";\n",
+ " apply(new Upcase(), s);\n",
+ " apply(new Downcase(), s);\n",
+ " apply(new Splitter(), s);\n",
+ " }\n",
+ "}"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "输出:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "Using Processor Upcase\n",
+ "WE ARE SUCH STUFF AS DREAMS ARE MADE ON\n",
+ "Using Processor Downcase\n",
+ "we are such stuff as dreams are made on\n",
+ "Using Processor Splitter\n",
+ "[We, are, such, stuff, as, dreams, are, made, on]"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "**Applicator** 的 `apply()` 方法可以接受任何类型的 **Processor**,并将其应用到一个 **Object** 对象上输出结果。像本例中这样,创建一个能根据传入的参数类型从而具备不同行为的方法称为*策略*设计模式。方法包含算法中不变的部分,策略包含变化的部分。策略就是传入的对象,它包含要执行的代码。在这里,**Processor** 对象是策略,`main()` 方法展示了三种不同的应用于 **String s** 上的策略。\n",
+ "\n",
+ "`split()` 是 **String** 类中的方法,它接受 **String** 类型的对象并以传入的参数作为分割界限,返回一个数组 **String[]**。在这里用它是为了更快地创建 **String** 数组。\n",
+ "\n",
+ "假设现在发现了一组电子滤波器,它们看起来好像能使用 **Applicator** 的 `apply()` 方法:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "// interfaces/filters/Waveform.java\n",
+ "package interfaces.filters;\n",
+ "\n",
+ "public class Waveform {\n",
+ " private static long counter;\n",
+ " private final long id = counter++;\n",
+ " \n",
+ " @Override\n",
+ " public String toString() {\n",
+ " return \"Waveform \" + id;\n",
+ " }\n",
+ "}\n",
+ "\n",
+ "// interfaces/filters/Filter.java\n",
+ "package interfaces.filters;\n",
+ "\n",
+ "public class Filter {\n",
+ " public String name() {\n",
+ " return getClass().getSimpleName();\n",
+ " }\n",
+ " \n",
+ " public Waveform process(Waveform input) {\n",
+ " return input;\n",
+ " }\n",
+ "}\n",
+ "\n",
+ "// interfaces/filters/LowPass.java\n",
+ "package interfaces.filters;\n",
+ "\n",
+ "public class LowPass extends Filter {\n",
+ " double cutoff;\n",
+ " \n",
+ " public LowPass(double cutoff) {\n",
+ " this.cutoff = cutoff;\n",
+ " }\n",
+ " \n",
+ " @Override\n",
+ " public Waveform process(Waveform input) {\n",
+ " return input; // Dummy processing 哑处理\n",
+ " }\n",
+ "}\n",
+ "\n",
+ "// interfaces/filters/HighPass.java\n",
+ "package interfaces.filters;\n",
+ "\n",
+ "public class HighPass extends Filter {\n",
+ " double cutoff;\n",
+ " \n",
+ " public HighPass(double cutoff) {\n",
+ " this.cutoff = cutoff;\n",
+ " }\n",
+ " \n",
+ " @Override\n",
+ " public Waveform process(Waveform input) {\n",
+ " return input;\n",
+ " }\n",
+ "}\n",
+ "\n",
+ "// interfaces/filters/BandPass.java\n",
+ "package interfaces.filters;\n",
+ "\n",
+ "public class BandPass extends Filter {\n",
+ " double lowCutoff, highCutoff;\n",
+ " \n",
+ " public BandPass(double lowCut, double highCut) {\n",
+ " lowCutoff = lowCut;\n",
+ " highCutoff = highCut;\n",
+ " }\n",
+ " \n",
+ " @Override\n",
+ " public Waveform process(Waveform input) {\n",
+ " return input;\n",
+ " }\n",
+ "}"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "**Filter** 类与 **Processor** 类具有相同的接口元素,但是因为它不是继承自 **Processor** —— 因为 **Filter** 类的创建者根本不知道你想将它当作 **Processor** 使用 —— 因此你不能将 **Applicator** 的 `apply()` 方法应用在 **Filter** 类上,即使这样做也能正常运行。主要是因为 **Applicator** 的 `apply()` 方法和 **Processor** 过于耦合,这阻止了 **Applicator** 的 `apply()` 方法被复用。另外要注意的一点是 Filter 类中 `process()` 方法的输入输出都是 **Waveform**。\n",
+ "\n",
+ "但如果 **Processor** 是一个接口,那么限制就会变得松动到足以复用 **Applicator** 的 `apply()` 方法,用来接受那个接口参数。下面是修改后的 **Processor** 和 **Applicator** 版本:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "// interfaces/interfaceprocessor/Processor.java\n",
+ "package interfaces.interfaceprocessor;\n",
+ "\n",
+ "public interface Processor {\n",
+ " default String name() {\n",
+ " return getClass().getSimpleName();\n",
+ " }\n",
+ " \n",
+ " Object process(Object input);\n",
+ "}\n",
+ "\n",
+ "// interfaces/interfaceprocessor/Applicator.java\n",
+ "package interfaces.interfaceprocessor;\n",
+ "\n",
+ "public class Applicator {\n",
+ " public static void apply(Processor p, Object s) {\n",
+ " System.out.println(\"Using Processor \" + p.name());\n",
+ " System.out.println(p.process(s));\n",
+ " }\n",
+ "}"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "复用代码的第一种方式是客户端程序员遵循接口编写类,像这样:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "// interfaces/interfaceprocessor/StringProcessor.java\n",
+ "// {java interfaces.interfaceprocessor.StringProcessor}\n",
+ "package interfaces.interfaceprocessor;\n",
+ "import java.util.*;\n",
+ "\n",
+ "interface StringProcessor extends Processor {\n",
+ " @Override\n",
+ " String process(Object input); // [1]\n",
+ " String S = \"If she weighs the same as a duck, she's made of wood\"; // [2]\n",
+ " \n",
+ " static void main(String[] args) { // [3]\n",
+ " Applicator.apply(new Upcase(), S);\n",
+ " Applicator.apply(new Downcase(), S);\n",
+ " Applicator.apply(new Splitter(), S);\n",
+ " }\n",
+ "}\n",
+ "\n",
+ "class Upcase implements StringProcessor {\n",
+ " // 返回协变类型\n",
+ " @Override\n",
+ " public String process(Object input) {\n",
+ " return ((String) input).toUpperCase();\n",
+ " }\n",
+ "}\n",
+ "\n",
+ "class Downcase implements StringProcessor {\n",
+ " @Override\n",
+ " public String process(Object input) {\n",
+ " return ((String) input).toLowerCase();\n",
+ " }\n",
+ "}\n",
+ "\n",
+ "class Splitter implements StringProcessor {\n",
+ " @Override\n",
+ " public String process(Object input) {\n",
+ " return Arrays.toString(((String) input).split(\" \"));\n",
+ " }\n",
+ "}"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "输出:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "Using Processor Upcase\n",
+ "IF SHE WEIGHS THE SAME AS A DUCK, SHE'S MADE OF WOOD\n",
+ "Using Processor Downcase\n",
+ "if she weighs the same as a duck, she's made of wood\n",
+ "Using Processor Splitter\n",
+ "[If, she, weighs, the, same, as, a, duck,, she's, made, of, wood]"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ ">[1] 该声明不是必要的,即使移除它,编译器也不会报错。但是注意这里的协变返回类型从 Object 变成了 String。\n",
+ ">\n",
+ ">[2] S 自动就是 final 和 static 的,因为它是在接口中定义的。\n",
+ ">\n",
+ ">[3] 可以在接口中定义 `main()` 方法。\n",
+ "\n",
+ "这种方式运作得很好,然而你经常遇到的情况是无法修改类。例如在电子滤波器的例子中,类库是被发现而不是创建的。在这些情况下,可以使用*适配器*设计模式。适配器允许代码接受已有的接口产生需要的接口,如下:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "// interfaces/interfaceprocessor/FilterProcessor.java\n",
+ "// {java interfaces.interfaceprocessor.FilterProcessor}\n",
+ "package interfaces.interfaceprocessor;\n",
+ "import interfaces.filters.*;\n",
+ "\n",
+ "class FilterAdapter implements Processor {\n",
+ " Filter filter;\n",
+ " \n",
+ " FilterAdapter(Filter filter) {\n",
+ " this.filter = filter;\n",
+ " }\n",
+ " \n",
+ " @Override\n",
+ " public String name() {\n",
+ " return filter.name();\n",
+ " }\n",
+ " \n",
+ " @Override\n",
+ " public Waveform process(Object input) {\n",
+ " return filter.process((Waveform) input);\n",
+ " }\n",
+ "}\n",
+ "\n",
+ "public class FilterProcessor {\n",
+ " public static void main(String[] args) {\n",
+ " Waveform w = new Waveform();\n",
+ " Applicator.apply(new FilterAdapter(new LowPass(1.0)), w);\n",
+ " Applicator.apply(new FilterAdapter(new HighPass(2.0)), w);\n",
+ " Applicator.apply(new FilterAdapter(new BandPass(3.0, 4.0)), w);\n",
+ " }\n",
+ "}"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "输出:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "Using Processor LowPass\n",
+ "Waveform 0\n",
+ "Using Processor HighPass\n",
+ "Waveform 0\n",
+ "Using Processor BandPass\n",
+ "Waveform 0"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "在这种使用适配器的方式中,**FilterAdapter** 的构造器接受已有的接口 **Filter**,继而产生需要的 **Processor** 接口的对象。你可能还注意到 **FilterAdapter** 中使用了委托。\n",
+ "\n",
+ "协变允许我们从 `process()` 方法中产生一个 **Waveform** 而非 **Object** 对象。\n",
+ "\n",
+ "将接口与实现解耦使得接口可以应用于多种不同的实现,因而代码更具可复用性。\n",
+ "\n",
+ "\n",
+ "\n",
+ "## 多接口结合\n",
+ "\n",
+ "接口没有任何实现——也就是说,没有任何与接口相关的存储——因此无法阻止结合的多接口。这是有价值的,因为你有时需要表示“一个 **x** 是一个 **a** 和一个 **b** 以及一个 **c**”。\n",
+ "\n",
+ "\n",
+ "\n",
+ "派生类并不要求必须继承自抽象的或“具体的”(没有任何抽象方法)的基类。如果继承一个非接口的类,那么只能继承一个类,其余的基元素必须都是接口。需要将所有的接口名称置于 **implements** 关键字之后且用逗号分隔。可以有任意多个接口,并可以向上转型为每个接口,因为每个接口都是独立的类型。下例展示了一个由多个接口组合而成的具体类产生的新类:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "// interfaces/Adventure.java\n",
+ "// Multiple interfaces\n",
+ "interface CanFight {\n",
+ " void fight();\n",
+ "}\n",
+ "\n",
+ "interface CanSwim {\n",
+ " void swim();\n",
+ "}\n",
+ "\n",
+ "interface CanFly {\n",
+ " void fly();\n",
+ "}\n",
+ "\n",
+ "class ActionCharacter {\n",
+ " public void fight(){}\n",
+ "}\n",
+ "\n",
+ "class Hero extends ActionCharacter implements CanFight, CanSwim, CanFly {\n",
+ " public void swim() {}\n",
+ " \n",
+ " public void fly() {}\n",
+ "}\n",
+ "\n",
+ "public class Adventure {\n",
+ " public static void t(CanFight x) {\n",
+ " x.fight();\n",
+ " }\n",
+ " \n",
+ " public static void u(CanSwim x) {\n",
+ " x.swim();\n",
+ " }\n",
+ " \n",
+ " public static void v(CanFly x) {\n",
+ " x.fly();\n",
+ " }\n",
+ " \n",
+ " public static void w(ActionCharacter x) {\n",
+ " x.fight();\n",
+ " }\n",
+ " \n",
+ " public static void main(String[] args) {\n",
+ " Hero h = new Hero();\n",
+ " t(h); // Treat it as a CanFight\n",
+ " u(h); // Treat it as a CanSwim\n",
+ " v(h); // Treat it as a CanFly\n",
+ " w(h); // Treat it as an ActionCharacter\n",
+ " }\n",
+ "}"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "类 **Hero** 结合了具体类 **ActionCharacter** 和接口 **CanFight**、**CanSwim** 和 **CanFly**。当通过这种方式结合具体类和接口时,需要将具体类放在前面,后面跟着接口(否则编译器会报错)。\n",
+ "\n",
+ "接口 **CanFight** 和类 **ActionCharacter** 中的 `fight()` 方法签名相同,而在类 Hero 中也没有提供 `fight()` 的定义。可以扩展一个接口,但是得到的是另一个接口。当想创建一个对象时,所有的定义必须首先都存在。类 **Hero** 中没有显式地提供 `fight()` 的定义,是由于该方法在类 **ActionCharacter** 中已经定义过,这样才使得创建 **Hero** 对象成为可能。\n",
+ "\n",
+ "在类 **Adventure** 中可以看到四个方法,它们把不同的接口和具体类作为参数。当创建一个 **Hero** 对象时,它可以被传入这些方法中的任意一个,意味着它可以依次向上转型为每个接口。Java 中这种接口的设计方式,使得程序员不需要付出特别的努力。\n",
+ "\n",
+ "记住,前面例子展示了使用接口的核心原因之一:为了能够向上转型为多个基类型(以及由此带来的灵活性)。然而,使用接口的第二个原因与使用抽象基类相同:防止客户端程序员创建这个类的对象,确保这仅仅只是一个接口。这带来了一个问题:应该使用接口还是抽象类呢?如果创建不带任何方法定义或成员变量的基类,就选择接口而不是抽象类。事实上,如果知道某事物是一个基类,可以考虑用接口实现它(这个主题在本章总结会再次讨论)。\n",
+ "\n",
+ "\n",
+ "\n",
+ "## 使用继承扩展接口\n",
+ "\n",
+ "通过继承,可以很容易在接口中增加方法声明,还可以在新接口中结合多个接口。这两种情况都可以得到新接口,如下例所示:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "// interfaces/HorrorShow.java\n",
+ "// Extending an interface with inheritance\n",
+ "interface Monster {\n",
+ " void menace();\n",
+ "}\n",
+ "\n",
+ "interface DangerousMonster extends Monster {\n",
+ " void destroy();\n",
+ "}\n",
+ "\n",
+ "interface Lethal {\n",
+ " void kill();\n",
+ "}\n",
+ "\n",
+ "class DragonZilla implements DangerousMonster {\n",
+ " @Override\n",
+ " public void menace() {}\n",
+ " \n",
+ " @Override\n",
+ " public void destroy() {}\n",
+ "}\n",
+ "\n",
+ "interface Vampire extends DangerousMonster, Lethal {\n",
+ " void drinkBlood();\n",
+ "}\n",
+ "\n",
+ "class VeryBadVampire implements Vampire {\n",
+ " @Override\n",
+ " public void menace() {}\n",
+ " \n",
+ " @Override\n",
+ " public void destroy() {}\n",
+ " \n",
+ " @Override\n",
+ " public void kill() {}\n",
+ " \n",
+ " @Override\n",
+ " public void drinkBlood() {}\n",
+ "}\n",
+ "\n",
+ "public class HorrorShow {\n",
+ " static void u(Monster b) {\n",
+ " b.menace();\n",
+ " }\n",
+ " \n",
+ " static void v(DangerousMonster d) {\n",
+ " d.menace();\n",
+ " d.destroy();\n",
+ " }\n",
+ " \n",
+ " static void w(Lethal l) {\n",
+ " l.kill();\n",
+ " }\n",
+ " \n",
+ " public static void main(String[] args) {\n",
+ " DangerousMonster barney = new DragonZilla();\n",
+ " u(barney);\n",
+ " v(barney);\n",
+ " Vampire vlad = new VeryBadVampire();\n",
+ " u(vlad);\n",
+ " v(vlad);\n",
+ " w(vlad);\n",
+ " }\n",
+ "}"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "接口 **DangerousMonster** 是 **Monster** 简单扩展的一个新接口,类 **DragonZilla** 实现了这个接口。\n",
+ "\n",
+ "**Vampire** 中使用的语法仅适用于接口继承。通常来说,**extends** 只能用于单一类,但是在构建接口时可以引用多个基类接口。注意到,接口名之间用逗号分隔。\n",
+ "\n",
+ "### 结合接口时的命名冲突\n",
+ "\n",
+ "当实现多个接口时可能会存在一个小陷阱。在前面的例子中,**CanFight** 和 **ActionCharacter** 具有完全相同的 `fight()` 方法。完全相同的方法没有问题,但是如果它们的签名或返回类型不同会怎么样呢?这里有一个例子:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "// interfaces/InterfaceCollision.java\n",
+ "interface I1 {\n",
+ " void f();\n",
+ "}\n",
+ "\n",
+ "interface I2 {\n",
+ " int f(int i);\n",
+ "}\n",
+ "\n",
+ "interface I3 {\n",
+ " int f();\n",
+ "}\n",
+ "\n",
+ "class C {\n",
+ " public int f() {\n",
+ " return 1;\n",
+ " }\n",
+ "}\n",
+ "\n",
+ "class C2 implements I1, I2 {\n",
+ " @Override\n",
+ " public void f() {}\n",
+ " \n",
+ " @Override\n",
+ " public int f(int i) {\n",
+ " return 1; // 重载\n",
+ " }\n",
+ "}\n",
+ "\n",
+ "class C3 extends C implements I2 {\n",
+ " @Override\n",
+ " public int f(int i) {\n",
+ " return 1; // 重载\n",
+ " }\n",
+ "}\n",
+ "\n",
+ "class C4 extends C implements I3 {\n",
+ " // 完全相同,没问题\n",
+ " @Override\n",
+ " public int f() {\n",
+ " return 1;\n",
+ " }\n",
+ "}\n",
+ "\n",
+ "// 方法的返回类型不同\n",
+ "//- class C5 extends C implements I1 {}\n",
+ "//- interface I4 extends I1, I3 {}"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "覆写、实现和重载令人不快地搅和在一起带来了困难。同时,重载方法仅根据返回类型是区分不了的。当不注释最后两行时,报错信息如下:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "error: C5 is not abstract and does not override abstract\n",
+ "method f() in I1\n",
+ "class C5 extends C implements I1 {}\n",
+ "error: types I3 and I1 are incompatible; both define f(),\n",
+ "but with unrelated return types\n",
+ "interfacce I4 extends I1, I3 {}"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "当打算组合接口时,在不同的接口中使用相同的方法名通常会造成代码可读性的混乱,尽量避免这种情况。\n",
+ "\n",
+ "\n",
+ "\n",
+ "## 接口适配\n",
+ "\n",
+ "接口最吸引人的原因之一是相同的接口可以有多个实现。在简单情况下体现在一个方法接受接口作为参数,该接口的实现和传递对象给方法则交由你来做。\n",
+ "\n",
+ "因此,接口的一种常见用法是前面提到的*策略*设计模式。编写一个方法执行某些操作并接受一个指定的接口作为参数。可以说:“只要对象遵循接口,就可以调用方法” ,这使得方法更加灵活,通用,并更具可复用性。\n",
+ "\n",
+ "例如,类 **Scanner** 的构造器接受的是一个 **Readable** 接口(在“字符串”一章中学习更多相关内容)。你会发现 **Readable** 没有用作 Java 标准库中其他任何方法的参数——它是单独为 **Scanner** 创建的,因此 **Scanner** 没有将其参数限制为某个特定类。通过这种方式,**Scanner** 可以与更多的类型协作。如果你创建了一个新类并想让 **Scanner** 作用于它,就让它实现 **Readable** 接口,像这样:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "// interfaces/RandomStrings.java\n",
+ "// Implementing an interface to conform to a method\n",
+ "import java.nio.*;\n",
+ "import java.util.*;\n",
+ "\n",
+ "public class RandomStrings implements Readable {\n",
+ " private static Random rand = new Random(47);\n",
+ " private static final char[] CAPITALS = \"ABCDEFGHIJKLMNOPQRSTUVWXYZ\".toCharArray();\n",
+ " private static final char[] LOWERS = \"abcdefghijklmnopqrstuvwxyz\".toCharArray();\n",
+ " private static final char[] VOWELS = \"aeiou\".toCharArray();\n",
+ " private int count;\n",
+ " \n",
+ " public RandomStrings(int count) {\n",
+ " this.count = count;\n",
+ " }\n",
+ " \n",
+ " @Override\n",
+ " public int read(CharBuffer cb) {\n",
+ " if (count-- == 0) {\n",
+ " return -1; // indicates end of input\n",
+ " }\n",
+ " cb.append(CAPITALS[rand.nextInt(CAPITALS.length)]);\n",
+ " for (int i = 0; i < 4; i++) {\n",
+ " cb.append(VOWELS[rand.nextInt(VOWELS.length)]);\n",
+ " cb.append(LOWERS[rand.nextInt(LOWERS.length)]);\n",
+ " }\n",
+ " cb.append(\" \");\n",
+ " return 10; // Number of characters appended\n",
+ " }\n",
+ " \n",
+ " public static void main(String[] args) {\n",
+ " Scanner s = new Scanner(new RandomStrings(10));\n",
+ " while (s.hasNext()) {\n",
+ " System.out.println(s.next());\n",
+ " }\n",
+ " }\n",
+ "}"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "输出:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "Yazeruyac\n",
+ "Fowenucor\n",
+ "Goeazimom\n",
+ "Raeuuacio\n",
+ "Nuoadesiw\n",
+ "Hageaikux\n",
+ "Ruqicibui\n",
+ "Numasetih\n",
+ "Kuuuuozog\n",
+ "Waqizeyoy"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "**Readable** 接口只需要实现 `read()` 方法(注意 `@Override` 注解的突出方法)。在 `read()` 方法里,将输入内容添加到 **CharBuffer** 参数中(有多种方法可以实现,查看 **CharBuffer** 文档),或在没有输入时返回 **-1**。\n",
+ "\n",
+ "假设你有一个类没有实现 **Readable** 接口,怎样才能让 **Scanner** 作用于它呢?下面是一个产生随机浮点数的例子:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "// interfaces/RandomDoubles.java\n",
+ "import java.util.*;\n",
+ "\n",
+ "public interface RandomDoubles {\n",
+ " Random RAND = new Random(47);\n",
+ " \n",
+ " default double next() {\n",
+ " return RAND.nextDouble();\n",
+ " }\n",
+ " \n",
+ " static void main(String[] args) {\n",
+ " RandomDoubles rd = new RandomDoubles(){};\n",
+ " for (int i = 0; i < 7; i++) {\n",
+ " System.out.println(rd.next() + \" \");\n",
+ " }\n",
+ " }\n",
+ "}"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "输出:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "0.7271157860730044 \n",
+ "0.5309454508634242 \n",
+ "0.16020656493302599 \n",
+ "0.18847866977771732 \n",
+ "0.5166020801268457 \n",
+ "0.2678662084200585 \n",
+ "0.2613610344283964"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "我们可以再次使用适配器模式,但这里适配器类可以实现两个接口。因此,通过关键字 **interface** 提供的多继承,我们可以创建一个既是 **RandomDoubles**,又是 **Readable** 的类:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "// interfaces/AdaptedRandomDoubles.java\n",
+ "// creating an adapter with inheritance\n",
+ "import java.nio.*;\n",
+ "import java.util.*;\n",
+ "\n",
+ "public class AdaptedRandomDoubles implements RandomDoubles, Readable {\n",
+ " private int count;\n",
+ " \n",
+ " public AdaptedRandomDoubles(int count) {\n",
+ " this.count = count;\n",
+ " }\n",
+ " \n",
+ " @Override\n",
+ " public int read(CharBuffer cb) {\n",
+ " if (count-- == 0) {\n",
+ " return -1;\n",
+ " }\n",
+ " String result = Double.toString(next()) + \" \";\n",
+ " cb.append(result);\n",
+ " return result.length();\n",
+ " }\n",
+ " \n",
+ " public static void main(String[] args) {\n",
+ " Scanner s = new Scanner(new AdaptedRandomDoubles(7));\n",
+ " while (s.hasNextDouble()) {\n",
+ " System.out.print(s.nextDouble() + \" \");\n",
+ " }\n",
+ " }\n",
+ "}"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "输出:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "0.7271157860730044 0.5309454508634242 \n",
+ "0.16020656493302599 0.18847866977771732 \n",
+ "0.5166020801268457 0.2678662084200585 \n",
+ "0.2613610344283964"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "因为你可以以这种方式在已有类中增加新接口,所以这就意味着一个接受接口类型的方法提供了一种让任何类都可以与该方法进行适配的方式。这就是使用接口而不是类的强大之处。\n",
+ "\n",
+ "\n",
+ "\n",
+ "## 接口字段\n",
+ "\n",
+ "因为接口中的字段都自动是 **static** 和 **final** 的,所以接口就成为了创建一组常量的方便的工具。在 Java 5 之前,这是产生与 C 或 C++ 中的 enum (枚举类型) 具有相同效果的唯一方式。所以你可能在 Java 5 之前的代码中看到:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "// interfaces/Months.java\n",
+ "// Using interfaces to create groups of constants\n",
+ "public interface Months {\n",
+ " int \n",
+ " JANUARY = 1, FEBRUARY = 2, MARCH = 3,\n",
+ " APRIL = 4, MAY = 5, JUNE = 6, JULY = 7,\n",
+ " AUGUST = 8, SEPTEMBER = 9, OCTOBER = 10,\n",
+ " NOVEMBER = 11, DECEMBER = 12;\n",
+ "}"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "注意 Java 中使用大写字母的风格定义具有初始化值的 **static** **final** 变量。接口中的字段自动是 **public** 的,所以没有显式指明这点。\n",
+ "\n",
+ "自 Java 5 开始,我们有了更加强大和灵活的关键字 **enum**,那么在接口中定义常量组就显得没什么意义了。然而当你阅读遗留的代码时,在很多场合你还会碰到这种旧的习惯用法。在“枚举”一章中你会学习到更多关于枚举的内容。\n",
+ "\n",
+ "### 初始化接口中的字段\n",
+ "\n",
+ "接口中定义的字段不能是“空 **final**\",但是可以用非常量表达式初始化。例如:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "// interfaces/RandVals.java\n",
+ "// Initializing interface fields with\n",
+ "// non-constant initializers\n",
+ "import java.util.*;\n",
+ "\n",
+ "public interface RandVals {\n",
+ " Random RAND = new Random(47);\n",
+ " int RANDOM_INT = RAND.nextInt(10);\n",
+ " long RANDOM_LONG = RAND.nextLong() * 10;\n",
+ " float RANDOM_FLOAT = RAND.nextLong() * 10;\n",
+ " double RANDOM_DOUBLE = RAND.nextDouble() * 10;\n",
+ "}"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "因为字段是 **static** 的,所以它们在类第一次被加载时初始化,这发生在任何字段首次被访问时。下面是个简单的测试:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "// interfaces/TestRandVals.java\n",
+ "public class TestRandVals {\n",
+ " public static void main(String[] args) {\n",
+ " System.out.println(RandVals.RANDOM_INT);\n",
+ " System.out.println(RandVals.RANDOM_LONG);\n",
+ " System.out.println(RandVals.RANDOM_FLOAT);\n",
+ " System.out.println(RandVals.RANDOM_DOUBLE);\n",
+ " }\n",
+ "}"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "输出:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "8\n",
+ "-32032247016559954\n",
+ "-8.5939291E18\n",
+ "5.779976127815049"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "这些字段不是接口的一部分,它们的值被存储在接口的静态存储区域中。\n",
+ "\n",
+ "\n",
+ "\n",
+ "## 接口嵌套\n",
+ "\n",
+ "接口可以嵌套在类或其他接口中。下面揭示一些有趣的特性:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "// interfaces/nesting/NestingInterfaces.java\n",
+ "// {java interfaces.nesting.NestingInterfaces}\n",
+ "package interfaces.nesting;\n",
+ "\n",
+ "class A {\n",
+ " interface B {\n",
+ " void f();\n",
+ " }\n",
+ " \n",
+ " public class BImp implements B {\n",
+ " @Override\n",
+ " public void f() {}\n",
+ " }\n",
+ " \n",
+ " public class BImp2 implements B {\n",
+ " @Override\n",
+ " public void f() {}\n",
+ " }\n",
+ " \n",
+ " public interface C {\n",
+ " void f();\n",
+ " }\n",
+ " \n",
+ " class CImp implements C {\n",
+ " @Override\n",
+ " public void f() {}\n",
+ " }\n",
+ " \n",
+ " private class CImp2 implements C {\n",
+ " @Override\n",
+ " public void f() {}\n",
+ " }\n",
+ " \n",
+ " private interface D {\n",
+ " void f();\n",
+ " }\n",
+ " \n",
+ " private class DImp implements D {\n",
+ " @Override\n",
+ " public void f() {}\n",
+ " }\n",
+ " \n",
+ " public class DImp2 implements D {\n",
+ " @Override\n",
+ " public void f() {}\n",
+ " }\n",
+ " \n",
+ " public D getD() {\n",
+ " return new DImp2();\n",
+ " }\n",
+ " \n",
+ " private D dRef;\n",
+ " \n",
+ " public void receiveD(D d) {\n",
+ " dRef = d;\n",
+ " dRef.f();\n",
+ " }\n",
+ "}\n",
+ "\n",
+ "interface E {\n",
+ " interface G {\n",
+ " void f();\n",
+ " }\n",
+ " // Redundant \"public\"\n",
+ " public interface H {\n",
+ " void f();\n",
+ " }\n",
+ " \n",
+ " void g();\n",
+ " // Cannot be private within an interface\n",
+ " //- private interface I {}\n",
+ "}\n",
+ "\n",
+ "public class NestingInterfaces {\n",
+ " public class BImp implements A.B {\n",
+ " @Override\n",
+ " public void f() {}\n",
+ " }\n",
+ " \n",
+ " class CImp implements A.C {\n",
+ " @Override\n",
+ " public void f() {}\n",
+ " }\n",
+ " // Cannot implements a private interface except\n",
+ " // within that interface's defining class:\n",
+ " //- class DImp implements A.D {\n",
+ " //- public void f() {}\n",
+ " //- }\n",
+ " class EImp implements E {\n",
+ " @Override\n",
+ " public void g() {}\n",
+ " }\n",
+ " \n",
+ " class EGImp implements E.G {\n",
+ " @Override\n",
+ " public void f() {}\n",
+ " }\n",
+ " \n",
+ " class EImp2 implements E {\n",
+ " @Override\n",
+ " public void g() {}\n",
+ " \n",
+ " class EG implements E.G {\n",
+ " @Override\n",
+ " public void f() {}\n",
+ " }\n",
+ " }\n",
+ " \n",
+ " public static void main(String[] args) {\n",
+ " A a = new A();\n",
+ " // Can't access to A.D:\n",
+ " //- A.D ad = a.getD();\n",
+ " // Doesn't return anything but A.D:\n",
+ " //- A.DImp2 di2 = a.getD();\n",
+ " // cannot access a member of the interface:\n",
+ " //- a.getD().f();\n",
+ " // Only another A can do anything with getD():\n",
+ " A a2 = new A();\n",
+ " a2.receiveD(a.getD());\n",
+ " }\n",
+ "}"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "在类中嵌套接口的语法是相当显而易见的。就像非嵌套接口一样,它们具有 **public** 或包访问权限的可见性。\n",
+ "\n",
+ "作为一种新添加的方式,接口也可以是 **private** 的,例如 **A.D**(同样的语法同时适用于嵌套接口和嵌套类)。那么 **private** 嵌套接口有什么好处呢?你可能猜测它只是被用来实现一个 **private** 内部类,就像 **DImp**。然而 **A.DImp2** 展示了它可以被实现为 **public** 类,但是 **A.DImp2** 只能被自己使用,你无法说它实现了 **private** 接口 **D**,所以实现 **private** 接口是一种可以强制该接口中的方法定义不会添加任何类型信息(即不可以向上转型)的方式。\n",
+ "\n",
+ "`getD()` 方法产生了一个与 **private** 接口有关的窘境。它是一个 **public** 方法却返回了对 **private** 接口的引用。能对这个返回值做些什么呢?`main()` 方法里进行了一些使用返回值的尝试但都失败了。返回值必须交给有权使用它的对象,本例中另一个 **A** 通过 `receiveD()` 方法接受了它。\n",
+ "\n",
+ "接口 **E** 说明了接口之间也能嵌套。然而,作用于接口的规则——尤其是,接口中的元素必须是 **public** 的——在此都会被严格执行,所以嵌套在另一个接口中的接口自动就是 **public** 的,不能指明为 **private**。\n",
+ "\n",
+ "类 **NestingInterfaces** 展示了嵌套接口的不同实现方式。尤其是当实现某个接口时,并不需要实现嵌套在其内部的接口。同时,**private** 接口不能在定义它的类之外被实现。\n",
+ "\n",
+ "添加这些特性的最初原因看起来像是出于对严格的语法一致性的考虑,但是我通常认为,一旦你了解了某种特性,就总能找到其用武之地。\n",
+ "\n",
+ "\n",
+ "\n",
+ "## 接口和工厂方法模式\n",
+ "\n",
+ "接口是多实现的途径,而生成符合某个接口的对象的典型方式是*工厂方法*设计模式。不同于直接调用构造器,只需调用工厂对象中的创建方法就能生成对象的实现——理论上,通过这种方式可以将接口与实现的代码完全分离,使得可以透明地将某个实现替换为另一个实现。这里是一个展示工厂方法结构的例子:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "// interfaces/Factories.java\n",
+ "interface Service {\n",
+ " void method1();\n",
+ " void method2();\n",
+ "}\n",
+ "\n",
+ "interface ServiceFactory {\n",
+ " Service getService();\n",
+ "}\n",
+ "\n",
+ "class Service1 implements Service {\n",
+ " Service1() {} // Package access\n",
+ " \n",
+ " @Override\n",
+ " public void method1() {\n",
+ " System.out.println(\"Service1 method1\");\n",
+ " }\n",
+ " \n",
+ " @Override\n",
+ " public void method2() {\n",
+ " System.out.println(\"Service1 method2\");\n",
+ " }\n",
+ "}\n",
+ "\n",
+ "class Service1Factory implements ServiceFactory {\n",
+ " @Override\n",
+ " public Service getService() {\n",
+ " return new Service1();\n",
+ " }\n",
+ "}\n",
+ "\n",
+ "class Service2 implements Service {\n",
+ " Service2() {} // Package access\n",
+ " \n",
+ " @Override\n",
+ " public void method1() {\n",
+ " System.out.println(\"Service2 method1\");\n",
+ " }\n",
+ " \n",
+ " @Override\n",
+ " public void method2() {\n",
+ " System.out.println(\"Service2 method2\");\n",
+ " }\n",
+ "}\n",
+ "\n",
+ "class Service2Factory implements ServiceFactory {\n",
+ " @Override\n",
+ " public Service getService() {\n",
+ " return new Service2();\n",
+ " }\n",
+ "}\n",
+ "\n",
+ "public class Factories {\n",
+ " public static void serviceConsumer(ServiceFactory fact) {\n",
+ " Service s = fact.getService();\n",
+ " s.method1();\n",
+ " s.method2();\n",
+ " }\n",
+ " \n",
+ " public static void main(String[] args) {\n",
+ " serviceConsumer(new Service1Factory());\n",
+ " // Services are completely interchangeable:\n",
+ " serviceConsumer(new Service2Factory());\n",
+ " }\n",
+ "}"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "输出:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "Service1 method1\n",
+ "Service1 method2\n",
+ "Service2 method1\n",
+ "Service2 method2"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "如果没有工厂方法,代码就必须在某处指定将要创建的 **Service** 的确切类型,从而调用恰当的构造器。\n",
+ "\n",
+ "为什么要添加额外的间接层呢?一个常见的原因是创建框架。假设你正在创建一个游戏系统;例如,在相同的棋盘下国际象棋和西洋跳棋:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "// interfaces/Games.java\n",
+ "// A Game framework using Factory Methods\n",
+ "interface Game {\n",
+ " boolean move();\n",
+ "}\n",
+ "\n",
+ "interface GameFactory {\n",
+ " Game getGame();\n",
+ "}\n",
+ "\n",
+ "class Checkers implements Game {\n",
+ " private int moves = 0;\n",
+ " private static final int MOVES = 3;\n",
+ " \n",
+ " @Override\n",
+ " public boolean move() {\n",
+ " System.out.println(\"Checkers move \" + moves);\n",
+ " return ++moves != MOVES;\n",
+ " }\n",
+ "}\n",
+ "\n",
+ "class CheckersFactory implements GameFactory {\n",
+ " @Override\n",
+ " public Game getGame() {\n",
+ " return new Checkers();\n",
+ " }\n",
+ "}\n",
+ "\n",
+ "class Chess implements Game {\n",
+ " private int moves = 0;\n",
+ " private static final int MOVES = 4;\n",
+ " \n",
+ " @Override\n",
+ " public boolean move() {\n",
+ " System.out.println(\"Chess move \" + moves);\n",
+ " return ++moves != MOVES;\n",
+ " }\n",
+ "}\n",
+ "\n",
+ "class ChessFactory implements GameFactory {\n",
+ " @Override\n",
+ " public Game getGame() {\n",
+ " return new Chess();\n",
+ " }\n",
+ "}\n",
+ "\n",
+ "public class Games {\n",
+ " public static void playGame(GameFactory factory) {\n",
+ " Game s = factory.getGame();\n",
+ " while (s.move()) {\n",
+ " ;\n",
+ " }\n",
+ " }\n",
+ " \n",
+ " public static void main(String[] args) {\n",
+ " playGame(new CheckersFactory());\n",
+ " playGame(new ChessFactory());\n",
+ " }\n",
+ "}"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "输出:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "Checkers move 0\n",
+ "Checkers move 1\n",
+ "Checkers move 2\n",
+ "Chess move 0\n",
+ "Chess move 1\n",
+ "Chess move 2\n",
+ "Chess move 3"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "如果类 **Games** 表示一段很复杂的代码,那么这种方式意味着你可以在不同类型的游戏里复用这段代码。你可以再想象一些能够从这个模式中受益的更加精巧的游戏。\n",
+ "\n",
+ "在下一章,你将会看到一种更加优雅的使用匿名内部类的工厂实现方式。\n",
+ "\n",
+ "\n",
+ "\n",
+ "## 本章小结\n",
+ "\n",
+ "认为接口是好的选择,从而使用接口不用具体类,这具有诱惑性。几乎任何时候,创建类都可以替代为创建一个接口和工厂。\n",
+ "\n",
+ "很多人都掉进了这个陷阱,只要有可能就创建接口和工厂。这种逻辑看起来像是可能会使用不同的实现,所以总是添加这种抽象性。这变成了一种过早的设计优化。\n",
+ "\n",
+ "任何抽象性都应该是由真正的需求驱动的。当有必要时才应该使用接口进行重构,而不是到处添加额外的间接层,从而带来额外的复杂性。这种复杂性非常显著,如果你让某人去处理这种复杂性,只是因为你意识到“以防万一”而添加新接口,而没有其他具有说服力的原因——好吧,如果我碰上了这种设计,就会质疑此人所作的所有其他设计了。\n",
+ "\n",
+ "恰当的原则是优先使用类而不是接口。从类开始,如果使用接口的必要性变得很明确,那么就重构。接口是一个伟大的工具,但它们容易被滥用。\n",
+ "\n",
+ "\n",
+ "\n",
+ ""
+ ]
+ }
+ ],
+ "metadata": {},
+ "nbformat": 4,
+ "nbformat_minor": 4
+}
diff --git a/jupyter/11-Inner-Classes.ipynb b/jupyter/11-Inner-Classes.ipynb
new file mode 100644
index 00000000..5d361004
--- /dev/null
+++ b/jupyter/11-Inner-Classes.ipynb
@@ -0,0 +1,2189 @@
+{
+ "cells": [
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "[TOC]\n",
+ "\n",
+ "\n",
+ "\n",
+ "# 第十一章 内部类\n",
+ "\n",
+ "> 一个定义在另一个类中的类,叫作内部类。\n",
+ "\n",
+ "内部类是一种非常有用的特性,因为它允许你把一些逻辑相关的类组织在一起,并控制位于内部的类的可见性。然而必须要了解,内部类与组合是完全不同的概念,这一点很重要。在最初,内部类看起来就像是一种代码隐藏机制:将类置于其他类的内部。但是,你将会了解到,内部类远不止如此,它了解外围类,并能与之通信,而且你用内部类写出的代码更加优雅而清晰,尽管并不总是这样(而且 Java 8 的 Lambda 表达式和方法引用减少了编写内部类的需求)。\n",
+ "\n",
+ "最初,内部类可能看起来有些奇怪,而且要花些时间才能在设计中轻松地使用它们。对内部类的需求并非总是很明显的,但是在描述完内部类的基本语法与语义之后,\"Why inner classes?\"就应该使得内部类的益处明确显现了。\n",
+ "\n",
+ "本章剩余部分包含了对内部类语法更加详尽的探索,这些特性是为了语言的完备性而设计的,但是你也许不需要使用它们,至少一开始不需要。因此,本章最初的部分也许就是你现在所需的全部,你可以将更详尽的探索当作参考资料。\n",
+ "\n",
+ "\n",
+ "\n",
+ "## 创建内部类\n",
+ "\n",
+ "创建内部类的方式就如同你想的一样——把类的定义置于外围类的里面:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "// innerclasses/Parcel1.java\n",
+ "// Creating inner classes\n",
+ "public class Parcel1 {\n",
+ " class Contents {\n",
+ " private int i = 11;\n",
+ " \n",
+ " public int value() { return i; }\n",
+ " }\n",
+ " \n",
+ " class Destination {\n",
+ " private String label;\n",
+ " \n",
+ " Destination(String whereTo) {\n",
+ " label = whereTo;\n",
+ " }\n",
+ " \n",
+ " String readLabel() { return label; }\n",
+ " }\n",
+ " // Using inner classes looks just like\n",
+ " // using any other class, within Parcel1:\n",
+ " public void ship(String dest) {\n",
+ " Contents c = new Contents();\n",
+ " Destination d = new Destination(dest);\n",
+ " System.out.println(d.readLabel());\n",
+ " }\n",
+ " \n",
+ " public static void main(String[] args) {\n",
+ " Parcel1 p = new Parcel1();\n",
+ " p.ship(\"Tasmania\");\n",
+ " }\n",
+ "}"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "输出为:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "Tasmania"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "当我们在 `ship()` 方法里面使用内部类的时候,与使用普通类没什么不同。在这里,明显的区别只是内部类的名字是嵌套在 **Parcel1** 里面的。\n",
+ "\n",
+ "更典型的情况是,外部类将有一个方法,该方法返回一个指向内部类的引用,就像在 `to()` 和 `contents()` 方法中看到的那样:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "// innerclasses/Parcel2.java\n",
+ "// Returning a reference to an inner class\n",
+ "public class Parcel2 {\n",
+ " class Contents {\n",
+ " private int i = 11;\n",
+ " \n",
+ " public int value() { return i; }\n",
+ " }\n",
+ " \n",
+ " class Destination {\n",
+ " private String label;\n",
+ " \n",
+ " Destination(String whereTo) {\n",
+ " label = whereTo;\n",
+ " }\n",
+ " \n",
+ " String readLabel() { return label; }\n",
+ " }\n",
+ " \n",
+ " public Destination to(String s) {\n",
+ " return new Destination(s);\n",
+ " }\n",
+ " \n",
+ " public Contents contents() {\n",
+ " return new Contents();\n",
+ " }\n",
+ " \n",
+ " public void ship(String dest) {\n",
+ " Contents c = contents();\n",
+ " Destination d = to(dest);\n",
+ " System.out.println(d.readLabel());\n",
+ " }\n",
+ " \n",
+ " public static void main(String[] args) {\n",
+ " Parcel2 p = new Parcel2();\n",
+ " p.ship(\"Tasmania\");\n",
+ " Parcel2 q = new Parcel2();\n",
+ " // Defining references to inner classes:\n",
+ " Parcel2.Contents c = q.contents();\n",
+ " Parcel2.Destination d = q.to(\"Borneo\");\n",
+ " }\n",
+ "}"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "输出为:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "Tasmania"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "如果想从外部类的非静态方法之外的任意位置创建某个内部类的对象,那么必须像在 `main()` 方法中那样,具体地指明这个对象的类型:*OuterClassName.InnerClassName*。(译者注:在外部类的静态方法中也可以直接指明类型 *InnerClassName*,在其他类中需要指明 *OuterClassName.InnerClassName*。)\n",
+ "\n",
+ "\n",
+ "\n",
+ "## 链接外部类\n",
+ "\n",
+ "到目前为止,内部类似乎还只是一种名字隐藏和组织代码的模式。这些是很有用,但还不是最引人注目的,它还有其他的用途。当生成一个内部类的对象时,此对象与制造它的外围对象(enclosing object)之间就有了一种联系,所以它能访问其外围对象的所有成员,而不需要任何特殊条件。此外,内部类还拥有其外围类的所有元素的访问权。"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "// innerclasses/Sequence.java\n",
+ "// Holds a sequence of Objects\n",
+ "interface Selector {\n",
+ " boolean end();\n",
+ " Object current();\n",
+ " void next();\n",
+ "}\n",
+ "public class Sequence {\n",
+ " private Object[] items;\n",
+ " private int next = 0;\n",
+ " public Sequence(int size) {\n",
+ " items = new Object[size];\n",
+ " }\n",
+ " public void add(Object x) {\n",
+ " if(next < items.length)\n",
+ " items[next++] = x;\n",
+ " }\n",
+ " private class SequenceSelector implements Selector {\n",
+ " private int i = 0;\n",
+ " @Override\n",
+ " public boolean end() { return i == items.length; }\n",
+ " @Override\n",
+ " public Object current() { return items[i]; }\n",
+ " @Override\n",
+ " public void next() { if(i < items.length) i++; }\n",
+ " }\n",
+ " public Selector selector() {\n",
+ " return new SequenceSelector();\n",
+ " }\n",
+ " public static void main(String[] args) {\n",
+ " Sequence sequence = new Sequence(10);\n",
+ " for(int i = 0; i < 10; i++)\n",
+ " sequence.add(Integer.toString(i));\n",
+ " Selector selector = sequence.selector();\n",
+ " while(!selector.end()) {\n",
+ " System.out.print(selector.current() + \" \");\n",
+ " selector.next();\n",
+ " }\n",
+ " }\n",
+ "}"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "输出为:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "0 1 2 3 4 5 6 7 8 9"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "**Sequence** 类只是一个固定大小的 **Object** 的数组,以类的形式包装了起来。可以调用 `add()` 在序列末尾增加新的 **Object**(只要还有空间),要获取 **Sequence** 中的每一个对象,可以使用 **Selector** 接口。这是“迭代器”设计模式的一个例子,在本书稍后的部分将更多地学习它。**Selector** 允许你检查序列是否到末尾了(`end()`),访问当前对象(`current()`),以及移到序列中的下一个对象(`next()`)。因为 **Selector** 是一个接口,所以别的类可以按它们自己的方式来实现这个接口,并且其他方法能以此接口为参数,来生成更加通用的代码。\n",
+ "\n",
+ "这里,**SequenceSelector** 是提供 **Selector** 功能的 **private** 类。可以看到,在 `main()` 中创建了一个 **Sequence**,并向其中添加了一些 **String** 对象。然后通过调用 `selector()` 获取一个 **Selector**,并用它在 **Sequence** 中移动和选择每一个元素。\n",
+ "最初看到 **SequenceSelector**,可能会觉得它只不过是另一个内部类罢了。但请仔细观察它,注意方法 `end()`,`current()` 和 `next()` 都用到了 **items**,这是一个引用,它并不是 **SequenceSelector** 的一部分,而是外围类中的一个 **private** 字段。然而内部类可以访问其外围类的方法和字段,就像自己拥有它们似的,这带来了很大的方便,就如前面的例子所示。\n",
+ "\n",
+ "所以内部类自动拥有对其外围类所有成员的访问权。这是如何做到的呢?当某个外围类的对象创建了一个内部类对象时,此内部类对象必定会秘密地捕获一个指向那个外围类对象的引用。然后,在你访问此外围类的成员时,就是用那个引用来选择外围类的成员。幸运的是,编译器会帮你处理所有的细节,但你现在可以看到:内部类的对象只能在与其外围类的对象相关联的情况下才能被创建(就像你应该看到的,内部类是非 **static** 类时)。构建内部类对象时,需要一个指向其外围类对象的引用,如果编译器访问不到这个引用就会报错。不过绝大多数时候这都无需程序员操心。\n",
+ "\n",
+ "\n",
+ "## 使用 .this 和 .new\n",
+ "\n",
+ "如果你需要生成对外部类对象的引用,可以使用外部类的名字后面紧跟圆点和 **this**。这样产生的引用自动地具有正确的类型,这一点在编译期就被知晓并受到检查,因此没有任何运行时开销。下面的示例展示了如何使用 **.this**:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "// innerclasses/DotThis.java\n",
+ "// Accessing the outer-class object\n",
+ "public class DotThis {\n",
+ " void f() { System.out.println(\"DotThis.f()\"); }\n",
+ " \n",
+ " public class Inner {\n",
+ " public DotThis outer() {\n",
+ " return DotThis.this;\n",
+ " // A plain \"this\" would be Inner's \"this\"\n",
+ " }\n",
+ " }\n",
+ " \n",
+ " public Inner inner() { return new Inner(); }\n",
+ " \n",
+ " public static void main(String[] args) {\n",
+ " DotThis dt = new DotThis();\n",
+ " DotThis.Inner dti = dt.inner();\n",
+ " dti.outer().f();\n",
+ " }\n",
+ "}"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "输出为:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "DotThis.f()"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "有时你可能想要告知某些其他对象,去创建其某个内部类的对象。要实现此目的,你必须在 **new** 表达式中提供对其他外部类对象的引用,这是需要使用 **.new** 语法,就像下面这样:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "// innerclasses/DotNew.java\n",
+ "// Creating an inner class directly using .new syntax\n",
+ "public class DotNew {\n",
+ " public class Inner {}\n",
+ " public static void main(String[] args) {\n",
+ " DotNew dn = new DotNew();\n",
+ " DotNew.Inner dni = dn.new Inner();\n",
+ " }\n",
+ "}"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "要想直接创建内部类的对象,你不能按照你想象的方式,去引用外部类的名字 **DotNew**,而是必须使用外部类的对象来创建该内部类对象,就像在上面的程序中所看到的那样。这也解决了内部类名字作用域的问题,因此你不必声明(实际上你不能声明)dn.new DotNew.Inner。\n",
+ "\n",
+ "下面你可以看到将 **.new** 应用于 Parcel 的示例:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "// innerclasses/Parcel3.java\n",
+ "// Using .new to create instances of inner classes\n",
+ "public class Parcel3 {\n",
+ " class Contents {\n",
+ " private int i = 11;\n",
+ " public int value() { return i; }\n",
+ " }\n",
+ " class Destination {\n",
+ " private String label;\n",
+ " Destination(String whereTo) { label = whereTo; }\n",
+ " String readLabel() { return label; }\n",
+ " }\n",
+ " public static void main(String[] args) {\n",
+ " Parcel3 p = new Parcel3();\n",
+ " // Must use instance of outer class\n",
+ " // to create an instance of the inner class:\n",
+ " Parcel3.Contents c = p.new Contents();\n",
+ " Parcel3.Destination d =\n",
+ " p.new Destination(\"Tasmania\");\n",
+ " }\n",
+ "}"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "在拥有外部类对象之前是不可能创建内部类对象的。这是因为内部类对象会暗暗地连接到建它的外部类对象上。但是,如果你创建的是嵌套类(静态内部类),那么它就不需要对外部类对象的引用。\n",
+ "\n",
+ "\n",
+ "\n",
+ "## 内部类与向上转型\n",
+ "\n",
+ "当将内部类向上转型为其基类,尤其是转型为一个接口的时候,内部类就有了用武之地。(从实现了某个接口的对象,得到对此接口的引用,与向上转型为这个对象的基类,实质上效果是一样的。)这是因为此内部类-某个接口的实现-能够完全不可见,并且不可用。所得到的只是指向基类或接口的引用,所以能够很方便地隐藏实现细节。\n",
+ "\n",
+ "我们可以创建前一个示例的接口:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "// innerclasses/Destination.java\n",
+ "public interface Destination {\n",
+ " String readLabel();\n",
+ "}"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "// innerclasses/Contents.java\n",
+ "public interface Contents {\n",
+ " int value();\n",
+ "}"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "现在 **Contents** 和 **Destination** 表示客户端程序员可用的接口。记住,接口的所有成员自动被设置为 **public**。\n",
+ "\n",
+ "当取得了一个指向基类或接口的引用时,甚至可能无法找出它确切的类型,看下面的例子:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "// innerclasses/TestParcel.java\n",
+ "class Parcel4 {\n",
+ " private class PContents implements Contents {\n",
+ " private int i = 11;\n",
+ " @Override\n",
+ " public int value() { return i; }\n",
+ " }\n",
+ " protected final class PDestination implements Destination {\n",
+ " private String label;\n",
+ " private PDestination(String whereTo) {\n",
+ " label = whereTo;\n",
+ " }\n",
+ " @Override\n",
+ " public String readLabel() { return label; }\n",
+ " }\n",
+ " public Destination destination(String s) {\n",
+ " return new PDestination(s);\n",
+ " }\n",
+ " public Contents contents() {\n",
+ " return new PContents();\n",
+ " }\n",
+ "}\n",
+ "public class TestParcel {\n",
+ " public static void main(String[] args) {\n",
+ " Parcel4 p = new Parcel4();\n",
+ " Contents c = p.contents();\n",
+ " Destination d = p.destination(\"Tasmania\");\n",
+ " // Illegal -- can't access private class:\n",
+ " //- Parcel4.PContents pc = p.new PContents();\n",
+ " }\n",
+ "}"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "在 **Parcel4** 中,内部类 **PContents** 是 **private**,所以除了 **Parcel4**,没有人能访问它。普通(非内部)类的访问权限不能被设为 **private** 或者 **protected**;他们只能设置为 **public** 或 **package** 访问权限。\n",
+ "\n",
+ "**PDestination** 是 **protected**,所以只有 **Parcel4** 及其子类、还有与 **Parcel4** 同一个包中的类(因为 **protected** 也给予了包访问权)能访问 **PDestination**,其他类都不能访问 **PDestination**,这意味着,如果客户端程序员想了解或访问这些成员,那是要受到限制的。实际上,甚至不能向下转型成 **private** 内部类(或 **protected** 内部类,除非是继承自它的子类),因为不能访问其名字,就像在 **TestParcel** 类中看到的那样。\n",
+ "\n",
+ "**private** 内部类给类的设计者提供了一种途径,通过这种方式可以完全阻止任何依赖于类型的编码,并且完全隐藏了实现的细节。此外,从客户端程序员的角度来看,由于不能访问任何新增加的、原本不属于公共接口的方法,所以扩展接口是没有价值的。这也给 Java 编译器提供了生成高效代码的机会。\n",
+ "\n",
+ "\n",
+ "\n",
+ "## 内部类方法和作用域\n",
+ "\n",
+ "到目前为止,读者所看到的只是内部类的典型用途。通常,如果所读、写的代码包含了内部类,那么它们都是“平凡的”内部类,简单并且容易理解。然而,内部类的语法覆盖了大量其他的更加难以理解的技术。例如,可以在一个方法里面或者在任意的作用域内定义内部类。\n",
+ "\n",
+ "这么做有两个理由:\n",
+ "\n",
+ "1. 如前所示,你实现了某类型的接口,于是可以创建并返回对其的引用。\n",
+ "2. 你要解决一个复杂的问题,想创建一个类来辅助你的解决方案,但是又不希望这个类是公共可用的。\n",
+ "\n",
+ "在后面的例子中,先前的代码将被修改,以用来实现:\n",
+ "\n",
+ "1. 一个定义在方法中的类。\n",
+ "2. 一个定义在作用域内的类,此作用域在方法的内部。\n",
+ "3. 一个实现了接口的匿名类。\n",
+ "4. 一个匿名类,它扩展了没有默认构造器的类。\n",
+ "5. 一个匿名类,它执行字段初始化。\n",
+ "6. 一个匿名类,它通过实例初始化实现构造(匿名内部类不可能有构造器)。\n",
+ "\n",
+ "第一个例子展示了在方法的作用域内(而不是在其他类的作用域内)创建一个完整的类。这被称作局部内部类:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "// innerclasses/Parcel5.java\n",
+ "// Nesting a class within a method\n",
+ "public class Parcel5 {\n",
+ " public Destination destination(String s) {\n",
+ " final class PDestination implements Destination {\n",
+ " private String label;\n",
+ " \n",
+ " private PDestination(String whereTo) {\n",
+ " label = whereTo;\n",
+ " }\n",
+ " \n",
+ " @Override\n",
+ " public String readLabel() { return label; }\n",
+ " }\n",
+ " return new PDestination(s);\n",
+ " }\n",
+ " \n",
+ " public static void main(String[] args) {\n",
+ " Parcel5 p = new Parcel5();\n",
+ " Destination d = p.destination(\"Tasmania\");\n",
+ " }\n",
+ "}"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "**PDestination** 类是 `destination()` 方法的一部分,而不是 **Parcel5** 的一部分。所以,在 `destination()` 之外不能访问 **PDestination**,注意出现在 **return** 语句中的向上转型-返回的是 **Destination** 的引用,它是 **PDestination** 的基类。当然,在 `destination()` 中定义了内部类 **PDestination**,并不意味着一旦 `destination()` 方法执行完毕,**PDestination** 就不可用了。\n",
+ "\n",
+ "你可以在同一个子目录下的任意类中对某个内部类使用类标识符 **PDestination**,这并不会有命名冲突。\n",
+ "\n",
+ "下面的例子展示了如何在任意的作用域内嵌入一个内部类:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "// innerclasses/Parcel6.java\n",
+ "// Nesting a class within a scope\n",
+ "public class Parcel6 {\n",
+ " private void internalTracking(boolean b) {\n",
+ " if(b) {\n",
+ " class TrackingSlip {\n",
+ " private String id;\n",
+ " TrackingSlip(String s) {\n",
+ " id = s;\n",
+ " }\n",
+ " String getSlip() { return id; }\n",
+ " }\n",
+ " TrackingSlip ts = new TrackingSlip(\"slip\");\n",
+ " String s = ts.getSlip();\n",
+ " }\n",
+ " // Can't use it here! Out of scope:\n",
+ " //- TrackingSlip ts = new TrackingSlip(\"x\");\n",
+ " }\n",
+ " public void track() { internalTracking(true); }\n",
+ " public static void main(String[] args) {\n",
+ " Parcel6 p = new Parcel6();\n",
+ " p.track();\n",
+ " }\n",
+ "}"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "**TrackingSlip** 类被嵌入在 **if** 语句的作用域内,这并不是说该类的创建是有条件的,它其实与别的类一起编译过了。然而,在定义 **Trackingslip** 的作用域之外,它是不可用的,除此之外,它与普通的类一样。\n",
+ "\n",
+ "\n",
+ "\n",
+ "## 匿名内部类\n",
+ "\n",
+ "下面的例子看起来有点奇怪:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "// innerclasses/Parcel7.java\n",
+ "// Returning an instance of an anonymous inner class\n",
+ "public class Parcel7 {\n",
+ " public Contents contents() {\n",
+ " return new Contents() { // Insert class definition\n",
+ " private int i = 11;\n",
+ " \n",
+ " @Override\n",
+ " public int value() { return i; }\n",
+ " }; // Semicolon required\n",
+ " }\n",
+ " \n",
+ " public static void main(String[] args) {\n",
+ " Parcel7 p = new Parcel7();\n",
+ " Contents c = p.contents();\n",
+ " }\n",
+ "}"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "`contents()` 方法将返回值的生成与表示这个返回值的类的定义结合在一起!另外,这个类是匿名的,它没有名字。更糟的是,看起来似乎是你正要创建一个 **Contents** 对象。但是然后(在到达语句结束的分号之前)你却说:“等一等,我想在这里插入一个类的定义。”\n",
+ "\n",
+ "这种奇怪的语法指的是:“创建一个继承自 **Contents** 的匿名类的对象。”通过 **new** 表达式返回的引用被自动向上转型为对 **Contents** 的引用。上述匿名内部类的语法是下述形式的简化形式:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "// innerclasses/Parcel7b.java\n",
+ "// Expanded version of Parcel7.java\n",
+ "public class Parcel7b {\n",
+ " class MyContents implements Contents {\n",
+ " private int i = 11;\n",
+ " @Override\n",
+ " public int value() { return i; }\n",
+ " }\n",
+ " \n",
+ " public Contents contents() {\n",
+ " return new MyContents();\n",
+ " }\n",
+ " \n",
+ " public static void main(String[] args) {\n",
+ " Parcel7b p = new Parcel7b();\n",
+ " Contents c = p.contents();\n",
+ " }\n",
+ "}"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "在这个匿名内部类中,使用了默认的构造器来生成 **Contents**。下面的代码展示的是,如果你的基类需要一个有参数的构造器,应该怎么办:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "// innerclasses/Parcel8.java\n",
+ "// Calling the base-class constructor\n",
+ "public class Parcel8 {\n",
+ " public Wrapping wrapping(int x) {\n",
+ " // Base constructor call:\n",
+ " return new Wrapping(x) { // [1]\n",
+ " @Override\n",
+ " public int value() {\n",
+ " return super.value() * 47;\n",
+ " }\n",
+ " }; // [2]\n",
+ " }\n",
+ " public static void main(String[] args) {\n",
+ " Parcel8 p = new Parcel8();\n",
+ " Wrapping w = p.wrapping(10);\n",
+ " }\n",
+ "}"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "- \\[1\\] 将合适的参数传递给基类的构造器。\n",
+ "- \\[2\\] 在匿名内部类末尾的分号,并不是用来标记此内部类结束的。实际上,它标记的是表达式的结束,只不过这个表达式正巧包含了匿名内部类罢了。因此,这与别的地方使用的分号是一致的。\n",
+ "\n",
+ "尽管 **Wrapping** 只是一个具有具体实现的普通类,但它还是被导出类当作公共“接口”来使用。"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "// innerclasses/Wrapping.java\n",
+ "public class Wrapping {\n",
+ " private int i;\n",
+ " public Wrapping(int x) { i = x; }\n",
+ " public int value() { return i; }\n",
+ "}"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "为了多样性,**Wrapping** 拥有一个要求传递一个参数的构造器。\n",
+ "\n",
+ "在匿名类中定义字段时,还能够对其执行初始化操作:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "// innerclasses/Parcel9.java\n",
+ "public class Parcel9 {\n",
+ " // Argument must be final or \"effectively final\"\n",
+ " // to use within the anonymous inner class:\n",
+ " public Destination destination(final String dest) {\n",
+ " return new Destination() {\n",
+ " private String label = dest;\n",
+ " @Override\n",
+ " public String readLabel() { return label; }\n",
+ " };\n",
+ " }\n",
+ " public static void main(String[] args) {\n",
+ " Parcel9 p = new Parcel9();\n",
+ " Destination d = p.destination(\"Tasmania\");\n",
+ " }\n",
+ "}"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "如果定义一个匿名内部类,并且希望它使用一个在其外部定义的对象,那么编译器会要求其参数引用是 **final** 的(也就是说,它在初始化后不会改变,所以可以被当作 **final**),就像你在 `destination()` 的参数中看到的那样。这里省略掉 **final** 也没问题,但是通常最好加上 **final** 作为一种暗示。\n",
+ "\n",
+ "如果只是简单地给一个字段赋值,那么此例中的方法是很好的。但是,如果想做一些类似构造器的行为,该怎么办呢?在匿名类中不可能有命名构造器(因为它根本没名字!),但通过实例初始化,就能够达到为匿名内部类创建一个构造器的效果,就像这样:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "// innerclasses/AnonymousConstructor.java\n",
+ "// Creating a constructor for an anonymous inner class\n",
+ "abstract class Base {\n",
+ " Base(int i) {\n",
+ " System.out.println(\"Base constructor, i = \" + i);\n",
+ " }\n",
+ " public abstract void f();\n",
+ "}\n",
+ "public class AnonymousConstructor {\n",
+ " public static Base getBase(int i) {\n",
+ " return new Base(i) {\n",
+ " { System.out.println(\n",
+ " \"Inside instance initializer\"); }\n",
+ " @Override\n",
+ " public void f() {\n",
+ " System.out.println(\"In anonymous f()\");\n",
+ " }\n",
+ " };\n",
+ " }\n",
+ " public static void main(String[] args) {\n",
+ " Base base = getBase(47);\n",
+ " base.f();\n",
+ " }\n",
+ "}"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "输出为:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "Base constructor, i = 47\n",
+ "Inside instance initializer\n",
+ "In anonymous f()"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "在此例中,不要求变量一定是 **final** 的。因为被传递给匿名类的基类的构造器,它并不会在匿名类内部被直接使用。\n",
+ "\n",
+ "下例是带实例初始化的\"parcel\"形式。注意 `destination()` 的参数必须是 **final** 的,因为它们是在匿名类内部使用的(译者注:即使不加 **final**, Java 8 的编译器也会为我们自动加上 **final**,以保证数据的一致性)。"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "// innerclasses/Parcel10.java\n",
+ "// Using \"instance initialization\" to perform\n",
+ "// construction on an anonymous inner class\n",
+ "public class Parcel10 {\n",
+ " public Destination\n",
+ " destination(final String dest, final float price) {\n",
+ " return new Destination() {\n",
+ " private int cost;\n",
+ " // Instance initialization for each object:\n",
+ " {\n",
+ " cost = Math.round(price);\n",
+ " if(cost > 100)\n",
+ " System.out.println(\"Over budget!\");\n",
+ " }\n",
+ " private String label = dest;\n",
+ " @Override\n",
+ " public String readLabel() { return label; }\n",
+ " };\n",
+ " }\n",
+ " public static void main(String[] args) {\n",
+ " Parcel10 p = new Parcel10();\n",
+ " Destination d = p.destination(\"Tasmania\", 101.395F);\n",
+ " }\n",
+ "}"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "输出为:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "Over budget!"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "在实例初始化操作的内部,可以看到有一段代码,它们不能作为字段初始化动作的一部分来执行(就是 **if** 语句)。所以对于匿名类而言,实例初始化的实际效果就是构造器。当然它受到了限制-你不能重载实例初始化方法,所以你仅有一个这样的构造器。\n",
+ "\n",
+ "匿名内部类与正规的继承相比有些受限,因为匿名内部类既可以扩展类,也可以实现接口,但是不能两者兼备。而且如果是实现接口,也只能实现一个接口。\n",
+ "\n",
+ "\n",
+ "\n",
+ "## 嵌套类\n",
+ "\n",
+ "如果不需要内部类对象与其外围类对象之间有联系,那么可以将内部类声明为 **static**,这通常称为嵌套类。想要理解 **static** 应用于内部类时的含义,就必须记住,普通的内部类对象隐式地保存了一个引用,指向创建它的外围类对象。然而,当内部类是 **static** 的时,就不是这样了。嵌套类意味着:\n",
+ "\n",
+ "1. 要创建嵌套类的对象,并不需要其外围类的对象。\n",
+ "2. 不能从嵌套类的对象中访问非静态的外围类对象。\n",
+ "\n",
+ "嵌套类与普通的内部类还有一个区别。普通内部类的字段与方法,只能放在类的外部层次上,所以普通的内部类不能有 **static** 数据和 **static** 字段,也不能包含嵌套类。但是嵌套类可以包含所有这些东西:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "// innerclasses/Parcel11.java\n",
+ "// Nested classes (static inner classes)\n",
+ "public class Parcel11 {\n",
+ " private static class ParcelContents implements Contents {\n",
+ " private int i = 11;\n",
+ " @Override\n",
+ " public int value() { return i; }\n",
+ " }\n",
+ " protected static final class ParcelDestination\n",
+ " implements Destination {\n",
+ " private String label;\n",
+ " private ParcelDestination(String whereTo) {\n",
+ " label = whereTo;\n",
+ " }\n",
+ " @Override\n",
+ " public String readLabel() { return label; }\n",
+ " // Nested classes can contain other static elements:\n",
+ " public static void f() {}\n",
+ " static int x = 10;\n",
+ " static class AnotherLevel {\n",
+ " public static void f() {}\n",
+ " static int x = 10;\n",
+ " }\n",
+ " }\n",
+ " public static Destination destination(String s) {\n",
+ " return new ParcelDestination(s);\n",
+ " }\n",
+ " public static Contents contents() {\n",
+ " return new ParcelContents();\n",
+ " }\n",
+ " public static void main(String[] args) {\n",
+ " Contents c = contents();\n",
+ " Destination d = destination(\"Tasmania\");\n",
+ " }\n",
+ "}"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "在 `main()` 中,没有任何 **Parcel11** 的对象是必需的;而是使用选取 **static** 成员的普通语法来调用方法-这些方法返回对 **Contents** 和 **Destination** 的引用。\n",
+ "\n",
+ "就像你在本章前面看到的那样,在一个普通的(非 **static**)内部类中,通过一个特殊的 **this** 引用可以链接到其外围类对象。嵌套类就没有这个特殊的 **this** 引用,这使得它类似于一个 **static** 方法。\n",
+ "\n",
+ "### 接口内部的类\n",
+ "\n",
+ "嵌套类可以作为接口的一部分。你放到接口中的任何类都自动地是 **public** 和 **static** 的。因为类是 **static** 的,只是将嵌套类置于接口的命名空间内,这并不违反接口的规则。你甚至可以在内部类中实现其外围接口,就像下面这样:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "// innerclasses/ClassInInterface.java\n",
+ "// {java ClassInInterface$Test}\n",
+ "public interface ClassInInterface {\n",
+ " void howdy();\n",
+ " class Test implements ClassInInterface {\n",
+ " @Override\n",
+ " public void howdy() {\n",
+ " System.out.println(\"Howdy!\");\n",
+ " }\n",
+ " public static void main(String[] args) {\n",
+ " new Test().howdy();\n",
+ " }\n",
+ " }\n",
+ "}"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "输出为:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "Howdy!"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "如果你想要创建某些公共代码,使得它们可以被某个接口的所有不同实现所共用,那么使用接口内部的嵌套类会显得很方便。\n",
+ "\n",
+ "我曾在本书中建议过,在每个类中都写一个 `main()` 方法,用来测试这个类。这样做有一个缺点,那就是必须带着那些已编译过的额外代码。如果这对你是个麻烦,那就可以使用嵌套类来放置测试代码。"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "// innerclasses/TestBed.java\n",
+ "// Putting test code in a nested class\n",
+ "// {java TestBed$Tester}\n",
+ "public class TestBed {\n",
+ " public void f() { System.out.println(\"f()\"); }\n",
+ " public static class Tester {\n",
+ " public static void main(String[] args) {\n",
+ " TestBed t = new TestBed();\n",
+ " t.f();\n",
+ " }\n",
+ " }\n",
+ "}"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "输出为:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "f()"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "这生成了一个独立的类 **TestBed$Tester**(要运行这个程序,执行 **java TestBed$Tester**,在 Unix/Linux 系统中需要转义 **$**)。你可以使用这个类测试,但是不必在发布的产品中包含它,可以在打包产品前删除 **TestBed$Tester.class**。\n",
+ "\n",
+ "### 从多层嵌套类中访问外部类的成员\n",
+ "\n",
+ "一个内部类被嵌套多少层并不重要——它能透明地访问所有它所嵌入的外围类的所有成员,如下所示:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "// innerclasses/MultiNestingAccess.java\n",
+ "// Nested classes can access all members of all\n",
+ "// levels of the classes they are nested within\n",
+ "class MNA {\n",
+ " private void f() {}\n",
+ " class A {\n",
+ " private void g() {}\n",
+ " public class B {\n",
+ " void h() {\n",
+ " g();\n",
+ " f();\n",
+ " }\n",
+ " }\n",
+ " }\n",
+ "}\n",
+ "public class MultiNestingAccess {\n",
+ " public static void main(String[] args) {\n",
+ " MNA mna = new MNA();\n",
+ " MNA.A mnaa = mna.new A();\n",
+ " MNA.A.B mnaab = mnaa.new B();\n",
+ " mnaab.h();\n",
+ " }\n",
+ "}"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "可以看到在 **MNA.A.B** 中,调用方法 `g()` 和 `f()` 不需要任何条件(即使它们被定义为 **private**)。这个例子同时展示了如何从不同的类里创建多层嵌套的内部类对象的基本语法。\"**.new**\"语法能产生正确的作用域,所以不必在调用构造器时限定类名。\n",
+ "\n",
+ "\n",
+ "\n",
+ "## 为什么需要内部类\n",
+ "\n",
+ "至此,我们已经看到了许多描述内部类的语法和语义,但是这并不能同答“为什么需要内部类”这个问题。那么,Java 设计者们为什么会如此费心地增加这项基本的语言特性呢?\n",
+ "\n",
+ "一般说来,内部类继承自某个类或实现某个接口,内部类的代码操作创建它的外围类的对象。所以可以认为内部类提供了某种进入其外围类的窗口。\n",
+ "\n",
+ "内部类必须要回答的一个问题是:如果只是需要一个对接口的引用,为什么不通过外围类实现那个接口呢?答案是:“如果这能满足需求,那么就应该这样做。”那么内部类实现一个接口与外围类实现这个接口有什么区别呢?答案是:后者不是总能享用到接口带来的方便,有时需要用到接口的实现。所以,使用内部类最吸引人的原因是:\n",
+ "\n",
+ "> 每个内部类都能独立地继承自一个(接口的)实现,所以无论外围类是否已经继承了某个(接口的)实现,对于内部类都没有影响。\n",
+ "\n",
+ "如果没有内部类提供的、可以继承多个具体的或抽象的类的能力,一些设计与编程问题就很难解决。从这个角度看,内部类使得多重继承的解决方案变得完整。接口解决了部分问题,而内部类有效地实现了“多重继承”。也就是说,内部类允许继承多个非接口类型(译注:类或抽象类)。\n",
+ "\n",
+ "为了看到更多的细节,让我们考虑这样一种情形:即必须在一个类中以某种方式实现两个接口。由于接口的灵活性,你有两种选择;使用单一类,或者使用内部类:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "// innerclasses/mui/MultiInterfaces.java\n",
+ "// Two ways a class can implement multiple interfaces\n",
+ "// {java innerclasses.mui.MultiInterfaces}\n",
+ "package innerclasses.mui;\n",
+ "interface A {}\n",
+ "interface B {}\n",
+ "class X implements A, B {}\n",
+ "class Y implements A {\n",
+ " B makeB() {\n",
+ " // Anonymous inner class:\n",
+ " return new B() {};\n",
+ " }\n",
+ "}\n",
+ "public class MultiInterfaces {\n",
+ " static void takesA(A a) {}\n",
+ " static void takesB(B b) {}\n",
+ " public static void main(String[] args) {\n",
+ " X x = new X();\n",
+ " Y y = new Y();\n",
+ " takesA(x);\n",
+ " takesA(y);\n",
+ " takesB(x);\n",
+ " takesB(y.makeB());\n",
+ " }\n",
+ "}"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "当然,这里假设在两种方式下的代码结构都确实有逻辑意义。然而遇到问题的时候,通常问题本身就能给出某些指引,告诉你是应该使用单一类,还是使用内部类。但如果没有任何其他限制,从实现的观点来看,前面的例子并没有什么区别,它们都能正常运作。\n",
+ "\n",
+ "如果拥有的是抽象的类或具体的类,而不是接口,那就只能使用内部类才能实现多重继承:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "// innerclasses/MultiImplementation.java\n",
+ "// For concrete or abstract classes, inner classes\n",
+ "// produce \"multiple implementation inheritance\"\n",
+ "// {java innerclasses.MultiImplementation}\n",
+ "package innerclasses;\n",
+ "\n",
+ "class D {}\n",
+ "\n",
+ "abstract class E {}\n",
+ "\n",
+ "class Z extends D {\n",
+ " E makeE() {\n",
+ " return new E() {}; \n",
+ " }\n",
+ "}\n",
+ "\n",
+ "public class MultiImplementation {\n",
+ " static void takesD(D d) {}\n",
+ " static void takesE(E e) {}\n",
+ " \n",
+ " public static void main(String[] args) {\n",
+ " Z z = new Z();\n",
+ " takesD(z);\n",
+ " takesE(z.makeE());\n",
+ " }\n",
+ "}"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "如果不需要解决“多重继承”的问题,那么自然可以用别的方式编码,而不需要使用内部类。但如果使用内部类,还可以获得其他一些特性:\n",
+ "\n",
+ "1. 内部类可以有多个实例,每个实例都有自己的状态信息,并且与其外围类对象的信息相互独立。\n",
+ "2. 在单个外围类中,可以让多个内部类以不同的方式实现同一个接口,或继承同一个类。\n",
+ "稍后就会展示一个这样的例子。\n",
+ "3. 创建内部类对象的时刻并不依赖于外围类对象的创建\n",
+ "4. 内部类并没有令人迷惑的\"is-a”关系,它就是一个独立的实体。\n",
+ "\n",
+ "举个例子,如果 **Sequence.java** 不使用内部类,就必须声明\"**Sequence** 是一个 **Selector**\",对于某个特定的 **Sequence** 只能有一个 **Selector**,然而使用内部类很容易就能拥有另一个方法 `reverseSelector()`,用它来生成一个反方向遍历序列的 **Selector**,只有内部类才有这种灵活性。\n",
+ "\n",
+ "### 闭包与回调\n",
+ "\n",
+ "闭包(**closure**)是一个可调用的对象,它记录了一些信息,这些信息来自于创建它的作用域。通过这个定义,可以看出内部类是面向对象的闭包,因为它不仅包含外围类对象(创建内部类的作用域)的信息,还自动拥有一个指向此外围类对象的引用,在此作用域内,内部类有权操作所有的成员,包括 **private** 成员。\n",
+ "\n",
+ "在 Java 8 之前,内部类是实现闭包的唯一方式。在 Java 8 中,我们可以使用 lambda 表达式来实现闭包行为,并且语法更加优雅和简洁,你将会在 [函数式编程 ]() 这一章节中学习相关细节。尽管相对于内部类,你可能更喜欢使用 lambda 表达式实现闭包,但是你会看到并需要理解那些在 Java 8 之前通过内部类方式实现闭包的代码,因此仍然有必要来理解这种方式。\n",
+ "\n",
+ "Java 最引人争议的问题之一就是,人们认为 Java 应该包含某种类似指针的机制,以允许回调(callback)。通过回调,对象能够携带一些信息,这些信息允许它在稍后的某个时刻调用初始的对象。稍后将会看到这是一个非常有用的概念。如果回调是通过指针实现的,那么就只能寄希望于程序员不会误用该指针。然而,读者应该已经了解到,Java 更小心仔细,所以没有在语言中包括指针。\n",
+ "\n",
+ "通过内部类提供闭包的功能是优良的解决方案,它比指针更灵活、更安全。见下例:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "// innerclasses/Callbacks.java\n",
+ "// Using inner classes for callbacks\n",
+ "// {java innerclasses.Callbacks}\n",
+ "package innerclasses;\n",
+ "interface Incrementable {\n",
+ " void increment();\n",
+ "}\n",
+ "// Very simple to just implement the interface:\n",
+ "class Callee1 implements Incrementable {\n",
+ " private int i = 0;\n",
+ " @Override\n",
+ " public void increment() {\n",
+ " i++;\n",
+ " System.out.println(i);\n",
+ " }\n",
+ "}\n",
+ "class MyIncrement {\n",
+ " public void increment() {\n",
+ " System.out.println(\"Other operation\");\n",
+ " }\n",
+ " static void f(MyIncrement mi) { mi.increment(); }\n",
+ "}\n",
+ "// If your class must implement increment() in\n",
+ "// some other way, you must use an inner class:\n",
+ "class Callee2 extends MyIncrement {\n",
+ " private int i = 0;\n",
+ " @Override\n",
+ " public void increment() {\n",
+ " super.increment();\n",
+ " i++;\n",
+ " System.out.println(i);\n",
+ " }\n",
+ " private class Closure implements Incrementable {\n",
+ " @Override\n",
+ " public void increment() {\n",
+ " // Specify outer-class method, otherwise\n",
+ " // you'll get an infinite recursion:\n",
+ " Callee2.this.increment();\n",
+ " }\n",
+ " }\n",
+ " Incrementable getCallbackReference() {\n",
+ " return new Closure();\n",
+ " }\n",
+ "}\n",
+ "class Caller {\n",
+ " private Incrementable callbackReference;\n",
+ " Caller(Incrementable cbh) {\n",
+ " callbackReference = cbh;\n",
+ " }\n",
+ " void go() { callbackReference.increment(); }\n",
+ "}\n",
+ "public class Callbacks {\n",
+ " public static void main(String[] args) {\n",
+ " Callee1 c1 = new Callee1();\n",
+ " Callee2 c2 = new Callee2();\n",
+ " MyIncrement.f(c2);\n",
+ " Caller caller1 = new Caller(c1);\n",
+ " Caller caller2 =\n",
+ " new Caller(c2.getCallbackReference());\n",
+ " caller1.go();\n",
+ " caller1.go();\n",
+ " caller2.go();\n",
+ " caller2.go();\n",
+ " }\n",
+ "}"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "输出为:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "Other operation\n",
+ "1\n",
+ "1\n",
+ "2\n",
+ "Other operation\n",
+ "2\n",
+ "Other operation\n",
+ "3"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "这个例子进一步展示了外围类实现一个接口与内部类实现此接口之间的区别。就代码而言,**Callee1** 是更简单的解决方式。**Callee2** 继承自 **MyIncrement**,后者已经有了一个不同的 `increment()` 方法,并且与 **Incrementable** 接口期望的 `increment()` 方法完全不相关。所以如果 **Callee2** 继承了 **MyIncrement**,就不能为了 **Incrementable** 的用途而覆盖 `increment()` 方法,于是只能使用内部类独立地实现 **Incrementable**,还要注意,当创建了一个内部类时,并没有在外围类的接口中添加东西,也没有修改外围类的接口。\n",
+ "\n",
+ "注意,在 **Callee2** 中除了 `getCallbackReference()` 以外,其他成员都是 **private** 的。要想建立与外部世界的任何连接,接口 **Incrementable** 都是必需的。在这里可以看到,**interface** 是如何允许接口与接口的实现完全独立的。\n",
+ "内部类 **Closure** 实现了 **Incrementable**,以提供一个返回 **Callee2** 的“钩子”(hook)-而且是一个安全的钩子。无论谁获得此 **Incrementable** 的引用,都只能调用 `increment()`,除此之外没有其他功能(不像指针那样,允许你做很多事情)。\n",
+ "\n",
+ "**Caller** 的构造器需要一个 **Incrementable** 的引用作为参数(虽然可以在任意时刻捕获回调引用),然后在以后的某个时刻,**Caller** 对象可以使用此引用回调 **Callee** 类。\n",
+ "\n",
+ "回调的价值在于它的灵活性-可以在运行时动态地决定需要调用什么方法。例如,在图形界面实现 GUI 功能的时候,到处都用到回调。\n",
+ "\n",
+ "### 内部类与控制框架\n",
+ "\n",
+ "在将要介绍的控制框架(control framework)中,可以看到更多使用内部类的具体例子。\n",
+ "\n",
+ "应用程序框架(application framework)就是被设计用以解决某类特定问题的一个类或一组类。要运用某个应用程序框架,通常是继承一个或多个类,并覆盖某些方法。在覆盖后的方法中,编写代码定制应用程序框架提供的通用解决方案,以解决你的特定问题。这是设计模式中模板方法的一个例子,模板方法包含算法的基本结构,并且会调用一个或多个可覆盖的方法,以完成算法的动作。设计模式总是将变化的事物与保持不变的事物分离开,在这个模式中,模板方法是保持不变的事物,而可覆盖的方法就是变化的事物。\n",
+ "\n",
+ "控制框架是一类特殊的应用程序框架,它用来解决响应事件的需求。主要用来响应事件的系统被称作*事件驱动*系统。应用程序设计中常见的问题之一是图形用户接口(GUI),它几乎完全是事件驱动的系统。\n",
+ "\n",
+ "要理解内部类是如何允许简单的创建过程以及如何使用控制框架的,请考虑这样一个控制框架,它的工作就是在事件“就绪”的时候执行事件。虽然“就绪”可以指任何事,但在本例中是指基于时间触发的事件。接下来的问题就是,对于要控制什么,控制框架并不包含任何具体的信息。那些信息是在实现算法的 `action()` 部分时,通过继承来提供的。\n",
+ "\n",
+ "首先,接口描述了要控制的事件。因为其默认的行为是基于时间去执行控制,所以使用抽象类代替实际的接口。下面的例子包含了某些实现:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "// innerclasses/controller/Event.java\n",
+ "// The common methods for any control event\n",
+ "package innerclasses.controller;\n",
+ "import java.time.*; // Java 8 time classes\n",
+ "public abstract class Event {\n",
+ " private Instant eventTime;\n",
+ " protected final Duration delayTime;\n",
+ " public Event(long millisecondDelay) {\n",
+ " delayTime = Duration.ofMillis(millisecondDelay);\n",
+ " start();\n",
+ " }\n",
+ " public void start() { // Allows restarting\n",
+ " eventTime = Instant.now().plus(delayTime);\n",
+ " }\n",
+ " public boolean ready() {\n",
+ " return Instant.now().isAfter(eventTime);\n",
+ " }\n",
+ " public abstract void action();\n",
+ "}"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "当希望运行 **Event** 并随后调用 `start()` 时,那么构造器就会捕获(从对象创建的时刻开始的)时间,此时间是这样得来的:`start()` 获取当前时间,然后加上一个延迟时间,这样生成触发事件的时间。`start()` 是一个独立的方法,而没有包含在构造器内,因为这样就可以在事件运行以后重新启动计时器,也就是能够重复使用 **Event** 对象。例如,如果想要重复一个事件,只需简单地在 `action()` 中调用 `start()` 方法。\n",
+ "\n",
+ "`ready()` 告诉你何时可以运行 `action()` 方法了。当然,可以在派生类中覆盖 `ready()` 方法,使得 **Event** 能够基于时间以外的其他因素而触发。\n",
+ "\n",
+ "下面的文件包含了一个用来管理并触发事件的实际控制框架。**Event** 对象被保存在 **List**\\<**Event**\\> 类型(读作“Event 的列表”)的容器对象中,容器会在 [集合 ]() 中详细介绍。目前读者只需要知道 `add()` 方法用来将一个 **Event** 添加到 **List** 的尾端,`size()` 方法用来得到 **List** 中元素的个数,foreach 语法用来连续获取 **List** 中的 **Event**,`remove()` 方法用来从 **List** 中移除指定的 **Event**。"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "// innerclasses/controller/Controller.java\n",
+ "// The reusable framework for control systems\n",
+ "package innerclasses.controller;\n",
+ "import java.util.*;\n",
+ "public class Controller {\n",
+ " // A class from java.util to hold Event objects:\n",
+ " private List eventList = new ArrayList<>();\n",
+ " public void addEvent(Event c) { eventList.add(c); }\n",
+ " public void run() {\n",
+ " while(eventList.size() > 0)\n",
+ " // Make a copy so you're not modifying the list\n",
+ " // while you're selecting the elements in it:\n",
+ " for(Event e : new ArrayList<>(eventList))\n",
+ " if(e.ready()) {\n",
+ " System.out.println(e);\n",
+ " e.action();\n",
+ " eventList.remove(e);\n",
+ " }\n",
+ " }\n",
+ "}"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "`run()` 方法循环遍历 **eventList**,寻找就绪的(`ready()`)、要运行的 **Event** 对象。对找到的每一个就绪的(`ready()`)事件,使用对象的 `toString()` 打印其信息,调用其 `action()` 方法,然后从列表中移除此 **Event**。\n",
+ "\n",
+ "注意,在目前的设计中你并不知道 **Event** 到底做了什么。这正是此设计的关键所在—\"使变化的事物与不变的事物相互分离”。用我的话说,“变化向量”就是各种不同的 **Event** 对象所具有的不同行为,而你通过创建不同的 **Event** 子类来表现不同的行为。\n",
+ "\n",
+ "这正是内部类要做的事情,内部类允许:\n",
+ "\n",
+ "1. 控制框架的完整实现是由单个的类创建的,从而使得实现的细节被封装了起来。内部类用来表示解决问题所必需的各种不同的 `action()`。\n",
+ "2. 内部类能够很容易地访问外围类的任意成员,所以可以避免这种实现变得笨拙。如果没有这种能力,代码将变得令人讨厌,以至于你肯定会选择别的方法。\n",
+ "\n",
+ "考虑此控制框架的一个特定实现,如控制温室的运作:控制灯光、水、温度调节器的开关,以及响铃和重新启动系统,每个行为都是完全不同的。控制框架的设计使得分离这些不同的代码变得非常容易。使用内部类,可以在单一的类里面产生对同一个基类 **Event** 的多种派生版本。对于温室系统的每一种行为,都继承创建一个新的 **Event** 内部类,并在要实现的 `action()` 中编写控制代码。\n",
+ "\n",
+ "作为典型的应用程序框架,**GreenhouseControls** 类继承自 **Controller**:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "// innerclasses/GreenhouseControls.java\n",
+ "// This produces a specific application of the\n",
+ "// control system, all in a single class. Inner\n",
+ "// classes allow you to encapsulate different\n",
+ "// functionality for each type of event.\n",
+ "import innerclasses.controller.*;\n",
+ "public class GreenhouseControls extends Controller {\n",
+ " private boolean light = false;\n",
+ " public class LightOn extends Event {\n",
+ " public LightOn(long delayTime) {\n",
+ " super(delayTime); \n",
+ " }\n",
+ " @Override\n",
+ " public void action() {\n",
+ " // Put hardware control code here to\n",
+ " // physically turn on the light.\n",
+ " light = true;\n",
+ " }\n",
+ " @Override\n",
+ " public String toString() {\n",
+ " return \"Light is on\";\n",
+ " }\n",
+ " }\n",
+ " public class LightOff extends Event {\n",
+ " public LightOff(long delayTime) {\n",
+ " super(delayTime);\n",
+ " }\n",
+ " @Override\n",
+ " public void action() {\n",
+ " // Put hardware control code here to\n",
+ " // physically turn off the light.\n",
+ " light = false;\n",
+ " }\n",
+ " @Override\n",
+ " public String toString() {\n",
+ " return \"Light is off\";\n",
+ " }\n",
+ " }\n",
+ " private boolean water = false;\n",
+ " public class WaterOn extends Event {\n",
+ " public WaterOn(long delayTime) {\n",
+ " super(delayTime);\n",
+ " }\n",
+ " @Override\n",
+ " public void action() {\n",
+ " // Put hardware control code here.\n",
+ " water = true;\n",
+ " }\n",
+ " @Override\n",
+ " public String toString() {\n",
+ " return \"Greenhouse water is on\";\n",
+ " }\n",
+ " }\n",
+ " public class WaterOff extends Event {\n",
+ " public WaterOff(long delayTime) {\n",
+ " super(delayTime);\n",
+ " }\n",
+ " @Override\n",
+ " public void action() {\n",
+ " // Put hardware control code here.\n",
+ " water = false;\n",
+ " }\n",
+ " @Override\n",
+ " public String toString() {\n",
+ " return \"Greenhouse water is off\";\n",
+ " }\n",
+ " }\n",
+ " private String thermostat = \"Day\";\n",
+ " public class ThermostatNight extends Event {\n",
+ " public ThermostatNight(long delayTime) {\n",
+ " super(delayTime);\n",
+ " }\n",
+ " @Override\n",
+ " public void action() {\n",
+ " // Put hardware control code here.\n",
+ " thermostat = \"Night\";\n",
+ " }\n",
+ " @Override\n",
+ " public String toString() {\n",
+ " return \"Thermostat on night setting\";\n",
+ " }\n",
+ " }\n",
+ " public class ThermostatDay extends Event {\n",
+ " public ThermostatDay(long delayTime) {\n",
+ " super(delayTime);\n",
+ " }\n",
+ " @Override\n",
+ " public void action() {\n",
+ " // Put hardware control code here.\n",
+ " thermostat = \"Day\";\n",
+ " }\n",
+ " @Override\n",
+ " public String toString() {\n",
+ " return \"Thermostat on day setting\";\n",
+ " }\n",
+ " }\n",
+ " // An example of an action() that inserts a\n",
+ " // new one of itself into the event list:\n",
+ " public class Bell extends Event {\n",
+ " public Bell(long delayTime) {\n",
+ " super(delayTime);\n",
+ " }\n",
+ " @Override\n",
+ " public void action() {\n",
+ " addEvent(new Bell(delayTime.toMillis()));\n",
+ " }\n",
+ " @Override\n",
+ " public String toString() {\n",
+ " return \"Bing!\";\n",
+ " }\n",
+ " }\n",
+ " public class Restart extends Event {\n",
+ " private Event[] eventList;\n",
+ " public\n",
+ " Restart(long delayTime, Event[] eventList) {\n",
+ " super(delayTime);\n",
+ " this.eventList = eventList;\n",
+ " for(Event e : eventList)\n",
+ " addEvent(e);\n",
+ " }\n",
+ " @Override\n",
+ " public void action() {\n",
+ " for(Event e : eventList) {\n",
+ " e.start(); // Rerun each event\n",
+ " addEvent(e);\n",
+ " }\n",
+ " start(); // Rerun this Event\n",
+ " addEvent(this);\n",
+ " }\n",
+ " @Override\n",
+ " public String toString() {\n",
+ " return \"Restarting system\";\n",
+ " }\n",
+ " }\n",
+ " public static class Terminate extends Event {\n",
+ " public Terminate(long delayTime) {\n",
+ " super(delayTime);\n",
+ " }\n",
+ " @Override\n",
+ " public void action() { System.exit(0); }\n",
+ " @Override\n",
+ " public String toString() {\n",
+ " return \"Terminating\";\n",
+ " }\n",
+ " }\n",
+ "}"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "注意,**light**,**water** 和 **thermostat** 都属于外围类 **GreenhouseControls**,而这些内部类能够自由地访问那些字段,无需限定条件或特殊许可。而且,`action()` 方法通常都涉及对某种硬件的控制。\n",
+ "\n",
+ "大多数 **Event** 类看起来都很相似,但是 **Bell** 和 **Restart** 则比较特别。**Bell** 控制响铃,然后在事件列表中增加一个 **Bell** 对象,于是过一会儿它可以再次响铃。读者可能注意到了内部类是多么像多重继承:**Bell** 和 **Restart** 有 **Event** 的所有方法,并且似乎也拥有外围类 **GreenhouseContrlos** 的所有方法。\n",
+ "\n",
+ "一个由 **Event** 对象组成的数组被递交给 **Restart**,该数组要加到控制器上。由于 `Restart()` 也是一个 **Event** 对象,所以同样可以将 **Restart** 对象添加到 `Restart.action()` 中,以使系统能够有规律地重新启动自己。\n",
+ "\n",
+ "下面的类通过创建一个 **GreenhouseControls** 对象,并添加各种不同的 **Event** 对象来配置该系统,这是命令设计模式的一个例子—**eventList** 中的每个对象都被封装成对象的请求:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "// innerclasses/GreenhouseController.java\n",
+ "// Configure and execute the greenhouse system\n",
+ "import innerclasses.controller.*;\n",
+ "public class GreenhouseController {\n",
+ " public static void main(String[] args) {\n",
+ " GreenhouseControls gc = new GreenhouseControls();\n",
+ " // Instead of using code, you could parse\n",
+ " // configuration information from a text file:\n",
+ " gc.addEvent(gc.new Bell(900));\n",
+ " Event[] eventList = {\n",
+ " gc.new ThermostatNight(0),\n",
+ " gc.new LightOn(200),\n",
+ " gc.new LightOff(400),\n",
+ " gc.new WaterOn(600),\n",
+ " gc.new WaterOff(800),\n",
+ " gc.new ThermostatDay(1400)\n",
+ " };\n",
+ " gc.addEvent(gc.new Restart(2000, eventList));\n",
+ " gc.addEvent(\n",
+ " new GreenhouseControls.Terminate(5000));\n",
+ " gc.run();\n",
+ " }\n",
+ "}"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "输出为:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "Thermostat on night setting\n",
+ "Light is on\n",
+ "Light is off\n",
+ "Greenhouse water is on\n",
+ "Greenhouse water is off\n",
+ "Bing!\n",
+ "Thermostat on day setting\n",
+ "Bing!\n",
+ "Restarting system\n",
+ "Thermostat on night setting\n",
+ "Light is on\n",
+ "Light is off\n",
+ "Greenhouse water is on\n",
+ "Bing!\n",
+ "Greenhouse water is off\n",
+ "Thermostat on day setting\n",
+ "Bing!\n",
+ "Restarting system\n",
+ "Thermostat on night setting\n",
+ "Light is on\n",
+ "Light is off\n",
+ "Bing!\n",
+ "Greenhouse water is on\n",
+ "Greenhouse water is off\n",
+ "Terminating"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "这个类的作用是初始化系统,所以它添加了所有相应的事件。**Restart** 事件反复运行,而且它每次都会将 **eventList** 加载到 **GreenhouseControls** 对象中。如果提供了命令行参数,系统会以它作为毫秒数,决定什么时候终止程序(这是测试程序时使用的)。\n",
+ "\n",
+ "当然,更灵活的方法是避免对事件进行硬编码。\n",
+ "\n",
+ "这个例子应该使读者更了解内部类的价值了,特别是在控制框架中使用内部类的时候。\n",
+ "\n",
+ "\n",
+ "\n",
+ "## 继承内部类\n",
+ "\n",
+ "因为内部类的构造器必须连接到指向其外围类对象的引用,所以在继承内部类的时候,事情会变得有点复杂。问题在于,那个指向外围类对象的“秘密的”引用必须被初始化,而在派生类中不再存在可连接的默认对象。要解决这个问题,必须使用特殊的语法来明确说清它们之间的关联:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "// innerclasses/InheritInner.java\n",
+ "// Inheriting an inner class\n",
+ "class WithInner {\n",
+ " class Inner {}\n",
+ "}\n",
+ "public class InheritInner extends WithInner.Inner {\n",
+ " //- InheritInner() {} // Won't compile\n",
+ " InheritInner(WithInner wi) {\n",
+ " wi.super();\n",
+ " }\n",
+ " public static void main(String[] args) {\n",
+ " WithInner wi = new WithInner();\n",
+ " InheritInner ii = new InheritInner(wi);\n",
+ " }\n",
+ "}"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "可以看到,**InheritInner** 只继承自内部类,而不是外围类。但是当要生成一个构造器时,默认的构造器并不算好,而且不能只是传递一个指向外围类对象的引用。此外,必须在构造器内使用如下语法:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "enclosingClassReference.super();"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "这样才提供了必要的引用,然后程序才能编译通过。\n",
+ "\n",
+ "\n",
+ "\n",
+ "## 内部类可以被覆盖么?\n",
+ "\n",
+ "如果创建了一个内部类,然后继承其外围类并重新定义此内部类时,会发生什么呢?也就是说,内部类可以被覆盖吗?这看起来似乎是个很有用的思想,但是“覆盖”内部类就好像它是外围类的一个方法,其实并不起什么作用:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "// innerclasses/BigEgg.java\n",
+ "// An inner class cannot be overridden like a method\n",
+ "class Egg {\n",
+ " private Yolk y;\n",
+ " protected class Yolk {\n",
+ " public Yolk() {\n",
+ " System.out.println(\"Egg.Yolk()\");\n",
+ " }\n",
+ " }\n",
+ " Egg() {\n",
+ " System.out.println(\"New Egg()\");\n",
+ " y = new Yolk();\n",
+ " }\n",
+ "}\n",
+ "public class BigEgg extends Egg {\n",
+ " public class Yolk {\n",
+ " public Yolk() {\n",
+ " System.out.println(\"BigEgg.Yolk()\");\n",
+ " }\n",
+ " }\n",
+ " public static void main(String[] args) {\n",
+ " new BigEgg();\n",
+ " }\n",
+ "}"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "输出为:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "New Egg()\n",
+ "Egg.Yolk()"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "默认的无参构造器是编译器自动生成的,这里是调用基类的默认构造器。你可能认为既然创建了 **BigEgg** 的对象,那么所使用的应该是“覆盖后”的 **Yolk** 版本,但从输出中可以看到实际情况并不是这样的。\n",
+ "\n",
+ "这个例子说明,当继承了某个外围类的时候,内部类并没有发生什么特别神奇的变化。这两个内部类是完全独立的两个实体,各自在自己的命名空间内。当然,明确地继承某个内部类也是可以的:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "// innerclasses/BigEgg2.java\n",
+ "// Proper inheritance of an inner class\n",
+ "class Egg2 {\n",
+ " protected class Yolk {\n",
+ " public Yolk() {\n",
+ " System.out.println(\"Egg2.Yolk()\");\n",
+ " }\n",
+ " public void f() {\n",
+ " System.out.println(\"Egg2.Yolk.f()\");\n",
+ " }\n",
+ " }\n",
+ " private Yolk y = new Yolk();\n",
+ " Egg2() { System.out.println(\"New Egg2()\"); }\n",
+ " public void insertYolk(Yolk yy) { y = yy; }\n",
+ " public void g() { y.f(); }\n",
+ "}\n",
+ "public class BigEgg2 extends Egg2 {\n",
+ " public class Yolk extends Egg2.Yolk {\n",
+ " public Yolk() {\n",
+ " System.out.println(\"BigEgg2.Yolk()\");\n",
+ " }\n",
+ " @Override\n",
+ " public void f() {\n",
+ " System.out.println(\"BigEgg2.Yolk.f()\");\n",
+ " }\n",
+ " }\n",
+ " public BigEgg2() { insertYolk(new Yolk()); }\n",
+ " public static void main(String[] args) {\n",
+ " Egg2 e2 = new BigEgg2();\n",
+ " e2.g();\n",
+ " }\n",
+ "}"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "输出为:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "Egg2.Yolk()\n",
+ "New Egg2()\n",
+ "Egg2.Yolk()\n",
+ "BigEgg2.Yolk()\n",
+ "BigEgg2.Yolk.f()"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "现在 **BigEgg2.Yolk** 通过 **extends Egg2.Yolk** 明确地继承了此内部类,并且覆盖了其中的方法。`insertYolk()` 方法允许 **BigEgg2** 将它自己的 **Yolk** 对象向上转型为 **Egg2** 中的引用 **y**。所以当 `g()` 调用 `y.f()` 时,覆盖后的新版的 `f()` 被执行。第二次调用 `Egg2.Yolk()`,结果是 **BigEgg2.Yolk** 的构造器调用了其基类的构造器。可以看到在调用 `g()` 的时候,新版的 `f()` 被调用了。\n",
+ "\n",
+ "\n",
+ "\n",
+ "## 局部内部类\n",
+ "\n",
+ "前面提到过,可以在代码块里创建内部类,典型的方式是在一个方法体的里面创建。局部内部类不能有访问说明符,因为它不是外围类的一部分;但是它可以访问当前代码块内的常量,以及此外围类的所有成员。下面的例子对局部内部类与匿名内部类的创建进行了比较。"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "// innerclasses/LocalInnerClass.java\n",
+ "// Holds a sequence of Objects\n",
+ "interface Counter {\n",
+ " int next();\n",
+ "}\n",
+ "public class LocalInnerClass {\n",
+ " private int count = 0;\n",
+ " Counter getCounter(final String name) {\n",
+ " // A local inner class:\n",
+ " class LocalCounter implements Counter {\n",
+ " LocalCounter() {\n",
+ " // Local inner class can have a constructor\n",
+ " System.out.println(\"LocalCounter()\");\n",
+ " }\n",
+ " @Override\n",
+ " public int next() {\n",
+ " System.out.print(name); // Access local final\n",
+ " return count++;\n",
+ " }\n",
+ " }\n",
+ " return new LocalCounter();\n",
+ " }\n",
+ " // Repeat, but with an anonymous inner class:\n",
+ " Counter getCounter2(final String name) {\n",
+ " return new Counter() {\n",
+ " // Anonymous inner class cannot have a named\n",
+ " // constructor, only an instance initializer:\n",
+ " {\n",
+ " System.out.println(\"Counter()\");\n",
+ " }\n",
+ " @Override\n",
+ " public int next() {\n",
+ " System.out.print(name); // Access local final\n",
+ " return count++;\n",
+ " }\n",
+ " };\n",
+ " }\n",
+ " public static void main(String[] args) {\n",
+ " LocalInnerClass lic = new LocalInnerClass();\n",
+ " Counter\n",
+ " c1 = lic.getCounter(\"Local inner \"),\n",
+ " c2 = lic.getCounter2(\"Anonymous inner \");\n",
+ " for(int i = 0; i < 5; i++)\n",
+ " System.out.println(c1.next());\n",
+ " for(int i = 0; i < 5; i++)\n",
+ " System.out.println(c2.next());\n",
+ " }\n",
+ "}"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "输出为:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "LocalCounter()\n",
+ "Counter()\n",
+ "Local inner 0\n",
+ "Local inner 1\n",
+ "Local inner 2\n",
+ "Local inner 3\n",
+ "Local inner 4\n",
+ "Anonymous inner 5\n",
+ "Anonymous inner 6\n",
+ "Anonymous inner 7\n",
+ "Anonymous inner 8\n",
+ "Anonymous inner 9"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "**Counter** 返回的是序列中的下一个值。我们分别使用局部内部类和匿名内部类实现了这个功能,它们具有相同的行为和能力,既然局部内部类的名字在方法外是不可见的,那为什么我们仍然使用局部内部类而不是匿名内部类呢?唯一的理由是,我们需要一个已命名的构造器,或者需要重载构造器,而匿名内部类只能用于实例初始化。\n",
+ "\n",
+ "所以使用局部内部类而不使用匿名内部类的另一个理由就是,需要不止一个该内部类的对象。\n",
+ "\n",
+ "\n",
+ "\n",
+ "## 内部类标识符\n",
+ "\n",
+ "由于编译后每个类都会产生一个**.class** 文件,其中包含了如何创建该类型的对象的全部信息(此信息产生一个\"meta-class\",叫做 **Class** 对象)。\n",
+ "\n",
+ "你可能猜到了,内部类也必须生成一个**.class** 文件以包含它们的 **Class** 对象信息。这些类文件的命名有严格的规则:外围类的名字,加上“**$**\",再加上内部类的名字。例如,**LocalInnerClass.java** 生成的 **.class** 文件包括:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "Counter.class\n",
+ "LocalInnerClass$1.class\n",
+ "LocalInnerClass$LocalCounter.class\n",
+ "LocalInnerClass.class"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "如果内部类是匿名的,编译器会简单地产生一个数字作为其标识符。如果内部类是嵌套在别的内部类之中,只需直接将它们的名字加在其外围类标识符与“**$**”的后面。\n",
+ "\n",
+ "虽然这种命名格式简单而直接,但它还是很健壮的,足以应对绝大多数情况。因为这是 java 的标准命名方式,所以产生的文件自动都是平台无关的。(注意,为了保证你的内部类能起作用,Java 编译器会尽可能地转换它们。)\n",
+ "\n",
+ "\n",
+ "\n",
+ "## 本章小结\n",
+ "\n",
+ "比起面向对象编程中其他的概念来,接口和内部类更深奥复杂,比如 C++ 就没有这些。将两者结合起来,同样能够解决 C++ 中的用多重继承所能解决的问题。然而,多重继承在 C++ 中被证明是相当难以使用的,相比较而言,Java 的接口和内部类就容易理解多了。\n",
+ "\n",
+ "虽然这些特性本身是相当直观的,但是就像多态机制一样,这些特性的使用应该是设计阶段考虑的问题。随着时间的推移,读者将能够更好地识别什么情况下应该使用接口,什么情况使用内部类,或者两者同时使用。但此时,读者至少应该已经完全理解了它们的语法和语义。\n",
+ "\n",
+ "当读者见到这些语言特性的实际应用时,就能最终理解它们了。\n",
+ "\n",
+ "\n",
+ "\n",
+ ""
+ ]
+ }
+ ],
+ "metadata": {},
+ "nbformat": 4,
+ "nbformat_minor": 4
+}
diff --git a/jupyter/12-Collections.ipynb b/jupyter/12-Collections.ipynb
new file mode 100644
index 00000000..c2141709
--- /dev/null
+++ b/jupyter/12-Collections.ipynb
@@ -0,0 +1,2589 @@
+{
+ "cells": [
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "[TOC]\n",
+ "\n",
+ "\n",
+ "# 第十二章 集合\n",
+ "\n",
+ "> 如果一个程序只包含固定数量的对象且对象的生命周期都是已知的,那么这是一个非常简单的程序。\n",
+ "\n",
+ "通常,程序总是根据运行时才知道的某些条件去创建新的对象。在此之前,无法知道所需对象的数量甚至确切类型。为了解决这个普遍的编程问题,需要在任意时刻和任意位置创建任意数量的对象。因此,不能依靠创建命名的引用来持有每一个对象:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "MyType aReference;"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "因为从来不会知道实际需要多少个这样的引用。\n",
+ "\n",
+ "大多数编程语言都提供了某种方法来解决这个基本问题。Java有多种方式保存对象(确切地说,是对象的引用)。例如前边曾经学习过的数组,它是编译器支持的类型。数组是保存一组对象的最有效的方式,如果想要保存一组基本类型数据,也推荐使用数组。但是数组具有固定的大小尺寸,而且在更一般的情况下,在写程序的时候并不知道将需要多少个对象,或者是否需要更复杂的方式来存储对象,因此数组尺寸固定这一限制就显得太过受限了。\n",
+ "\n",
+ "**java.util** 库提供了一套相当完整的*集合类*(collection classes)来解决这个问题,其中基本的类型有 **List** 、 **Set** 、 **Queue** 和 **Map**。这些类型也被称作*容器类*(container classes),但我将使用Java类库使用的术语。集合提供了完善的方法来保存对象,可以使用这些工具来解决大量的问题。\n",
+ "\n",
+ "集合还有一些其它特性。例如, **Set** 对于每个值都只保存一个对象, **Map** 是一个关联数组,允许将某些对象与其他对象关联起来。Java集合类都可以自动地调整自己的大小。因此,与数组不同,在编程时,可以将任意数量的对象放置在集合中,而不用关心集合应该有多大。\n",
+ "\n",
+ "尽管在 Java 中没有直接的关键字支持,[^1]但集合类仍然是可以显著增强编程能力的基本工具。在本章中,将介绍 Java 集合类库的基本知识,并重点介绍一些典型用法。这里将专注于在日常编程中使用的集合。稍后,在[附录:集合主题]()中,还将学习到其余的那些集合和相关功能,以及如何使用它们的更多详细信息。\n",
+ "\n",
+ "\n",
+ "## 泛型和类型安全的集合\n",
+ "\n",
+ "使用 Java 5 之前的集合的一个主要问题是编译器允许你向集合中插入不正确的类型。例如,考虑一个 **Apple** 对象的集合,这里使用最基本最可靠的 **ArrayList** 。现在,可以把 **ArrayList** 看作“可以自动扩充自身尺寸的数组”来看待。使用 **ArrayList** 相当简单:创建一个实例,用 `add()` 插入对象;然后用 `get()` 来访问这些对象,此时需要使用索引,就像数组那样,但是不需要方括号。[^2] **ArrayList** 还有一个 `size()` 方法,来说明集合中包含了多少个元素,所以不会不小心因数组越界而引发错误(通过抛出*运行时异常*,[异常]()章节介绍了异常)。\n",
+ "\n",
+ "在本例中, **Apple** 和 **Orange** 都被放到了集合中,然后将它们取出。正常情况下,Java编译器会给出警告,因为这个示例没有使用泛型。在这里,使用特定的注解来抑制警告信息。注解以“@”符号开头,可以带参数。这里的 `@SuppressWarning` 注解及其参数表示只抑制“unchecked”类型的警告([注解]()章节将介绍更多有关注解的信息):"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "// collections/ApplesAndOrangesWithoutGenerics.java\n",
+ "// Simple collection use (suppressing compiler warnings)\n",
+ "// {ThrowsException}\n",
+ "import java.util.*;\n",
+ "\n",
+ "class Apple {\n",
+ " private static long counter;\n",
+ " private final long id = counter++;\n",
+ " public long id() { return id; }\n",
+ "}\n",
+ "\n",
+ "class Orange {}\n",
+ "\n",
+ "public class ApplesAndOrangesWithoutGenerics {\n",
+ " @SuppressWarnings(\"unchecked\")\n",
+ " public static void main(String[] args) {\n",
+ " ArrayList apples = new ArrayList();\n",
+ " for(int i = 0; i < 3; i++)\n",
+ " apples.add(new Apple());\n",
+ " // No problem adding an Orange to apples:\n",
+ " apples.add(new Orange());\n",
+ " for(Object apple : apples) {\n",
+ " ((Apple) apple).id();\n",
+ " // Orange is detected only at run time\n",
+ " }\n",
+ " }\n",
+ "}\n",
+ "/* Output:\n",
+ "___[ Error Output ]___\n",
+ "Exception in thread \"main\"\n",
+ "java.lang.ClassCastException: Orange cannot be cast to\n",
+ "Apple\n",
+ " at ApplesAndOrangesWithoutGenerics.main(ApplesA\n",
+ "ndOrangesWithoutGenerics.java:23)\n",
+ "*/"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "**Apple** 和 **Orange** 是截然不同的,它们除了都是 **Object** 之外没有任何共同点(如果一个类没有显式地声明继承自哪个类,那么它就自动继承自 **Object**)。因为 **ArrayList** 保存的是 **Object** ,所以不仅可以通过 **ArrayList** 的 `add()` 方法将 **Apple** 对象放入这个集合,而且可以放入 **Orange** 对象,这无论在编译期还是运行时都不会有问题。当使用 **ArrayList** 的 `get()` 方法来取出你认为是 **Apple** 的对象时,得到的只是 **Object** 引用,必须将其转型为 **Apple**。然后需要将整个表达式用括号括起来,以便在调用 **Apple** 的 `id()` 方法之前,强制执行转型。否则,将会产生语法错误。\n",
+ "\n",
+ "在运行时,当尝试将 **Orange** 对象转为 **Apple** 时,会出现输出中显示的错误。\n",
+ "\n",
+ "在[泛型]()章节中,你将了解到使用 Java 泛型来创建类可能很复杂。但是,使用预先定义的泛型类却相当简单。例如,要定义一个用于保存 **Apple** 对象的 **ArrayList** ,只需要使用 **ArrayList\\** 来代替 **ArrayList** 。尖括号括起来的是*类型参数*(可能会有多个),它指定了这个集合实例可以保存的类型。\n",
+ "\n",
+ "通过使用泛型,就可以在编译期防止将错误类型的对象放置到集合中。[^3]下面还是这个示例,但是使用了泛型:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "// collections/ApplesAndOrangesWithGenerics.java\n",
+ "import java.util.*;\n",
+ "\n",
+ "public class ApplesAndOrangesWithGenerics {\n",
+ " public static void main(String[] args) {\n",
+ " ArrayList apples = new ArrayList<>();\n",
+ " for(int i = 0; i < 3; i++)\n",
+ " apples.add(new Apple());\n",
+ " // Compile-time error:\n",
+ " // apples.add(new Orange());\n",
+ " for(Apple apple : apples) {\n",
+ " System.out.println(apple.id());\n",
+ " }\n",
+ " }\n",
+ "}\n",
+ "/* Output:\n",
+ "0\n",
+ "1\n",
+ "2\n",
+ "*/"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "在 **apples** 定义的右侧,可以看到 `new ArrayList<>()` 。这有时被称为“菱形语法”(diamond syntax)。在 Java 7 之前,必须要在两端都进行类型声明,如下所示:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "ArrayList apples = new ArrayList();"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "随着类型变得越来越复杂,这种重复产生的代码非常混乱且难以阅读。程序员发现所有类型信息都可以从左侧获得,因此,编译器没有理由强迫右侧再重复这些。虽然*类型推断*(type inference)只是个很小的请求,Java 语言团队仍然欣然接受并进行了改进。\n",
+ "\n",
+ "有了 **ArrayList** 声明中的类型指定,编译器会阻止将 **Orange** 放入 **apples** ,因此,这会成为一个编译期错误而不是运行时错误。\n",
+ "\n",
+ "使用泛型,从 **List** 中获取元素不需要强制类型转换。因为 **List** 知道它持有什么类型,因此当调用 `get()` 时,它会替你执行转型。因此,使用泛型,你不仅知道编译器将检查放入集合的对象类型,而且在使用集合中的对象时也可以获得更清晰的语法。\n",
+ "\n",
+ "当指定了某个类型为泛型参数时,并不仅限于只能将确切类型的对象放入集合中。向上转型也可以像作用于其他类型一样作用于泛型:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "// collections/GenericsAndUpcasting.java\n",
+ "import java.util.*;\n",
+ "\n",
+ "class GrannySmith extends Apple {}\n",
+ "class Gala extends Apple {}\n",
+ "class Fuji extends Apple {}\n",
+ "class Braeburn extends Apple {}\n",
+ "\n",
+ "public class GenericsAndUpcasting {\n",
+ " public static void main(String[] args) {\n",
+ " ArrayList apples = new ArrayList<>();\n",
+ " apples.add(new GrannySmith());\n",
+ " apples.add(new Gala());\n",
+ " apples.add(new Fuji());\n",
+ " apples.add(new Braeburn());\n",
+ " for(Apple apple : apples)\n",
+ " System.out.println(apple);\n",
+ " }\n",
+ "}\n",
+ "/* Output:\n",
+ "GrannySmith@15db9742\n",
+ "Gala@6d06d69c\n",
+ "Fuji@7852e922\n",
+ "Braeburn@4e25154f\n",
+ "*/"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "因此,可以将 **Apple** 的子类型添加到被指定为保存 **Apple** 对象的集合中。\n",
+ "\n",
+ "程序的输出是从 **Object** 默认的 `toString()` 方法产生的,该方法打印类名,后边跟着对象的散列码的无符号十六进制表示(这个散列码是通过 `hashCode()` 方法产生的)。将在[附录:理解equals和hashCode方法]()中了解有关散列码的内容。\n",
+ "\n",
+ "\n",
+ "## 基本概念\n",
+ "\n",
+ "Java集合类库采用“持有对象”(holding objects)的思想,并将其分为两个不同的概念,表示为类库的基本接口:\n",
+ "\n",
+ "1. **集合(Collection)** :一个独立元素的序列,这些元素都服从一条或多条规则。**List** 必须以插入的顺序保存元素, **Set** 不能包含重复元素, **Queue** 按照*排队规则*来确定对象产生的顺序(通常与它们被插入的顺序相同)。\n",
+ "2. **映射(Map)** : 一组成对的“键值对”对象,允许使用键来查找值。 **ArrayList** 使用数字来查找对象,因此在某种意义上讲,它是将数字和对象关联在一起。 **map** 允许我们使用一个对象来查找另一个对象,它也被称作*关联数组*(associative array),因为它将对象和其它对象关联在一起;或者称作*字典*(dictionary),因为可以使用一个键对象来查找值对象,就像在字典中使用单词查找定义一样。 **Map** 是强大的编程工具。\n",
+ "\n",
+ "尽管并非总是可行,但在理想情况下,你编写的大部分代码都在与这些接口打交道,并且唯一需要指定所使用的精确类型的地方就是在创建的时候。因此,可以像下面这样创建一个 **List** :"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "List apples = new ArrayList<>();"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "请注意, **ArrayList** 已经被向上转型为了 **List** ,这与之前示例中的处理方式正好相反。使用接口的目的是,如果想要改变具体实现,只需在创建时修改它就行了,就像下面这样:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "List apples = new LinkedList<>();"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "因此,应该创建一个具体类的对象,将其向上转型为对应的接口,然后在其余代码中都是用这个接口。\n",
+ "\n",
+ "这种方式并非总是有效的,因为某些具体类有额外的功能。例如, **LinkedList** 具有 **List** 接口中未包含的额外方法,而 **TreeMap** 也具有在 **Map** 接口中未包含的方法。如果需要使用这些方法,就不能将它们向上转型为更通用的接口。\n",
+ "\n",
+ "**Collection** 接口概括了*序列*的概念——一种存放一组对象的方式。下面是个简单的示例,用 **Integer** 对象填充了一个 **Collection** (这里用 **ArrayList** 表示),然后打印集合中的每个元素:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "// collections/SimpleCollection.java\n",
+ "import java.util.*;\n",
+ "\n",
+ "public class SimpleCollection {\n",
+ " public static void main(String[] args) {\n",
+ " Collection c = new ArrayList<>();\n",
+ " for(int i = 0; i < 10; i++)\n",
+ " c.add(i); // Autoboxing\n",
+ " for(Integer i : c)\n",
+ " System.out.print(i + \", \");\n",
+ " }\n",
+ "}\n",
+ "/* Output:\n",
+ "0, 1, 2, 3, 4, 5, 6, 7, 8, 9,\n",
+ "*/"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "这个例子仅使用了 **Collection** 中的方法(即 `add()` ),所以使用任何继承自 **Collection** 的类的对象都可以正常工作。但是 **ArrayList** 是最基本的序列类型。\n",
+ "\n",
+ "`add()` 方法的名称就表明它是在 **Collection** 中添加一个新元素。但是,文档中非常详细地叙述到 `add()` “要确保这个 **Collection** 包含指定的元素。”这是因为考虑到了 **Set** 的含义,因为在 **Set**中,只有当元素不存在时才会添加元素。在使用 **ArrayList** ,或任何其他类型的 **List** 时,`add()` 总是表示“把它放进去”,因为 **List** 不关心是否存在重复元素。\n",
+ "\n",
+ "可以使用 *for-in* 语法来遍历所有的 **Collection** ,就像这里所展示的那样。在本章的后续部分,还将学习到一个更灵活的概念,*迭代器*。\n",
+ "\n",
+ "\n",
+ "\n",
+ "## 添加元素组\n",
+ "\n",
+ "在 **java.util** 包中的 **Arrays** 和 **Collections** 类中都有很多实用的方法,可以在一个 **Collection** 中添加一组元素。\n",
+ "\n",
+ "`Arrays.asList()` 方法接受一个数组或是逗号分隔的元素列表(使用可变参数),并将其转换为 **List** 对象。 `Collections.addAll()` 方法接受一个 **Collection** 对象,以及一个数组或是一个逗号分隔的列表,将其中元素添加到 **Collection** 中。下边的示例展示了这两个方法,以及更通用的 、所有 **Collection** 类型都包含的`addAll()` 方法:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "// collections/AddingGroups.java\n",
+ "// Adding groups of elements to Collection objects\n",
+ "import java.util.*;\n",
+ "\n",
+ "public class AddingGroups {\n",
+ " public static void main(String[] args) {\n",
+ " Collection collection =\n",
+ " new ArrayList<>(Arrays.asList(1, 2, 3, 4, 5));\n",
+ " Integer[] moreInts = { 6, 7, 8, 9, 10 };\n",
+ " collection.addAll(Arrays.asList(moreInts));\n",
+ " // Runs significantly faster, but you can't\n",
+ " // construct a Collection this way:\n",
+ " Collections.addAll(collection, 11, 12, 13, 14, 15);\n",
+ " Collections.addAll(collection, moreInts);\n",
+ " // Produces a list \"backed by\" an array:\n",
+ " List list = Arrays.asList(16,17,18,19,20);\n",
+ " list.set(1, 99); // OK -- modify an element\n",
+ " // list.add(21); // Runtime error; the underlying\n",
+ " // array cannot be resized.\n",
+ " }\n",
+ "}"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "**Collection** 的构造器可以接受另一个 **Collection**,用它来将自身初始化。因此,可以使用 `Arrays.asList()` 来为这个构造器产生输入。但是, `Collections.addAll()` 运行得更快,而且很容易构建一个不包含元素的 **Collection** ,然后调用 `Collections.addAll()` ,因此这是首选方式。\n",
+ "\n",
+ "`Collection.addAll()` 方法只能接受另一个 **Collection** 作为参数,因此它没有 `Arrays.asList()` 或 `Collections.addAll()` 灵活。这两个方法都使用可变参数列表。\n",
+ "\n",
+ "也可以直接使用 `Arrays.asList()` 的输出作为一个 **List** ,但是这里的底层实现是数组,没法调整大小。如果尝试在这个 **List** 上调用 `add()` 或 `remove()`,由于这两个方法会尝试修改数组大小,所以会在运行时得到“Unsupported Operation(不支持的操作)”错误:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "// collections/AsListInference.java\n",
+ "import java.util.*;\n",
+ "\n",
+ "class Snow {}\n",
+ "class Powder extends Snow {}\n",
+ "class Light extends Powder {}\n",
+ "class Heavy extends Powder {}\n",
+ "class Crusty extends Snow {}\n",
+ "class Slush extends Snow {}\n",
+ "\n",
+ "public class AsListInference {\n",
+ " public static void main(String[] args) {\n",
+ " List snow1 = Arrays.asList(\n",
+ " new Crusty(), new Slush(), new Powder());\n",
+ " //- snow1.add(new Heavy()); // Exception\n",
+ "\n",
+ " List snow2 = Arrays.asList(\n",
+ " new Light(), new Heavy());\n",
+ " //- snow2.add(new Slush()); // Exception\n",
+ "\n",
+ " List snow3 = new ArrayList<>();\n",
+ " Collections.addAll(snow3,\n",
+ " new Light(), new Heavy(), new Powder());\n",
+ " snow3.add(new Crusty());\n",
+ "\n",
+ " // Hint with explicit type argument specification:\n",
+ " List snow4 = Arrays.asList(\n",
+ " new Light(), new Heavy(), new Slush());\n",
+ " //- snow4.add(new Powder()); // Exception\n",
+ " }\n",
+ "}"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "在 **snow4** 中,注意 `Arrays.asList()` 中间的“暗示”(即 `` ),告诉编译器 `Arrays.asList()` 生成的结果 **List** 类型的实际目标类型是什么。这称为*显式类型参数说明*(explicit type argument specification)。\n",
+ "\n",
+ "\n",
+ "## 集合的打印\n",
+ "\n",
+ "必须使用 `Arrays.toString()` 来生成数组的可打印形式。但是打印集合无需任何帮助。下面是一个例子,这个例子中也介绍了基本的Java集合:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "// collections/PrintingCollections.java\n",
+ "// Collections print themselves automatically\n",
+ "import java.util.*;\n",
+ "\n",
+ "public class PrintingCollections {\n",
+ " static Collection\n",
+ " fill(Collection collection) {\n",
+ " collection.add(\"rat\");\n",
+ " collection.add(\"cat\");\n",
+ " collection.add(\"dog\");\n",
+ " collection.add(\"dog\");\n",
+ " return collection;\n",
+ " }\n",
+ " static Map fill(Map map) {\n",
+ " map.put(\"rat\", \"Fuzzy\");\n",
+ " map.put(\"cat\", \"Rags\");\n",
+ " map.put(\"dog\", \"Bosco\");\n",
+ " map.put(\"dog\", \"Spot\");\n",
+ " return map;\n",
+ " }\n",
+ " public static void main(String[] args) {\n",
+ " System.out.println(fill(new ArrayList<>()));\n",
+ " System.out.println(fill(new LinkedList<>()));\n",
+ " System.out.println(fill(new HashSet<>()));\n",
+ " System.out.println(fill(new TreeSet<>()));\n",
+ " System.out.println(fill(new LinkedHashSet<>()));\n",
+ " System.out.println(fill(new HashMap<>()));\n",
+ " System.out.println(fill(new TreeMap<>()));\n",
+ " System.out.println(fill(new LinkedHashMap<>()));\n",
+ " }\n",
+ "}\n",
+ "/* Output:\n",
+ "[rat, cat, dog, dog]\n",
+ "[rat, cat, dog, dog]\n",
+ "[rat, cat, dog]\n",
+ "[cat, dog, rat]\n",
+ "[rat, cat, dog]\n",
+ "{rat=Fuzzy, cat=Rags, dog=Spot}\n",
+ "{cat=Rags, dog=Spot, rat=Fuzzy}\n",
+ "{rat=Fuzzy, cat=Rags, dog=Spot}\n",
+ "*/"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "这显示了Java集合库中的两个主要类型。它们的区别在于集合中的每个“槽”(slot)保存的元素个数。 **Collection** 类型在每个槽中只能保存一个元素。此类集合包括: **List** ,它以特定的顺序保存一组元素; **Set** ,其中元素不允许重复; **Queue** ,只能在集合一端插入对象,并从另一端移除对象(就本例而言,这只是查看序列的另一种方式,因此并没有显示它)。 **Map** 在每个槽中存放了两个元素,即*键*和与之关联的*值*。\n",
+ "\n",
+ "默认的打印行为,使用集合提供的 `toString()` 方法即可生成可读性很好的结果。 **Collection** 打印出的内容用方括号括住,每个元素由逗号分隔。 **Map** 则由大括号括住,每个键和值用等号连接(键在左侧,值在右侧)。\n",
+ "\n",
+ "第一个 `fill()` 方法适用于所有类型的 **Collection** ,这些类型都实现了 `add()` 方法以添加新元素。\n",
+ "\n",
+ "**ArrayList** 和 **LinkedList** 都是 **List** 的类型,从输出中可以看出,它们都按插入顺序保存元素。两者之间的区别不仅在于执行某些类型的操作时的性能,而且 **LinkedList** 包含的操作多于 **ArrayList** 。本章后面将对这些内容进行更全面的探讨。\n",
+ "\n",
+ "**HashSet** , **TreeSet** 和 **LinkedHashSet** 是 **Set** 的类型。从输出中可以看到, **Set** 仅保存每个相同项中的一个,并且不同的 **Set** 实现存储元素的方式也不同。 **HashSet** 使用相当复杂的方法存储元素,这在[附录:集合主题]()中进行了探讨。现在只需要知道,这种技术是检索元素的最快方法,因此,存储顺序看上去没有什么意义(通常只关心某事物是否是 **Set** 的成员,而存储顺序并不重要)。如果存储顺序很重要,则可以使用 **TreeSet** ,它将按比较结果的升序保存对象)或 **LinkedHashSet** ,它按照被添加的先后顺序保存对象。\n",
+ "\n",
+ "**Map** (也称为*关联数组*)使用*键*来查找对象,就像一个简单的数据库。所关联的对象称为*值*。 假设有一个 **Map** 将美国州名与它们的首府联系在一起,如果想要俄亥俄州(Ohio)的首府,可以用“Ohio”作为键来查找,几乎就像使用数组下标一样。正是由于这种行为,对于每个键, **Map** 只存储一次。\n",
+ "\n",
+ "`Map.put(key, value)` 添加一个所想要添加的值并将它与一个键(用来查找值)相关联。 `Map.get(key)` 生成与该键相关联的值。上面的示例仅添加键值对,并没有执行查找。这将在稍后展示。\n",
+ "\n",
+ "请注意,这里没有指定(或考虑) **Map** 的大小,因为它会自动调整大小。 此外, **Map** 还知道如何打印自己,它会显示相关联的键和值。\n",
+ "\n",
+ "本例使用了 **Map** 的三种基本风格: **HashMap** , **TreeMap** 和 **LinkedHashMap** 。\n",
+ "\n",
+ "键和值保存在 **HashMap** 中的顺序不是插入顺序,因为 **HashMap** 实现使用了非常快速的算法来控制顺序。 **TreeMap** 通过比较结果的升序来保存键, **LinkedHashMap** 在保持 **HashMap** 查找速度的同时按键的插入顺序保存键。\n",
+ "\n",
+ "\n",
+ "\n",
+ "## 列表List\n",
+ "\n",
+ "**List**s承诺将元素保存在特定的序列中。 **List** 接口在 **Collection** 的基础上添加了许多方法,允许在 **List** 的中间插入和删除元素。\n",
+ "\n",
+ "有两种类型的 **List** :\n",
+ "\n",
+ "- 基本的 **ArrayList** ,擅长随机访问元素,但在 **List** 中间插入和删除元素时速度较慢。\n",
+ "- **LinkedList** ,它通过代价较低的在 **List** 中间进行的插入和删除操作,提供了优化的顺序访问。 **LinkedList** 对于随机访问来说相对较慢,但它具有比 **ArrayList** 更大的特征集。\n",
+ "\n",
+ "下面的示例导入 **typeinfo.pets** ,超前使用了[类型信息]()一章中的类库。这个类库包含了 **Pet** 类层次结构,以及用于随机生成 **Pet** 对象的一些工具类。此时不需要了解完整的详细信息,只需要知道两点:\n",
+ "\n",
+ "1. 有一个 **Pet** 类,以及 **Pet** 的各种子类型。\n",
+ "2. 静态的 `Pets.arrayList()` 方法返回一个填充了随机选取的 **Pet** 对象的 **ArrayList**:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "// collections/ListFeatures.java\n",
+ "import typeinfo.pets.*;\n",
+ "import java.util.*;\n",
+ "\n",
+ "public class ListFeatures {\n",
+ " public static void main(String[] args) {\n",
+ " Random rand = new Random(47);\n",
+ " List pets = Pets.list(7);\n",
+ " System.out.println(\"1: \" + pets);\n",
+ " Hamster h = new Hamster();\n",
+ " pets.add(h); // Automatically resizes\n",
+ " System.out.println(\"2: \" + pets);\n",
+ " System.out.println(\"3: \" + pets.contains(h));\n",
+ " pets.remove(h); // Remove by object\n",
+ " Pet p = pets.get(2);\n",
+ " System.out.println(\n",
+ " \"4: \" + p + \" \" + pets.indexOf(p));\n",
+ " Pet cymric = new Cymric();\n",
+ " System.out.println(\"5: \" + pets.indexOf(cymric));\n",
+ " System.out.println(\"6: \" + pets.remove(cymric));\n",
+ " // Must be the exact object:\n",
+ " System.out.println(\"7: \" + pets.remove(p));\n",
+ " System.out.println(\"8: \" + pets);\n",
+ " pets.add(3, new Mouse()); // Insert at an index\n",
+ " System.out.println(\"9: \" + pets);\n",
+ " List sub = pets.subList(1, 4);\n",
+ " System.out.println(\"subList: \" + sub);\n",
+ " System.out.println(\"10: \" + pets.containsAll(sub));\n",
+ " Collections.sort(sub); // In-place sort\n",
+ " System.out.println(\"sorted subList: \" + sub);\n",
+ " // Order is not important in containsAll():\n",
+ " System.out.println(\"11: \" + pets.containsAll(sub));\n",
+ " Collections.shuffle(sub, rand); // Mix it up\n",
+ " System.out.println(\"shuffled subList: \" + sub);\n",
+ " System.out.println(\"12: \" + pets.containsAll(sub));\n",
+ " List copy = new ArrayList<>(pets);\n",
+ " sub = Arrays.asList(pets.get(1), pets.get(4));\n",
+ " System.out.println(\"sub: \" + sub);\n",
+ " copy.retainAll(sub);\n",
+ " System.out.println(\"13: \" + copy);\n",
+ " copy = new ArrayList<>(pets); // Get a fresh copy\n",
+ " copy.remove(2); // Remove by index\n",
+ " System.out.println(\"14: \" + copy);\n",
+ " copy.removeAll(sub); // Only removes exact objects\n",
+ " System.out.println(\"15: \" + copy);\n",
+ " copy.set(1, new Mouse()); // Replace an element\n",
+ " System.out.println(\"16: \" + copy);\n",
+ " copy.addAll(2, sub); // Insert a list in the middle\n",
+ " System.out.println(\"17: \" + copy);\n",
+ " System.out.println(\"18: \" + pets.isEmpty());\n",
+ " pets.clear(); // Remove all elements\n",
+ " System.out.println(\"19: \" + pets);\n",
+ " System.out.println(\"20: \" + pets.isEmpty());\n",
+ " pets.addAll(Pets.list(4));\n",
+ " System.out.println(\"21: \" + pets);\n",
+ " Object[] o = pets.toArray();\n",
+ " System.out.println(\"22: \" + o[3]);\n",
+ " Pet[] pa = pets.toArray(new Pet[0]);\n",
+ " System.out.println(\"23: \" + pa[3].id());\n",
+ " }\n",
+ "}\n",
+ "/* Output:\n",
+ "1: [Rat, Manx, Cymric, Mutt, Pug, Cymric, Pug]\n",
+ "2: [Rat, Manx, Cymric, Mutt, Pug, Cymric, Pug, Hamster]\n",
+ "3: true\n",
+ "4: Cymric 2\n",
+ "5: -1\n",
+ "6: false\n",
+ "7: true\n",
+ "8: [Rat, Manx, Mutt, Pug, Cymric, Pug]\n",
+ "9: [Rat, Manx, Mutt, Mouse, Pug, Cymric, Pug]\n",
+ "subList: [Manx, Mutt, Mouse]\n",
+ "10: true\n",
+ "sorted subList: [Manx, Mouse, Mutt]\n",
+ "11: true\n",
+ "shuffled subList: [Mouse, Manx, Mutt]\n",
+ "12: true\n",
+ "sub: [Mouse, Pug]\n",
+ "13: [Mouse, Pug]\n",
+ "14: [Rat, Mouse, Mutt, Pug, Cymric, Pug]\n",
+ "15: [Rat, Mutt, Cymric, Pug]\n",
+ "16: [Rat, Mouse, Cymric, Pug]\n",
+ "17: [Rat, Mouse, Mouse, Pug, Cymric, Pug]\n",
+ "18: false\n",
+ "19: []\n",
+ "20: true\n",
+ "21: [Manx, Cymric, Rat, EgyptianMau]\n",
+ "22: EgyptianMau\n",
+ "23: 14\n",
+ "*/"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "打印行都编了号,因此可从输出追溯到源代码。 第 1 行输出展示了原始的由 **Pet** 组成的 **List** 。 与数组不同, **List** 可以在创建后添加或删除元素,并自行调整大小。这正是它的重要价值:一种可修改的序列。在第 2 行输出中可以看到添加一个 **Hamster** 的结果,该对象将被追加到列表的末尾。\n",
+ "\n",
+ "可以使用 `contains()` 方法确定对象是否在列表中。如果要删除一个对象,可以将该对象的引用传递给 `remove()` 方法。同样,如果有一个对象的引用,可以使用 `indexOf()` 在 **List** 中找到该对象所在位置的下标号,如第 4 行输出所示中所示。\n",
+ "\n",
+ "当确定元素是否是属于某个 **List** ,寻找某个元素的索引,以及通过引用从 **List** 中删除元素时,都会用到 `equals()` 方法(根类 **Object** 的一个方法)。每个 **Pet** 被定义为一个唯一的对象,所以即使列表中已经有两个 **Cymrics** ,如果再创建一个新的 **Cymric** 对象并将其传递给 `indexOf()` 方法,结果仍为 **-1** (表示未找到),并且尝试调用 `remove()` 方法来删除这个对象将返回 **false** 。对于其他类, `equals()` 的定义可能有所不同。例如,如果两个 **String** 的内容相同,则这两个 **String** 相等。因此,为了防止出现意外,请务必注意 **List** 行为会根据 `equals()` 行为而发生变化。\n",
+ "\n",
+ "第 7、8 行输出展示了删除与 **List** 中的对象完全匹配的对象是成功的。\n",
+ "\n",
+ "可以在 **List** 的中间插入一个元素,就像在第 9 行输出和它之前的代码那样。但这会带来一个问题:对于 **LinkedList** ,在列表中间插入和删除都是廉价操作(在本例中,除了对列表中间进行的真正的随机访问),但对于 **ArrayList** ,这可是代价高昂的操作。这是否意味着永远不应该在 **ArrayList** 的中间插入元素,并最好是转换为 **LinkedList** ?不,它只是意味着你应该意识到这个问题,如果你开始在某个 **ArrayList** 中间执行很多插入操作,并且程序开始变慢,那么你应该看看你的 **List** 实现有可能就是罪魁祸首(发现此类瓶颈的最佳方式是使用分析器 profiler)。优化是一个很棘手的问题,最好的策略就是置之不顾,直到发现必须要去担心它了(尽管去理解这些问题总是一个很好的主意)。\n",
+ "\n",
+ "`subList()` 方法可以轻松地从更大的列表中创建切片,当将切片结果传递给原来这个较大的列表的 `containsAll()` 方法时,很自然地会得到 **true**。请注意,顺序并不重要,在第 11、12 行输出中可以看到,在 **sub** 上调用直观命名的 `Collections.sort()` 和 `Collections.shuffle()` 方法,不会影响 `containsAll()` 的结果。 `subList()` 所产生的列表的幕后支持就是原始列表。因此,对所返回列表的更改都将会反映在原始列表中,反之亦然。\n",
+ "\n",
+ "`retainAll()` 方法实际上是一个“集合交集”操作,在本例中,它保留了同时在 **copy** 和 **sub** 中的所有元素。请再次注意,所产生的结果行为依赖于 `equals()` 方法。\n",
+ "\n",
+ "第 14 行输出展示了使用索引号来删除元素的结果,与通过对象引用来删除元素相比,它显得更加直观,因为在使用索引时,不必担心 `equals()` 的行为。\n",
+ "\n",
+ "`removeAll()` 方法也是基于 `equals()` 方法运行的。 顾名思义,它会从 **List** 中删除在参数 **List** 中的所有元素。\n",
+ "\n",
+ "`set()` 方法的命名显得很不合时宜,因为它与 **Set** 类存在潜在的冲突。在这里使用“replace”可能更适合,因为它的功能是用第二个参数替换索引处的元素(第一个参数)。\n",
+ "\n",
+ "第 17 行输出表明,对于 **List** ,有一个重载的 `addAll()` 方法可以将新列表插入到原始列表的中间位置,而不是仅能用 **Collection** 的 `addAll()` 方法将其追加到列表的末尾。\n",
+ "\n",
+ "第 18 - 20 行输出展示了 `isEmpty()` 和 `clear()` 方法的效果。\n",
+ "\n",
+ "第 22、23 行输出展示了如何使用 `toArray()` 方法将任意的 **Collection** 转换为数组。这是一个重载方法,其无参版本返回一个 **Object** 数组,但是如果将目标类型的数组传递给这个重载版本,那么它会生成一个指定类型的数组(假设它通过了类型检查)。如果参数数组太小而无法容纳 **List** 中的所有元素(就像本例一样),则 `toArray()` 会创建一个具有合适尺寸的新数组。 **Pet** 对象有一个 `id()` 方法,可以在所产生的数组中的对象上调用这个方法。\n",
+ "\n",
+ "\n",
+ "\n",
+ "## 迭代器Iterators\n",
+ "\n",
+ "在任何集合中,都必须有某种方式可以插入元素并再次获取它们。毕竟,保存事物是集合最基本的工作。对于 **List** , `add()` 是插入元素的一种方式, `get()` 是获取元素的一种方式。\n",
+ "\n",
+ "如果从更高层次的角度考虑,会发现这里有个缺点:要使用集合,必须对集合的确切类型编程。这一开始可能看起来不是很糟糕,但是考虑下面的情况:如果原本是对 **List** 编码的,但是后来发现如果能够将相同的代码应用于 **Set** 会更方便,此时应该怎么做?或者假设想从一开始就编写一段通用代码,它不知道或不关心它正在使用什么类型的集合,因此它可以用于不同类型的集合,那么如何才能不重写代码就可以应用于不同类型的集合?\n",
+ "\n",
+ "*迭代器*(也是一种设计模式)的概念实现了这种抽象。迭代器是一个对象,它在一个序列中移动并选择该序列中的每个对象,而客户端程序员不知道或不关心该序列的底层结构。另外,迭代器通常被称为*轻量级对象*(lightweight object):创建它的代价小。因此,经常可以看到一些对迭代器有些奇怪的约束。例如,Java 的 **Iterator** 只能单向移动。这个 **Iterator** 只能用来:\n",
+ "\n",
+ "1. 使用 `iterator()` 方法要求集合返回一个 **Iterator**。 **Iterator** 将准备好返回序列中的第一个元素。\n",
+ "2. 使用 `next()` 方法获得序列中的下一个元素。\n",
+ "3. 使用 `hasNext()` 方法检查序列中是否还有元素。\n",
+ "4. 使用 `remove()` 方法将迭代器最近返回的那个元素删除。\n",
+ "\n",
+ "为了观察它的工作方式,这里再次使用[类型信息]()章节中的 **Pet** 工具:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "// collections/SimpleIteration.java\n",
+ "import typeinfo.pets.*;\n",
+ "import java.util.*;\n",
+ "\n",
+ "public class SimpleIteration {\n",
+ " public static void main(String[] args) {\n",
+ " List pets = Pets.list(12);\n",
+ " Iterator it = pets.iterator();\n",
+ " while(it.hasNext()) {\n",
+ " Pet p = it.next();\n",
+ " System.out.print(p.id() + \":\" + p + \" \");\n",
+ " }\n",
+ " System.out.println();\n",
+ " // A simpler approach, when possible:\n",
+ " for(Pet p : pets)\n",
+ " System.out.print(p.id() + \":\" + p + \" \");\n",
+ " System.out.println();\n",
+ " // An Iterator can also remove elements:\n",
+ " it = pets.iterator();\n",
+ " for(int i = 0; i < 6; i++) {\n",
+ " it.next();\n",
+ " it.remove();\n",
+ " }\n",
+ " System.out.println(pets);\n",
+ " }\n",
+ "}\n",
+ "/* Output:\n",
+ "0:Rat 1:Manx 2:Cymric 3:Mutt 4:Pug 5:Cymric 6:Pug\n",
+ "7:Manx 8:Cymric 9:Rat 10:EgyptianMau 11:Hamster\n",
+ "0:Rat 1:Manx 2:Cymric 3:Mutt 4:Pug 5:Cymric 6:Pug\n",
+ "7:Manx 8:Cymric 9:Rat 10:EgyptianMau 11:Hamster\n",
+ "[Pug, Manx, Cymric, Rat, EgyptianMau, Hamster]\n",
+ "*/"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "有了 **Iterator** ,就不必再为集合中元素的数量操心了。这是由 `hasNext()` 和 `next()` 关心的事情。\n",
+ "\n",
+ "如果只是想向前遍历 **List** ,并不打算修改 **List** 对象本身,那么使用 *for-in* 语法更加简洁。\n",
+ "\n",
+ "**Iterator** 还可以删除由 `next()` 生成的最后一个元素,这意味着在调用 `remove()` 之前必须先调用 `next()` 。[^4]\n",
+ "\n",
+ "在集合中的每个对象上执行操作,这种思想十分强大,并且贯穿于本书。\n",
+ "\n",
+ "现在考虑创建一个 `display()` 方法,它不必知晓集合的确切类型:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "// collections/CrossCollectionIteration.java\n",
+ "import typeinfo.pets.*;\n",
+ "import java.util.*;\n",
+ "\n",
+ "public class CrossCollectionIteration {\n",
+ " public static void display(Iterator it) {\n",
+ " while(it.hasNext()) {\n",
+ " Pet p = it.next();\n",
+ " System.out.print(p.id() + \":\" + p + \" \");\n",
+ " }\n",
+ " System.out.println();\n",
+ " }\n",
+ " public static void main(String[] args) {\n",
+ " List pets = Pets.list(8);\n",
+ " LinkedList petsLL = new LinkedList<>(pets);\n",
+ " HashSet petsHS = new HashSet<>(pets);\n",
+ " TreeSet petsTS = new TreeSet<>(pets);\n",
+ " display(pets.iterator());\n",
+ " display(petsLL.iterator());\n",
+ " display(petsHS.iterator());\n",
+ " display(petsTS.iterator());\n",
+ " }\n",
+ "}\n",
+ "/* Output:\n",
+ "0:Rat 1:Manx 2:Cymric 3:Mutt 4:Pug 5:Cymric 6:Pug\n",
+ "7:Manx\n",
+ "0:Rat 1:Manx 2:Cymric 3:Mutt 4:Pug 5:Cymric 6:Pug\n",
+ "7:Manx\n",
+ "0:Rat 1:Manx 2:Cymric 3:Mutt 4:Pug 5:Cymric 6:Pug\n",
+ "7:Manx\n",
+ "5:Cymric 2:Cymric 7:Manx 1:Manx 3:Mutt 6:Pug 4:Pug\n",
+ "0:Rat\n",
+ "*/"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "`display()` 方法不包含任何有关它所遍历的序列的类型信息。这也展示了 **Iterator** 的真正威力:能够将遍历序列的操作与该序列的底层结构分离。出于这个原因,我们有时会说:迭代器统一了对集合的访问方式。\n",
+ "\n",
+ "我们可以使用 **Iterable** 接口生成上一个示例的更简洁版本,该接口描述了“可以产生 **Iterator** 的任何东西”:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "// collections/CrossCollectionIteration2.java\n",
+ "import typeinfo.pets.*;\n",
+ "import java.util.*;\n",
+ "\n",
+ "public class CrossCollectionIteration2 {\n",
+ " public static void display(Iterable ip) {\n",
+ " Iterator it = ip.iterator();\n",
+ " while(it.hasNext()) {\n",
+ " Pet p = it.next();\n",
+ " System.out.print(p.id() + \":\" + p + \" \");\n",
+ " }\n",
+ " System.out.println();\n",
+ " }\n",
+ " public static void main(String[] args) {\n",
+ " List pets = Pets.list(8);\n",
+ " LinkedList petsLL = new LinkedList<>(pets);\n",
+ " HashSet petsHS = new HashSet<>(pets);\n",
+ " TreeSet petsTS = new TreeSet<>(pets);\n",
+ " display(pets);\n",
+ " display(petsLL);\n",
+ " display(petsHS);\n",
+ " display(petsTS);\n",
+ " }\n",
+ "}\n",
+ "/* Output:\n",
+ "0:Rat 1:Manx 2:Cymric 3:Mutt 4:Pug 5:Cymric 6:Pug\n",
+ "7:Manx\n",
+ "0:Rat 1:Manx 2:Cymric 3:Mutt 4:Pug 5:Cymric 6:Pug\n",
+ "7:Manx\n",
+ "0:Rat 1:Manx 2:Cymric 3:Mutt 4:Pug 5:Cymric 6:Pug\n",
+ "7:Manx\n",
+ "5:Cymric 2:Cymric 7:Manx 1:Manx 3:Mutt 6:Pug 4:Pug\n",
+ "0:Rat\n",
+ "*/"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "这里所有的类都是 **Iterable** ,所以现在对 `display()` 的调用显然更简单。\n",
+ "\n",
+ "\n",
+ "### ListIterator\n",
+ "\n",
+ "**ListIterator** 是一个更强大的 **Iterator** 子类型,它只能由各种 **List** 类生成。 **Iterator** 只能向前移动,而 **ListIterator** 可以双向移动。它还可以生成相对于迭代器在列表中指向的当前位置的后一个和前一个元素的索引,并且可以使用 `set()` 方法替换它访问过的最近一个元素。可以通过调用 `listIterator()` 方法来生成指向 **List** 开头处的 **ListIterator** ,还可以通过调用 `listIterator(n)` 创建一个一开始就指向列表索引号为 **n** 的元素处的 **ListIterator** 。 下面的示例演示了所有这些能力:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "// collections/ListIteration.java\n",
+ "import typeinfo.pets.*;\n",
+ "import java.util.*;\n",
+ "\n",
+ "public class ListIteration {\n",
+ " public static void main(String[] args) {\n",
+ " List pets = Pets.list(8);\n",
+ " ListIterator it = pets.listIterator();\n",
+ " while(it.hasNext())\n",
+ " System.out.print(it.next() +\n",
+ " \", \" + it.nextIndex() +\n",
+ " \", \" + it.previousIndex() + \"; \");\n",
+ " System.out.println();\n",
+ " // Backwards:\n",
+ " while(it.hasPrevious())\n",
+ " System.out.print(it.previous().id() + \" \");\n",
+ " System.out.println();\n",
+ " System.out.println(pets);\n",
+ " it = pets.listIterator(3);\n",
+ " while(it.hasNext()) {\n",
+ " it.next();\n",
+ " it.set(Pets.get());\n",
+ " }\n",
+ " System.out.println(pets);\n",
+ " }\n",
+ "}\n",
+ "/* Output:\n",
+ "Rat, 1, 0; Manx, 2, 1; Cymric, 3, 2; Mutt, 4, 3; Pug,\n",
+ "5, 4; Cymric, 6, 5; Pug, 7, 6; Manx, 8, 7;\n",
+ "7 6 5 4 3 2 1 0\n",
+ "[Rat, Manx, Cymric, Mutt, Pug, Cymric, Pug, Manx]\n",
+ "[Rat, Manx, Cymric, Cymric, Rat, EgyptianMau, Hamster,\n",
+ "EgyptianMau]\n",
+ "*/"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "`Pets.get()` 方法用来从位置 3 开始替换 **List** 中的所有 Pet 对象。\n",
+ "\n",
+ "\n",
+ "\n",
+ "## 链表LinkedList\n",
+ "\n",
+ "**LinkedList** 也像 **ArrayList** 一样实现了基本的 **List** 接口,但它在 **List** 中间执行插入和删除操作时比 **ArrayList** 更高效。然而,它在随机访问操作效率方面却要逊色一些。\n",
+ "\n",
+ "**LinkedList 还添加了一些方法,使其可以被用作栈、队列或双端队列(deque)** 。在这些方法中,有些彼此之间可能只是名称有些差异,或者只存在些许差异,以使得这些名字在特定用法的上下文环境中更加适用(特别是在 **Queue** 中)。例如:\n",
+ "\n",
+ "- `getFirst()` 和 `element()` 是相同的,它们都返回列表的头部(第一个元素)而并不删除它,如果 **List** 为空,则抛出 **NoSuchElementException** 异常。 `peek()` 方法与这两个方法只是稍有差异,它在列表为空时返回 **null** 。\n",
+ "- `removeFirst()` 和 `remove()` 也是相同的,它们删除并返回列表的头部元素,并在列表为空时抛出 **NoSuchElementException** 异常。 `poll()` 稍有差异,它在列表为空时返回 **null** 。\n",
+ "- `addFirst()` 在列表的开头插入一个元素。\n",
+ "- `offer()` 与 `add()` 和 `addLast()` 相同。 它们都在列表的尾部(末尾)添加一个元素。\n",
+ "- `removeLast()` 删除并返回列表的最后一个元素。\n",
+ "\n",
+ "下面的示例展示了这些功能之间基本的相似性和差异性。它并不是重复执行 **ListFeatures.java** 中所示的行为:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "// collections/LinkedListFeatures.java\n",
+ "import typeinfo.pets.*;\n",
+ "import java.util.*;\n",
+ "\n",
+ "public class LinkedListFeatures {\n",
+ " public static void main(String[] args) {\n",
+ " LinkedList pets =\n",
+ " new LinkedList<>(Pets.list(5));\n",
+ " System.out.println(pets);\n",
+ " // Identical:\n",
+ " System.out.println(\n",
+ " \"pets.getFirst(): \" + pets.getFirst());\n",
+ " System.out.println(\n",
+ " \"pets.element(): \" + pets.element());\n",
+ " // Only differs in empty-list behavior:\n",
+ " System.out.println(\"pets.peek(): \" + pets.peek());\n",
+ " // Identical; remove and return the first element:\n",
+ " System.out.println(\n",
+ " \"pets.remove(): \" + pets.remove());\n",
+ " System.out.println(\n",
+ " \"pets.removeFirst(): \" + pets.removeFirst());\n",
+ " // Only differs in empty-list behavior:\n",
+ " System.out.println(\"pets.poll(): \" + pets.poll());\n",
+ " System.out.println(pets);\n",
+ " pets.addFirst(new Rat());\n",
+ " System.out.println(\"After addFirst(): \" + pets);\n",
+ " pets.offer(Pets.get());\n",
+ " System.out.println(\"After offer(): \" + pets);\n",
+ " pets.add(Pets.get());\n",
+ " System.out.println(\"After add(): \" + pets);\n",
+ " pets.addLast(new Hamster());\n",
+ " System.out.println(\"After addLast(): \" + pets);\n",
+ " System.out.println(\n",
+ " \"pets.removeLast(): \" + pets.removeLast());\n",
+ " }\n",
+ "}\n",
+ "/* Output:\n",
+ "[Rat, Manx, Cymric, Mutt, Pug]\n",
+ "pets.getFirst(): Rat\n",
+ "pets.element(): Rat\n",
+ "pets.peek(): Rat\n",
+ "pets.remove(): Rat\n",
+ "pets.removeFirst(): Manx\n",
+ "pets.poll(): Cymric\n",
+ "[Mutt, Pug]\n",
+ "After addFirst(): [Rat, Mutt, Pug]\n",
+ "After offer(): [Rat, Mutt, Pug, Cymric]\n",
+ "After add(): [Rat, Mutt, Pug, Cymric, Pug]\n",
+ "After addLast(): [Rat, Mutt, Pug, Cymric, Pug, Hamster]\n",
+ "pets.removeLast(): Hamster\n",
+ "*/"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "`Pets.list()` 的结果被传递给 **LinkedList** 的构造器,以便使用它来填充 **LinkedList** 。如果查看 **Queue** 接口就会发现,它在 **LinkedList** 的基础上添加了 `element()` , `offer()` , `peek()` , `poll()` 和 `remove()` 方法,以使其可以成为一个 **Queue** 的实现。 **Queue** 的完整示例将在本章稍后给出。\n",
+ "\n",
+ "\n",
+ "\n",
+ "## 堆栈Stack\n",
+ "\n",
+ "堆栈是“后进先出”(LIFO)集合。它有时被称为*叠加栈*(pushdown stack),因为最后“压入”(push)栈的元素,第一个被“弹出”(pop)栈。经常用来类比栈的事物是带有弹簧支架的自助餐厅托盘。最后装入的托盘总是最先拿出来使用的。\n",
+ "\n",
+ "Java 1.0 中附带了一个 **Stack** 类,结果设计得很糟糕(为了向后兼容,我们永远坚持 Java 中的旧设计错误)。Java 6 添加了 **ArrayDeque** ,其中包含直接实现堆栈功能的方法:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "// collections/StackTest.java\n",
+ "import java.util.*;\n",
+ "\n",
+ "public class StackTest {\n",
+ " public static void main(String[] args) {\n",
+ " Deque stack = new ArrayDeque<>();\n",
+ " for(String s : \"My dog has fleas\".split(\" \"))\n",
+ " stack.push(s);\n",
+ " while(!stack.isEmpty())\n",
+ " System.out.print(stack.pop() + \" \");\n",
+ " }\n",
+ "}\n",
+ "/* Output:\n",
+ "fleas has dog My\n",
+ "*/"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "即使它是作为一个堆栈在使用,我们仍然必须将其声明为 **Deque** 。有时一个名为 **Stack** 的类更能把事情讲清楚:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "// onjava/Stack.java\n",
+ "// A Stack class built with an ArrayDeque\n",
+ "package onjava;\n",
+ "import java.util.Deque;\n",
+ "import java.util.ArrayDeque;\n",
+ "\n",
+ "public class Stack {\n",
+ " private Deque storage = new ArrayDeque<>();\n",
+ " public void push(T v) { storage.push(v); }\n",
+ " public T peek() { return storage.peek(); }\n",
+ " public T pop() { return storage.pop(); }\n",
+ " public boolean isEmpty() { return storage.isEmpty(); }\n",
+ " @Override\n",
+ " public String toString() {\n",
+ " return storage.toString();\n",
+ " }\n",
+ "}"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "这里引入了使用泛型的类定义的最简单的可能示例。类名称后面的 **** 告诉编译器这是一个参数化类型,而其中的类型参数 **T** 会在使用类时被实际类型替换。基本上,这个类是在声明“我们在定义一个可以持有 **T** 类型对象的 **Stack** 。” **Stack** 是使用 **ArrayDeque** 实现的,而 **ArrayDeque** 也被告知它将持有 **T** 类型对象。注意, `push()` 接受类型为 **T** 的对象,而 `peek()` 和 `pop()` 返回类型为 **T** 的对象。 `peek()` 方法将返回栈顶元素,但并不将其从栈顶删除,而 `pop()` 删除并返回顶部元素。\n",
+ "\n",
+ "如果只需要栈的行为,那么使用继承是不合适的,因为这将产生一个具有 **ArrayDeque** 的其它所有方法的类(在[附录:集合主题]()中将会看到, **Java 1.0** 设计者在创建 **java.util.Stack** 时,就犯了这个错误)。使用组合,可以选择要公开的方法以及如何命名它们。\n",
+ "\n",
+ "下面将使用 **StackTest.java** 中的相同代码来演示这个新的 **Stack** 类:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "// collections/StackTest2.java\n",
+ "import onjava.*;\n",
+ "\n",
+ "public class StackTest2 {\n",
+ " public static void main(String[] args) {\n",
+ " Stack stack = new Stack<>();\n",
+ " for(String s : \"My dog has fleas\".split(\" \"))\n",
+ " stack.push(s);\n",
+ " while(!stack.isEmpty())\n",
+ " System.out.print(stack.pop() + \" \");\n",
+ " }\n",
+ "}\n",
+ "/* Output:\n",
+ "fleas has dog My\n",
+ "*/"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "如果想在自己的代码中使用这个 **Stack** 类,当在创建其实例时,就需要完整指定包名,或者更改这个类的名称;否则,就有可能会与 **java.util** 包中的 **Stack** 发生冲突。例如,如果我们在上面的例子中导入 **java.util.***,那么就必须使用包名来防止冲突:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "// collections/StackCollision.java\n",
+ "\n",
+ "public class StackCollision {\n",
+ " public static void main(String[] args) {\n",
+ " onjava.Stack stack = new onjava.Stack<>();\n",
+ " for(String s : \"My dog has fleas\".split(\" \"))\n",
+ " stack.push(s);\n",
+ " while(!stack.isEmpty())\n",
+ " System.out.print(stack.pop() + \" \");\n",
+ " System.out.println();\n",
+ " java.util.Stack stack2 =\n",
+ " new java.util.Stack<>();\n",
+ " for(String s : \"My dog has fleas\".split(\" \"))\n",
+ " stack2.push(s);\n",
+ " while(!stack2.empty())\n",
+ " System.out.print(stack2.pop() + \" \");\n",
+ " }\n",
+ "}\n",
+ "/* Output:\n",
+ "fleas has dog My\n",
+ "fleas has dog My\n",
+ "*/"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "尽管已经有了 **java.util.Stack** ,但是 **ArrayDeque** 可以产生更好的 **Stack** ,因此更可取。\n",
+ "\n",
+ "还可以使用显式导入来控制对“首选” **Stack** 实现的选择:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "import onjava.Stack;"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "现在,任何对 **Stack** 的引用都将选择 **onjava** 版本,而在选择 **java.util.Stack** 时,必须使用全限定名称(full qualification)。\n",
+ "\n",
+ "\n",
+ "## 集合Set\n",
+ "\n",
+ "**Set** 不保存重复的元素。 如果试图将相同对象的多个实例添加到 **Set** 中,那么它会阻止这种重复行为。 **Set** 最常见的用途是测试归属性,可以很轻松地询问某个对象是否在一个 **Set** 中。因此,查找通常是 **Set** 最重要的操作,因此通常会选择 **HashSet** 实现,该实现针对快速查找进行了优化。\n",
+ "\n",
+ "**Set** 具有与 **Collection** 相同的接口,因此没有任何额外的功能,不像前面两种不同类型的 **List** 那样。实际上, **Set** 就是一个 **Collection** ,只是行为不同。(这是继承和多态思想的典型应用:表现不同的行为。)**Set** 根据对象的“值”确定归属性,更复杂的内容将在[附录:集合主题]()中介绍。\n",
+ "\n",
+ "下面是使用存放 **Integer** 对象的 **HashSet** 的示例:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "// collections/SetOfInteger.java\n",
+ "import java.util.*;\n",
+ "\n",
+ "public class SetOfInteger {\n",
+ " public static void main(String[] args) {\n",
+ " Random rand = new Random(47);\n",
+ " Set intset = new HashSet<>();\n",
+ " for(int i = 0; i < 10000; i++)\n",
+ " intset.add(rand.nextInt(30));\n",
+ " System.out.println(intset);\n",
+ " }\n",
+ "}\n",
+ "/* Output:\n",
+ "[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15,\n",
+ "16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29]\n",
+ "*/"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "在 0 到 29 之间的 10000 个随机整数被添加到 **Set** 中,因此可以想象每个值都重复了很多次。但是从结果中可以看到,每一个数只有一个实例出现在结果中。\n",
+ "\n",
+ "早期 Java 版本中的 **HashSet** 产生的输出没有可辨别的顺序。这是因为出于对速度的追求, **HashSet** 使用了散列,请参阅[附录:集合主题]()一章。由 **HashSet** 维护的顺序与 **TreeSet** 或 **LinkedHashSet** 不同,因为它们的实现具有不同的元素存储方式。 **TreeSet** 将元素存储在红-黑树数据结构中,而 **HashSet** 使用散列函数。 **LinkedHashSet** 因为查询速度的原因也使用了散列,但是看起来使用了链表来维护元素的插入顺序。看起来散列算法好像已经改变了,现在 **Integer** 按顺序排序。但是,您不应该依赖此行为:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "// collections/SetOfString.java\n",
+ "import java.util.*;\n",
+ "\n",
+ "public class SetOfString {\n",
+ " public static void main(String[] args) {\n",
+ " Set colors = new HashSet<>();\n",
+ " for(int i = 0; i < 100; i++) {\n",
+ " colors.add(\"Yellow\");\n",
+ " colors.add(\"Blue\");\n",
+ " colors.add(\"Red\");\n",
+ " colors.add(\"Red\");\n",
+ " colors.add(\"Orange\");\n",
+ " colors.add(\"Yellow\");\n",
+ " colors.add(\"Blue\");\n",
+ " colors.add(\"Purple\");\n",
+ " }\n",
+ " System.out.println(colors);\n",
+ " }\n",
+ "}\n",
+ "/* Output:\n",
+ "[Red, Yellow, Blue, Purple, Orange]\n",
+ "*/"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "**String** 对象似乎没有排序。要对结果进行排序,一种方法是使用 **TreeSet** 而不是 **HashSet** :"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "// collections/SortedSetOfString.java\n",
+ "import java.util.*;\n",
+ "\n",
+ "public class SortedSetOfString {\n",
+ " public static void main(String[] args) {\n",
+ " Set colors = new TreeSet<>();\n",
+ " for(int i = 0; i < 100; i++) {\n",
+ " colors.add(\"Yellow\");\n",
+ " colors.add(\"Blue\");\n",
+ " colors.add(\"Red\");\n",
+ " colors.add(\"Red\");\n",
+ " colors.add(\"Orange\");\n",
+ " colors.add(\"Yellow\");\n",
+ " colors.add(\"Blue\");\n",
+ " colors.add(\"Purple\");\n",
+ " }\n",
+ " System.out.println(colors);\n",
+ " }\n",
+ "}\n",
+ "/* Output:\n",
+ "[Blue, Orange, Purple, Red, Yellow]\n",
+ "*/"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "最常见的操作之一是使用 `contains()` 测试成员归属性,但也有一些其它操作,这可能会让你想起在小学学过的维恩图(译者注:利用图形的交合表示多个集合之间的逻辑关系):"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "attributes": {
+ "classes": [
+ "java"
+ ],
+ "id": ""
+ }
+ },
+ "outputs": [],
+ "source": [
+ "// collections/SetOperations.java\n",
+ "import java.util.*;\n",
+ "\n",
+ "public class SetOperations {\n",
+ " public static void main(String[] args) {\n",
+ " Set