mirror of
https://github.com/LingCoder/OnJava8.git
synced 2026-08-24 06:53:27 +08:00
@@ -36,6 +36,10 @@ $ git rebase upstream/master
|
||||
$ git push -f origin master
|
||||
```
|
||||
|
||||
## 视频演示教程
|
||||
|
||||
- https://www.bilibili.com/video/av58040840
|
||||
|
||||
## 排版规范
|
||||
|
||||
本开源书籍排版布局和翻译风格上参考**阮一峰**老师的 [中文技术文档的写作规范](https://github.com/ruanyf/document-style-guide)
|
||||
|
||||
11
README.md
11
README.md
@@ -29,15 +29,15 @@
|
||||
- [x] [第七章 封装](docs/book/07-Implementation-Hiding.md)
|
||||
- [x] [第八章 复用](docs/book/08-Reuse.md)
|
||||
- [x] [第九章 多态](docs/book/09-Polymorphism.md)
|
||||
- [ ] [第十章 接口](docs/book/10-Interfaces.md)
|
||||
- [x] [第十章 接口](docs/book/10-Interfaces.md)
|
||||
- [x] [第十一章 内部类](docs/book/11-Inner-Classes.md)
|
||||
- [ ] [第十二章 集合](docs/book/12-Collections.md)
|
||||
- [x] [第十二章 集合](docs/book/12-Collections.md)
|
||||
- [x] [第十三章 函数式编程](docs/book/13-Functional-Programming.md)
|
||||
- [x] [第十四章 流式编程](docs/book/14-Streams.md)
|
||||
- [x] [第十五章 异常](docs/book/15-Exceptions.md)
|
||||
- [ ] [第十六章 代码校验](docs/book/16-Validating-Your-Code.md)
|
||||
- [ ] [第十七章 文件](docs/book/17-Files.md)
|
||||
- [ ] [第十八章 字符串](docs/book/18-Strings.md)
|
||||
- [x] [第十八章 字符串](docs/book/18-Strings.md)
|
||||
- [ ] [第十九章 类型信息](docs/book/19-Type-Information.md)
|
||||
- [ ] [第二十章 泛型](docs/book/20-Generics.md)
|
||||
- [ ] [第二十一章 数组](docs/book/21-Arrays.md)
|
||||
@@ -45,7 +45,7 @@
|
||||
- [x] [第二十三章 注解](docs/book/23-Annotations.md)
|
||||
- [ ] [第二十四章 并发编程](docs/book/24-Concurrent-Programming.md)
|
||||
- [ ] [第二十五章 设计模式](docs/book/25-Patterns.md)
|
||||
- [ ] [附录:补充](docs/book/Appendix-Supplements.md)
|
||||
- [x] [附录:补充](docs/book/Appendix-Supplements.md)
|
||||
- [ ] [附录:编程指南](docs/book/Appendix-Programming-Guidelines.md)
|
||||
- [ ] [附录:文档注释](docs/book/Appendix-Javadoc.md)
|
||||
- [ ] [附录:对象传递和返回](docs/book/Appendix-Passing-and-Returning-Objects.md)
|
||||
@@ -58,6 +58,7 @@
|
||||
- [x] [附录:数据压缩](docs/book/Appendix-Data-Compression.md)
|
||||
- [ ] [附录:对象序列化](docs/book/Appendix-Object-Serialization.md)
|
||||
- [ ] [附录:静态语言类型检查](docs/book/Appendix-Benefits-and-Costs-of-Static-Type-Checking.md)
|
||||
- [x] [附录:C++和Java的优良传统](docs/book/Appendix-The-Positive-Legacy-of-C-plus-plus-and-Java.md)
|
||||
- [ ] [附录:成为一名程序员](docs/book/Appendix-Becoming-a-Programmer.md)
|
||||
|
||||
|
||||
@@ -90,7 +91,7 @@
|
||||
## 贡献者
|
||||
|
||||
* 主译:[LingCoder](https://github.com/LingCoder),[sjsdfg](https://github.com/sjsdfg)
|
||||
* 参译:[xiangflight](https://github.com/xiangflight),[LortSir](https://github.com/LortSir)
|
||||
* 参译:[xiangflight](https://github.com/xiangflight),[1326670425](https://github.com/1326670425),[LortSir](https://github.com/LortSir)
|
||||
* 校对:[LingCoder](https://github.com/LingCoder),[xiangflight](https://github.com/xiangflight),[nickChenyx](https://github.com/nickChenyx)
|
||||
|
||||
|
||||
|
||||
@@ -202,7 +202,7 @@ void doSomething(Shape shape) {
|
||||
```java
|
||||
doSomething(circle);
|
||||
```
|
||||
当预期接收 **Shape** 的方法被传入了 **Circle**,会发生什么。由于 **Circle** 也是一种 **Shape**,所
|
||||
当预期接收 **Shape** 的方法被传入了 **Circle**,会发生什么。由于 **Circle** 也是一种 **Shape**,所
|
||||
以 `doSomething(circle)` 能正确地执行。也就是说,`doSomething()` 能接收任意发送给 **Shape** 的消息。这是完全安全和合乎逻辑的事情。
|
||||
|
||||
这种把子类当成其基类来处理的过程叫做“向上转型”(**upcasting**)。在面向对象的编程里,经常利用这种方法来给程序解耦。再看下面的 `doSomething()` 代码示例:
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -4,9 +4,9 @@
|
||||
|
||||
# 第十一章 内部类
|
||||
|
||||
> 一个类的定义在另一个类的定义内部,这就是内部类。
|
||||
> 一个定义在另一个类中的类,叫作内部类。
|
||||
|
||||
内部类是一种非常有用的特性,因为它允许你把一些逻辑相关的类组织在一起,并控制位于内部的类的可视性。然而必须要了解,内部类与组合是完全不同的概念,这一点很重要。在最初,内部类看起来就像是一种代码隐藏机制:将类置于其他类的内部。但是,你将会了解到,内部类远不止如此,它了解外围类,并能与之通信,而且你用内部类写出的代码更加优雅而清晰,尽管并不总是这样。
|
||||
内部类是一种非常有用的特性,因为它允许你把一些逻辑相关的类组织在一起,并控制位于内部的类的可见性。然而必须要了解,内部类与组合是完全不同的概念,这一点很重要。在最初,内部类看起来就像是一种代码隐藏机制:将类置于其他类的内部。但是,你将会了解到,内部类远不止如此,它了解外围类,并能与之通信,而且你用内部类写出的代码更加优雅而清晰,尽管并不总是这样(而且 Java 8 的 Lambda 表达式和方法引用减少了编写内部类的需求)。
|
||||
|
||||
最初,内部类可能看起来有些奇怪,而且要花些时间才能在设计中轻松地使用它们。对内部类的需求并非总是很明显的,但是在描述完内部类的基本语法与语义之后,"Why inner classes?"就应该使得内部类的益处明确显现了。
|
||||
|
||||
@@ -24,13 +24,17 @@
|
||||
public class Parcel1 {
|
||||
class Contents {
|
||||
private int i = 11;
|
||||
|
||||
public int value() { return i; }
|
||||
}
|
||||
|
||||
class Destination {
|
||||
private String label;
|
||||
|
||||
Destination(String whereTo) {
|
||||
label = whereTo;
|
||||
}
|
||||
|
||||
String readLabel() { return label; }
|
||||
}
|
||||
// Using inner classes looks just like
|
||||
@@ -40,6 +44,7 @@ public class Parcel1 {
|
||||
Destination d = new Destination(dest);
|
||||
System.out.println(d.readLabel());
|
||||
}
|
||||
|
||||
public static void main(String[] args) {
|
||||
Parcel1 p = new Parcel1();
|
||||
p.ship("Tasmania");
|
||||
@@ -53,9 +58,9 @@ public class Parcel1 {
|
||||
Tasmania
|
||||
```
|
||||
|
||||
当我们在 ship() 方法里面使用内部类的时候,与使用普通类没什么不同。在这里,实际的区别只是内部类的名字是嵌套在 Parcel1 里面的。
|
||||
当我们在 `ship()` 方法里面使用内部类的时候,与使用普通类没什么不同。在这里,明显的区别只是内部类的名字是嵌套在 **Parcel1** 里面的。
|
||||
|
||||
更典型的情况是,外部类将有一个方法,该方法返回一个指向内部类的引用,就像在 to() 和 contents() 方法中看到的那样:
|
||||
更典型的情况是,外部类将有一个方法,该方法返回一个指向内部类的引用,就像在 `to()` 和 `contents()` 方法中看到的那样:
|
||||
|
||||
```java
|
||||
// innerclasses/Parcel2.java
|
||||
@@ -63,31 +68,39 @@ Tasmania
|
||||
public class Parcel2 {
|
||||
class Contents {
|
||||
private int i = 11;
|
||||
|
||||
public int value() { return i; }
|
||||
}
|
||||
|
||||
class Destination {
|
||||
private String label;
|
||||
|
||||
Destination(String whereTo) {
|
||||
label = whereTo;
|
||||
}
|
||||
|
||||
String readLabel() { return label; }
|
||||
}
|
||||
|
||||
public Destination to(String s) {
|
||||
return new Destination(s);
|
||||
}
|
||||
|
||||
public Contents contents() {
|
||||
return new Contents();
|
||||
}
|
||||
|
||||
public void ship(String dest) {
|
||||
Contents c = contents();
|
||||
Destination d = to(dest);
|
||||
System.out.println(d.readLabel());
|
||||
}
|
||||
|
||||
public static void main(String[] args) {
|
||||
Parcel2 p = new Parcel2();
|
||||
p.ship("Tasmania");
|
||||
Parcel2 q = new Parcel2();
|
||||
// Defining references to inner classes:
|
||||
// Defining references to inner classes:
|
||||
Parcel2.Contents c = q.contents();
|
||||
Parcel2.Destination d = q.to("Borneo");
|
||||
}
|
||||
@@ -100,7 +113,7 @@ public class Parcel2 {
|
||||
Tasmania
|
||||
```
|
||||
|
||||
如果想从外部类的非静态方法之外的任意位置创建某个内部类的对象,那么必须像在 main() 方法中那样,具体地指明这个对象的类型:OuterClassName.InnerClassName。
|
||||
如果想从外部类的非静态方法之外的任意位置创建某个内部类的对象,那么必须像在 `main()` 方法中那样,具体地指明这个对象的类型:*OuterClassName.InnerClassName*。(译者注:在外部类的静态方法中也可以直接指明类型 *InnerClassName*,在其他类中需要指明 *OuterClassName.InnerClassName*。)
|
||||
|
||||
<!-- The Link to the Outer Class -->
|
||||
|
||||
@@ -157,30 +170,33 @@ public class Sequence {
|
||||
0 1 2 3 4 5 6 7 8 9
|
||||
```
|
||||
|
||||
Sequence 类只是一个固定大小的 Object 的数组,以类的形式包装了起来。可以调用 add)在序列末增加新的 Object(只要还有空间),要获取 Sequence 中的每一个对象,可以使用 Selector 接口。这是“迭代器”设计模式的一个例子,在本书稍后的部分将更多地学习它。Selector 允许你检查序列是否到末尾了(end()),访问当前对象(current()),以及移到序列中的下一个对象(next()),因为 Selector 是一个接口,所以别的类可以按它们自己的方式来实现这个接口,并且另的方法能以此接口为参数,来生成更加通用的代码。
|
||||
**Sequence** 类只是一个固定大小的 **Object** 的数组,以类的形式包装了起来。可以调用 `add()` 在序列末尾增加新的 **Object**(只要还有空间),要获取 **Sequence** 中的每一个对象,可以使用 **Selector** 接口。这是“迭代器”设计模式的一个例子,在本书稍后的部分将更多地学习它。**Selector** 允许你检查序列是否到末尾了(`end()`),访问当前对象(`current()`),以及移到序列中的下一个对象(`next()`)。因为 **Selector** 是一个接口,所以别的类可以按它们自己的方式来实现这个接口,并且其他方法能以此接口为参数,来生成更加通用的代码。
|
||||
|
||||
这里,SequenceSelector 是提供 Selector 功能的 private 类。可以看到,在 main() 中创建了一个 Sequence,并向其中添加了一些 String 对象。然后通过调用 selector() 获取一个 Selector,并用它在 Sequence 中移动和选择每一个元素。
|
||||
最初看到 SequenceSelector,可能会觉得它只不过是另一个内部类罢了。但请仔细观察它,注意方法 end(),current() 和 next() 都用到了 items,这是一个引用,它并不是 SequenceSelector 的一部分,而是外围类中的一个 private 字段。然而内部类可以访问其外围类的方法和字段,就像自己拥有它们似的,这带来了很大的方便,就如前面的例子所示。
|
||||
这里,**SequenceSelector** 是提供 **Selector** 功能的 **private** 类。可以看到,在 `main()` 中创建了一个 **Sequence**,并向其中添加了一些 **String** 对象。然后通过调用 `selector()` 获取一个 **Selector**,并用它在 **Sequence** 中移动和选择每一个元素。
|
||||
最初看到 **SequenceSelector**,可能会觉得它只不过是另一个内部类罢了。但请仔细观察它,注意方法 `end()`,`current()` 和 `next()` 都用到了 **items**,这是一个引用,它并不是 **SequenceSelector** 的一部分,而是外围类中的一个 **private** 字段。然而内部类可以访问其外围类的方法和字段,就像自己拥有它们似的,这带来了很大的方便,就如前面的例子所示。
|
||||
|
||||
所以内部类自动拥有对其外围类所有成员的访问权。这是如何做到的呢?当某个外围类的对象创建了一个内部类对象时,此内部类对象必定会秘密地捕获一个指向那个外围类对象的引用。然后,在你访问此外围类的成员时,就是用那个引用来选择外围类的成员。幸运的是,编译器会帮你处理所有的细节,但你现在可以看到:内部类的对象只能在与其外围类的对象相关联的情况下才能被创建(就像你应该看到的,在内部类是非 static 类时)。构建内部类对象时,需要一个指向其外围类对象的引用,如果编译器访问不到这个引用就会报错。不过绝大多数时候这都无需程序员操心。
|
||||
所以内部类自动拥有对其外围类所有成员的访问权。这是如何做到的呢?当某个外围类的对象创建了一个内部类对象时,此内部类对象必定会秘密地捕获一个指向那个外围类对象的引用。然后,在你访问此外围类的成员时,就是用那个引用来选择外围类的成员。幸运的是,编译器会帮你处理所有的细节,但你现在可以看到:内部类的对象只能在与其外围类的对象相关联的情况下才能被创建(就像你应该看到的,内部类是非 **static** 类时)。构建内部类对象时,需要一个指向其外围类对象的引用,如果编译器访问不到这个引用就会报错。不过绝大多数时候这都无需程序员操心。
|
||||
|
||||
<!-- Using .this and .new -->
|
||||
## 使用 .this 和 .new
|
||||
|
||||
如果你需要生成对外部类对象的引用,可以使用外部类的名字后面紧跟圆点和 this。这样产生的引用自动地具有正确的类型,这一点在编译期就被知晓并受到检查,因此没有任何运行时开销。下面的示例展示了如何使用.this:
|
||||
如果你需要生成对外部类对象的引用,可以使用外部类的名字后面紧跟圆点和 **this**。这样产生的引用自动地具有正确的类型,这一点在编译期就被知晓并受到检查,因此没有任何运行时开销。下面的示例展示了如何使用 **.this**:
|
||||
|
||||
```java
|
||||
// innerclasses/DotThis.java
|
||||
// Accessing the outer-class object
|
||||
public class DotThis {
|
||||
void f() { System.out.println("DotThis.f()"); }
|
||||
|
||||
public class Inner {
|
||||
public DotThis outer() {
|
||||
return DotThis.this;
|
||||
// A plain "this" would be Inner's "this"
|
||||
}
|
||||
}
|
||||
|
||||
public Inner inner() { return new Inner(); }
|
||||
|
||||
public static void main(String[] args) {
|
||||
DotThis dt = new DotThis();
|
||||
DotThis.Inner dti = dt.inner();
|
||||
@@ -195,7 +211,7 @@ public class DotThis {
|
||||
DotThis.f()
|
||||
```
|
||||
|
||||
有时你可能想要告知某些其他对象,去创建其某个内部类的对象。要实现此目的,你必须在 mew 表达式中提供对其他外部类对象的引用,这是需要使用.new 语法,就像下面这样:
|
||||
有时你可能想要告知某些其他对象,去创建其某个内部类的对象。要实现此目的,你必须在 **new** 表达式中提供对其他外部类对象的引用,这是需要使用 **.new** 语法,就像下面这样:
|
||||
|
||||
```java
|
||||
// innerclasses/DotNew.java
|
||||
@@ -209,9 +225,9 @@ public class DotNew {
|
||||
}
|
||||
```
|
||||
|
||||
要想直接创建内部类的对象,你不能按照你想象的方式,去引用外部类的名字 DotNew,而是必须使用外部类的对象来创建该内部类对象,就像在上面的程序中所看到的那样。这也解决了内部类名字作用域的问题,因此你不必声明(实际上你不能声明)dn.new DotNew.Innero。
|
||||
要想直接创建内部类的对象,你不能按照你想象的方式,去引用外部类的名字 **DotNew**,而是必须使用外部类的对象来创建该内部类对象,就像在上面的程序中所看到的那样。这也解决了内部类名字作用域的问题,因此你不必声明(实际上你不能声明)dn.new DotNew.Innero。
|
||||
|
||||
下面你可以看到将.new 应用于 Parcel 的示例:
|
||||
下面你可以看到将 **.new** 应用于 Parcel 的示例:
|
||||
|
||||
```java
|
||||
// innerclasses/Parcel3.java
|
||||
@@ -228,8 +244,8 @@ public class Parcel3 {
|
||||
}
|
||||
public static void main(String[] args) {
|
||||
Parcel3 p = new Parcel3();
|
||||
// Must use instance of outer class
|
||||
// to create an instance of the inner class:
|
||||
// Must use instance of outer class
|
||||
// to create an instance of the inner class:
|
||||
Parcel3.Contents c = p.new Contents();
|
||||
Parcel3.Destination d =
|
||||
p.new Destination("Tasmania");
|
||||
@@ -240,6 +256,7 @@ public class Parcel3 {
|
||||
在拥有外部类对象之前是不可能创建内部类对象的。这是因为内部类对象会暗暗地连接到建它的外部类对象上。但是,如果你创建的是嵌套类(静态内部类),那么它就不需要对外部类对象的引用。
|
||||
|
||||
<!-- Inner Classes and Upcasting -->
|
||||
|
||||
## 内部类与向上转型
|
||||
|
||||
当将内部类向上转型为其基类,尤其是转型为一个接口的时候,内部类就有了用武之地。(从实现了某个接口的对象,得到对此接口的引用,与向上转型为这个对象的基类,实质上效果是一样的。)这是因为此内部类-某个接口的实现-能够完全不可见,并且不可用。所得到的只是指向基类或接口的引用,所以能够很方便地隐藏实现细节。
|
||||
@@ -260,7 +277,7 @@ public interface Contents {
|
||||
}
|
||||
```
|
||||
|
||||
现在 Contents 和 Destination 表示客户端程序员可用的接口。记住,接口的所有成员自动被设置为 public 的。
|
||||
现在 **Contents** 和 **Destination** 表示客户端程序员可用的接口。记住,接口的所有成员自动被设置为 **public**。
|
||||
|
||||
当取得了一个指向基类或接口的引用时,甚至可能无法找出它确切的类型,看下面的例子:
|
||||
|
||||
@@ -292,17 +309,17 @@ public class TestParcel {
|
||||
Parcel4 p = new Parcel4();
|
||||
Contents c = p.contents();
|
||||
Destination d = p.destination("Tasmania");
|
||||
// Illegal -- can't access private class:
|
||||
//- Parcel4.PContents pc = p.new PContents();
|
||||
// Illegal -- can't access private class:
|
||||
//- Parcel4.PContents pc = p.new PContents();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
在 Parcel4 中,内部类 PContents 是 private,所以除了 Parcel4,没有人能访问它。普通(非内部)类的访问权限不能被设为 private 或者 protected;他们只能设置为 public 或 package 访问权限。
|
||||
在 **Parcel4** 中,内部类 **PContents** 是 **private**,所以除了 **Parcel4**,没有人能访问它。普通(非内部)类的访问权限不能被设为 **private** 或者 **protected**;他们只能设置为 **public** 或 **package** 访问权限。
|
||||
|
||||
PDestination 是 protected,所以只有 Parcel4 及其子类、还有与 Parcel4 同一个包中的类(因为 protected 也给予了包访问权)能访问 PDestination,其他类都不能访问 PDestination,这意味着,如果客户端程序员想了解或访问这些成员,那是要受到限制的。实际上,甚至不能向下转型成 private 内部类(或 protected 内部类,除非是继承自它的子类),因为不能访问其名字,就像在 TestParcel 类中看到的那样。
|
||||
**PDestination** 是 **protected**,所以只有 **Parcel4** 及其子类、还有与 **Parcel4** 同一个包中的类(因为 **protected** 也给予了包访问权)能访问 **PDestination**,其他类都不能访问 **PDestination**,这意味着,如果客户端程序员想了解或访问这些成员,那是要受到限制的。实际上,甚至不能向下转型成 **private** 内部类(或 **protected** 内部类,除非是继承自它的子类),因为不能访问其名字,就像在 **TestParcel** 类中看到的那样。
|
||||
|
||||
private 内部类给类的设计者提供了一种途径,通过这种方式可以完全阻止任何依赖于类型的编码,并且完全隐藏了实现的细节。此外,从客户端程序员的角度来看,由于不能访问任何新增加的、原本不属于公共接口的方法,所以扩展接口是没值的。这也给 Java 编译器
|
||||
**private** 内部类给类的设计者提供了一种途径,通过这种方式可以完全阻止任何依赖于类型的编码,并且完全隐藏了实现的细节。此外,从客户端程序员的角度来看,由于不能访问任何新增加的、原本不属于公共接口的方法,所以扩展接口是没有价值的。这也给 Java 编译器提供了生成高效代码的机会。
|
||||
|
||||
<!-- Inner Classes in Methods and Scopes -->
|
||||
|
||||
@@ -320,9 +337,9 @@ private 内部类给类的设计者提供了一种途径,通过这种方式可
|
||||
1. 一个定义在方法中的类。
|
||||
2. 一个定义在作用域内的类,此作用域在方法的内部。
|
||||
3. 一个实现了接口的匿名类。
|
||||
4. 一个匿名类,它扩展了有非默认构造器的类。
|
||||
4. 一个匿名类,它扩展了没有默认构造器的类。
|
||||
5. 一个匿名类,它执行字段初始化。
|
||||
6. 一个匿名类,它通过实例初始化实现构造(匿名类不可能有构造器)。
|
||||
6. 一个匿名类,它通过实例初始化实现构造(匿名内部类不可能有构造器)。
|
||||
|
||||
第一个例子展示了在方法的作用域内(而不是在其他类的作用域内)创建一个完整的类。这被称作局部内部类:
|
||||
|
||||
@@ -333,14 +350,17 @@ public class Parcel5 {
|
||||
public Destination destination(String s) {
|
||||
final class PDestination implements Destination {
|
||||
private String label;
|
||||
|
||||
private PDestination(String whereTo) {
|
||||
label = whereTo;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String readLabel() { return label; }
|
||||
}
|
||||
return new PDestination(s);
|
||||
}
|
||||
|
||||
public static void main(String[] args) {
|
||||
Parcel5 p = new Parcel5();
|
||||
Destination d = p.destination("Tasmania");
|
||||
@@ -348,9 +368,9 @@ 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**,并不意味着一旦 `dest()` 方法执行完毕,**PDestination** 就不可用了。
|
||||
|
||||
你可以在同一个子目录下的任意类中对某个内部类使用类标识符 PDestination,这并不会有命名冲突。
|
||||
你可以在同一个子目录下的任意类中对某个内部类使用类标识符 **PDestination**,这并不会有命名冲突。
|
||||
|
||||
下面的例子展示了如何在任意的作用域内嵌入一个内部类:
|
||||
|
||||
@@ -381,7 +401,7 @@ public class Parcel6 {
|
||||
}
|
||||
```
|
||||
|
||||
TrackingSlip 类被嵌入在 if 语句的作用域内,这并不是说该类的创建是有条件的,它其实与别的类一起编译过了。然而,在定义 Trackingslip 的作用域之外,它是不可用的,除此之外,它与普通的类一样。
|
||||
**TrackingSlip** 类被嵌入在 **if** 语句的作用域内,这并不是说该类的创建是有条件的,它其实与别的类一起编译过了。然而,在定义 **Trackingslip** 的作用域之外,它是不可用的,除此之外,它与普通的类一样。
|
||||
|
||||
<!-- Anonymous Inner Classes -->
|
||||
|
||||
@@ -396,10 +416,12 @@ public class Parcel7 {
|
||||
public Contents contents() {
|
||||
return new Contents() { // Insert class definition
|
||||
private int i = 11;
|
||||
|
||||
@Override
|
||||
public int value() { return i; }
|
||||
}; // Semicolon required
|
||||
}
|
||||
|
||||
public static void main(String[] args) {
|
||||
Parcel7 p = new Parcel7();
|
||||
Contents c = p.contents();
|
||||
@@ -407,9 +429,9 @@ public class Parcel7 {
|
||||
}
|
||||
```
|
||||
|
||||
contents() 方法将返回值的生成与表示这个返回值的类的定义结合在一起!另外,这个类是匿名的,它没有名字。更糟的是,看起来似乎是你正要创建一个 Contents 对象。但是然后(在到达语句结束的分号之前)你却说:“等一等,我想在这里插入一个类的定义。
|
||||
`contents()` 方法将返回值的生成与表示这个返回值的类的定义结合在一起!另外,这个类是匿名的,它没有名字。更糟的是,看起来似乎是你正要创建一个 **Contents** 对象。但是然后(在到达语句结束的分号之前)你却说:“等一等,我想在这里插入一个类的定义。
|
||||
|
||||
这种奇怪的语法指的是:“创建一个继承自 Contents 的匿名类的对象。”通过 new 表达式返回的引用被自动向上转型为对 Contents 的引用。上述匿名内部类的语法是下述形式的简化形式:
|
||||
这种奇怪的语法指的是:“创建一个继承自 **Contents** 的匿名类的对象。”通过 **new** 表达式返回的引用被自动向上转型为对 **Contents** 的引用。上述匿名内部类的语法是下述形式的简化形式:
|
||||
|
||||
```java
|
||||
// innerclasses/Parcel7b.java
|
||||
@@ -420,9 +442,11 @@ public class Parcel7b {
|
||||
@Override
|
||||
public int value() { return i; }
|
||||
}
|
||||
|
||||
public Contents contents() {
|
||||
return new MyContents();
|
||||
}
|
||||
|
||||
public static void main(String[] args) {
|
||||
Parcel7b p = new Parcel7b();
|
||||
Contents c = p.contents();
|
||||
@@ -430,14 +454,14 @@ public class Parcel7b {
|
||||
}
|
||||
```
|
||||
|
||||
在这个匿名内部类中,使用了默认的构造器来生成 Contents。下面的代码展示的是,如果你的基类需要一个有参数的构造器,应该怎么办:
|
||||
在这个匿名内部类中,使用了默认的构造器来生成 **Contents**。下面的代码展示的是,如果你的基类需要一个有参数的构造器,应该怎么办:
|
||||
|
||||
```java
|
||||
// innerclasses/Parcel8.java
|
||||
// Calling the base-class constructor
|
||||
public class Parcel8 {
|
||||
public Wrapping wrapping(int x) {
|
||||
// Base constructor call:
|
||||
// Base constructor call:
|
||||
return new Wrapping(x) { // [1]
|
||||
@Override
|
||||
public int value() {
|
||||
@@ -455,7 +479,11 @@ public class Parcel8 {
|
||||
- \[1\] 将合适的参数传递给基类的构造器。
|
||||
- \[2\] 在匿名内部类末尾的分号,并不是用来标记此内部类结束的。实际上,它标记的是表达式的结束,只不过这个表达式正巧包含了匿名内部类罢了。因此,这与别的地方使用的分号是一致的。
|
||||
|
||||
尽管 Wrapping 只是一个具有具体实现的普通类,但它还是被共导出类当作公共“接口”来使用。
|
||||
<<<<<<< Updated upstream
|
||||
尽管 Wrapping 只是一个具有具体实现的普通类,但它还是被派生类当作公共“接口”来使用。
|
||||
=======
|
||||
尽管 **Wrapping** 只是一个具有具体实现的普通类,但它还是被导出类当作公共“接口”来使用。
|
||||
>>>>>>> Stashed changes
|
||||
|
||||
```java
|
||||
// innerclasses/Wrapping.java
|
||||
@@ -466,7 +494,7 @@ public class Wrapping {
|
||||
}
|
||||
```
|
||||
|
||||
为了多样性,Wrapping 拥有一个要求传递一个参数的构造器。
|
||||
为了多样性,**Wrapping** 拥有一个要求传递一个参数的构造器。
|
||||
|
||||
在匿名类中定义字段时,还能够对其执行初始化操作:
|
||||
|
||||
@@ -474,7 +502,7 @@ public class Wrapping {
|
||||
// innerclasses/Parcel9.java
|
||||
public class Parcel9 {
|
||||
// Argument must be final or "effectively final"
|
||||
// to use within the anonymous inner class:
|
||||
// to use within the anonymous inner class:
|
||||
public Destination destination(final String dest) {
|
||||
return new Destination() {
|
||||
private String label = dest;
|
||||
@@ -489,9 +517,9 @@ public class Parcel9 {
|
||||
}
|
||||
```
|
||||
|
||||
如果定义一个匿名内部类,并且希望它使用一个在其外部定义的对象,那么编译器会要求其参数引用是 final 的,就像你在 destination() 的参数中看到的那样。如果你忘记了,将会得到一个编译时错误消息。
|
||||
如果定义一个匿名内部类,并且希望它使用一个在其外部定义的对象,那么编译器会要求其参数引用是 **final** 的(也就是说,它在初始化后不会改变,所以可以被当作 **final**),就像你在 `destination()` 的参数中看到的那样。这里省略掉 **final** 也没问题,但是通常最好加上 **final** 作为一种暗示。
|
||||
|
||||
如果只是简单地给一个字段赋值,那么此例中的方法是很好的。但是,如果想做一些类似勾造器的行为,该怎么办呢?在匿名类中不可能有命名构造器(因为它根本没名字!),但通过实例初始化,就能够达到为匿名内部类创建一个构造器的效果,就像这样:
|
||||
如果只是简单地给一个字段赋值,那么此例中的方法是很好的。但是,如果想做一些类似构造器的行为,该怎么办呢?在匿名类中不可能有命名构造器(因为它根本没名字!),但通过实例初始化,就能够达到为匿名内部类创建一个构造器的效果,就像这样:
|
||||
|
||||
```java
|
||||
// innerclasses/AnonymousConstructor.java
|
||||
@@ -528,9 +556,9 @@ Inside instance initializer
|
||||
In anonymous f()
|
||||
```
|
||||
|
||||
在此例中,不要求变量一定是 final 的。因为被传递给匿名类的基类的构造器,它并不会在匿名类内部被直接使用。
|
||||
在此例中,不要求变量一定是 **final** 的。因为被传递给匿名类的基类的构造器,它并不会在匿名类内部被直接使用。
|
||||
|
||||
下例是带实例初始化的"parcel"形式。注意 destination() 的参数必须是 final 的,因为它们是在匿名类内部使用的。
|
||||
下例是带实例初始化的"parcel"形式。注意 `destination()` 的参数必须是 **final** 的,因为它们是在匿名类内部使用的(译者注:即使不加 **final**, Java 8 的编译器也会为我们自动加上 **final**,以保证数据的一致性)。
|
||||
|
||||
```java
|
||||
// innerclasses/Parcel10.java
|
||||
@@ -565,7 +593,7 @@ public class Parcel10 {
|
||||
Over budget!
|
||||
```
|
||||
|
||||
在实例初始化操作的内部,可以看到有一段代码,它们不能作为字段初始化动作的一部分来执行(就是 if 语句)。所以对于匿名类而言,实例初始化的实际效果就是构造器。当然它受到了限制-你不能重载实例初始化方法,所以你仅有一个这样的构造器。
|
||||
在实例初始化操作的内部,可以看到有一段代码,它们不能作为字段初始化动作的一部分来执行(就是 **if** 语句)。所以对于匿名类而言,实例初始化的实际效果就是构造器。当然它受到了限制-你不能重载实例初始化方法,所以你仅有一个这样的构造器。
|
||||
|
||||
匿名内部类与正规的继承相比有些受限,因为匿名内部类既可以扩展类,也可以实现接口,但是不能两者兼备。而且如果是实现接口,也只能实现一个接口。
|
||||
|
||||
@@ -573,12 +601,12 @@ Over budget!
|
||||
|
||||
## 嵌套类
|
||||
|
||||
如果不需要内部类对象与其外围类对象之间有联系,那么可以将内部类声明为 static,这通常称为嵌套类。想要理解 static 应用于内部类时的含义,就必须记住,普通的内部类对象隐式地保存了一个引用,指向创建它的外围类对象。然而,当内部类是 static 的时,就不是这样了。嵌套类意味着:
|
||||
如果不需要内部类对象与其外围类对象之间有联系,那么可以将内部类声明为 **static**,这通常称为嵌套类。想要理解 **static** 应用于内部类时的含义,就必须记住,普通的内部类对象隐式地保存了一个引用,指向创建它的外围类对象。然而,当内部类是 **static** 的时,就不是这样了。嵌套类意味着:
|
||||
|
||||
1. 要创建嵌套类的对象,并不需要其外围类的对象。
|
||||
2. 不能从嵌套类的对象中访问非静态的外围类对象。
|
||||
|
||||
嵌套类与普通的内部类还有一个区别。普通内部类的字段与方法,只能放在类的外部层次上,所以普通的内部类不能有 static 数据和 static 字段,也不能包含嵌套类。但是嵌套类类可以包含所有这些东西:
|
||||
嵌套类与普通的内部类还有一个区别。普通内部类的字段与方法,只能放在类的外部层次上,所以普通的内部类不能有 **static** 数据和 **static** 字段,也不能包含嵌套类。但是嵌套类可以包含所有这些东西:
|
||||
|
||||
```java
|
||||
// innerclasses/Parcel11.java
|
||||
@@ -618,13 +646,13 @@ public class Parcel11 {
|
||||
}
|
||||
```
|
||||
|
||||
在 main() 中,没有任何 Parcell1 的对象是必需的;而是使用选取 static 成员的普通语法来调用方法-这些方法返回对 Contents 和 Destination 的引用。
|
||||
在 `main()` 中,没有任何 **Parcel11** 的对象是必需的;而是使用选取 **static** 成员的普通语法来调用方法-这些方法返回对 **Contents** 和 **Destination** 的引用。
|
||||
|
||||
就像你在本章前面看到的那样,在一个普通的(非 static)内部类中,通过一个特殊的 this 引用可以链接到其外围类对象。嵌套类就没有这个特殊的 this 引用,这使得它类似于一个 static 方法。
|
||||
就像你在本章前面看到的那样,在一个普通的(非 **static**)内部类中,通过一个特殊的 **this** 引用可以链接到其外围类对象。嵌套类就没有这个特殊的 **this** 引用,这使得它类似于一个 **static** 方法。
|
||||
|
||||
### 接口内部的类
|
||||
|
||||
嵌套类可以作为接口的一部分。你放到接口中的任何类都自动地是 public 和 static 的。因为类是 static 的,只是将嵌套类置于接口的命名空间内,这并不违反接口的规则。你甚至可以在内部类中实现其外围接口,就像下面这样:
|
||||
嵌套类可以作为接口的一部分。你放到接口中的任何类都自动地是 **public** 和 **static** 的。因为类是 **static** 的,只是将嵌套类置于接口的命名空间内,这并不违反接口的规则。你甚至可以在内部类中实现其外围接口,就像下面这样:
|
||||
|
||||
```java
|
||||
// innerclasses/ClassInInterface.java
|
||||
@@ -649,9 +677,9 @@ public interface ClassInInterface {
|
||||
Howdy!
|
||||
```
|
||||
|
||||
如果你想要创建某些公共代码,使得它们可以被某个接口的所有不同实现所共用,那么使用接口内部的铁套类会显得很方便。
|
||||
如果你想要创建某些公共代码,使得它们可以被某个接口的所有不同实现所共用,那么使用接口内部的嵌套类会显得很方便。
|
||||
|
||||
我曾在本书中建议过,在每个类中都写一个 main() 方法,用来测试这个类。这样做有一个缺点,那就是必须带着那些已编译过的额外代码。如果这对你是个麻烦,那就可以使用嵌套类来放置测试代码。
|
||||
我曾在本书中建议过,在每个类中都写一个 `main()` 方法,用来测试这个类。这样做有一个缺点,那就是必须带着那些已编译过的额外代码。如果这对你是个麻烦,那就可以使用嵌套类来放置测试代码。
|
||||
|
||||
```java
|
||||
// innerclasses/TestBed.java
|
||||
@@ -674,6 +702,8 @@ public class TestBed {
|
||||
f()
|
||||
```
|
||||
|
||||
这生成了一个独立的类 **TestBed$Tester**(要运行这个程序,执行 **java TestBed$Tester**,在 Unix/Linux 系统中需要转义 **$**)。你可以使用这个类测试,但是不必在发布的产品中包含它,可以在打包产品前删除 **TestBed$Tester.class**。
|
||||
|
||||
### 从多层嵌套类中访问外部类的成员
|
||||
|
||||
一个内部类被嵌套多少层并不重要——它能透明地访问所有它所嵌入的外围类的所有成员,如下所示:
|
||||
@@ -704,7 +734,7 @@ public class MultiNestingAccess {
|
||||
}
|
||||
```
|
||||
|
||||
可以看到在 MNA.A.B 中,调用方法 g() 和 f() 不需要任何条件(即使它们被定义为 private)。这个例子同时展示了如何从不同的类里创建多层嵌套的内部类对象的基本语法。".new"语法能产生正确的作用域,所以不必在调用构造器时限定类名。
|
||||
可以看到在 **MNA.A.B** 中,调用方法 `g()` 和 `f()` 不需要任何条件(即使它们被定义为 **private**)。这个例子同时展示了如何从不同的类里创建多层嵌套的内部类对象的基本语法。"**.new**"语法能产生正确的作用域,所以不必在调用构造器时限定类名。
|
||||
|
||||
<!-- Why Inner Classes? -->
|
||||
|
||||
@@ -900,7 +930,7 @@ public abstract class Event {
|
||||
|
||||
当希望运行 Event 并随后调用 start() 时,那么构造器就会捕获(从对象创建的时刻开始的)时间,此时间是这样得来的:start() 获取当前时间,然后加上一个延迟时间,这样生成触发事件的时间。start() 是一个独立的方法,而没有包含在构造器内,因为这样就可以在事件运行以后重新启动计时器,也就是能够重复使用 Event 对象。例如,如果想要重复一个事件,只需简单地在 action() 中调用 start() 方法。
|
||||
|
||||
ready() 告诉你何时可以运行 action() 方法了。当然,可以在导出类中覆盖 ready() 方法,使得 Event 能够基于时间以外的其他因素而触发。
|
||||
ready() 告诉你何时可以运行 action() 方法了。当然,可以在派生类中覆盖 ready() 方法,使得 Event 能够基于时间以外的其他因素而触发。
|
||||
|
||||
下面的文件包含了一个用来管理并触发事件的实际控制框架。Event 对象被保存在 List\<Event\> 类型(读作“Event 的列表”)的容器对象中,容器会在 [集合 ]() 中详细介绍。目前读者只需要知道 add() 方法用来将一个 Object 添加到 List 的尾端,size() 方法用来得到 List 中元素的个数,foreach 语法用来连续获联 List 中的 Event,remove() 方法用来从 List 中移除指定的 Event。
|
||||
|
||||
@@ -1163,7 +1193,7 @@ Terminating
|
||||
|
||||
## 继承内部类
|
||||
|
||||
因为内部类的构造器必须连接到指向其外围类对象的引用,所以在继承内部类的时候,事情会变得有点复杂。问题在干,那个指向外围类对象的“秘密的”引用必须被初始化,而在导出类中不再存在可连接的默认对象。要解决这个问题,必须使用特殊的语法来明确说清它们之间的关联:
|
||||
因为内部类的构造器必须连接到指向其外围类对象的引用,所以在继承内部类的时候,事情会变得有点复杂。问题在干,那个指向外围类对象的“秘密的”引用必须被初始化,而在派生类中不再存在可连接的默认对象。要解决这个问题,必须使用特殊的语法来明确说清它们之间的关联:
|
||||
|
||||
```java
|
||||
// innerclasses/InheritInner.java
|
||||
@@ -1393,4 +1423,4 @@ LocalInnerClass.class
|
||||
|
||||
<!-- 分页 -->
|
||||
|
||||
<div style="page-break-after: always;"></div>
|
||||
<div style="page-break-after: always;"></div>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -3,18 +3,389 @@
|
||||
<!-- Appendix: Low-Level Concurrency -->
|
||||
# 附录:并发底层原理
|
||||
|
||||
> 尽管不建议您自己编写底层 Java 并发代码,但是这样通常有助于了解它是如何工作的。
|
||||
|
||||
[并发编程](./24-Concurrent-Programming.md) 章节中介绍了一些用于高级并发的概念,包括为 Java 并发编程而最新提出的,更安全的概念( parallel Streams 和 CompletableFutures )。本附录则介绍在 Java 中底层并发概念,因此在阅读本篇时,您能有所了解掌握这些代码。您还会将进一步了解并发的普遍问题。
|
||||
|
||||
在 Java 的早期版本中, 底层并发概念是并发编程的重要组成部分。我们会着眼于围绕这些技巧的复杂性以及为何您应该避免它们而谈。 “并发编程” 章节展示最新的 Java 版本(尤其是 Java 8)所提供的改进技巧,这些技巧使得并发的使用,如果本来不容易使用,也会变得更容易些。
|
||||
|
||||
|
||||
|
||||
<!-- What is a Thread? -->
|
||||
## 线程
|
||||
## 什么是线程?
|
||||
|
||||
并发将程序划分成分离的,独立运行的任务。每个任务都由一个 *执行线程* 来驱动,我们通常将其简称为 *线程* 。而一个 *线程* 就是操作系统进程中单一顺序的控制流。因此,单个进程可以有多个并发执行的任务,但是你的程序使得每个任务都好像有自己的处理器一样。这线程模型为编程带来了便利,它简化了在单一程序中处理变戏法般的多任务过程。操作系统则从处理器上分配时间到您程序的所有线程中。
|
||||
|
||||
Java 并发的核心机制是 **Thread** 类,在该语言最初版本中, **Thread (线程)** 是由程序员直接创建和管理的。随着语言的发展以及人们发现了更好的一些方法,中间层机制 - 特别是 **Executor** 框架 - 被添加进来,以消除自己管理线程时候的心理负担(及错误)。 最终,甚至发展出比 **Executor** 更好的机制,如 [并发编程](./24-Concurrent-Programming.md) 一章所示。
|
||||
|
||||
**Thread(线程)** 是将任务关联到处理器的软件概念。虽然创建和使用 **Thread** 类看起来与任何其他类都很相似,但实际上它们是非常不同的。当你创建一个 **Thread** 时,JVM 将分配一大块内存到专为线程保留的特殊区域上,用于提供运行任务时所需的一切,包括:
|
||||
|
||||
* 程序计数器,指明要执行的下一个 JVM 字节码指令。
|
||||
* 用于支持 Java 代码执行的栈,包含有关此线程已到达当时执行位置所调用方法的信息。它也包含每个正在执行的方法的所有局部变量(包括原语和堆对象的引用)。每个线程的栈通常在 64K 到 1M 之间 [^1] 。
|
||||
* 第二个则用于 native code(本机方法代码)执行的栈
|
||||
* *thread-local variables* (线程本地变量)的存储区域
|
||||
* 用于控制线程的状态管理变量
|
||||
|
||||
包括 **main()** 在内的所有代码都会在某个线程内运行。 每当调用一个方法时,当前程序计数器被推到该线程的栈上,然后栈指针向下移动以足够来创建一个栈帧,其栈帧里存储该方法的所有局部变量,参数和返回值。所有基本类型变量都直接在栈上,虽然方法中创建(或方法中使用)对象的任何引用都位于栈帧中,但对象本身存于堆中。这仅且只有一个堆,被程序中所有线程所共享。
|
||||
|
||||
除此以外,线程必须绑定到操作系统,这样它就可以在某个时候连接到处理器。这是作为线程构建过程的一部分为您管理的。Java 使用底层操作系统中的机制来管理线程的执行。
|
||||
|
||||
### 最佳线程数
|
||||
|
||||
如果你查看第 24 章 [并发编程](./24-Concurrent-Programming.md) 中使用 *CachedThreadPool* 的用例,你会发现 **ExecutorService** 为每个我们提交的任务分配一个线程。然而,并行流(**parallel Stream**)在 [**CountingStream.java** ](https://github.com/BruceEckel/OnJava8-Examples/blob/master/concurrent/CountingStream.java
|
||||
) 中只分配了 8 个线程(id 中 1-7 为工作线程,8 为 **main()** 方法的主线程,它巧妙地将其用作额外的并行流)。如果你尝试提高 **range()** 方法中的上限值,你会看到没有创建额外的线程。这是为什么?
|
||||
|
||||
我们可以查出当前机器上处理器的数量:
|
||||
|
||||
```Java
|
||||
// lowlevel/NumberOfProcessors.java
|
||||
|
||||
public class NumberOfProcessors {
|
||||
public static void main(String[] args) {
|
||||
System.out.println(
|
||||
Runtime.getRuntime().availableProcessors());
|
||||
}
|
||||
}
|
||||
/* Output:
|
||||
8
|
||||
*/
|
||||
```
|
||||
|
||||
在我的机器上(使用英特尔酷睿i7),我有四个内核,每个内核呈现两个*超线程*(指一种硬件技巧,能在单个处理器上产生非常快速的上下文切换,在某些情况下可以使内核看起来像运行两个硬件线程)。虽然这是 “最近” 计算机上的常见配置(在撰写本文时),但你可能会看到不同的结果,包括 **CountingStream.java ** 中同等数量的默认线程。
|
||||
|
||||
你的操作系统可能有办法来查出关于处理器的更多信息,例如,在Windows 10上,按下 “开始” 键,输入 “任务管理器” 和 Enter 键。点击 “详细信息” 。选择 “性能” 标签,您将会看到各种各样的关于您的硬件信息,包括“内核” 和 “逻辑处理器” 。
|
||||
|
||||
事实证明,“通用”线程的最佳数量就算是可用处理器的数量(对于特定的问题可能不是这样)。这原因来自在Java线程之间切换上下文的代价:存储被挂起线程的当前状态,并检索另一个线程的当前状态,以便从它进入挂起的位置继续执行。对于 8 个处理器和 8 个(计算密集型)Java线程,JVM 在运行这8个任务时从不需要切换上下文。对于比处理器数量少的任务,分配更多线程没有帮助。
|
||||
|
||||
定义了 “逻辑处理器” 数量的 Intel 超线程,但并没有增加计算能力 - 该特性在硬件级别维护额外的线程上下文,从而加快了上下文切换,这有助于提高用户界面的响应能力。对于计算密集型任务,请考虑将线程数量与物理内核(而不是超线程)的数量匹配。尽管Java认为每个超线程都是一个处理器,但这似乎是由于 Intel 对超线程的过度营销造成的错误。尽管如此,为了简化编程,我只允许 JVM 决定默认的线程数。 你将需要试验你的产品应用。 这并不意味着将线程数与处理器数相匹配就适用于所有问题; 相反,它主要用于计算密集型解决方案。
|
||||
|
||||
### 我可以创建多少个线程?
|
||||
|
||||
Thread(线程)对象的最大部分是用于执行方法的 Java 堆栈。查看 Thread (线程)对象的大小因操作系统而异。该程序通过创建 Thread 对象来测试它,直到 JVM 内存不足为止:
|
||||
|
||||
```java
|
||||
// lowlevel/ThreadSize.java
|
||||
// {ExcludeFromGradle} Takes a long time or hangs
|
||||
import java.util.concurrent.*;
|
||||
import onjava.Nap;
|
||||
|
||||
public class ThreadSize {
|
||||
static class Dummy extends Thread {
|
||||
@Override
|
||||
public void run() { new Nap(1); }
|
||||
}
|
||||
public static void main(String[] args) {
|
||||
ExecutorService exec =
|
||||
Executors.newCachedThreadPool();
|
||||
int count = 0;
|
||||
try {
|
||||
while(true) {
|
||||
exec.execute(new Dummy());
|
||||
count++;
|
||||
}
|
||||
} catch(Error e) {
|
||||
System.out.println(
|
||||
e.getClass().getSimpleName() + ": " + count);
|
||||
System.exit(0);
|
||||
} finally {
|
||||
exec.shutdown();
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
只要你不断递交任务,**CachedThreadPool** 就会继续创建线程。将 **Dummy** 对象递交到 **execute()** 方法以开始任务,如果线程池无可用线程,则分配一个新线程。执行的暂停方法 **pause()** 运行时间必须足够长,使任务不会开始即完成(从而为新任务释放现有线程)。只要任务不断进入而没有完成,**CachedThreadPool** 最终就会耗尽内存。
|
||||
|
||||
我并不总是能够在我尝试的每台机器上造成内存不足的错误。在一台机器上,我看到这样的结果:
|
||||
|
||||
```shell
|
||||
> java ThreadSize
|
||||
OutOfMemoryError: 2816
|
||||
```
|
||||
|
||||
我们可以使用 **-Xss** 标记减少每个线程栈分配的内存大小。允许的最小线程栈大小是 64k:
|
||||
|
||||
```shell
|
||||
>java -Xss64K ThreadSize
|
||||
OutOfMemoryError: 4952
|
||||
```
|
||||
|
||||
如果我们将线程栈大小增加到 2M ,我们就可以分配更少的线程。
|
||||
|
||||
```shell
|
||||
>java -Xss2M ThreadSize
|
||||
OutOfMemoryError: 722
|
||||
```
|
||||
|
||||
Windows 操作系统默认栈大小是 320K,我们可以通过验证它给出的数字与我们完全不设置栈大小时的数字是大致相同:
|
||||
|
||||
```shell
|
||||
>java -Xss320K ThreadSize
|
||||
OutOfMemoryError: 2816
|
||||
```
|
||||
|
||||
你还可以使用 **-Xmx** 标志增加 JVM 的最大内存分配:
|
||||
|
||||
```shell
|
||||
>java -Xss64K -Xmx5M ThreadSize
|
||||
OutOfMemoryError: 5703
|
||||
```
|
||||
|
||||
请注意的是操作系统还可能对允许的线程数施加限制。
|
||||
|
||||
因此,“我可以拥有多少线程”这一问题的答案是“几千个”。但是,如果你发现自己分配了数千个线程,那么您可能需要重新考虑您的做法; 恰当的问题是“我需要多少线程?”
|
||||
|
||||
### The WorkStealingPool
|
||||
|
||||
这是一个 **ExecutorService** ,它使用所有可用的(由JVM报告) 处理器自动创建线程池。
|
||||
|
||||
```java
|
||||
// lowlevel/WorkStealingPool.java
|
||||
import java.util.stream.*;
|
||||
import java.util.concurrent.*;
|
||||
|
||||
class ShowThread implements Runnable {
|
||||
@Override
|
||||
public void run() {
|
||||
System.out.println(
|
||||
Thread.currentThread().getName());
|
||||
}
|
||||
}
|
||||
|
||||
public class WorkStealingPool {
|
||||
public static void main(String[] args)
|
||||
throws InterruptedException {
|
||||
System.out.println(
|
||||
Runtime.getRuntime().availableProcessors());
|
||||
ExecutorService exec =
|
||||
Executors.newWorkStealingPool();
|
||||
IntStream.range(0, 10)
|
||||
.mapToObj(n -> new ShowThread())
|
||||
.forEach(exec::execute);
|
||||
exec.awaitTermination(1, TimeUnit.SECONDS);
|
||||
}
|
||||
}
|
||||
/* Output:
|
||||
8
|
||||
ForkJoinPool-1-worker-2
|
||||
ForkJoinPool-1-worker-1
|
||||
ForkJoinPool-1-worker-2
|
||||
ForkJoinPool-1-worker-3
|
||||
ForkJoinPool-1-worker-2
|
||||
ForkJoinPool-1-worker-1
|
||||
ForkJoinPool-1-worker-3
|
||||
ForkJoinPool-1-worker-1
|
||||
ForkJoinPool-1-worker-4
|
||||
ForkJoinPool-1-worker-2
|
||||
*/
|
||||
```
|
||||
|
||||
工作窃取算法允许已经耗尽输入队列中的工作项的线程从其他队列“窃取”工作项。目标是在处理器之间分配工作项,从而最大限度地利用所有可用的处理器来完成计算密集型任务。这项算法也用于 Java 的fork/join 框架。
|
||||
|
||||
<!-- Catching Exceptions -->
|
||||
## 异常捕获
|
||||
|
||||
这可能会让你感到惊讶:
|
||||
|
||||
```java
|
||||
// lowlevel/SwallowedException.java
|
||||
import java.util.concurrent.*;
|
||||
|
||||
public class SwallowedException {
|
||||
public static void main(String[] args)
|
||||
throws InterruptedException {
|
||||
ExecutorService exec =
|
||||
Executors.newSingleThreadExecutor();
|
||||
exec.submit(() -> {
|
||||
throw new RuntimeException();
|
||||
});
|
||||
exec.shutdown();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
这个程序什么也不输出(然而,如果你用 **execute** 方法替换 **submit()** 方法,你就将会看到异常抛出。这说明在线程中抛出异常是很棘手的,需要特别注意的事情。
|
||||
|
||||
你无法捕获到从线程逃逸的异常。一旦异常越过了任务的 **run()** 方法,它就会传递至控制台,除非您采取特殊步骤来捕获此类错误异常。
|
||||
|
||||
下面是一个抛出异常的代码,该异常会传递到它的 **run()** 方法之外,而 **main()** 方法会显示运行它时会发生什么:
|
||||
|
||||
```java
|
||||
// lowlevel/ExceptionThread.java
|
||||
// {ThrowsException}
|
||||
import java.util.concurrent.*;
|
||||
|
||||
public class ExceptionThread implements Runnable {
|
||||
@Override
|
||||
public void run() {
|
||||
throw new RuntimeException();
|
||||
}
|
||||
public static void main(String[] args) {
|
||||
ExecutorService es =
|
||||
Executors.newCachedThreadPool();
|
||||
es.execute(new ExceptionThread());
|
||||
es.shutdown();
|
||||
}
|
||||
}
|
||||
/* Output:
|
||||
___[ Error Output ]___
|
||||
Exception in thread "pool-1-thread-1"
|
||||
java.lang.RuntimeException
|
||||
at ExceptionThread.run(ExceptionThread.java:8)
|
||||
at java.util.concurrent.ThreadPoolExecutor.runW
|
||||
orker(ThreadPoolExecutor.java:1142)
|
||||
at java.util.concurrent.ThreadPoolExecutor$Work
|
||||
er.run(ThreadPoolExecutor.java:617)
|
||||
at java.lang.Thread.run(Thread.java:745)
|
||||
*/
|
||||
```
|
||||
|
||||
输出是(经过调整一些限定符以适应阅读):
|
||||
|
||||
```
|
||||
Exception in thread "pool-1-thread-1" RuntimeException
|
||||
at ExceptionThread.run(ExceptionThread.java:9)
|
||||
at ThreadPoolExecutor.runWorker(...)
|
||||
at ThreadPoolExecutor$Worker.run(...)
|
||||
at java.lang.Thread.run(Thread.java:745)
|
||||
```
|
||||
|
||||
即使在 **main()** 方法体内包裹 **try-catch** 代码块来捕获异常也不成功:
|
||||
|
||||
```java
|
||||
// lowlevel/NaiveExceptionHandling.java
|
||||
// {ThrowsException}
|
||||
import java.util.concurrent.*;
|
||||
|
||||
public class NaiveExceptionHandling {
|
||||
public static void main(String[] args) {
|
||||
ExecutorService es =
|
||||
Executors.newCachedThreadPool();
|
||||
try {
|
||||
es.execute(new ExceptionThread());
|
||||
} catch(RuntimeException ue) {
|
||||
// This statement will NOT execute!
|
||||
System.out.println("Exception was handled!");
|
||||
} finally {
|
||||
es.shutdown();
|
||||
}
|
||||
}
|
||||
}
|
||||
/* Output:
|
||||
___[ Error Output ]___
|
||||
Exception in thread "pool-1-thread-1"
|
||||
java.lang.RuntimeException
|
||||
at ExceptionThread.run(ExceptionThread.java:8)
|
||||
at java.util.concurrent.ThreadPoolExecutor.runW
|
||||
orker(ThreadPoolExecutor.java:1142)
|
||||
at java.util.concurrent.ThreadPoolExecutor$Work
|
||||
er.run(ThreadPoolExecutor.java:617)
|
||||
at java.lang.Thread.run(Thread.java:745)
|
||||
*/
|
||||
```
|
||||
|
||||
这会产生与前一个示例相同的结果:未捕获异常。
|
||||
|
||||
为解决这个问题,需要改变 **Executor** (执行器)生成线程的方式。 **Thread.UncaughtExceptionHandler** 是一个添加给每个 **Thread** 对象,用于进行异常处理的接口。
|
||||
|
||||
当该线程即将死于未捕获的异常时,将自动调用 **Thread.UncaughtExceptionHandler.uncaughtException()**
|
||||
方法。为了调用该方法,我们创建一个新的 `ThreadFactory` 类型来让 **Thread.UncaughtExceptionHandler** 对象附加到每个它所新创建的 **Thread**(线程)对象上。我们赋值该工厂对象给 **Executors** 对象的 方法,让它的方法来生成新的 **ExecutorService** 对象:
|
||||
|
||||
```java
|
||||
// lowlevel/CaptureUncaughtException.java
|
||||
import java.util.concurrent.*;
|
||||
|
||||
class ExceptionThread2 implements Runnable {
|
||||
@Override
|
||||
public void run() {
|
||||
Thread t = Thread.currentThread();
|
||||
System.out.println("run() by " + t.getName());
|
||||
System.out.println(
|
||||
"eh = " + t.getUncaughtExceptionHandler());
|
||||
throw new RuntimeException();
|
||||
}
|
||||
}
|
||||
|
||||
class MyUncaughtExceptionHandler implements
|
||||
Thread.UncaughtExceptionHandler {
|
||||
@Override
|
||||
public void uncaughtException(Thread t, Throwable e) {
|
||||
System.out.println("caught " + e);
|
||||
}
|
||||
}
|
||||
|
||||
class HandlerThreadFactory implements ThreadFactory {
|
||||
@Override
|
||||
public Thread newThread(Runnable r) {
|
||||
System.out.println(this + " creating new Thread");
|
||||
Thread t = new Thread(r);
|
||||
System.out.println("created " + t);
|
||||
t.setUncaughtExceptionHandler(
|
||||
new MyUncaughtExceptionHandler());
|
||||
System.out.println(
|
||||
"eh = " + t.getUncaughtExceptionHandler());
|
||||
return t;
|
||||
}
|
||||
}
|
||||
|
||||
public class CaptureUncaughtException {
|
||||
public static void main(String[] args) {
|
||||
ExecutorService exec =
|
||||
Executors.newCachedThreadPool(
|
||||
new HandlerThreadFactory());
|
||||
exec.execute(new ExceptionThread2());
|
||||
exec.shutdown();
|
||||
}
|
||||
}
|
||||
/* Output:
|
||||
HandlerThreadFactory@4e25154f creating new Thread
|
||||
created Thread[Thread-0,5,main]
|
||||
eh = MyUncaughtExceptionHandler@70dea4e
|
||||
run() by Thread-0
|
||||
eh = MyUncaughtExceptionHandler@70dea4e
|
||||
caught java.lang.RuntimeException
|
||||
*/
|
||||
```
|
||||
|
||||
额外会跟踪验证工厂对象创建的线程是否获得新 **UncaughtExceptionHandler** 。现在未捕获的异常由 **uncaughtException** 方法捕获。
|
||||
|
||||
上面的示例根据具体情况来设置处理对象。如果你明白你想要在任何地方使用相同的异常处理对象,一个更简单的方法是设置默认的未捕获异常处理对象,它定义在 **Thread** 类中作为一个 **static**(静态) 字段:
|
||||
|
||||
```java
|
||||
// lowlevel/SettingDefaultHandler.java
|
||||
import java.util.concurrent.*;
|
||||
|
||||
public class SettingDefaultHandler {
|
||||
public static void main(String[] args) {
|
||||
Thread.setDefaultUncaughtExceptionHandler(
|
||||
new MyUncaughtExceptionHandler());
|
||||
ExecutorService es =
|
||||
Executors.newCachedThreadPool();
|
||||
es.execute(new ExceptionThread());
|
||||
es.shutdown();
|
||||
}
|
||||
}
|
||||
/* Output:
|
||||
caught java.lang.RuntimeException
|
||||
*/
|
||||
```
|
||||
|
||||
只有在每个线程没有设置异常处理对象时候,默认处理对象才会被调用。系统会检查每个线程的版本,如果没有找到,则检查是否线程组中有专门的 **uncaughtException()** 方法;如果都没有,就会调用 **defaultUncaughtExceptionHandler** 方法。
|
||||
|
||||
可以将此方法与 **CompletableFuture**s 的改进方法进行比较。
|
||||
|
||||
<!-- Sharing Resources -->
|
||||
## 资源共享
|
||||
|
||||
你可以将单线程程序看作一个孤独的实体,在你的问题空间中移动并一次只做一件事。因为只有一个实体,你永远不会想到两个实体试图同时使用相同资源的问题:问题犹如两个人试图同时停放在同一个空间,同时走过一扇门,甚至同时说话。
|
||||
|
||||
通过并发,事情不再孤单,但现在两个或更多任务可能会相互干扰。如果您不阻止这种冲突,您将有两个任务同时尝试访问同一个银行帐户,打印到同一个打印机,调整相同的阀门,等等。
|
||||
|
||||
### 资源竞争
|
||||
|
||||
### 解决资源竞争
|
||||
|
||||
### 同步多个生产者
|
||||
|
||||
<!-- The volatile Keyword -->
|
||||
## volatile关键字
|
||||
@@ -35,8 +406,28 @@
|
||||
<!-- Summary -->
|
||||
## 本章小结
|
||||
|
||||
本附录主要是为了让您在遇到底层并发代码时能对此有一定的了解,尽管本文还远没对这个主题进行全面的讨论。为此,你需要先从阅读由 Brian Goetz, Tim Peierls, Joshua Bloch, Joseph Bowbeer, David Holmes, and Doug Lea (Addison-Wesley 出版社, 2006)所著作的 *Java Concurrency in Practice* (国内译名:Java并发编程实战)开始了解。理想情况下,这本书会完全吓跑你在 Java 中尝试去编写底层并发代码。如果没有,那么你几乎肯定患上了达克效应(DunningKruger Effect),这是一种认知偏差,“你知道的越少,对自己的能力就越有信心”。请记住,当前的语言设计人员仍然在清理早期语言设计人员过于自信造成的混乱(例如,查看 Thread 类中有多少方法被弃用,而 volatile 直到 Java 5 才正确工作)。
|
||||
|
||||
以下是并发编程的步骤:
|
||||
|
||||
1. 不要使用它。想一些其他方法来使你写的程序变的更快。
|
||||
2. 如果你必须使用它,请使用在 [并发编程](./24-Concurrent-Programming.md) - parallel Streams and CompletableFutures 中展示的现代高级工具。
|
||||
3. 不要在任务间共享变量,必须在任务之间传递的任何信息都应该使用 Java.util.concurrent 库中的并发数据结构。
|
||||
4. 如果必须在任务之间共享变量,请使用 java.util.concurrent.atomic 里面其中一种类型,或在任何直接或间接访问这些变量的方法上应用 synchronized。 当你不这样做时,很容易被愚弄,以为你已经把所有东西都包括在内。 说真的,尝试使用步骤 3。
|
||||
5. 如果步骤 4 产生的结果太慢,你可以尝试使用volatile 或其他技术来调整代码,但是如果你正在阅读本书并认为你已经准备好尝试这些方法,那么你就超出了你的深度。 返回步骤#1。
|
||||
|
||||
通常可以只使用 java.util.concurrent 库组件来编写并发程序,完全避免来自应用 volatile 和 synchronized 的挑战。注意,我可以通过 [并发编程](./24-Concurrent-Programming.md) 中的示例来做到这一点。
|
||||
|
||||
[^1]: 在某些平台上,特别是 Windows,默认值可能非常难以查明。您可以使用 -Xss 标志调整堆栈大小。
|
||||
|
||||
[^2]: 出自 Brian Goetz, Java Concurrency in Practice 一书的作者 , 该书由 Brian Goetz, Tim Peierls, Joshua Bloch, Joseph Bowbeer, David Holmes, and Doug Lea 联合著作 (Addison-Wesley 出版社, 2006)。↩
|
||||
|
||||
[^3]: 请注意,在64位处理器上可能不会发生这种情况,从而消除了这个问题。
|
||||
|
||||
[^4]: 这个测试的一个推论是,“如果有人暗示线程是直接的,请确保这个人没有对您的项目做出重要的决策。如果那个人已经做出,那么你就有麻烦了。”
|
||||
|
||||
[^5]: 这版本是我参与的;这可能在以后的标准中得到了修正
|
||||
|
||||
|
||||
<!-- 分页 -->
|
||||
<div style="page-break-after: always;"></div>
|
||||
|
||||
|
||||
@@ -3,18 +3,24 @@
|
||||
<!-- Appendix: Supplements -->
|
||||
# 附录:补充
|
||||
|
||||
> 本书有许多补充内容,包括MindView网站提供的项目和服务。
|
||||
|
||||
本附录介绍了这些补充内容,你可以自行决定它们是否对你有所帮助。
|
||||
|
||||
<!-- Downloadable Supplements -->
|
||||
## 可下载的补充
|
||||
|
||||
可以从 [https://github.com/BruceEckel/OnJava8-examples](https://github.com/BruceEckel/OnJava8-examples) 免费下载本书的代码。这里包括Gradle构建文件和其它一些必要的支持文件,以便成功构建和执行本书中所有的示例代码。
|
||||
|
||||
<!-- Thinking in C: Foundations for Java -->
|
||||
## 通过Thinking-in-C来巩固Java基础
|
||||
|
||||
在 [www.OnJava8.com](www.OnJava8.com) 上,可以免费下载*Thinking in C*的演示文稿。 此演示文稿由Chuck Allison创建,由MindView有限责任公司开发。这是一个电子演示文稿,介绍了Java语法所基于的C语法,运算符和函数。
|
||||
|
||||
<!-- Hands-On Java eSeminar -->
|
||||
## 动手实践
|
||||
## Hand-On Java 电子演示文稿
|
||||
|
||||
*Hand-On Java 电子演示文稿*(Hands-On Java eSeminar)是基于*Thinking in Java*第2版。对应于该书中的每一章,它附带有一个音频讲解和相应的幻灯片。我创建了这个电子演示文稿,并讲述了这些材料。这个资料是HTML5格式的,所以它应该可以在大多数现代浏览器上运行。该演示文稿将在[www.OnJava8.com](www.OnJava8.com)上发售,你可以在该网站上找到该产品的试用版演示。
|
||||
|
||||
<!-- 分页 -->
|
||||
<div style="page-break-after: always;"></div>
|
||||
@@ -3,6 +3,29 @@
|
||||
<!-- Appendix: The Positive Legacy of C++ and Java -->
|
||||
# 附录:C++和Java的优良传统
|
||||
|
||||
> 在各种讨论声中,有一些人认为C++是一种设计糟糕的语言。 我认为理解C++和Java语言的选择有助于了解更大的视角。
|
||||
|
||||
也就是说,我几乎不再使用C++了。当我使用它的时候,要么是用来检查遗留代码,要么是编写性能关键(performance-critical)部分,程序通常尽可能小,以便用其他语言编写的其他程序来调用。
|
||||
|
||||
因为我在最初的8年里一直在C++标准委员会工作,所以我见证了那些被做出的决定。它们都经过了极其谨慎的考虑,远远超过了许多在Java中做出的决定。
|
||||
|
||||
然而,正如人们正确地指出的那样,由此产生的语言使用起来既复杂又痛苦,而且只要我一段时间不使用它,我就会忘记那些古怪的规则。在我写书的时候,我是从第一原理(first principles)处了解这些规则的,而不是记住了它们。
|
||||
|
||||
为了理解C++语言为何既令人不愉快且复杂,同时又是精心设计的,必须要牢记C++中所有内容的主要设计决策:与C. Bjarne Stroustrup(该语言的创造者,即“C++之父”)的兼容性决定。这样的设计似乎是为了可以让大量的C程序员透明地转移到对象(代指C++)上:允许他们在C++下编译他们的C代码。这是一个巨大的限制,一直是C++最大的优势......而且也是它的祸根。这就是使得C++成功的原因,也是使它复杂的原因。
|
||||
|
||||
它也欺骗了那些不太了解C++的Java设计师。例如,他们认为运算符重载对于程序员来说很难正确使用。这在C++中基本上是正确的,因为C++既有栈分配又有堆分配,你必须重载运算符来处理所有情况而且不要造成内存泄漏。这确实很困难。然而,Java有单一的内存分配机制和一个垃圾收集器,这使得运算符重载变得微不足道,正如C#中那样(但在早于Java的Python中已经可以看到)。但多年来,来自Java团队的一贯态度是“运算符重载太复杂了”。这里还有许多决策,所做的事明显不应该是他们做的。正是由于这些原因,让我有了蔑视Gosling(即“Java之父”)和Java团队决策的名声。(Java 7和8由于某种原因包含了更好的决策。但是向后兼容性这个约束总是会阻碍真正的改进。语言永远不会是它本来的样子。)
|
||||
|
||||
还有很多其他的例子。“为了提高效率,必须包含基本类型”;坚持“万物皆对象”是正确的;当对性能有要求的时候,提供一个陷阱门(trap door)来做低级别的活动(lower-level activities)(这里也可以使用hotspot技术透明地提高性能,正如他们最终做的那样);不能直接使用浮点处理器去计算超越函数,它用软件来完成。我已经尽可能多地提出了这样的问题,但我得到的却一直是类似“这是Java方式”这样的回复。
|
||||
|
||||
当我提出关于泛型的设计有多糟糕的时候,我得到了相同的回复,以及“我们必须向后兼容那样以前用Java做出的决策”(即使它们是糟糕的决策)。最近越来越多的人已经获得了足够的泛型经验,可以发现泛型真的很难用。事实上,C++模板更强大、更一致(现在更容易使用,因为编译器的错误消息是可以容忍的)。人们一直在认真对待物化(reification),这可能是有用的东西,但是在那种被严格约束所削弱的设计中并没有多大影响。
|
||||
|
||||
这样的例子还有很多很多。这是否意味着Java失败了?绝对不。Java将程序员的主流带入了垃圾收集、虚拟机和一致的错误处理模型的世界。由于它的所有缺陷,它将我们提升到了一个水平,现在我们已经准备好使用更高级别的语言了。
|
||||
|
||||
有一点,C++是领先的语言,人们认为它总是如此。许多人对Java有同样的看法,但由于JVM,Java使得取代自己变得更加容易。现在有可能会有人创建一种新语言,并使其在短时间内像Java一样高效运行。以前,为新语言开发一个正确有效的编译器需要花费大部分开发时间。
|
||||
|
||||
这种情况已经发生了,包括像Scala这样的高级静态语言,以及动态语言,新的且可移植的,如Groovy,Clojure,JRuby和Jython。这是未来,并且过渡很顺畅,因为可以很轻易地将这些新语言与现有Java代码结合使用,并且必要时可以重写那些在Java中的瓶颈。
|
||||
|
||||
在撰写本文时,Java是世界上的头号编程语言。然而,Java最终将会减弱,就像C++一样,沦只在特殊情况下使用(或者只是用来支持传统的代码,因为它不能像C++那样和硬件连接)。但是无意中的好处,也是Java真正意外的光彩之处在于它为自己的替代品创造了一条非常畅通的道路,即使Java本身已经达到了无法再发展的程度。未来所有的语言都应该从中学习:要么创建一个可以重构的文化(像Python和Ruby做的那样),要么就让竞争者茁壮成长。
|
||||
|
||||
<!-- 分页 -->
|
||||
<div style="page-break-after: always;"></div>
|
||||
|
||||
|
||||
@@ -1,11 +1,395 @@
|
||||
[TOC]
|
||||
|
||||
|
||||
|
||||
<!-- Appendix: Understanding equals() and hashCode() -->
|
||||
# 附录:理解equals和hashCode方法
|
||||
假设有一个容器使用hash函数,当你创建一个放到这个容器时,你必须定义 **hashCode()** 函数和 **equals()** 函数。这两个函数一起被用于hash容器中的查询操作。
|
||||
|
||||
|
||||
<!-- A Canonical equals() -->
|
||||
## equals典范
|
||||
## equals规范
|
||||
当你创建一个类的时候,它自动继承自 **Objcet** 类。如果你不覆写 **equals()** ,你将会获得 **Objcet** 对象的 **equals()** 函数。默认情况下,这个函数会比较对象的地址。所以只有你在比较同一个对象的时候,你才会获得**true**。默认的情况是"区分度最高的"。
|
||||
|
||||
```java
|
||||
// equalshashcode/DefaultComparison.java
|
||||
class DefaultComparison {
|
||||
private int i, j, k;
|
||||
DefaultComparison(int i, int j, int k) {
|
||||
this.i = i;
|
||||
this.j = j;
|
||||
this.k = k;
|
||||
}
|
||||
|
||||
public static void main(String[] args) {
|
||||
DefaultComparison
|
||||
a = new DefaultComparison(1, 2, 3),
|
||||
b = new DefaultComparison(1, 2, 3);
|
||||
System.out.println(a == a);
|
||||
System.out.println(a == b);
|
||||
}
|
||||
}
|
||||
/*
|
||||
Output:
|
||||
true
|
||||
false
|
||||
*/
|
||||
|
||||
```
|
||||
通常你会希望放宽这个限制。一般来说如果两个对象有相同的类型和相同的字段,你会认为这两个对象相等,但也会有一些你不想加入 **equals()** 函数中来比较的字段。这是类型设计的一部分。
|
||||
|
||||
一个合适的 **equals()**函数必须满足以下五点条件:
|
||||
1. 反身性:对于任何 **x**, **x.equals(x)** 应该返回 **true**。
|
||||
2. 对称性:对于任何 **x** 和 **y**, **x.equals(y)** 应该返回 **true**当且仅当 **y.equals(x)** 返回 **true** 。
|
||||
3. 传递性:对于任何**x**,**y**,还有**z**,如果 **x.equals(y)** 返回 **true** 并且 **y.equals(z)** 返回 **true**,那么 **x.equals(z)** 应该返回 **true**。
|
||||
4. 一致性:对于任何 **x**和**y**,在对象没有被改变的情况下,多次调用 **x.equals(y)** 应该总是返回 **true** 或者**false**。
|
||||
5. 对于任何非**null**的**x**,**x.equals(null)**应该返回**false**。
|
||||
|
||||
下面是满足这些条件的测试,并且判断对象是否和自己相等(我们这里称呼其为**右值**):
|
||||
1. 如果**右值**是**null**,那么不相等。
|
||||
2. 如果**右值**是**this**,那么两个对象相等。
|
||||
3. 如果**右值**不是同一个类型或者子类,那么两个对象不相等。
|
||||
4. 如果所有上面的检查通过了,那么你必须决定 **右值** 中的哪些字段是重要的,然后比较这些字段。
|
||||
Java 7 引入了 **Objects** 类型来帮助这个流程,这样我们能够写出更好的 **equals()** 函数。
|
||||
|
||||
下面的例子比较了不同类型的 **Equality**类。为了避免重复的代码,我们使用*工厂函数设计模*式来实现样例。 **EqualityFactory**接口提供**make()**函数来生成一个**Equaity**对象,这样不同的**EqualityFactory**能够生成**Equality**不同的子类。
|
||||
|
||||
```java
|
||||
// equalshashcode/EqualityFactory.java
|
||||
import java.util.*;
|
||||
interface EqualityFactory {
|
||||
Equality make(int i, String s, double d);
|
||||
}
|
||||
```
|
||||
现在我们来定义 **Equality**,它包含三个字段(所有的字段我们认为在比较中都很重要)和一个 **equals()** 函数用来满足上述的四种检查。构造函数展示了它的类名来保证我们在执行我们想要的测试:
|
||||
|
||||
```java
|
||||
// equalshashcode/Equality.java
|
||||
import java.util.*;
|
||||
public class Equality {
|
||||
protected int i;
|
||||
protected String s;
|
||||
protected double d;public Equality(int i, String s, double d) {
|
||||
this.i = i;
|
||||
this.s = s;
|
||||
this.d = d;
|
||||
System.out.println("made 'Equality'");
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object rval) {
|
||||
if(rval == null)
|
||||
return false;
|
||||
if(rval == this)
|
||||
return true;
|
||||
if(!(rval instanceof Equality))
|
||||
return false;
|
||||
Equality other = (Equality)rval;
|
||||
if(!Objects.equals(i, other.i))
|
||||
return false;
|
||||
if(!Objects.equals(s, other.s))
|
||||
return false;
|
||||
if(!Objects.equals(d, other.d))return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
public void test(String descr, String expected, Object rval) {
|
||||
System.out.format("-- Testing %s --%n" + "%s instanceof Equality: %s%n" +
|
||||
"Expected %s, got %s%n",
|
||||
descr, descr, rval instanceof Equality,
|
||||
expected, equals(rval));
|
||||
}
|
||||
|
||||
public static void testAll(EqualityFactory eqf) {
|
||||
Equality
|
||||
e = eqf.make(1, "Monty", 3.14),
|
||||
eq = eqf.make(1, "Monty", 3.14),
|
||||
neq = eqf.make(99, "Bob", 1.618);
|
||||
e.test("null", "false", null);
|
||||
e.test("same object", "true", e);
|
||||
e.test("different type",
|
||||
"false", Integer.valueOf(99));e.test("same values", "true", eq);
|
||||
e.test("different values", "false", neq);
|
||||
}
|
||||
|
||||
public static void main(String[] args) {
|
||||
testAll( (i, s, d) -> new Equality(i, s, d));
|
||||
}
|
||||
|
||||
}
|
||||
/*
|
||||
Output:
|
||||
made 'Equality'
|
||||
made 'Equality'
|
||||
made 'Equality'
|
||||
-- Testing null --
|
||||
null instanceof Equality: false
|
||||
Expected false, got false
|
||||
-- Testing same object --
|
||||
same object instanceof Equality: true
|
||||
Expected true, got true
|
||||
-- Testing different type --
|
||||
different type instanceof Equality: false
|
||||
Expected false, got false-- Testing same values --
|
||||
same values instanceof Equality: true
|
||||
Expected true, got true
|
||||
-- Testing different values --
|
||||
different values instanceof Equality: true
|
||||
Expected false, got false
|
||||
*/
|
||||
```
|
||||
|
||||
**testAll()** 执行了我们期望的所有不同类型对象的比较。它使用工厂创建了**Equality**对象。
|
||||
|
||||
在 **main()** 里,请注意对 **testAll()** 的调用很简单。因为**EqualityFactory**有着单一的函数,它能够和lambda表达式一起使用来表示**make()**函数。
|
||||
|
||||
上述的 **equals()** 函数非常繁琐,并且我们能够将其简化成规范的形式,请注意:
|
||||
1. **instanceof**检查减少了**null**检查的需要。
|
||||
2. 和**this**的比较是多余的。一个正确书写的 **equals()** 函数能正确地和自己比较。
|
||||
|
||||
|
||||
因为 **&&** 是一个短路比较,它会在第一次遇到失败的时候退出并返回**false**。所以,通过使用 **&&** 将检查链接起来,我们可以写出更精简的 **equals()** 函数:
|
||||
|
||||
```java
|
||||
// equalshashcode/SuccinctEquality.java
|
||||
import java.util.*;
|
||||
public class SuccinctEquality extends Equality {
|
||||
public SuccinctEquality(int i, String s, double d) {
|
||||
super(i, s, d);
|
||||
System.out.println("made 'SuccinctEquality'");
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object rval) {
|
||||
return rval instanceof SuccinctEquality &&
|
||||
Objects.equals(i, ((SuccinctEquality)rval).i) &&
|
||||
Objects.equals(s, ((SuccinctEquality)rval).s) &&
|
||||
Objects.equals(d, ((SuccinctEquality)rval).d);
|
||||
}
|
||||
public static void main(String[] args) {
|
||||
Equality.testAll( (i, s, d) ->
|
||||
new SuccinctEquality(i, s, d));
|
||||
}
|
||||
|
||||
}
|
||||
/* Output:
|
||||
made 'Equality'
|
||||
made 'SuccinctEquality'
|
||||
made 'Equality'
|
||||
made 'SuccinctEquality'
|
||||
made 'Equality'
|
||||
made 'SuccinctEquality'
|
||||
-- Testing null --
|
||||
null instanceof Equality: false
|
||||
Expected false, got false
|
||||
-- Testing same object --
|
||||
same object instanceof Equality: true
|
||||
Expected true, got true
|
||||
-- Testing different type --
|
||||
different type instanceof Equality: false
|
||||
Expected false, got false
|
||||
-- Testing same values --
|
||||
same values instanceof Equality: true
|
||||
Expected true, got true
|
||||
-- Testing different values --different values instanceof Equality: true
|
||||
Expected false, got false
|
||||
*/
|
||||
```
|
||||
对于每个 **SuccinctEquality**,基类构造函数在派生类构造函数前被调用,输出显示我们依然获得了正确的结果,你可以发现短路返回已经发生了,不然的话,**null**测试和“不同类型”的测试会在 **equals()** 函数下面的比较中强制转化的时候抛出异常。
|
||||
**Objects.equals()** 会在你组合其他类型的时候发挥很大的作用。
|
||||
|
||||
```java
|
||||
// equalshashcode/ComposedEquality.java
|
||||
import java.util.*;
|
||||
class Part {
|
||||
String ss;
|
||||
double dd;
|
||||
|
||||
Part(String ss, double dd) {
|
||||
this.ss = ss;
|
||||
this.dd = dd;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object rval) {
|
||||
return rval instanceof Part &&
|
||||
Objects.equals(ss, ((Part)rval).ss) &&
|
||||
Objects.equals(dd, ((Part)rval).dd);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public class ComposedEquality extends SuccinctEquality {
|
||||
Part part;
|
||||
public ComposedEquality(int i, String s, double d) {
|
||||
super(i, s, d);
|
||||
part = new Part(s, d);
|
||||
System.out.println("made 'ComposedEquality'");
|
||||
}
|
||||
@Override
|
||||
public boolean equals(Object rval) {
|
||||
return rval instanceof ComposedEquality &&
|
||||
super.equals(rval) &&
|
||||
Objects.equals(part,
|
||||
((ComposedEquality)rval).part);
|
||||
|
||||
}
|
||||
|
||||
public static void main(String[] args) {
|
||||
Equality.testAll( (i, s, d) ->
|
||||
new ComposedEquality(i, s, d));
|
||||
}
|
||||
}
|
||||
/*
|
||||
Output:
|
||||
made 'Equality'
|
||||
made 'SuccinctEquality'
|
||||
made 'ComposedEquality'
|
||||
made 'Equality'
|
||||
made 'SuccinctEquality'
|
||||
made 'ComposedEquality'
|
||||
made 'Equality'
|
||||
made 'SuccinctEquality'
|
||||
made 'ComposedEquality'
|
||||
-- Testing null --null instanceof Equality: false
|
||||
Expected false, got false
|
||||
-- Testing same object --
|
||||
same object instanceof Equality: true
|
||||
Expected true, got true
|
||||
-- Testing different type --
|
||||
different type instanceof Equality: false
|
||||
Expected false, got false
|
||||
-- Testing same values --
|
||||
same values instanceof Equality: true
|
||||
Expected true, got true
|
||||
-- Testing different values --
|
||||
different values instanceof Equality: true
|
||||
Expected false, got false
|
||||
*/
|
||||
```
|
||||
注意super.equals()这个调用,没有必要重新发明它(因为你不总是有权限访问基类所有的必要字段)
|
||||
|
||||
<!--Equality Across Subtypes -->
|
||||
### 不同子类的相等性
|
||||
继承意味着两个不同子类的对象当其向上转型的时候可以是相等的。假设你有一个Animal对象的集合。这个集合天然接受**Animal**的子类。在这个例子中是**Dog**和**Pig**。每个**Animal**有一个**name**和**size**,还有唯一的内部**id**数字。
|
||||
|
||||
我们通过**Objects**类,以规范的形式定义 **equals()**函数和**hashCode()**。但是我们只能在基类**Animal**中定义他们。并且我们在这两个函数中没有包含**id**字段。从**equals()**函数的角度看待,这意味着我们只关心它是否是**Animal**,而不关心是否是**Animal**的某个子类。
|
||||
|
||||
```java
|
||||
// equalshashcode/SubtypeEquality.java
|
||||
import java.util.*;
|
||||
enum Size { SMALL, MEDIUM, LARGE }
|
||||
class Animal {
|
||||
private static int counter = 0;
|
||||
private final int id = counter++;
|
||||
private final String name;
|
||||
private final Size size;
|
||||
Animal(String name, Size size) {
|
||||
this.name = name;
|
||||
this.size = size;
|
||||
}
|
||||
@Override
|
||||
public boolean equals(Object rval) {
|
||||
return rval instanceof Animal &&
|
||||
// Objects.equals(id, ((Animal)rval).id) && // [1]
|
||||
Objects.equals(name, ((Animal)rval).name) &&
|
||||
Objects.equals(size, ((Animal)rval).size);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(name, size);
|
||||
// return Objects.hash(name, size, id); // [2]
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return String.format("%s[%d]: %s %s %x",
|
||||
getClass().getSimpleName(), id,
|
||||
name, size, hashCode());
|
||||
}
|
||||
}
|
||||
|
||||
class Dog extends Animal {
|
||||
Dog(String name, Size size) {
|
||||
super(name, size);
|
||||
}
|
||||
}
|
||||
|
||||
class Pig extends Animal {
|
||||
Pig(String name, Size size) {
|
||||
super(name, size);
|
||||
}
|
||||
}
|
||||
|
||||
public class SubtypeEquality {
|
||||
public static void main(String[] args) {
|
||||
Set<Animal> pets = new HashSet<>();
|
||||
pets.add(new Dog("Ralph", Size.MEDIUM));
|
||||
pets.add(new Pig("Ralph", Size.MEDIUM));
|
||||
pets.forEach(System.out::println);
|
||||
}
|
||||
}
|
||||
/*
|
||||
Output:
|
||||
Dog[0]: Ralph MEDIUM a752aeee
|
||||
*/
|
||||
```
|
||||
如果我们只考虑类型的话,某些情况下它的确说得通——只从基类的角度看待问题,这是李氏替换原则的基石。这个代码完美符合替换理论因为派生类没有添加任何额外不再基类中的额外函数。派生类只是在表现上不同,而不是在接口上。(当然这不是常态)
|
||||
|
||||
但是当我们提供了两个有着相同数据的不同的对象类型,然后将他们放置在 **HashSet<Animal>** 中。只有他们中的一个能存活。这强调了 **equals()** 不是完美的数学理论,而只是机械般的理论。
|
||||
**hashCode()** 和 **equals()** 必须能够允许类型在hash数据结构中正常工作。例子中 **Dog** 和 **Pig** 会被映射到同 **HashSet** 的同一个桶中。这个时候,**HashSet** 回退到 **equals()** 来区分对象,但是 **equals()** 也认为两个对象是相同的。**HashSet**因为已经有一个相同的对象了,所以没有添加 **Pig**。
|
||||
我们依然能够通过使得其他字段对象不同来让例子能够正常工作。在这里每个 **Animal** 已经有了一个独一无二的 **id** ,所以你能够取消 **equals()** 函数中的 **[1]** 行注释,或者取消 **hashCode()** 函数中的 **[2]** 行注释。按照规范,你应该同时完成这两个操作,如此能够将所有“不变的”字段包含在两个操作中(“不变”所以 **equals()** 和 **hashCode()** 在哈希数据结构中的排序和取值时,不会生成不同的值。我将“不变的”放在引号中因为你必须计算出是否已经发生变化)。
|
||||
|
||||
> **旁注**: 在**hashCode()**中,如果你只能够使用一个字段,使用**Objcets.hashCode()**。如果你使用多个字段,那么使用 **Objects.hash()**。
|
||||
|
||||
我们也可以通过标准方式,将 **equals()** 定义在子类中(不包含 **id** )解决这个问题:
|
||||
|
||||
```java
|
||||
// equalshashcode/SubtypeEquality2.java
|
||||
import java.util.*;
|
||||
class Dog2 extends Animal {
|
||||
Dog2(String name, Size size) {
|
||||
super(name, size);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object rval) {
|
||||
return rval instanceof Dog2 &&super.equals(rval);
|
||||
}
|
||||
}
|
||||
|
||||
class Pig2 extends Animal {
|
||||
Pig2(String name, Size size) {
|
||||
super(name, size);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object rval) {
|
||||
return rval instanceof Pig2 &&
|
||||
super.equals(rval);
|
||||
}
|
||||
}
|
||||
|
||||
public class SubtypeEquality2 {
|
||||
public static void main(String[] args) {
|
||||
Set<Animal> pets = new HashSet<>();
|
||||
pets.add(new Dog2("Ralph", Size.MEDIUM));
|
||||
pets.add(new Pig2("Ralph", Size.MEDIUM));
|
||||
pets.forEach(System.out::println);
|
||||
}
|
||||
}
|
||||
/*
|
||||
Output:
|
||||
Dog2[0]: Ralph MEDIUM a752aeee
|
||||
Pig2[1]: Ralph MEDIUM a752aeee
|
||||
*/
|
||||
```
|
||||
注意 **hashCode()** 是独一无二的,但是因为对象不再 **equals()** ,所以两个函数都出现在**HashSet**中。另外,**super.equals()** 意味着我们不需要访问基类的**private**字段。
|
||||
|
||||
|
||||
一种说法是Java从**equals()** 和**hashCode()** 的定义中分离了可替代性。我们仍然能够将**Dog**和**Pig**放置在 **Set<Animal>** 中,无论 **equals()** 和 **hashCode()** 是如何定义的,但是对象不会在哈希数据结构中正常工作,除非这些函数能够被合理定义。不幸的是,**equals()** 不总是和 **hashCode()** 一起使用,这在你尝试为了某个特殊类型避免定义它的时候会让问题复杂化。并且这也是为什么遵循规范是有价值的。然而这会变得更加复杂,因为你不总是需要定义其中一个函数。
|
||||
|
||||
|
||||
|
||||
<!-- Hashing and Hash Codes -->
|
||||
@@ -16,4 +400,4 @@
|
||||
## 调整HashMap
|
||||
|
||||
<!-- 分页 -->
|
||||
<div style="page-break-after: always;"></div>
|
||||
<div style="page-break-after: always;"></div>
|
||||
|
||||
BIN
docs/images/1562999314238.png
Normal file
BIN
docs/images/1562999314238.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 161 KiB |
BIN
docs/images/simple-collection-taxonomy.png
Normal file
BIN
docs/images/simple-collection-taxonomy.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 165 KiB |
Reference in New Issue
Block a user