diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 2eb2ef8..907a705 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -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) diff --git a/README.md b/README.md index a326871..4c34e04 100644 --- a/README.md +++ b/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) diff --git a/docs/book/01-What-is-an-Object.md b/docs/book/01-What-is-an-Object.md index b7d5511..61a5a55 100644 --- a/docs/book/01-What-is-an-Object.md +++ b/docs/book/01-What-is-an-Object.md @@ -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()` 代码示例: diff --git a/docs/book/10-Interfaces.md b/docs/book/10-Interfaces.md index 539c056..aff455e 100644 --- a/docs/book/10-Interfaces.md +++ b/docs/book/10-Interfaces.md @@ -760,40 +760,1059 @@ Woodwind.play() MIDDLE_C ## 抽象类和接口 +尤其是在 Java 8 引入 **default** 方法之后,区分何时抽象类还是接口是最好的选择变得更加令人困惑。下表做了明确的区分: +| 特性 | 接口 | 抽象类 | +| :------------------: | :--------------------------------------------------------: | :--------------------------------------: | +| 组合 | 新类可以组合多个接口 | 只能继承单一抽象类 | +| 状态 | 不能包含属性(除了静态属性,不支持对象状态) | 可以包含属性,非抽象方法可能引用这些属性 | +| 默认方法 和 抽象方法 | 不需要在子类中实现默认方法。默认方法可以引用其他接口的方法 | 必须在子类中实现抽象方法 | +| 构造器 | 没有构造器 | 可以有构造器 | +| 可见性 | 隐式 **public** | 可以是 **protected** 或友元 | +抽象类仍然是一个类,在创建新类时只能继承它一个。而创建类的过程中可以实现多个接口。 + +有一条实际经验:尽可能地抽象。因此,更倾向使用接口而不是抽象类。只有当必要时才使用抽象类。除非必须使用,否则不要用接口和抽象类。大多数时候,普通类已经做得很好,如果不行的话,再移动到接口或抽象类中。 + ## 完全解耦 +当方法操纵的是一个类而非接口时,它就只能作用于那个类或其子类。如果想把方法应用于那个继承层级结构之外的类,就会触霉头。接口在很大程度上放宽了这个限制,因而使用接口可以编写复用性更好的代码。 + +例如有一个类 **Process** 有两个方法 `name()` 和 `process()`。`process()` 方法接受输入,修改并输出。把这个类作为基类用来创建各种不同类型的 **Processor**。下例中,**Processor** 的各个子类修改 String 对象(注意,返回类型可能是协变类型而非参数类型): + +```java +// interfaces/Applicator.java +import java.util.*; + +class Processor { + public String name() { + return getClass().getSimpleName(); + } + + public Object process(Object input) { + return input; + } +} + +class Upcase extends Processor { + // 返回协变类型 + @Override + public String process(Object input) { + return ((String) input).toUpperCase(); + } +} + +class Downcase extends Processor { + @Override + public String process(Object input) { + return ((String) input).toLowerCase(); + } +} + +class Splitter extends Processor { + @Override + public String process(Object input) { + // split() divides a String into pieces: + return Arrays.toString(((String) input).split(" ")); + } +} + +public class Applicator { + public static void apply(Processor p, Object s) { + System.out.println("Using Processor " + p.name()); + System.out.println(p.process(s)); + } + + public static void main(String[] args) { + String s = "We are such stuff as dreams are made on"; + apply(new Upcase(), s); + apply(new Downcase(), s); + apply(new Splitter(), s); + } +} +``` + +输出: + +``` +Using Processor Upcase +WE ARE SUCH STUFF AS DREAMS ARE MADE ON +Using Processor Downcase +we are such stuff as dreams are made on +Using Processor Splitter +[We, are, such, stuff, as, dreams, are, made, on] +``` + +**Applicator** 的 `apply()` 方法可以接受任何类型的 **Processor**,并将其应用到一个 **Object** 对象上输出结果。像本例中这样,创建一个能根据传入的参数类型从而具备不同行为的方法称为*策略*设计模式。方法包含算法中不变的部分,策略包含变化的部分。策略就是传入的对象,它包含要执行的代码。在这里,**Processor** 对象是策略,`main()` 方法展示了三种不同的应用于 **String s** 上的策略。 + +`split()` 是 **String** 类中的方法,它接受 **String** 类型的对象并以传入的参数作为分割界限,返回一个数组 **String[]**。在这里用它是为了更快地创建 **String** 数组。 + +假设现在发现了一组电子滤波器,它们看起来好像能使用 **Applicator** 的 `apply()` 方法: + +```java +// interfaces/filters/Waveform.java +package interfaces.filters; + +public class Waveform { + private static long counter; + private final long id = count++; + + @Override + public String toString() { + return "Waveform " + id; + } +} + +// interfaces/filters/Filter.java +package interfaces.filters; + +public class Filter { + public String name() { + return getClass().getSimpleName(); + } + + public Waveform process(Waveform input) { + return input; + } +} + +// interfaces/filters/LowPass.java +package interfaces.filters; + +public class LowPass extends Filter { + double cutoff; + + public LowPass(double cutoff) { + this.cutoff = cutoff; + } + + @Override + public Waveform process(Waveform input) { + return input; // Dummy processing 哑处理 + } +} + +// interfaces/filters/HighPass.java +package interfaces.filters; + +public class HighPass extends Filter { + double cutoff; + + public HighPass(double cutoff) { + this.cutoff = cutoff; + } + + @Override + public Waveform process(Waveform input) { + return input; + } +} + +// interfaces/filters/BandPass.java +package interfaces.filters; + +public class BandPass extends Filter { + double lowCutoff, highCutoff; + + public BandPass(double lowCut, double highCut) { + lowCutoff = lowCut; + highCutoff = highCut; + } + + @Override + public Waveform process(Waveform input) { + return input; + } +} +``` + +**Filter** 类与 **Processor** 类具有相同的接口元素,但是因为它不是继承自 **Processor** —— 因为 **Filter** 类的创建者根本不知道你想将它当作 **Processor** 使用 —— 因此你不能将 **Applicator** 的 `apply()` 方法应用在 **Filter** 类上,即使这样做也能正常运行。主要是因为 **Applicator** 的 `apply()` 方法和 **Processor** 过于耦合,这阻止了 **Applicator** 的 `apply()` 方法被复用。另外要注意的一点是 Filter 类中 `process()` 方法的输入输出都是 **Waveform**。 + +但如果 **Processor** 是一个接口,那么限制就会变得松动到足以复用 **Applicator** 的 `apply()` 方法,用来接受那个接口参数。下面是修改后的 **Processor** 和 **Applicator** 版本: + +```java +// interfaces/interfaceprocessor/Processor.java +package interfaces.interfaceprocessor; + +public interface Processor { + default String name() { + return getClass().getSimpleName(); + } + + Object process(Object input); +} + +// interfaces/interfaceprocessor/Applicator.java +package interfaces.interfaceprocessor; + +public class Applicator { + public static void apply(Processor p, Object s) { + System.out.println("Using Processor " + p.name()); + System.out.println(p.process(s)); + } +} +``` + +复用代码的第一种方式是客户端程序员遵循接口编写类,像这样: + +```java +// interfaces/interfaceprocessor/StringProcessor.java +// {java interfaces.interfaceprocessor.StringProcessor} +package interfaces.interfaceprocessor; +import java.util.*; + +interface StringProcessor extends Processor { + @Override + String process(Object input); // [1] + String S = "If she weighs the same as a duck, she's made of wood"; // [2] + + static void main(String[] args) { // [3] + Applicator.apply(new Upcase(), S); + Applicator.apply(new Downcase(), S); + Applicator.apply(new Splitter(), S); + } +} + +class Upcase implements StringProcessor { + // 返回协变类型 + @Override + public String process(Object input) { + return ((String) input).toUpperCase(); + } +} + +class Downcase implements StringProcessor { + @Override + public String process(Object input) { + return ((String) input).toLowerCase(); + } +} + +class Splitter implements StringProcessor { + @Override + public String process(Object input) { + return Arrays.toString(((String) input).split(" ")); + } +} +``` + +输出: + +``` +Using Processor Upcase +IF SHE WEIGHS THE SAME AS A DUCK, SHE'S MADE OF WOOD +Using Processor Downcase +if she weighs the same as a duck, she's made of wood +Using Processor Splitter +[If, she, weighs, the, same, as, a, duck,, she's, made, of, wood] +``` + +>[1] 该声明不是必要的,即使移除它,编译器也不会报错。但是注意这里的协变返回类型从 Object 变成了 String。 +> +>[2] S 自动就是 final 和 static 的,因为它是在接口中定义的。 +> +>[3] 可以在接口中定义 `main()` 方法。 + +这种方式运作得很好,然而你经常遇到的情况是无法修改类。例如在电子滤波器的例子中,类库是被发现而不是创建的。在这些情况下,可以使用*适配器*设计模式。适配器允许代码接受已有的接口产生需要的接口,如下: + +```java +// interfaces/interfaceprocessor/FilterProcessor.java +// {java interfaces.interfaceprocessor.FilterProcessor} +package interfaces.interfaceprocessor; +import interfaces.filters.*; + +class FilterAdapter implements Processor { + Filter filter; + + FilterAdapter(Filter filter) { + this.filter = filter; + } + + @Override + public String name() { + return filter.name(); + } + + @Override + public Waveform process(Object input) { + return filter.process((Waveform) input); + } +} + +punlic class FilterProcessor { + public static void main(String[] args) { + Waveform w = new Waveform(); + Applicator.apply(new FilterAdapter(new LowPass(1.0)), w); + Applicator.apply(new FilterAdapter(new HighPass(2.0)), w); + Applicator.apply(new FilterAdapter(new BandPass(3.0, 4.0)), w); + } +} +``` + +输出: + +``` +Using Processor LowPass +Waveform 0 +Using Processor HighPass +Waveform 0 +Using Processor BandPass +Waveform 0 +``` + +在这种使用适配器的方式中,**FilterAdapter** 的构造器接受已有的接口 **Filter**,继而产生需要的 **Processor** 接口的对象。你可能还注意到 **FilterAdapter** 中使用了委托。 + +协变允许我们从 `process()` 方法中产生一个 **Waveform** 而非 **Object** 对象。 + +将接口与实现解耦使得接口可以应用于多种不同的实现,因而代码更具可复用性。 + ## 多接口结合 +接口没有任何实现——也就是说,没有任何与接口相关的存储——因此无法阻止结合的多接口。这是有价值的,因为你有时需要表示“一个 **x** 是一个 **a** 和一个 **b** 以及一个 **c**”。 + +![类图](../images/1562999314238.png) + +在一个派生类中,你不能要求必须有一个是抽象的活“具体的”(没有任何抽象方法)的基类。如果确实要继承一个非接口的类,那么只能继承一个,其余的基元素必须都是接口。需要将所有的接口名称置于 **implements** 关键字之后且用逗号分隔。可以有任意多个接口,并可以向上转型为每个接口,因为每个接口都是独立的类型。下例展示了一个组合了多接口的具体类产生了新类: + +```java +// interfaces/Adventure.java +// Multiple interfaces +interface CanFight { + void fight(); +} + +interface CanSwim { + void swim(); +} + +interface CanFly { + void fly(); +} + +class ActionCharacter { + public void fight(){} +} + +class Hero extends ActionCharacter implements CanFight, CanSwim, CanFly { + public void swim() {} + + pubilc void fly() {} +} + +public class Adventure { + public static void t(CanFight x) { + x.fight(); + } + + public static void u(CanSwim x) { + x.swim(); + } + + public staic void v(CanFly x) { + x.fly(); + } + + public static void w(ActionCharacter x) { + x.fight(); + } + + public static void main(String[] args) { + Hero h = new Hero(); + t(h); // Treat it as a CanFight + u(h); // Treat it as a CanSwim + v(h); // Treat it as a CanFly + w(h); // Treat it as an ActionCharacter + } +} +``` + +类 **Hero** 结合了具体类 **ActionCharacter** 和接口 **CanFight**、**CanSwim** 和 **CanFly**。当通过这种方式结合具体类和接口时,需要将具体类放在前面,后面跟着接口(否则编译器会报错)。 + +接口 **CanFight** 和类 **ActionCharacter** 中的 `flight()` 方法签名相同,而在类 Hero 中也没有提供 `fight()` 的定义。可以扩展一个接口,但是得到的是另一个接口。当想创建一个对象时,所有的定义必须首先都存在。类 **Hero** 中没有显式地提供 `fight()` 的定义,是由于该方法在类 **ActionCharacter** 中已经定义过,这样才使得创建 **Hero** 对象成为可能。 + +在类 **Adventure** 中可以看到四个方法,它们把不同的接口和具体类作为参数。当创建一个 **Hero** 对象时,它可以被传入这些方法中的任意一个,意味着它可以依次向上转型为每个接口。Java 中这种接口的设计方式,使得程序员不需要付出特别的努力。 + +记住,前面例子展示了使用接口的核心原因之一:为了能够向上转型为多个基类型(以及由此带来的灵活性)。然而,使用接口的第二个原因与使用抽象基类相同:防止客户端程序员创建这个类的对象,确保这仅仅只是一个接口。这带来了一个问题:应该使用接口还是抽象类呢?如果创建不带任何方法定义或成员变量的基类,就选择接口而不是抽象类。事实上,如果知道某事物是一个基类,可以考虑用接口实现它(这个主题在本章总结会再次讨论)。 + ## 使用继承扩展接口 +通过继承,可以很容易在接口中增加方法声明,还可以在新接口中结合多个接口。这两种情况都可以得到新接口,如下例所示: + +```java +// interfaces/HorrorShow.java +// Extending an interface with inheritance +interface Monster { + void menace(); +} + +interface DangerousMonster extends Monster { + void destroy(); +} + +interface Lethal { + void kill(); +} + +class DragonZilla implements DangerousMonster { + @Override + public void menace() {} + + @Override + public void destroy() {} +} + +interface Vampire extends DangerousMonster, Lethal { + void drinkBlood(); +} + +class VeryBadVampire implements Vampire { + @Override + public void menace() {} + + @Override + public void destroy() {} + + @Override + public void kill() {} + + @Override + public void drinkBlood() {} +} + +public class HorrorShow { + static void u(Monster b) { + b.menace(); + } + + static void v(DangerousMonster d) { + d.menace(); + d.destroy(); + } + + static void w(Lethal l) { + l.kill(); + } + + public static void main(String[] args) { + DangerousMonster barney = new DragonZilla(); + u(barney); + v(barney); + Vampire vlad = new VeryBadVampire(); + u(vlad); + v(vlad); + w(vlad); + } +} +``` + +接口 **DangerousMonster** 是 **Monster** 简单扩展的一个新接口,类 **DragonZilla** 实现了这个接口。 + +**Vampire** 中使用的语法仅适用于接口继承。通常来说,**extends** 只能用于单一类,但是在构建接口时可以引用多个基类接口。注意到,接口名之间用逗号分隔。 + +### 结合接口时的命名冲突 + +当实现多个接口时可能会存在一个小陷阱。在前面的例子中,**CanFlight** 和 **ActionCharacter** 具有完全相同的 `flight()` 方法。完全相同的方法没有问题,但是如果它们的签名或返回类型不同会怎么样呢?这里有一个例子: + +```java +// interfaces/InterfaceCollision.java +interface I1 { + void f(); +} + +interface I2 { + int f(int i); +} + +interface I3 { + int f(); +} + +class C { + public int f() { + return 1; + } +} + +class C2 implements I1, I2 { + @Override + public void f() {} + + @Override + public int f(int i) { + return 1; // 重载 + } +} + +class C3 extends C implements I2 { + @Override + public int f(int i) { + return 1; // 重载 + } +} + +class C4 extends C implements I3 { + // 完全相同,没问题 + @Override + public int f() { + return 1; + } +} + +// 方法的返回类型不同 +//- class C5 extends C implements I1 {} +//- interface I4 extends I1, I3 {} +``` + +覆写、实现和重载令人不快地搅和在一起带来了困难。同时,重载方法仅根据返回类型是区分不了的。当不注释最后两行时,报错信息如下: + +``` +error: C5 is not abstract and does not override abstract +method f() in I1 +class C5 extends C implements I1 {} +error: types I3 and I1 are incompatible; both define f(), +but with unrelated return types +interfacce I4 extends I1, I3 {} +``` + +当打算组合接口时,在不同的接口中使用相同的方法名通常会造成代码可读性的混乱,尽量避免这种情况。 + ## 接口适配 +接口最吸引人的原因之一是相同的接口可以有多个实现。在简单情况下体现在一个方法接受接口作为参数,该接口的实现和传递对象给方法则交由你来做。 + +因此,接口的一种常见用法是前面提到的*策略*设计模式。编写一个方法执行某些操作并接受一个指定的接口作为参数。可以说:“只要对象遵循接口,就可以调用方法” ,这使得方法更加灵活,通用,并更具可复用性。 + +例如,类 **Scanner** 的构造器接受的是一个 **Readable** 接口(在“字符串”一章中学习更多相关内容)。你会发现 **Readable** 没有用作 Java 标准库中其他任何方法的参数——它是单独为 **Scanner** 创建的,因此 **Scanner** 没有将其参数限制为某个特定类。通过这种方式,**Scanner** 可以与更多的类型协作。如果你创建了一个新类并想让 **Scanner** 作用于它,就让它实现 **Readable** 接口,像这样: + +```java +// interfaces/RandomStrings.java +// Implementing an interface to conform to a method +import java.nio.*; +import java.util.*; + +public class RandomStrings implements Readable { + private statdic Random rand = new Random(47); + private static final char[] CAPITALS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ".toCharArray(); + private static final char[] LOWERS = "abcdefghijklmnopqrstuvwxyz".toCharArray(); + private static final char[] VOWELS = "aeiou".toCharArray(); + private int count; + + public RandomStrings(int count) { + this.count = count; + } + + @Override + public int read(CharBuffer cb) { + if (count-- == 0) { + return -1; // indicates end of input + } + cb.append(CAPITALS[rand.nextInt(CAPITALS.length)]); + for (int i = 0; i < 4; i++) { + cb.append(VOWELS[rand.nextInt(VOWELS.length)]); + cb.append(LOWERS[rand.nextInt(LOWERS.length)]); + } + cb.append(" "); + return 10; // Number of characters appended + } + + public static void main(String[] args) { + Scanner s = new Scanner(new RandomStrings(10)); + while (s.hasNext()) { + System.out.println(s.next()); + } + } +} +``` + +输出: + +``` +Yazeruyac +Fowenucor +Goeazimom +Raeuuacio +Nuoadesiw +Hageaikux +Ruqicibui +Numasetih +Kuuuuozog +Waqizeyoy +``` + +**Readable** 接口只需要实现 `read()` 方法(注意 `@Override` 注解的突出方法)。在 `read()` 方法里,将输入内容添加到 **CharBuffer** 参数中(有多种方法可以实现,查看 **CharBuffer** 文档),或在没有输入时返回 **-1**。 + +假设你有一个类没有实现 **Readable** 接口,怎样才能让 **Scanner** 作用于它呢?下面是一个产生随机浮点数的例子: + +```java +// interfaces/RandomDoubles.java +import java.util.*; + +public interface RandomDoubles { + Random RAND = new Random(47); + + default double next() { + return RAND.nextDouble(); + } + + static void main(String[] args) { + RandomDoubles rd = new RandomDoubles(){}; + for (int i = 0; i < 7; i++) { + System.out.println(rd.next() + " "); + } + } +} +``` + +输出: + +``` +0.7271157860730044 +0.5309454508634242 +0.16020656493302599 +0.18847866977771732 +0.5166020801268457 +0.2678662084200585 +0.2613610344283964 +``` + +我们可以再次使用适配器模式,但这里适配器类可以实现两个接口。因此,通过关键字 **interface** 提供的多继承,我们可以创建一个既是 **RandomDoubles**,又是 **Readable** 的类: + +```java +// interfaces/AdaptedRandomDoubles.java +// creating an adapter with inheritance +import java.nio.*; +import java.util.*; + +public class AdaptedRandomDoubles implements RandomDoubles, Readable { + private int count; + + public AdaptedRandomDoubles(int count) { + this.count = count; + } + + @Override + public int read(CharBuffer cb) { + if (count-- == 0) { + return -1; + } + String result = Double.toString(next()) + " "; + cb.append(result); + return result.length(); + } + + public static void main(String[] args) { + Scanner s = new Scanner(new AdaptedRandomDoubles(7)); + while (s.hasNextDouble()) { + System.out.print(s.nextDouble() + " "); + } + } +} +``` + +输出: + +``` +0.7271157860730044 0.5309454508634242 +0.16020656493302599 0.18847866977771732 +0.5166020801268457 0.2678662084200585 +0.2613610344283964 +``` + +因为你可以以这种方式在已有类中增加新接口,所以这就意味着一个接受接口类型的方法提供了一种让任何类都可以与该方法进行适配的方式。这就是使用接口而不是类的强大之处。 + ## 接口字段 +因为接口中的字段都自动是 **static** 和 **final** 的,所以接口就成为了创建一组常量的方便的工具。在 Java 5 之前,这是产生与 C 或 C++ 中的 enum (枚举类型) 具有相同效果的唯一方式。所以你可能在 Java 5 之前的代码中看到: + +```java +// interfaces/Months.java +// Using interfaces to create groups of constants +public interface Months { + int + JANUARY = 1, FEBRUARY = 2, MARCH = 3, + APRIL = 4, MAY = 5, JUNE = 6, JULY = 7, + AUGUST = 8, SEPTEMBER = 9, OCTOBER = 10, + NOVEMBER = 11, DECEMBER = 12; +} +``` + +注意 Java 中使用大写字母的风格定义具有初始化值的 **static** **final** 变量。接口中的字段自动是 **public** 的,所以没有显式指明这点。 + +自 Java 5 开始,我们有了更加强大和灵活的关键字 **enum**,那么在接口中定义常量组就显得没什么意义了。然而当你阅读遗留的代码时,在很多场合你还会碰到这种旧的习惯用法。在“枚举”一章中你会学习到更多关于枚举的内容。 + +### 初始化接口中的字段 + +接口中定义的字段不能是“空 **final**",但是可以用非常量表达式初始化。例如: + +```java +// interfaces/RandVals.java +// Initializing interface fields with +// non-constant initializers +import java.util.*; + +public interface RandVals { + Random RAND = new Random(47); + int RANDOM_INT = RAND.nextInt(10); + long RANDOM_LONG = RAND.nextLong() * 10; + float RANDOM_FLOAT = RAND.nextLong() * 10; + double RANDOM_DOUBLE = RAND.nextDouble() * 10; +} +``` + +因为字段是 **static** 的,所以它们在类第一次被加载时初始化,这发生在任何字段首次被访问时。下面是个简单的测试: + +```java +// interfaces/TestRandVals.java +public class TestRandVals { + public static void main(String[] args) { + System.out.println(RandVals.RANDOM_INT); + System.out.println(RandVals.RANDOM_LONG); + System.out.println(RandVals.RANDOM_FLOAT); + System.out.println(RandVals.RANDOM_DOUBLE); + } +} +``` + +输出: + +``` +8 +-32032247016559954 +-8.5939291E18 +5.779976127815049 +``` + +这些字段不是接口的一部分,它们的值被存储在接口的静态存储区域中。 + ## 接口嵌套 +接口可以嵌套在类或其他接口中。下面揭示一些有趣的特性: + +```java +// interfaces/nesting/NestingInterfaces.java +// {java interfaces.nesting.NestingInterfaces} +package interfaces.nesting; + +class A { + interface B { + void f(); + } + + public class BImp implements B { + @Override + public void f() {} + } + + public class BImp2 implements B { + @Override + public void f() {} + } + + public interface C { + void f(); + } + + class CImp implements C { + @Override + public void f() {} + } + + private class CImp2 implements C { + @Override + public void f() {} + } + + private interface D { + void f(); + } + + private class DImp implements D { + @Override + public void f() {} + } + + public class DImp2 implements D { + @Override + public void f() {} + } + + public D getD() { + return new DImp2(); + } + + private D dRef; + + public void receiveD(D d) { + dRef = d; + dRef.f(); + } +} + +interface E { + interface G { + void f(); + } + // Redundant "public" + public interface H { + void f(); + } + + void g(); + // Cannot be private within an interface + //- private interface I {} +} + +public class NestingInterfaces { + public class BImp implements A.B { + @Override + public void f() {} + } + + class CImp implements A.C { + @Override + public void f() {} + } + // Cannot implements a private interface except + // within that interface's defining class: + //- class DImp implements A.D { + //- public void f() {} + //- } + class EImp implements E { + @Override + public void g() {} + } + + class EGImp implements E.G { + @Override + public void f() {} + } + + class EImp2 implements E { + @Override + public void g() {} + + class EG implements E.G { + @Override + public void f() {} + } + } + + public static void main(String[] args) { + A a = new A(); + // Can't access to A.D: + //- A.D ad = a.getD(); + // Doesn't return anything but A.D: + //- A.DImp2 di2 = a.getD(); + // cannot access a member of the interface: + //- a.getD().f(); + // Only another A can do anything with getD(): + A a2 = new A(); + a2.receiveD(a.getD()); + } +} +``` + +在类中嵌套接口的语法是相当显而易见的。就像非嵌套接口一样,它们具有 **public** 或包访问权限的可见性。 + +作为一种新添加的方式,接口也可以是 **private** 的,例如 **A.D**(同样的语法同时适用于嵌套接口和嵌套类)。那么 **private** 嵌套接口有什么好处呢?你可能猜测它只是被用来实现一个 **private** 内部类,就像 **DImp**。然而 **A.DImp2** 展示了它可以被实现为 **public** 类,但是 **A.DImp2** 只能被自己使用,你无法说它实现了 **private** 接口 **D**,所以实现 **private** 接口是一种可以强制该接口中的方法定义不会添加任何类型信息(即不可以向上转型)的方式。 + +`getD()` 方法产生了一个与 **private** 接口有关的窘境。它是一个 **public** 方法却返回了对 **private** 接口的引用。能对这个返回值做些什么呢?`main()` 方法里进行了一些使用返回值的尝试但都失败了。返回值必须交给有权使用它的对象,本例中另一个 **A** 通过 `receiveD()` 方法接受了它。 + +接口 **E** 说明了接口之间也能嵌套。然而,作用于接口的规则——尤其是,接口中的元素必须是 **public** 的——在此都会被严格执行,所以嵌套在另一个接口中的接口自动就是 **public** 的,不能指明为 **private**。 + +类 **NestingInterfaces** 展示了嵌套接口的不同实现方式。尤其是当实现某个接口时,并不需要实现嵌套在其内部的接口。同时,**private** 接口不能在定义它的类之外被实现。 + +添加这些特性的最初原因看起来像是出于对严格的语法一致性的考虑,但是我通常认为,一旦你了解了某种特性,就总能找到其用武之地。 + ## 接口和工厂方法模式 +接口是多实现的途径,而生成符合某个接口的对象的典型方式是*工厂方法*设计模式。不同于直接调用构造器,只需调用工厂对象中的创建方法就能生成对象的实现——理论上,通过这种方式可以将接口与实现的代码完全分离,使得可以透明地将某个实现替换为另一个实现。这里是一个展示工厂方法结构的例子: + +```java +// interfaces/Factories.java +interface Service { + void method1(); + void method2(); +} + +interface ServiceFactory { + Service getService(); +} + +class Service1 implements Service { + Service1() {} // Package access + + @Override + public void method1() { + System.out.println("Service1 method1"); + } + + @Override + public void method2() { + System.out.println("Service1 method2"); + } +} + +class Service1Factory implements ServiceFactory { + @Override + public Service getService() { + return new Service1(); + } +} + +class Service2 implements Service { + Service2() {} // Package access + + @Override + public void method1() { + System.out.println("Service2 method1"); + } + + @Override + public void method2() { + System.out.println("Service2 method2"); + } +} + +class Service2Factory implements ServiceFactory { + @Override + public Service getService() { + return new Service2(); + } +} + +public class Factories { + public static void serviceConsumer(ServiceFactory fact) { + Service s = fact.getService(); + s.method1(); + s.method2(); + } + + public static void main(String[] args) { + serviceConsumer(new Service1Factory()); + // Services are completely interchangeable: + serviceConsumer(new Service2Factory()); + } +} +``` + +输出: + +``` +Service1 method1 +Service1 method2 +Service2 method1 +Service2 method2 +``` + +如果没有工厂方法,代码就必须在某处指定将要创建的 **Service** 的确切类型,从而调用恰当的构造器。 + +为什么要添加额外的间接层呢?一个常见的原因是创建框架。假设你正在创建一个游戏系统;例如,在相同的棋盘下国际象棋和西洋跳棋: + +```java +// interfaces/Games.java +// A Game framework using Factory Methods +interface Game { + boolean move(); +} + +interface GameFactory { + Game getGame(); +} + +class Checkers implements Game { + private int moves = 0; + private static final int MOVES = 3; + + @Override + public boolean move() { + System.out.println("Checkers move " + moves); + return ++moves != MOVES; + } +} + +class CheckersFactory implements GameFactory { + @Override + public Game getGame() { + return new Checkers(); + } +} + +class Chess implements Game { + private int moves = 0; + private static final int MOVES = 4; + + @Override + public boolean move() { + System.out.println("Chess move " + moves); + return ++moves != MOVES; + } +} + +class ChessFactory implements GameFactory { + @Override + public Game getGame() { + return new Chess(); + } +} + +public class Games { + public static void playGame(GameFactory factory) { + Game s = factory.getGame(); + while (s.move()) { + ; + } + } + + public static void main(String[] args) { + playGame(new CheckersFactory()); + playGame(new ChessFactory()); + } +} +``` + +输出: + +``` +Checkers move 0 +Checkers move 1 +Checkers move 2 +Chess move 0 +Chess move 1 +Chess move 2 +Chess move 3 +``` + +如果类 **Games** 表示一段很复杂的代码,那么这种方式意味着你可以在不同类型的游戏里复用这段代码。你可以再想象一些能够从这个模式中受益的更加精巧的游戏。 + +在下一章,你将会看到一种更加优雅的使用匿名内部类的工厂实现方式。 + ## 本章小结 +认为接口是好的选择,从而使用接口不用具体类,这具有诱惑性。几乎任何时候,创建类都可以替代为创建一个接口和工厂。 + +很多人都掉进了这个陷阱,只要有可能就创建接口和工厂。这种逻辑看起来像是可能会使用不同的实现,所以总是添加这种抽象性。这变成了一种过早的设计优化。 + +任何抽象性都应该是由真正的需求驱动的。当有必要时才应该使用接口进行重构,而不是到处添加额外的间接层,从而带来额外的复杂性。这种复杂性非常显著,如果你让某人去处理这种复杂性,只是因为你意识到“以防万一”而添加新接口,而没有其他具有说服力的原因——好吧,如果我碰上了这种设计,就会质疑此人所作的所有其他设计了。 + +恰当的原则是优先使用类而不是接口。从类开始,如果使用接口的必要性变得很明确,那么就重构。接口是一个伟大的工具,但它们容易被滥用。 +
\ No newline at end of file diff --git a/docs/book/11-Inner-Classes.md b/docs/book/11-Inner-Classes.md index c31b371..b6e8ec9 100644 --- a/docs/book/11-Inner-Classes.md +++ b/docs/book/11-Inner-Classes.md @@ -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*。) @@ -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** 类时)。构建内部类对象时,需要一个指向其外围类对象的引用,如果编译器访问不到这个引用就会报错。不过绝大多数时候这都无需程序员操心。 ## 使用 .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 { 在拥有外部类对象之前是不可能创建内部类对象的。这是因为内部类对象会暗暗地连接到建它的外部类对象上。但是,如果你创建的是嵌套类(静态内部类),那么它就不需要对外部类对象的引用。 + ## 内部类与向上转型 当将内部类向上转型为其基类,尤其是转型为一个接口的时候,内部类就有了用武之地。(从实现了某个接口的对象,得到对此接口的引用,与向上转型为这个对象的基类,实质上效果是一样的。)这是因为此内部类-某个接口的实现-能够完全不可见,并且不可用。所得到的只是指向基类或接口的引用,所以能够很方便地隐藏实现细节。 @@ -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 编译器提供了生成高效代码的机会。 @@ -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** 的作用域之外,它是不可用的,除此之外,它与普通的类一样。 @@ -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**"语法能产生正确的作用域,所以不必在调用构造器时限定类名。 @@ -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 的列表”)的容器对象中,容器会在 [集合 ]() 中详细介绍。目前读者只需要知道 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 -
\ No newline at end of file +
diff --git a/docs/book/12-Collections.md b/docs/book/12-Collections.md index bf50850..970f9fb 100644 --- a/docs/book/12-Collections.md +++ b/docs/book/12-Collections.md @@ -3,62 +3,1806 @@ # 第十二章 集合 +> 如果一个程序只包含固定数量的对象且对象的生命周期都是已知的,那么这是一个非常简单的程序。 + +通常,程序总是根据运行时才知道的某些条件去创建新的对象。在此之前,无法知道所需对象的数量甚至确切类型。为了解决这个普遍的编程问题,需要在任意时刻和任意位置创建任意数量的对象。因此,不能依靠创建命名的引用来持有每一个对象: +```java +MyType aReference +``` +因为从来不会知道实际需要多少个这样的引用。 + +大多数编程语言都提供了某种方法来解决这个基本问题。Java有多种方式保存对象(确切地说,是对象的引用)。例如前边曾经学习过的数组,它是编译器支持的类型。数组是保存一组对象的最有效的方式,如果想要保存一组基本类型数据,也推荐使用数组。但是数组具有固定的大小尺寸,而且在更一般的情况下,在写程序的时候并不知道将需要多少个对象,或者是否需要更复杂的方式来存储对象,因此数组尺寸固定这一限制就显得太过受限了。 + +**java.util** 库提供了一套相当完整的*集合类*(collection classes)来解决这个问题,其中基本的类型有 **List** 、**Set** 、 **Queue** 和 **Map**。这些类型也被称作*容器类*(container classes),但我将使用Java类库使用的术语。集合提供了完善的方法来保存对象,可以使用这些工具来解决大量的问题。 + +集合还有一些其它特性。例如, **Set** 对于每个值都只保存一个对象, **Map** 是一个关联数组,允许将某些对象与其他对象关联起来。Java集合类都可以自动地调整自己的大小。因此,与数组不同,在编程时,可以将任意数量的对象放置在集合中,而不用关心集合应该有多大。 + +尽管在Java中没有直接的关键字支持,[^1]但集合类仍然是可以显著增强编程能力的基本工具。在本章中,将介绍Java集合类库的基本知识,并重点介绍一些典型用法。这里将专注于在日常编程中使用的集合。稍后,在[附录:集合主题]()中,还将学习到其余的那些集合和相关功能,以及如何使用它们的更多详细信息。 ## 泛型和类型安全的集合 +使用Java SE5之前的集合的一个主要问题是编译器允许你向集合中插入不正确的类型。例如,考虑一个 **Apple** 对象的集合,这里使用最基本最可靠的 **ArrayList** 。现在,可以把 **ArrayList** 看作“可以自动扩充自身尺寸的数组”来看待。使用 **ArrayList** 相当简单:创建一个实例,用 **add()** 插入对象;然后用 **get()** 来访问这些对象,此时需要使用索引,就像数组那样,但是不需要方括号。[^2] **ArrayList** 还有一个 **size()** 方法,来说明集合中包含了多少个元素,所以不会不小心因数组越界而引发错误(通过抛出*运行时异常*,[异常]()章节介绍了异常)。 + +在本例中, **Apple** 和 **Orange** 都被放到了集合中,然后将它们取出。正常情况下,Java编译器会给出警告,因为这个示例没有使用泛型。在这里,使用特定的注解来抑制警告信息。注解以“@”符号开头,可以带参数。这里的 **@SuppressWarning** 注解及其参数表示只抑制“unchecked”类型的警告([注解]()章节将介绍更多有关注解的信息): + +```java +// collections/ApplesAndOrangesWithoutGenerics.java +// Simple collection use (suppressing compiler warnings) +// {ThrowsException} +import java.util.*; + +class Apple { + private static long counter; + private final long id = counter++; + public long id() { return id; } +} + +class Orange {} + +public class ApplesAndOrangesWithoutGenerics { + @SuppressWarnings("unchecked") + public static void main(String[] args) { + ArrayList apples = new ArrayList(); + for(int i = 0; i < 3; i++) + apples.add(new Apple()); + // No problem adding an Orange to apples: + apples.add(new Orange()); + for(Object apple : apples) { + ((Apple) apple).id(); + // Orange is detected only at run time + } + } +} +/* Output: +___[ Error Output ]___ +Exception in thread "main" +java.lang.ClassCastException: Orange cannot be cast to +Apple + at ApplesAndOrangesWithoutGenerics.main(ApplesA +ndOrangesWithoutGenerics.java:23) +*/ +``` + +**Apple** 和 **Orange** 是截然不同的,它们除了都是 **Object** 之外没有任何共同点(如果一个类没有显式地声明继承自哪个类,那么它就自动继承自 **Object**)。因为 **ArrayList** 保存的是 **Object** ,所以不仅可以通过 **ArrayList** 的 **add()** 方法将 **Apple** 对象放入这个集合,而且可以放入 **Orange** 对象,这无论在编译期还是运行时都不会有问题。当使用 **ArrayList** 的 **get()** 方法来取出你认为是 **Apple** 的对象时,得到的只是 **Object** 引用,必须将其转型为 **Apple**。然后需要将整个表达式用括号括起来,以便在调用 **Apple** 的 **id()** 方法之前,强制执行转型。否则,将会产生语法错误。 + +在运行时,当尝试将 **Orange** 对象转为 **Apple**时,会出现输出中显示的错误。 + +在[泛型]()章节中,你将了解到使用Java泛型来创建类可能很复杂。但是,使用预先定义的泛型类却相当简单。例如,要定义一个用于保存 **Apple** 对象的 **ArrayList** ,只需要使用 **ArrayList** 来代替 **ArrayList** 。尖括号括起来的是*类型参数*(可能会有多个),它指定了这个集合实例可以保存的类型。 + +通过使用泛型,就可以在编译期防止将错误类型的对象放置到集合中。[^3]下面还是这个示例,但是使用了泛型: +```java +// collections/ApplesAndOrangesWithGenerics.java +import java.util.*; + +public class ApplesAndOrangesWithGenerics { + public static void main(String[] args) { + ArrayList apples = new ArrayList<>(); + for(int i = 0; i < 3; i++) + apples.add(new Apple()); + // Compile-time error: + // apples.add(new Orange()); + for(Apple apple : apples) { + System.out.println(apple.id()); + } + } +} +/* Output: +0 +1 +2 +*/ +``` + +在 **apples** 定义的右侧,可以看到 **new ArrayList<>()** 。这有时被称为“菱形语法”(diamond syntax)。在Java 7之前,必须要在两端都进行类型声明,如下所示: + +```java +ArrayList apples = new ArrayList(); +``` + +随着类型变得越来越复杂,这种重复产生的代码非常混乱且难以阅读。程序员发现所有类型信息都可以从左侧获得,因此,编译器没有理由强迫右侧在重复这些。Java语言团队采纳了这种*类型推断*(type inference)的请求,即使只是这么小的一点。 + +有了 **ArrayList** 声明中的类型指定,编译器会阻止将 **Orange** 放入 **apples** ,因此,这会成为一个编译期错误而不是运行时错误。 + +使用泛型,从 **List** 中获取元素不需要强制类型转换。因为 **List** 知道它持有什么类型,因此当调用 **get()** 时,它会替你执行转型。这样,不仅可以知道编译器将会检查放入集合中的对象的类型,而且在使用集合中的对象时,可以使用更加清晰的语法。 + +当指定了某个类型为泛型参数时,并不仅限于只能将确切类型的对象放入集合中。向上转型也可以像作用于其他类型一样作用于泛型: +```java +// collections/GenericsAndUpcasting.java +import java.util.*; + +class GrannySmith extends Apple {} +class Gala extends Apple {} +class Fuji extends Apple {} +class Braeburn extends Apple {} + +public class GenericsAndUpcasting { + public static void main(String[] args) { + ArrayList apples = new ArrayList<>(); + apples.add(new GrannySmith()); + apples.add(new Gala()); + apples.add(new Fuji()); + apples.add(new Braeburn()); + for(Apple apple : apples) + System.out.println(apple); + } +} +/* Output: +GrannySmith@15db9742 +Gala@6d06d69c +Fuji@7852e922 +Braeburn@4e25154f +*/ +``` + +因此,可以将 **Apple** 的子类型添加到被指定为保存 **Apple** 对象的集合中。 + +程序的输出是从 **Object** 默认的 **toString()** 方法产生的,该方法打印类名,后边跟着对象的散列码的无符号十六进制表示(这个散列码是通过 **hashCode()** 方法产生的)。将在[附录:理解equals和hashCode方法]()中了解有关散列码的内容。 ## 基本概念 +Java集合类库采用“保持对象”(holding objects)的思想,并将其分为两个不同的概念,表示为类库的基本接口: + +1. **集合(Collection)** :一个独立元素的序列,这些元素都服从一条或多条规则。 **List** 必须以插入的方式保存元素, **Set** 不能包含重复元素, **Queue** 按照*排队规则*来确定对象产生的顺序(通常与它们被插入的顺序相同)。 +2. **映射(Map)** : 一组成对的“键值对”对象,允许使用键来查找值。 **ArrayList** 使用数字来查找对象,因此在某种意义上讲,它是将数字和对象关联在一起。 **map**允许我们使用一个对象来查找另一个对象,它也被称作*关联数组*(associative array),因为它将对象和其它对象关联在一起;或者称作*字典*(dictionary),因为可以使用一个键对象来查找值对象,就像在字典中使用单词查找定义一样。 **Map**s是强大的编程工具。 + +尽管并非总是可行,但在理想情况下,你编写的大部分代码都在与这些接口打交道,并且唯一需要指定所使用的精确类型的地方就是在创建的时候。因此,可以像下面这样创建一个 **List** : + +```java +List apples = new ArrayList<>(); +``` + +请注意, **ArrayList** 已经被向上转型为了 **List** ,这与之前示例中的处理方式正好相反。使用接口的目的是,如果想要改变具体实现,只需在创建时修改它就行了,就像下面这样: + +```java +List apples = new LinkedList<>(); +``` + +因此,应该创建一个具体类的对象,将其向上转型为对应的接口,然后在其余代码中都是用这个接口。 + +这种方式并非总是有效的,因为某些具体类有额外的功能。例如, **LinkedList** 具有 **List** 接口中未包含的额外方法,而 **TreeMap** 也具有在 **Map** 接口中未包含的方法。如果需要使用这些方法,就不能将它们向上转型为更通用的接口。 + +**Collection** 接口概括了*序列*的概念——一种存放一组对象的方式。下面是个简单的示例,用 **Integer** 对象填充了一个 **Collection** (这里用 **ArrayList** 表示),然后打印集合中的每个元素: +```java +// collections/SimpleCollection.java +import java.util.*; + +public class SimpleCollection { + public static void main(String[] args) { + Collection c = new ArrayList<>(); + for(int i = 0; i < 10; i++) + c.add(i); // Autoboxing + for(Integer i : c) + System.out.print(i + ", "); + } +} +/* Output: +0, 1, 2, 3, 4, 5, 6, 7, 8, 9, +*/ +``` + +这个例子仅适用 **Collection** 方法,所以任何继承自 **Collection** 的类的对象都可以使用。但是 **ArrayList** 是最基本的序列类型。 + +**add()** 方法的名称就表明它是在 **Collection** 中添加一个新元素。但是,文档中非常详细地叙述到 **add()** “要确保这个 **Collection** 包含指定的元素。”这是因为考虑到了 **Set** 的含义,因为在 **Set**中,只有当元素不存在时才会添加元素。在使用 **ArrayList** ,或任何其他类型的 **List** 时,**add()** 总是表示“把它放进去”,因为 **List** 不关心是否存在重复元素。 + +可以使用 *for-in* 语法来遍历所有的 **Collection** ,就像这里所展示的那样。在本章的后续部分,还将学习到一个更灵活的概念,*迭代器*。 ## 添加元素组 +在**java.util**包中的 **Arrays** 和 **Collections** 类中都有很多实用的方法,可以在一个 **Collection** 中添加一组元素。 **Arrays.asList()** 方法接受一个数组或是逗号分隔的元素列表(使用可变参数),并将其转换为 **List** 对象。 **Collections.addAll()** 方法接受一个 **Collection** 对象,以及一个数组或是一个逗号分隔的列表,将其中元素添加到 **Collection** 中。下边的示例展示了这两个方法,以及更通用的 **addAll()** 方法,所有 **Collection** 类型都包含该方法: + +```java +// collections/AddingGroups.java +// Adding groups of elements to Collection objects +import java.util.*; + +public class AddingGroups { + public static void main(String[] args) { + Collection collection = + new ArrayList<>(Arrays.asList(1, 2, 3, 4, 5)); + Integer[] moreInts = { 6, 7, 8, 9, 10 }; + collection.addAll(Arrays.asList(moreInts)); + // Runs significantly faster, but you can't + // construct a Collection this way: + Collections.addAll(collection, 11, 12, 13, 14, 15); + Collections.addAll(collection, moreInts); + // Produces a list "backed by" an array: + List list = Arrays.asList(16,17,18,19,20); + list.set(1, 99); // OK -- modify an element + // list.add(21); // Runtime error; the underlying + // array cannot be resized. + } +} +``` + +**Collection** 的构造器可以接受另一个 **Collection**,用它来将自身初始化。因此,可以使用 **Arrays.asList()** 来为这个构造器产生输入。但是, **Collections.addAll()** 运行得更快,而且很容易构建一个不包含元素的 **Collection** ,然后调用 **Collections.addAll()** ,因此这是首选方式。 + +**Collection.addAll()** 方法只能接受另一个 **Collection** 作为参数,因此它没有 **Arrays.asList()** 或 **Collections.addAll()** 灵活。这两个方法都使用可变参数列表。 + +也可以直接使用 **Arrays.asList()** 的输出作为一个 **List** ,但是这里的底层实现是数组,没法调整大小。如果尝试在这个 **List** 上调用 **add()** 或 **delete()**,由于这两个方法会尝试修改数组大小,所以会在运行时得到“Unsupported Operation(不支持的操作)”错误: + +```java +// collections/AsListInference.java +import java.util.*; + +class Snow {} +class Powder extends Snow {} +class Light extends Powder {} +class Heavy extends Powder {} +class Crusty extends Snow {} +class Slush extends Snow {} + +public class AsListInference { + public static void main(String[] args) { + List snow1 = Arrays.asList( + new Crusty(), new Slush(), new Powder()); + //- snow1.add(new Heavy()); // Exception + + List snow2 = Arrays.asList( + new Light(), new Heavy()); + //- snow2.add(new Slush()); // Exception + + List snow3 = new ArrayList<>(); + Collections.addAll(snow3, + new Light(), new Heavy(), new Powder()); + snow3.add(new Crusty()); + + // Hint with explicit type argument specification: + List snow4 = Arrays.asList( + new Light(), new Heavy(), new Slush()); + //- snow4.add(new Powder()); // Exception + } +} +``` + +在 **snow4** 中,注意 **Arrays.asList()** 中间的“hint”,告诉编译器 **Arrays.asList()** 生成的结果 **List** 类型的实际目标类型是什么。这称为*显式类型参数说明*(explicit type argument specification)。 ## 集合的打印 +必须使用 **Arrays.toString()** 来生成数组的可打印形式。但是打印集合无需任何帮助。下面是一个例子,这个例子中也介绍了基本的Java集合: +```java +// collections/PrintingCollections.java +// Collections print themselves automatically +import java.util.*; + +public class PrintingCollections { + static Collection + fill(Collection collection) { + collection.add("rat"); + collection.add("cat"); + collection.add("dog"); + collection.add("dog"); + return collection; + } + static Map fill(Map map) { + map.put("rat", "Fuzzy"); + map.put("cat", "Rags"); + map.put("dog", "Bosco"); + map.put("dog", "Spot"); + return map; + } + public static void main(String[] args) { + System.out.println(fill(new ArrayList<>())); + System.out.println(fill(new LinkedList<>())); + System.out.println(fill(new HashSet<>())); + System.out.println(fill(new TreeSet<>())); + System.out.println(fill(new LinkedHashSet<>())); + System.out.println(fill(new HashMap<>())); + System.out.println(fill(new TreeMap<>())); + System.out.println(fill(new LinkedHashMap<>())); + } +} +/* Output: +[rat, cat, dog, dog] +[rat, cat, dog, dog] +[rat, cat, dog] +[cat, dog, rat] +[rat, cat, dog] +{rat=Fuzzy, cat=Rags, dog=Spot} +{cat=Rags, dog=Spot, rat=Fuzzy} +{rat=Fuzzy, cat=Rags, dog=Spot} +*/ +``` + +这显示了Java集合库中的两个主要类型。它们的区别在于集合中的每个“槽”(slot)保存的元素个数。 **Collection** 类型在每个槽中只能保存一个元素。此类集合包括: **List**, 它以特定的顺序保存一组元素; **Set** ,其中元素不允许重复; **Queue** ,只能在集合一端插入对象,并从另一端移除对象(就本例而言,这只是查看序列的另一种方式,因此并没有显示它)。 **Map** 在每个槽中存放了两个元素,即*键*和与之关联的*值*。 + +默认的打印行为,(使用集合几桶的toString()方法)即可生成可读性很好的结果。 **Collection** 打印出的内容用方括号括住,每个元素由逗号分隔。 **Map** 则由大括号括住,每个键和值用等号连接(键在左侧,值在右侧)。 + +第一个 **fill()** 方法适用于所有类型的 **Collection** ,这些类型都实现了 **add()** 方法以添加新元素。 + +**ArrayList** 和 **LinkedList** 都是 **List** 的类型,从输出中可以看出,它们都按插入顺序保存元素。两者之间的区别不仅在于执行某些类型的操作时的性能,而且 **LinkedList** 包含的操作多于 **ArrayList** 。本章后面将对这些内容进行更全面的探讨。 + +**HashSet** , **TreeSet** 和 **LinkedHashSet** 是 **Set** 的类型。从输出中可以看到, **Set** 仅保存每个相同项中的一个,并且不同的 **Set** 实现存储元素的方式也不同。 **HashSet** 使用相当复杂的方法存储元素,这在[附录:集合主题]()中进行了探讨。现在只需要知道,这种技术是检索元素的最快方法,因此,存储顺序看似荒谬(通常只关心某事物是否是 **Set** 的成员,而存储顺序并不重要)。如果存储顺序很重要,则可以使用**TreeSet** ,它将按比较结果的升序保存对象)或 **LinkedHashSet** ,它按照被添加的先后顺序保存对象。 + +**Map** (也称为*关联数组*)使用*键*来查找对象,就像一个简单的数据库。所关联的对象称为*值*。 假设有一个 **Map** 将美国州名与它们的首府联系在一起,如果想要俄亥俄州(Ohio)的首府,可以用“Ohio”作为键来查找,几乎就像使用数组下标一样。 这是由于这种行为,对于每个键, **Map** 只接受一次。 + +**Map.put(key, value)** 添加一个所想要添加的值并将它与一个键(用来查找值)相关联。 **Map.get(key)** 生成与该键相关联的值。上面的示例仅添加键值对,并没有执行查找。这将在稍后展示。 + + +请注意,这里没有指定(或考虑) **Map** 的大小,因为它会自动调整大小。 此外, **Map** 还知道如何打印自己,它会显示相关联的键和值。 + +本例使用了 **Map** 的三种基本风格: **HashMap** , **TreeMap**和 **LinkedHashMap** 。 + +键和值保存在 **HashMap** 中的顺序不是插入顺序,因为 **HashMap** 实现使用了非常快速的算法来控制顺序。 **TreeMap** 通过比较结果的升序来保存键, **LinkedHashMap** 在保持 **HashMap** 查找速度的同时按键的插入顺序保存键。 ## 列表List +**List**s承诺以特定顺序保存元素。 **List** 接口在 **Collection** 的基础上添加了许多方法,允许在 **List** 的中间插入和删除元素。 + +有两种类型的 **List** : + +- 基本的 **ArrayList** ,擅长随机访问元素,但在 **List** 中间插入和删除元素时速度较慢。 +- **LinkedList** ,它通过代价较低的在 **List** 中间进行的插入和删除操作,提供了优化的顺序访问。 **LinkedList** 对于随机访问来说相对较慢,但它具有比 **ArrayList** 更大的特征集。 + +下面的示例导入 **typeinfo.pets** ,超前使用了[类型信息]()一章中的类库。这个类库包含了 **Pet** 类层次结构,以及用于随机生成 **Pet** 对象的一些工具类。此时不需要了解完整的详细信息,只需要知道两点: + +1. 有一个 **Pet** 类,以及 **Pet** 的各种子类型。 +2. 静态的 **Pets.arrayList()** 方法返回一个填充了随机选取的 **Pet** 对象的 **ArrayList**: + +```java +// collections/ListFeatures.java +import typeinfo.pets.*; +import java.util.*; + +public class ListFeatures { + public static void main(String[] args) { + Random rand = new Random(47); + List pets = Pets.list(7); + System.out.println("1: " + pets); + Hamster h = new Hamster(); + pets.add(h); // Automatically resizes + System.out.println("2: " + pets); + System.out.println("3: " + pets.contains(h)); + pets.remove(h); // Remove by object + Pet p = pets.get(2); + System.out.println( + "4: " + p + " " + pets.indexOf(p)); + Pet cymric = new Cymric(); + System.out.println("5: " + pets.indexOf(cymric)); + System.out.println("6: " + pets.remove(cymric)); + // Must be the exact object: + System.out.println("7: " + pets.remove(p)); + System.out.println("8: " + pets); + pets.add(3, new Mouse()); // Insert at an index + System.out.println("9: " + pets); + List sub = pets.subList(1, 4); + System.out.println("subList: " + sub); + System.out.println("10: " + pets.containsAll(sub)); + Collections.sort(sub); // In-place sort + System.out.println("sorted subList: " + sub); + // Order is not important in containsAll(): + System.out.println("11: " + pets.containsAll(sub)); + Collections.shuffle(sub, rand); // Mix it up + System.out.println("shuffled subList: " + sub); + System.out.println("12: " + pets.containsAll(sub)); + List copy = new ArrayList<>(pets); + sub = Arrays.asList(pets.get(1), pets.get(4)); + System.out.println("sub: " + sub); + copy.retainAll(sub); + System.out.println("13: " + copy); + copy = new ArrayList<>(pets); // Get a fresh copy + copy.remove(2); // Remove by index + System.out.println("14: " + copy); + copy.removeAll(sub); // Only removes exact objects + System.out.println("15: " + copy); + copy.set(1, new Mouse()); // Replace an element + System.out.println("16: " + copy); + copy.addAll(2, sub); // Insert a list in the middle + System.out.println("17: " + copy); + System.out.println("18: " + pets.isEmpty()); + pets.clear(); // Remove all elements + System.out.println("19: " + pets); + System.out.println("20: " + pets.isEmpty()); + pets.addAll(Pets.list(4)); + System.out.println("21: " + pets); + Object[] o = pets.toArray(); + System.out.println("22: " + o[3]); + Pet[] pa = pets.toArray(new Pet[0]); + System.out.println("23: " + pa[3].id()); + } +} +/* Output: +1: [Rat, Manx, Cymric, Mutt, Pug, Cymric, Pug] +2: [Rat, Manx, Cymric, Mutt, Pug, Cymric, Pug, Hamster] +3: true +4: Cymric 2 +5: -1 +6: false +7: true +8: [Rat, Manx, Mutt, Pug, Cymric, Pug] +9: [Rat, Manx, Mutt, Mouse, Pug, Cymric, Pug] +subList: [Manx, Mutt, Mouse] +10: true +sorted subList: [Manx, Mouse, Mutt] +11: true +shuffled subList: [Mouse, Manx, Mutt] +12: true +sub: [Mouse, Pug] +13: [Mouse, Pug] +14: [Rat, Mouse, Mutt, Pug, Cymric, Pug] +15: [Rat, Mutt, Cymric, Pug] +16: [Rat, Mouse, Cymric, Pug] +17: [Rat, Mouse, Mouse, Pug, Cymric, Pug] +18: false +19: [] +20: true +21: [Manx, Cymric, Rat, EgyptianMau] +22: EgyptianMau +23: 14 +*/ +``` + +打印行都编了号,因此输出可以与源代码相关。 第1行输出展示了原始的由 **Pet** 组成的 **List** 。 与数组不同, **List** 可以在创建后添加或删除元素,并自行调整大小。这正是它的重要价值:一种可修改的序列。在第2行输出中可以看到添加一个 **Hamster** 的结果,该对象将被追加到列表的末尾。 + +可以使用 **contains()** 方法确定对象是否在列表中。如果要删除一个对象,可以将该对象的引用传递给 **remove()** 方法。同样,如果有一个对象的引用,可以使用 **indexOf()** 在 **List** 中找到该对象所在位置的下标号,如第4行输出所示中所示。 + +当确定元素是否是属于某个 **List** ,寻找某个元素的索引,以及通过引用从 **List** 中删除元素时,都会用到 **equals()** 方法(根类 **Object** 的一个方法)。每个 **Pet** 被定义为一个唯一的对象,所以即使列表中已经有两个 **Cymrics** ,如果再创建一个新的 **Cymric** 对象并将其传递给 **indexOf()** 方法,结果仍为 **-1** (表示未找到),并且尝试调用 **remove()** 方法来删除这个对象将返回 **false** 。对于其他类, **equals()** 的定义可能有所不同。例如,如果两个 **String** 的内容相同,则这两个 **String** 相等。因此,为了防止出现意外,请务必注意 **List** 行为会根据 **equals()** 行为而发生变化。 + +第7、8行输出展示了删除与 **List** 中的对象完全匹配的对象是成功的。 + +可以在 **List** 的中间插入一个元素,就像在第9行输出和它之前的代码那样。但这会带来一个问题:对于 **LinkedList** ,在列表中间插入和删除都是廉价操作(在本例中,除了对列表中间进行的真正的随机访问),但对于 **ArrayList** ,这可是代价高昂的操作。这是否意味着永远不应该在 **ArrayList** 的中间插入元素,并最好是转换为 **LinkedList** ?不,它只是意味着你应该意识到这个问题,如果你开始在某个 **ArrayList** 中间进执行很多插入操作,并且程序开始变慢,那么你应该看看你的 **List** 实现有可能就是罪魁祸首(发现此类瓶颈的最佳方式是使用仿真器)。优化是一个很棘手的问题,最好的策略就是置之不顾,直到发现必须要去担心它了(尽管去理解这些问题总是一个很好的主意)。 + +**subList()** 方法可以轻松地从更大的列表中创建切片,当将切片结果传递给原来这个较大的列表的 **containsAll()** 方法时,很自然地会得到 **true**。请注意,顺序并不重要,在第11、12行输出中可以看到,在 **sub** 上调用直观命名的 **Collections.sort()** 和 **Collections.shuffle()** 方法,不会影响 **containsAll()** 的结果。 **subList()** 所产生的列表的幕后支持就是原始列表。因此,对所返回列表的更改都将会反映在原始列表中,反之亦然。 + +**retainAll()** 方法实际上是一个“集合交集”操作,在本例中,它保留了同时在 **copy** 和 **sub** 中的所有元素。请再次注意,所产生的结果行为依赖于 **equals()** 方法。 + +第14行输出展示了使用索引号来删除元素的结果,与通过对象引用来删除元素相比,它显得更加直观,因为在使用索引时,不必担心 **equals()** 的行为。 + +**removeAll()** 方法也是基于 **equals()** 方法运行的。 顾名思义,它会从 **List** 中删除在参数 **List** 中的所有元素。 + +**set()** 方法的命名显得很不合时宜,因为它与 **Set** 类存在潜在的冲突。在这里使用“replace”可能更适合,因为它的功能是用第二个参数替换索引处的元素(第一个参数)。 + +第17行输出表明,对于 **List** ,有一个重载的 **addAll()** 方法可以将新列表插入到原始列表的中间,而不仅仅只能用 **Collection** 的 **addAll()** 方法将追加到列表末尾。 + +第18-20行输出展示了 **isEmpty()** 和 **clear()** 方法的效果。 + +第22、23行输出展示了如何使用 **toArray()** 方法将任意的 **Collection** 转换为数组。这是一个重载方法,其无参版本返回一个 **Object** 数组,但是如果将目标类型的数组传递给这个重载版本,那么它会生成一个指定类型的数组(假设它通过了类型检查)。如果参数数组太小而无法容纳 **List** 中的所有元素(就像本例一样),则 **toArray()** 会创建一个具有合适尺寸的新数组。 **Pet** 对象有一个 **id()** 方法,可以在所产生的数组中的对象上调用这个方法。 ## 迭代器Iterators +在任何集合中,都必须有某种方式可以插入元素并再次获取它们。毕竟,保存事物是集合最基本的工作。对于 **List** , **add()** 是插入元素的一种方式, **get()** 是获取元素的一种方式。 + +如果从更高层次的角度考虑,会发现这里有个缺点:要使用集合,必须对集合的确切类型编程。这一开始可能看起来不是很糟糕,但是考虑下面的情况:如果原本是对 **List** 编码的,但是后来发现如果能够将相同的代码应用于 **Set** 会更方便,此时应该怎么做?或者假设想从一开始就编写一段通用代码,它不知道或不关心它正在使用什么类型的集合,因此它可以用于不同类型的集合,那么如何才能不重写代码就可以应用于不同类型的集合? + +*迭代器*(也是一种设计模式)的概念实现了这种抽象。迭代器是一个对象,它在一个序列中移动并选择该序列中的每个对象,而客户端程序员不知道或不关心该序列的底层结构。另外,迭代器通常被称为*轻量级对象*(lightweight object):创建它的代价小。因此,经常可以看到一些对迭代器有些奇怪的约束。例如,Java的 **Iterator** 只能单向移动。这个 **Iterator** 只能用来: +1. 使用 **iterator()** 方法要求集合返回一个 **Iterator**。 **Iterator** 将准备好返回序列中的第一个元素。 +2. 使用 **next()** 方法获得序列中的下一个元素。 +3. 使用 **hasNext()** 方法检查序列中是否还有元素。 +4. 使用 **remove()** 方法将迭代器最近返回的那个元素删除。 + +为了观察它的工作方式,这里再次使用[类型信息]()章节中的 **Pet** 工具: + +```java +// collections/SimpleIteration.java +import typeinfo.pets.*; +import java.util.*; + +public class SimpleIteration { + public static void main(String[] args) { + List pets = Pets.list(12); + Iterator it = pets.iterator(); + while(it.hasNext()) { + Pet p = it.next(); + System.out.print(p.id() + ":" + p + " "); + } + System.out.println(); + // A simpler approach, when possible: + for(Pet p : pets) + System.out.print(p.id() + ":" + p + " "); + System.out.println(); + // An Iterator can also remove elements: + it = pets.iterator(); + for(int i = 0; i < 6; i++) { + it.next(); + it.remove(); + } + System.out.println(pets); + } +} +/* Output: +0:Rat 1:Manx 2:Cymric 3:Mutt 4:Pug 5:Cymric 6:Pug +7:Manx 8:Cymric 9:Rat 10:EgyptianMau 11:Hamster +0:Rat 1:Manx 2:Cymric 3:Mutt 4:Pug 5:Cymric 6:Pug +7:Manx 8:Cymric 9:Rat 10:EgyptianMau 11:Hamster +[Pug, Manx, Cymric, Rat, EgyptianMau, Hamster] +*/ +``` + +有了 **Iterator** ,就不必再为集合中元素的数量操心了。这是由 **hasNext()** 和 **next()** 关心的事情。 + +如果只是想向前遍历 **List** ,并不打算修改 **List** 对象本身,那么使用 *for-in* 语法更加简洁。 + +**Iterator** 还可以删除由 **next()** 生成的最后一个元素,这意味着在调用 **remove()** 之前必须先调用 **next()** 。[^4] + +在集合中的每个对象上执行操作,这种思想十分强大,并且贯穿于本书。 + +现在考虑创建一个 **display()** 方法,它不必知晓集合的确切类型: + +```java +// collections/CrossCollectionIteration.java +import typeinfo.pets.*; +import java.util.*; + +public class CrossCollectionIteration { + public static void display(Iterator it) { + while(it.hasNext()) { + Pet p = it.next(); + System.out.print(p.id() + ":" + p + " "); + } + System.out.println(); + } + public static void main(String[] args) { + List pets = Pets.list(8); + LinkedList petsLL = new LinkedList<>(pets); + HashSet petsHS = new HashSet<>(pets); + TreeSet petsTS = new TreeSet<>(pets); + display(pets.iterator()); + display(petsLL.iterator()); + display(petsHS.iterator()); + display(petsTS.iterator()); + } +} +/* Output: +0:Rat 1:Manx 2:Cymric 3:Mutt 4:Pug 5:Cymric 6:Pug +7:Manx +0:Rat 1:Manx 2:Cymric 3:Mutt 4:Pug 5:Cymric 6:Pug +7:Manx +0:Rat 1:Manx 2:Cymric 3:Mutt 4:Pug 5:Cymric 6:Pug +7:Manx +5:Cymric 2:Cymric 7:Manx 1:Manx 3:Mutt 6:Pug 4:Pug +0:Rat +*/ +``` + +**display()** 方法不包含任何有关它所遍历的序列的类型信息。这也展示了 **Iterator** 的真正威力:能够将遍历序列的操作与该序列的底层结构分离。出于这个原因,我们有时会说:迭代器统一了对集合的访问方式。 + +我们可以使用 **Iterable** 接口生成上一个示例的更简洁版本,该接口描述了“可以产生 **Iterator** 的任何东西”: + +```java +// collections/CrossCollectionIteration2.java +import typeinfo.pets.*; +import java.util.*; + +public class CrossCollectionIteration2 { + public static void display(Iterable ip) { + Iterator it = ip.iterator(); + while(it.hasNext()) { + Pet p = it.next(); + System.out.print(p.id() + ":" + p + " "); + } + System.out.println(); + } + public static void main(String[] args) { + List pets = Pets.list(8); + LinkedList petsLL = new LinkedList<>(pets); + HashSet petsHS = new HashSet<>(pets); + TreeSet petsTS = new TreeSet<>(pets); + display(pets); + display(petsLL); + display(petsHS); + display(petsTS); + } +} +/* Output: +0:Rat 1:Manx 2:Cymric 3:Mutt 4:Pug 5:Cymric 6:Pug +7:Manx +0:Rat 1:Manx 2:Cymric 3:Mutt 4:Pug 5:Cymric 6:Pug +7:Manx +0:Rat 1:Manx 2:Cymric 3:Mutt 4:Pug 5:Cymric 6:Pug +7:Manx +5:Cymric 2:Cymric 7:Manx 1:Manx 3:Mutt 6:Pug 4:Pug +0:Rat +*/ +``` + +这里所有的类都是 **Iterable** ,所以现在对 **display()** 的调用显然更简单。 + + +### ListIterator + +**ListIterator** 是一个更强大的 **Iterator**子类型,它只能由各种 **List** 类生成。虽然 **Iterator** 只能向前移动,但 **ListIterator** 可以双向移动。它还可以生成相对于迭代器在列表中指向的当前位置的后一个和前一个元素的索引,并且可以使用 **set()** 方法替换它访问过的最后一个元素。可以通过调用 **listIterator()** 方法来生成指向 **List** 开头处的 **ListIterator** ,还可以通过调用** listIterator(n)** 创建一个一开始就指向列表索引号为 **n** 的元素处的 **ListIterator** 。 下面的示例演示了所有这些能力: + +```java +// collections/ListIteration.java +import typeinfo.pets.*; +import java.util.*; + +public class ListIteration { + public static void main(String[] args) { + List pets = Pets.list(8); + ListIterator it = pets.listIterator(); + while(it.hasNext()) + System.out.print(it.next() + + ", " + it.nextIndex() + + ", " + it.previousIndex() + "; "); + System.out.println(); + // Backwards: + while(it.hasPrevious()) + System.out.print(it.previous().id() + " "); + System.out.println(); + System.out.println(pets); + it = pets.listIterator(3); + while(it.hasNext()) { + it.next(); + it.set(Pets.get()); + } + System.out.println(pets); + } +} +/* Output: +Rat, 1, 0; Manx, 2, 1; Cymric, 3, 2; Mutt, 4, 3; Pug, +5, 4; Cymric, 6, 5; Pug, 7, 6; Manx, 8, 7; +7 6 5 4 3 2 1 0 +[Rat, Manx, Cymric, Mutt, Pug, Cymric, Pug, Manx] +[Rat, Manx, Cymric, Cymric, Rat, EgyptianMau, Hamster, +EgyptianMau] +*/ +``` + +**Pets.get()** 方法用来从位置3开始替换 **List** 中的所有Pet对象。 ## 链表LinkedList +**LinkedList** 也像 **ArrayList** 一样实现了基本的 **List** 接口,但它在 **List** 中间执行插入和删除操作时,比 **ArrayList** 更高效。但在随机访问操作效率方面却要逊色一些。 +**LinkedList还添加了一些方法,使其可以被用作栈、队列或双端队列(deque)** 。在这些方法中,有些彼此之间可能只是名称有些差异,或者只存在些许差异,以使得这些名字在特定用法的上下文环境中更加适用(特别是在 **Queue** 中)。例如: + +- **getFirst()** 和 **element()** 是相同的,它们都返回列表的头部(第一个元素)而并不删除它,如果 **List** 为空,则抛出 **NoSuchElementException** 异常。 **peek()** 方法与这两个方法只是稍有差异,它在列表为空时返回 **null** 。 +- **removeFirst()** 和 **remove()** 也是相同的,它们删除并返回列表的头部元素,并在列表为空时抛出 **NoSuchElementException** 异常。 **poll()** 稍有差异,它在列表为空时返回 **null** 。 +- **addFirst()** 在列表的开头插入一个元素。 +- **offer()** 与 **add()** 和 **addLast()** 相同。 它们都在列表的尾部(末尾)添加一个元素。 +- **removeLast()** 删除并返回列表的最后一个元素。 + +下面的示例展示了这些功能之间基本的相似性和差异性。它并不是重复执行 **ListFeatures.java** 中所示的行为: + +```java +// collections/LinkedListFeatures.java +import typeinfo.pets.*; +import java.util.*; + +public class LinkedListFeatures { + public static void main(String[] args) { + LinkedList pets = + new LinkedList<>(Pets.list(5)); + System.out.println(pets); + // Identical: + System.out.println( + "pets.getFirst(): " + pets.getFirst()); + System.out.println( + "pets.element(): " + pets.element()); + // Only differs in empty-list behavior: + System.out.println("pets.peek(): " + pets.peek()); + // Identical; remove and return the first element: + System.out.println( + "pets.remove(): " + pets.remove()); + System.out.println( + "pets.removeFirst(): " + pets.removeFirst()); + // Only differs in empty-list behavior: + System.out.println("pets.poll(): " + pets.poll()); + System.out.println(pets); + pets.addFirst(new Rat()); + System.out.println("After addFirst(): " + pets); + pets.offer(Pets.get()); + System.out.println("After offer(): " + pets); + pets.add(Pets.get()); + System.out.println("After add(): " + pets); + pets.addLast(new Hamster()); + System.out.println("After addLast(): " + pets); + System.out.println( + "pets.removeLast(): " + pets.removeLast()); + } +} +/* Output: +[Rat, Manx, Cymric, Mutt, Pug] +pets.getFirst(): Rat +pets.element(): Rat +pets.peek(): Rat +pets.remove(): Rat +pets.removeFirst(): Manx +pets.poll(): Cymric +[Mutt, Pug] +After addFirst(): [Rat, Mutt, Pug] +After offer(): [Rat, Mutt, Pug, Cymric] +After add(): [Rat, Mutt, Pug, Cymric, Pug] +After addLast(): [Rat, Mutt, Pug, Cymric, Pug, Hamster] +pets.removeLast(): Hamster +*/ +``` + +**Pets.list()** 的结果被传递给 **LinkedList** 的构造器,以便使用它来填充 **LinkedList** 。如果查看 **Queue** 接口就会发现,它在 **LinkedList** 的基础上添加了 **element()** , **offer()** , **peek()** , **poll()** 和 **remove()** 方法,以使其可以成为一个 **Queue** 的实现。 **Queue** 的完整示例将在本章稍后给出。 + ## 堆栈Stack +堆栈是“后进先出”(LIFO)集合。它有时被称为*叠加栈*(pushdown stack),因为最后“压入”(push)栈的元素,第一个被“弹出”(pop)栈。经常用来类比栈的事物是带有弹簧支架的自助餐厅托盘。最后装入的托盘总是最先拿出来使用的。 + +Java 1.0中附带了一个 **Stack** 类,结果设计得很糟糕(为了向后兼容,我们永远坚持Java中的旧设计错误)。Java 6添加了 **ArrayDeque** ,其中包含直接实现堆栈功能的方法: + +```java +// collections/StackTest.java +import java.util.*; + +public class StackTest { + public static void main(String[] args) { + Deque stack = new ArrayDeque<>(); + for(String s : "My dog has fleas".split(" ")) + stack.push(s); + while(!stack.isEmpty()) + System.out.print(stack.pop() + " "); + } +} +/* Output: +fleas has dog My +*/ +``` + +即使它是作为一个堆栈在使用,我们仍然必须将其声明为 **Deque** 。有时一个名为 **Stack** 的类更能把事情讲清楚: + +```java +// onjava/Stack.java +// A Stack class built with an ArrayDeque +package onjava; +import java.util.Deque; +import java.util.ArrayDeque; + +public class Stack { + private Deque storage = new ArrayDeque<>(); + public void push(T v) { storage.push(v); } + public T peek() { return storage.peek(); } + public T pop() { return storage.pop(); } + public boolean isEmpty() { return storage.isEmpty(); } + @Override + public String toString() { + return storage.toString(); + } +} +``` + +这里引入了使用泛型的类定义的最简单的可能示例。类名称后面的 **** 告诉编译器这是一个参数化类型,而其中的类型参数,即在类被使用时将会被实际类型替换的参数,就是 **T** 。基本上,这个类是在声明“我们在定义一个可以持有 **T** 类型对象的 **Stack** 。” **Stack** 是使用 **ArrayDeque** 实现的,而 **ArrayDeque** 也被告知它将持有 **T** 类型对象。注意, **push()** 接受类型为 **T** 的对象,而 **peek()** 和 **pop()** 返回类型为 **T** 的对象。 **peek()** 方法将返回栈顶元素,但并不将其从栈顶删除,而 **pop()** 删除并返回顶部元素。 + +如果只需要栈的行为,那么使用继承是不合适的,因为这将产生一个具有 **ArrayDeque** 的其它所有方法的类(在[附录:集合主题]()中将会看到, **Java 1.0** 设计者在创建 **java.util.Stack** 时,就犯了这个错误)。使用组合,可以选择要公开的方法以及如何命名它们。 + +下面将使用 **StackTest.java** 中的相同代码来演示这个新的 **Stack** 类: + +```java +// collections/StackTest2.java +import onjava.*; + +public class StackTest2 { + public static void main(String[] args) { + Stack stack = new Stack<>(); + for(String s : "My dog has fleas".split(" ")) + stack.push(s); + while(!stack.isEmpty()) + System.out.print(stack.pop() + " "); + } +} +/* Output: +fleas has dog My +*/ +``` + +如果想在自己的代码中使用这个 **Stack** 类,当在创建其实例时,就需要完整指定包名,或者更改这个类的名称;否则,就有可能会与 **java.util** 包中的 **Stack** 发生冲突。例如,如果我们在上面的例子中导入 **java.util.***,那么就必须使用包名来防止冲突: + +```java +// collections/StackCollision.java + +public class StackCollision { + public static void main(String[] args) { + onjava.Stack stack = new onjava.Stack<>(); + for(String s : "My dog has fleas".split(" ")) + stack.push(s); + while(!stack.isEmpty()) + System.out.print(stack.pop() + " "); + System.out.println(); + java.util.Stack stack2 = + new java.util.Stack<>(); + for(String s : "My dog has fleas".split(" ")) + stack2.push(s); + while(!stack2.empty()) + System.out.print(stack2.pop() + " "); + } +} +/* Output: +fleas has dog My +fleas has dog My +*/ +``` + +尽管已经有了 **java.util.Stack** ,但是 **ArrayDeque** 可以产生更好的 **Stack** ,因此更可取。 + +还可以使用显式导入来控制对“首选” **Stack** 实现的选择: +```java +import onjava.Stack; +``` + +现在,任何对 **Stack** 的引用都将选择 **onjava** 版本,而在选择 **java.util.Stack** 时,必须使用全限定名称(full qualification)。 ## 集合Set +**Set** 不保存重复的元素。 如果试图将相同对象的多个实例添加到 **Set** 中,那么它会阻止这种行为。 **Set** 最常见的用途是测试归属性,可以很轻松地询问某个对象是否在一个 **Set** 中。因此,查找通常是 **Set** 最重要的操作,因此通常会选择 **HashSet** 实现,该实现针对快速查找进行了优化。 + +**Set** 具有与 **Collection** 相同的接口,因此没有任何额外的功能,不像前面两种不同类型的 **List** 那样。实际上, **Set** 就是一个 **Collection** ,只是行为不同。(这是继承和多态思想的典型应用:表现不同的行为。)**Set** 根据对象的“值”确定归属性,更复杂的问题将在[附录:集合主题]()中介绍。 + +下面是使用存放 **Integer** 对象的 **HashSet** 的示例: + +```java +// collections/SetOfInteger.java +import java.util.*; + +public class SetOfInteger { + public static void main(String[] args) { + Random rand = new Random(47); + Set intset = new HashSet<>(); + for(int i = 0; i < 10000; i++) + intset.add(rand.nextInt(30)); + System.out.println(intset); + } +} +/* Output: +[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, +16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29] +*/ +``` + +在0到29之间的10000个随机整数被添加到 **Set** 中,因此可以想象每个值都重复了很多次。但是从结果中可以看到,每一个数只有一个实例出现在结果中。 + +早期Java版本中的 **HashSet** 产生的输出没有可辨别的顺序。这是因为出于对速度的追求, **HashSet** 使用了散列,请参阅[附录:集合主题]()一章。由 **HashSet** 维护的顺序与 **TreeSet** 或 **LinkedHashSet** 不同,因为它们的实现具有不同的元素存储方式。 **TreeSet** 将元素存储在红-黑树数据结构中,而 **HashSet** 使用散列函数。 **LinkedHashSet** 因为查询速度的原因也使用了散列,但是看起来使用了链表来维护元素的插入顺序。显然,哈希算法已经更改,现在 **Integer** 按顺序排序。 但是,您不应该依赖此行为: + +```java +// collections/SetOfString.java +import java.util.*; + +public class SetOfString { + public static void main(String[] args) { + Set colors = new HashSet<>(); + for(int i = 0; i < 100; i++) { + colors.add("Yellow"); + colors.add("Blue"); + colors.add("Red"); + colors.add("Red"); + colors.add("Orange"); + colors.add("Yellow"); + colors.add("Blue"); + colors.add("Purple"); + } + System.out.println(colors); + } +} +/* Output: +[Red, Yellow, Blue, Purple, Orange] +*/ +``` + +**String** 对象似乎没有排序。要对结果进行排序,一种方法是使用 **TreeSet** 而不是 **HashSet** : + +```java +// collections/SortedSetOfString.java +import java.util.*; + +public class SortedSetOfString { + public static void main(String[] args) { + Set colors = new TreeSet<>(); + for(int i = 0; i < 100; i++) { + colors.add("Yellow"); + colors.add("Blue"); + colors.add("Red"); + colors.add("Red"); + colors.add("Orange"); + colors.add("Yellow"); + colors.add("Blue"); + colors.add("Purple"); + } + System.out.println(colors); + } +} +/* Output: +[Blue, Orange, Purple, Red, Yellow] +*/ +``` + +最常见的操作之一是使用 **contains()** 测试成员归属性,但也有一些其它操作,可能会让你想起在小学学过的文氏图(译者注:用圆表示集与集之间关系的图): + +```java +// collections/SetOperations.java +import java.util.*; + +public class SetOperations { + public static void main(String[] args) { + Set set1 = new HashSet<>(); + Collections.addAll(set1, + "A B C D E F G H I J K L".split(" ")); + set1.add("M"); + System.out.println("H: " + set1.contains("H")); + System.out.println("N: " + set1.contains("N")); + Set set2 = new HashSet<>(); + Collections.addAll(set2, "H I J K L".split(" ")); + System.out.println( + "set2 in set1: " + set1.containsAll(set2)); + set1.remove("H"); + System.out.println("set1: " + set1); + System.out.println( + "set2 in set1: " + set1.containsAll(set2)); + set1.removeAll(set2); + System.out.println( + "set2 removed from set1: " + set1); + Collections.addAll(set1, "X Y Z".split(" ")); + System.out.println( + "'X Y Z' added to set1: " + set1); + } +} +/* Output: +H: true +N: false +set2 in set1: true +set1: [A, B, C, D, E, F, G, I, J, K, L, M] +set2 in set1: false +set2 removed from set1: [A, B, C, D, E, F, G, M] +'X Y Z' added to set1: [A, B, C, D, E, F, G, M, X, Y, +Z] +*/ +``` + +这些方法名都是自解释的,JDK文档中还有一些其它的方法。 + +能够产生每个元素都唯一的列表是相当有用的功能。例如,假设想要列出上面的 **SetOperations.java** 文件中的所有单词,通过使用本书后面介绍的 **java.nio.file.Files.readAllLines()** 方法,可以打开一个文件,并将其作为一个 **List\** 读取,每个 **String** 都是输入文件中的一行: + +```java +// collections/UniqueWords.java +import java.util.*; +import java.nio.file.*; + +public class UniqueWords { + public static void + main(String[] args) throws Exception { + List lines = Files.readAllLines( + Paths.get("SetOperations.java")); + Set words = new TreeSet<>(); + for(String line : lines) + for(String word : line.split("\\W+")) + if(word.trim().length() > 0) + words.add(word); + System.out.println(words); + } +} +/* Output: +[A, B, C, Collections, D, E, F, G, H, HashSet, I, J, K, +L, M, N, Output, Set, SetOperations, String, System, X, +Y, Z, add, addAll, added, args, class, collections, +contains, containsAll, false, from, import, in, java, +main, new, out, println, public, remove, removeAll, +removed, set1, set2, split, static, to, true, util, +void] +*/ +``` + +我们逐步浏览文件中的每一行,并使用 **String.split()** 将其分解为单词,这里使用正则表达式 **\\\ W +** ,这意味着它会依据一个或多个(即 **+** )非单词字母来拆分字符串(正则表达式将在[字符串]()章节介绍)。每个结果单词都会添加到 **Set words** 中。因为它是 **TreeSet** ,所以对结果进行排序。这里,排序是按*字典顺序*(lexicographically)完成的,因此大写和小写字母位于不同的组中。如果想按*字母顺序*(alphabetically)对其进行排序,可以向 **TreeSet** 构造器传入 **String.CASE_INSENSITIVE_ORDER** 比较器(比较器是一个建立排序顺序的对象): + +```java +// collections/UniqueWordsAlphabetic.java +// Producing an alphabetic listing +import java.util.*; +import java.nio.file.*; + +public class UniqueWordsAlphabetic { + public static void + main(String[] args) throws Exception { + List lines = Files.readAllLines( + Paths.get("SetOperations.java")); + Set words = + new TreeSet<>(String.CASE_INSENSITIVE_ORDER); + for(String line : lines) + for(String word : line.split("\\W+")) + if(word.trim().length() > 0) + words.add(word); + System.out.println(words); + } +} +/* Output: +[A, add, addAll, added, args, B, C, class, collections, +contains, containsAll, D, E, F, false, from, G, H, +HashSet, I, import, in, J, java, K, L, M, main, N, new, +out, Output, println, public, remove, removeAll, +removed, Set, set1, set2, SetOperations, split, static, +String, System, to, true, util, void, X, Y, Z] +*/ +``` + +**Comparator** 比较器将在[数组]()章节详细介绍。 ## 映射Map +将对象映射到其他对象的能力是解决编程问题的有效方法。例如,考虑一个程序,它被用来检查Java的 **Random** 类的随机性。理想情况下, **Random** 会产生完美的数字分布,但为了测试这一点,则需要生成大量的随机数,并计算落在各种范围内的数字个数。 **Map** 可以很容易地解决这个问题。在本例中,键是 **Random** 生成的数字,而值是该数字出现的次数: + +```java +// collections/Statistics.java +// (c)2017 MindView LLC: see Copyright.txt +// We make no guarantees that this code is fit for any purpose. +// Visit http://OnJava8.com for more book information. +// Simple demonstration of HashMap +import java.util.*; + +public class Statistics { + public static void main(String[] args) { + Random rand = new Random(47); + Map m = new HashMap<>(); + for(int i = 0; i < 10000; i++) { + // Produce a number between 0 and 20: + int r = rand.nextInt(20); + Integer freq = m.get(r); // [1] + m.put(r, freq == null ? 1 : freq + 1); + } + System.out.println(m); + } +} +/* Output: +{0=481, 1=502, 2=489, 3=508, 4=481, 5=503, 6=519, +7=471, 8=468, 9=549, 10=513, 11=531, 12=521, 13=506, +14=477, 15=497, 16=533, 17=509, 18=478, 19=464} +*/ +``` + +- **[1]** 自动包装机制将随机生成的 **int** 转换为可以与 **HashMap** 一起使用的 **Integer** 引用(不能使用基本类型的集合)。如果键不在集合中,则 **get()** 返回 **null** (这意味着这是第一次找到该数字)。否则, **get()** 会为键生成与之关联的 **Integer** 值,然后该值被递增(自动包装机制再次简化了表达式,但实际上确实发生了对 **Integer** 的装箱和拆箱)。 + +接下来的示例将使用一个 **String** 描述来查找 **Pet** 对象。它还展示了通过使用 **containsKey()**和 **containsValue()** 方法去测试一个 **Map** ,以查看它是否包含某个键或某个值: + +```java +// collections/PetMap.java +import typeinfo.pets.*; +import java.util.*; + +public class PetMap { + public static void main(String[] args) { + Map petMap = new HashMap<>(); + petMap.put("My Cat", new Cat("Molly")); + petMap.put("My Dog", new Dog("Ginger")); + petMap.put("My Hamster", new Hamster("Bosco")); + System.out.println(petMap); + Pet dog = petMap.get("My Dog"); + System.out.println(dog); + System.out.println(petMap.containsKey("My Dog")); + System.out.println(petMap.containsValue(dog)); + } +} +/* Output: +{My Dog=Dog Ginger, My Cat=Cat Molly, My +Hamster=Hamster Bosco} +Dog Ginger +true +true +*/ +``` + +**Map** 与数组和其他的 **Collection** 一样,可以轻松地扩展到多个维度,只需要创建一个值为 **Map** 的 **Map**(这些 **Map** 的值可以是其他集合,甚至是其他 **Map**)。因此,能够很容易地将集合组合起来以快速生成强大的数据结构。例如,假设你正在追踪有多个宠物的人,只需要一个 **Map\\>** 即可: + +```java + +// collections/MapOfList.java +// {java collections.MapOfList} +package collections; +import typeinfo.pets.*; +import java.util.*; + +public class MapOfList { + public static final Map> + petPeople = new HashMap<>(); + static { + petPeople.put(new Person("Dawn"), + Arrays.asList( + new Cymric("Molly"), + new Mutt("Spot"))); + petPeople.put(new Person("Kate"), + Arrays.asList(new Cat("Shackleton"), + new Cat("Elsie May"), new Dog("Margrett"))); + petPeople.put(new Person("Marilyn"), + Arrays.asList( + new Pug("Louie aka Louis Snorkelstein Dupree"), + new Cat("Stanford"), + new Cat("Pinkola"))); + petPeople.put(new Person("Luke"), + Arrays.asList( + new Rat("Fuzzy"), new Rat("Fizzy"))); + petPeople.put(new Person("Isaac"), + Arrays.asList(new Rat("Freckly"))); + } + public static void main(String[] args) { + System.out.println("People: " + petPeople.keySet()); + System.out.println("Pets: " + petPeople.values()); + for(Person person : petPeople.keySet()) { + System.out.println(person + " has:"); + for(Pet pet : petPeople.get(person)) + System.out.println(" " + pet); + } + } +} +/* Output: +People: [Person Dawn, Person Kate, Person Isaac, Person +Marilyn, Person Luke] +Pets: [[Cymric Molly, Mutt Spot], [Cat Shackleton, Cat +Elsie May, Dog Margrett], [Rat Freckly], [Pug Louie aka +Louis Snorkelstein Dupree, Cat Stanford, Cat Pinkola], +[Rat Fuzzy, Rat Fizzy]] +Person Dawn has: + Cymric Molly + Mutt Spot +Person Kate has: + Cat Shackleton + Cat Elsie May + Dog Margrett +Person Isaac has: + Rat Freckly +Person Marilyn has: + Pug Louie aka Louis Snorkelstein Dupree + Cat Stanford + Cat Pinkola +Person Luke has: + Rat Fuzzy + Rat Fizzy +*/ +``` + +**Map** 可以返回由其键组成的 **Set** ,由其值组成的 **Collection** ,或者其键值对的 **Set** 。 **keySet()** 方法生成由在 **petPeople** 中的所有键组成的 **Set** ,它在*for-in*语句中被用来遍历该 **Map** 。 ## 队列Queue +队列是一个典型的“先进先出”(FIFO)集合。 即从集合的一端放入事物,再从另一端去获取它们,事物放入集合的顺序和被取出的顺序是相同的。队列通常被当做一种可靠的将对象从程序的某个区域传输到另一个区域的途径。队列在[并发编程]()中尤为重要,因为它们可以安全地将对象从一个任务传输到另一个任务。 + +**LinkedList** 实现了 **Queue** 接口,并且提供了一些方法以支持队列行为,因此 **LinkedList** 可以用作 **Queue** 的一种实现。 通过将 **LinkedList** 向上转换为 **Queue** ,下面的示例使用了在 **Queue** 接口中与 **Queue** 相关(Queue-specific)的方法: + +```java +// collections/QueueDemo.java +// Upcasting to a Queue from a LinkedList +import java.util.*; + +public class QueueDemo { + public static void printQ(Queue queue) { + while(queue.peek() != null) + System.out.print(queue.remove() + " "); + System.out.println(); + } + public static void main(String[] args) { + Queue queue = new LinkedList<>(); + Random rand = new Random(47); + for(int i = 0; i < 10; i++) + queue.offer(rand.nextInt(i + 10)); + printQ(queue); + Queue qc = new LinkedList<>(); + for(char c : "Brontosaurus".toCharArray()) + qc.offer(c); + printQ(qc); + } +} +/* Output: +8 1 1 1 5 14 3 1 0 1 +B r o n t o s a u r u s +*/ +``` + +**offer()** 是与 **Queue** 相关的方法之一,它在允许的情况下,在队列的尾部插入一个元素,或者返回 **false** 。 **peek()** 和 **element()** 都返回队头元素而不删除它,但是如果队列为空,则 **element()** 抛出 **NoSuchElementException** ,而 **peek()** 返回 **null** 。 **poll()** 和 **remove()** 都删除并返回队头元素,但如果队列为空,**poll()** 返回 **null** ,而 **remove()** 抛出 **NoSuchElementException** 。 + +自动包装机制会自动将 **nextInt()** 的 **int** 结果转换为 **queue** 所需的 **Integer** 对象,并将 **char c** 转换为 **qc** 所需的 **Character** 对象。 **Queue** 接口窄化了对 **LinkedList** 方法的访问权限,因此只有适当的方法才能使用,因此能够访问到的 **LinkedList** 的方法会变少(这里实际上可以将 **Queue** 强制转换回 **LinkedList** ,但至少我们不鼓励这样做)。 + +与 **Queue** 相关的方法提供了完整而独立的功能。 也就是说,对于 **Queue** 所继承的 **Collection** ,在不需要使用它的任何方法的情况下,就可以拥有一个可用的 **Queue** 。 + + +### 优先级队列PriorityQueue + +先进先出(FIFO)描述了最典型的*队列规则*(queuing discipline)。队列规则是指在给定队列中的一组元素的情况下,确定下一个弹出队列的元素的规则。先进先出声明的是下一个弹出的元素应该是等待时间最长的元素。 + +优先级队列声明下一个弹出的元素是最需要的元素(具有最高的优先级)。例如,在机场,当飞机临近起飞时,这架飞机的乘客可以在办理登机手续时排到队头。如果构建了一个消息传递系统,某些消息比其他消息更重要,应该尽快处理,而不管它们何时到达。在Java 5中添加了 **PriorityQueue** ,以便自动实现这种行为。 + +当在 **PriorityQueue** 上调用 **offer()** 方法来插入一个对象时,该对象会在队列中被排序。[^5]默认的排序使用队列中对象的*自然顺序*(natural order),但是可以通过提供自己的 **Comparator** 来修改这个顺序。 **PriorityQueue** 确保在调用**peek()** , **poll()** 或 **remove()** 方法时,获得的元素将是队列中优先级最高的元素。 + +让 **PriorityQueue** 与 **Integer** , **String** 和 **Character** 这样的内置类型一起工作易如反掌。在下面的示例中,第一组值与前一个示例中的随机值相同,可以看到它们从 **PriorityQueue** 中弹出的顺序与前一个示例不同: + +```java +// collections/PriorityQueueDemo.java +import java.util.*; + +public class PriorityQueueDemo { + public static void main(String[] args) { + PriorityQueue priorityQueue = + new PriorityQueue<>(); + Random rand = new Random(47); + for(int i = 0; i < 10; i++) + priorityQueue.offer(rand.nextInt(i + 10)); + QueueDemo.printQ(priorityQueue); + + List ints = Arrays.asList(25, 22, 20, + 18, 14, 9, 3, 1, 1, 2, 3, 9, 14, 18, 21, 23, 25); + priorityQueue = new PriorityQueue<>(ints); + QueueDemo.printQ(priorityQueue); + priorityQueue = new PriorityQueue<>( + ints.size(), Collections.reverseOrder()); + priorityQueue.addAll(ints); + QueueDemo.printQ(priorityQueue); + + String fact = "EDUCATION SHOULD ESCHEW OBFUSCATION"; + List strings = + Arrays.asList(fact.split("")); + PriorityQueue stringPQ = + new PriorityQueue<>(strings); + QueueDemo.printQ(stringPQ); + stringPQ = new PriorityQueue<>( + strings.size(), Collections.reverseOrder()); + stringPQ.addAll(strings); + QueueDemo.printQ(stringPQ); + + Set charSet = new HashSet<>(); + for(char c : fact.toCharArray()) + charSet.add(c); // Autoboxing + PriorityQueue characterPQ = + new PriorityQueue<>(charSet); + QueueDemo.printQ(characterPQ); + } +} +/* Output: +0 1 1 1 1 1 3 5 8 14 +1 1 2 3 3 9 9 14 14 18 18 20 21 22 23 25 25 +25 25 23 22 21 20 18 18 14 14 9 9 3 3 2 1 1 + A A B C C C D D E E E F H H I I L N N O O O O S S +S T T U U U W +W U U U T T S S S O O O O N N L I I H H F E E E D D C C +C B A A + A B C D E F H I L N O S T U W +*/ +``` + +**PriorityQueue** 是允许重复的,最小的值具有最高的优先级(如果是 **String** ,空格也可以算作值,并且比字母的优先级高)。为了展示如何通过提供自己的 **Comparator** 对象来改变顺序,第三个对 **PriorityQueue\** 构造器的调用,和第二个对 **PriorityQueue\** 的调用使用了由 **Collections.reverseOrder()** (Java SE5中新添加的)产生的反序的 **Comparator** 。 + +最后一部分添加了一个 **HashSet** 来消除重复的 **Character**。 + +**Integer** , **String** 和 **Character** 可以与 **PriorityQueue** 一起使用,因为这些类已经内置了自然排序。如果想在 **PriorityQueue** 中使用自己的类,则必须包含额外的功能以产生自然排序,或者必须提供自己的 **Comparator** 。在[附录:集合主题]()中有一个更复杂的示例来演示这种情况。 ## 集合与迭代器 +**Collection** 是所有序列集合共有的根接口。它可能会被认为是一种“附属接口”(incidental interface),即因为要表示其他若干个接口的共性而出现的接口。此外,**java.util.AbstractCollection** 类提供了 **Collection** 的默认实现,使得你可以创建 **AbstractCollection** 的子类型,而其中没有不必要的代码重复。 + +使用接口描述的一个理由是它可以使我们创建更通用的代码。通过针对接口而非具体实现来编写代码,我们的代码可以应用于更多类型的对象。[^6]因此,如果所编写的方法接受一个 **Collection** ,那么该方法可以应用于任何实现了 **Collection** 的类——这也就使得一个新类可以选择去实现 **Collection** 接口,以便该方法可以使用它。标准C++类库中的的集合并没有共同的基类——集合之间的所有共性都是通过迭代器实现的。在Java中,遵循C++的方式看起来似乎很明智,即用迭代器而不是 **Collection** 来表示集合之间的共性。但是,这两种方法绑定在了一起,因为实现 **Collection** 就意味着需要提供 **iterator()** 方法: + +```java +// collections/InterfaceVsIterator.java +import typeinfo.pets.*; +import java.util.*; + +public class InterfaceVsIterator { + public static void display(Iterator it) { + while(it.hasNext()) { + Pet p = it.next(); + System.out.print(p.id() + ":" + p + " "); + } + System.out.println(); + } + public static void display(Collection pets) { + for(Pet p : pets) + System.out.print(p.id() + ":" + p + " "); + System.out.println(); + } + public static void main(String[] args) { + List petList = Pets.list(8); + Set petSet = new HashSet<>(petList); + Map petMap = new LinkedHashMap<>(); + String[] names = ("Ralph, Eric, Robin, Lacey, " + + "Britney, Sam, Spot, Fluffy").split(", "); + for(int i = 0; i < names.length; i++) + petMap.put(names[i], petList.get(i)); + display(petList); + display(petSet); + display(petList.iterator()); + display(petSet.iterator()); + System.out.println(petMap); + System.out.println(petMap.keySet()); + display(petMap.values()); + display(petMap.values().iterator()); + } +} +/* Output: +0:Rat 1:Manx 2:Cymric 3:Mutt 4:Pug 5:Cymric 6:Pug +7:Manx +0:Rat 1:Manx 2:Cymric 3:Mutt 4:Pug 5:Cymric 6:Pug +7:Manx +0:Rat 1:Manx 2:Cymric 3:Mutt 4:Pug 5:Cymric 6:Pug +7:Manx +0:Rat 1:Manx 2:Cymric 3:Mutt 4:Pug 5:Cymric 6:Pug +7:Manx +{Ralph=Rat, Eric=Manx, Robin=Cymric, Lacey=Mutt, +Britney=Pug, Sam=Cymric, Spot=Pug, Fluffy=Manx} +[Ralph, Eric, Robin, Lacey, Britney, Sam, Spot, Fluffy] +0:Rat 1:Manx 2:Cymric 3:Mutt 4:Pug 5:Cymric 6:Pug +7:Manx +0:Rat 1:Manx 2:Cymric 3:Mutt 4:Pug 5:Cymric 6:Pug +7:Manx +*/ +``` + +两个版本的 **display()** 方法都可以使用 **Map** 或 **Collection** 的子类型来工作。 而且**Collection** 接口和 **Iterator** 都将 **display()** 方法与低层集合的特定实现解耦。 + +在本例中,这两种方式都可以奏效。事实上, **Collection** 要更方便一点,因为它是 **Iterable** 类型,因此在 **display(Collection)** 的实现中可以使用*for-in*构造,这使得代码更加清晰。 + +当需要实现一个不是 **Collection** 的外部类时,由于让它去实现 **Collection** 接口可能非常困难或麻烦,因此使用 **Iterator** 就会变得非常吸引人。例如,如果我们通过继承一个持有 **Pet** 对象的类来创建一个 **Collection** 的实现,那么我们必须实现 **Collection** 所有的方法,即使我们不在 **display()** 方法中使用它们,也必须这样做。虽然这可以通过继承 **AbstractCollection** 而很容易地实现,但是无论如何还是要被强制去实现 **iterator()** 和 **size()** 方法,这些方法 **AbstractCollection** 没有实现,但是 **AbstractCollection** 中的其它方法会用到: + +```java +// collections/CollectionSequence.java +import typeinfo.pets.*; +import java.util.*; + +public class CollectionSequence +extends AbstractCollection { + private Pet[] pets = Pets.array(8); + @Override + public int size() { return pets.length; } + @Override + public Iterator iterator() { + return new Iterator() { // [1] + private int index = 0; + @Override + public boolean hasNext() { + return index < pets.length; + } + @Override + public Pet next() { return pets[index++]; } + @Override + public void remove() { // Not implemented + throw new UnsupportedOperationException(); + } + }; + } + public static void main(String[] args) { + CollectionSequence c = new CollectionSequence(); + InterfaceVsIterator.display(c); + InterfaceVsIterator.display(c.iterator()); + } +} +/* Output: +0:Rat 1:Manx 2:Cymric 3:Mutt 4:Pug 5:Cymric 6:Pug +7:Manx +0:Rat 1:Manx 2:Cymric 3:Mutt 4:Pug 5:Cymric 6:Pug +7:Manx +*/ +``` + +**remove()** 方法是一个“可选操作”,在[附录:集合主题]()中详细介绍。 这里可以不必实现它,如果你调用它,它将抛出异常。 + +- **[1]** 你可能会认为,因为 **iterator()** 返回 **Iterator\** ,匿名内部类定义可以使用菱形语法,Java可以推断出类型。但这不起作用,类型推断仍然非常有限。 + +这个例子表明,如果实现了 **Collection** ,就必须实现 **iterator()** ,并且只拿实现 **iterator()** 与继承 **AbstractCollection** 相比,花费的代价只有略微减少。但是,如果类已经继承了其他的类,那么就不能继承再 **AbstractCollection** 了。 在这种情况下,要实现 **Collection** ,就必须实现该接口中的所有方法。 此时,继承并提供创建迭代器的能力要容易得多: + +```java +// collections/NonCollectionSequence.java +import typeinfo.pets.*; +import java.util.*; + +class PetSequence { + protected Pet[] pets = Pets.array(8); +} + +public class NonCollectionSequence extends PetSequence { + public Iterator iterator() { + return new Iterator() { + private int index = 0; + @Override + public boolean hasNext() { + return index < pets.length; + } + @Override + public Pet next() { return pets[index++]; } + @Override + public void remove() { // Not implemented + throw new UnsupportedOperationException(); + } + }; + } + public static void main(String[] args) { + NonCollectionSequence nc = + new NonCollectionSequence(); + InterfaceVsIterator.display(nc.iterator()); + } +} +/* Output: +0:Rat 1:Manx 2:Cymric 3:Mutt 4:Pug 5:Cymric 6:Pug +7:Manx +*/ +``` + +生成 **Iterator** 是将序列与消费该序列的方法连接在一起耦合度最小的方式,并且与实现 **Collection** 相比,它在序列类上所施加的约束也少得多。 ## for-in和迭代器 +到目前为止,*for-in*语法主要用于数组,但它也适用于任何 **Collection** 对象。实际上在使用 **ArrayList** 时,已经看到了一些使用它的示例,下面是一个更通用的证明: + +```java +// collections/ForInCollections.java +// All collections work with for-in +import java.util.*; + +public class ForInCollections { + public static void main(String[] args) { + Collection cs = new LinkedList<>(); + Collections.addAll(cs, + "Take the long way home".split(" ")); + for(String s : cs) + System.out.print("'" + s + "' "); + } +} +/* Output: +'Take' 'the' 'long' 'way' 'home' +*/ +``` + +由于 **cs** 是一个 **Collection** ,因此该代码展示了使用*for-in*是所有 **Collection** 对象的特征。 + +这样做的原因是Java SE5引入了一个名为 **Iterable** 的接口,该接口包含一个能够生成 **Iterator** 的 **iterator()** 方法。*for-in*使用此 **Iterable** 接口来遍历序列。因此,如果创建了任何实现了 **Iterable** 的类,都可以将它用于*for-in*语句中: + +```java +// collections/IterableClass.java +// Anything Iterable works with for-in +import java.util.*; + +public class IterableClass implements Iterable { + protected String[] words = ("And that is how " + + "we know the Earth to be banana-shaped." + ).split(" "); + @Override + public Iterator iterator() { + return new Iterator() { + private int index = 0; + @Override + public boolean hasNext() { + return index < words.length; + } + @Override + public String next() { return words[index++]; } + @Override + public void remove() { // Not implemented + throw new UnsupportedOperationException(); + } + }; + } + public static void main(String[] args) { + for(String s : new IterableClass()) + System.out.print(s + " "); + } +} +/* Output: +And that is how we know the Earth to be banana-shaped. +*/ +``` + +**iterator()** 返回的是实现了 **Iterator\** 的匿名内部类的实例,该匿名内部类可以遍历数组中的每个单词。在 **main()** 中,可以看到 **IterableClass** 确实可以用于*for-in*语句。 + +在Java SE5中,许多类都是 **Iterable** ,主要包括所有的 **Collection** 类(但不包括各种 **Maps** )。 例如,下面的代码可以显示所有的操作系统环境变量: + +```java +// collections/EnvironmentVariables.java +// {VisuallyInspectOutput} +import java.util.*; + +public class EnvironmentVariables { + public static void main(String[] args) { + for(Map.Entry entry: System.getenv().entrySet()) { + System.out.println(entry.getKey() + ": " + + entry.getValue()); + } + } +} +``` + +**System.getenv()** [^7]返回一个 **Map** , **entrySet()** 产生一个由 **Map.Entry** 的元素构成的 **Set** ,并且这个 **Set** 是一个 **Iterable** ,因此它可以用于*for-in*循环。 + +*for-in*语句适用于数组或其它任何 **Iterable** ,但这并不意味着数组肯定也是个 **Iterable** ,也不会发生任何自动装箱: + +```java +// collections/ArrayIsNotIterable.java +import java.util.*; + +public class ArrayIsNotIterable { + static void test(Iterable ib) { + for(T t : ib) + System.out.print(t + " "); + } + public static void main(String[] args) { + test(Arrays.asList(1, 2, 3)); + String[] strings = { "A", "B", "C" }; + // An array works in for-in, but it's not Iterable: + //- test(strings); + // You must explicitly convert it to an Iterable: + test(Arrays.asList(strings)); + } +} +/* Output: +1 2 3 A B C +*/ +``` + +尝试将数组作为一个 **Iterable** 参数传递会导致失败。这说明不存在任何从数组到 **Iterable** 的自动转换; 必须手工执行这种转换。 + + +### 适配器方法惯用法 + +如果现在有一个 **Iterable** 类,你想要添加一种或多种在*for-in*语句中使用这个类的方法,应该怎么做呢? 例如,假设你希望可以选择是在正向还是反向遍历一个单词列表。如果直接继承这个类,并覆盖 **iterator()** 方法,则只能替换现有的方法,而不能实现选择。 + +一种解决方案是所谓*适配器方法*(Adapter Method)的惯用法。“适配器”部分来自于设计模式,因为必须要提供特定的接口来满足*for-in*语句。如果已经有一个接口并且需要另一个接口时,则编写适配器就可以解决这个问题。 +在这里,若希望在默认的正向迭代器的基础上,添加产生反向迭代器的能力,因此不能使用覆盖,相反,而是添加了一个能够生成 **Iterable** 对象的方法,该对象可以用于*for-in*语句。这使得我们可以提供多种使用*for-in*语句的方式: + +```java +// collections/AdapterMethodIdiom.java +// The "Adapter Method" idiom uses for-in +// with additional kinds of Iterables +import java.util.*; + +class ReversibleArrayList extends ArrayList { + ReversibleArrayList(Collection c) { + super(c); + } + public Iterable reversed() { + return new Iterable() { + public Iterator iterator() { + return new Iterator() { + int current = size() - 1; + public boolean hasNext() { + return current > -1; + } + public T next() { return get(current--); } + public void remove() { // Not implemented + throw new UnsupportedOperationException(); + } + }; + } + }; + } +} + +public class AdapterMethodIdiom { + public static void main(String[] args) { + ReversibleArrayList ral = + new ReversibleArrayList( + Arrays.asList("To be or not to be".split(" "))); + // Grabs the ordinary iterator via iterator(): + for(String s : ral) + System.out.print(s + " "); + System.out.println(); + // Hand it the Iterable of your choice + for(String s : ral.reversed()) + System.out.print(s + " "); + } +} +/* Output: +To be or not to be +be to not or be To +*/ +``` + +在 **main()** 中,如果直接将 **ral** 对象放在*for-in*语句中,则会得到(默认的)正向迭代器。但是如果在该对象上调用 **reversed()** 方法,它会产生不同的行为。 + +通过使用这种方式,可以在 **IterableClass.java** 示例中添加两种适配器方法: + +```java +// collections/MultiIterableClass.java +// Adding several Adapter Methods +import java.util.*; + +public class MultiIterableClass extends IterableClass { + public Iterable reversed() { + return new Iterable() { + public Iterator iterator() { + return new Iterator() { + int current = words.length - 1; + public boolean hasNext() { + return current > -1; + } + public String next() { + return words[current--]; + } + public void remove() { // Not implemented + throw new UnsupportedOperationException(); + } + }; + } + }; + } + public Iterable randomized() { + return new Iterable() { + public Iterator iterator() { + List shuffled = + new ArrayList(Arrays.asList(words)); + Collections.shuffle(shuffled, new Random(47)); + return shuffled.iterator(); + } + }; + } + public static void main(String[] args) { + MultiIterableClass mic = new MultiIterableClass(); + for(String s : mic.reversed()) + System.out.print(s + " "); + System.out.println(); + for(String s : mic.randomized()) + System.out.print(s + " "); + System.out.println(); + for(String s : mic) + System.out.print(s + " "); + } +} +/* Output: +banana-shaped. be to Earth the know we how is that And +is banana-shaped. Earth that how the be And we know to +And that is how we know the Earth to be banana-shaped. +*/ +``` + +注意,第二个方法 **random()** 没有创建它自己的 **Iterator** ,而是直接返回被打乱的 **List** 中的 **Iterator** 。 + +从输出中可以看到, **Collections.shuffle()** 方法不会影响到原始数组,而只是打乱了 **shuffled** 中的引用。之所以这样,是因为 **randomized()** 方法用一个 **ArrayList** 将 **Arrays.asList()** 的结果包装了起来。如果这个由 **Arrays.asList()** 生成的 **List** 被直接打乱,那么它将修改底层数组,如下所示: + +```java +// collections/ModifyingArraysAsList.java +import java.util.*; + +public class ModifyingArraysAsList { + public static void main(String[] args) { + Random rand = new Random(47); + Integer[] ia = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }; + List list1 = + new ArrayList<>(Arrays.asList(ia)); + System.out.println("Before shuffling: " + list1); + Collections.shuffle(list1, rand); + System.out.println("After shuffling: " + list1); + System.out.println("array: " + Arrays.toString(ia)); + + List list2 = Arrays.asList(ia); + System.out.println("Before shuffling: " + list2); + Collections.shuffle(list2, rand); + System.out.println("After shuffling: " + list2); + System.out.println("array: " + Arrays.toString(ia)); + } +} +/* Output: +Before shuffling: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] +After shuffling: [4, 6, 3, 1, 8, 7, 2, 5, 10, 9] +array: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] +Before shuffling: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] +After shuffling: [9, 1, 6, 3, 7, 2, 5, 10, 4, 8] +array: [9, 1, 6, 3, 7, 2, 5, 10, 4, 8] +*/ +``` + +在第一种情况下, **Arrays.asList()** 的输出被传递给了 **ArrayList** 的构造器,这将创建一个引用 **ia** 的元素的 **ArrayList** ,因此打乱这些引用不会修改该数组。但是,如果直接使用 **Arrays.asList(ia)** 的结果,这种打乱就会修改 **ia** 的顺序。重要的是要注意 **Arrays.asList()** 生成一个 **List** 对象,该对象使用底层数组作为其物理实现。如果执行的会修改这个 **List** ,并且不希望修改原始数组,那么就应该在另一个集合中创建一个副本。 ## 本章小结 +Java提供了许多保存对象的方法: + +1. 数组将数字索引与对象相关联。它保存类型明确的对象,因此在查找对象时不必对结果做类型转换。它可以是多维的,可以保存基本类型的数据。虽然可以在运行时创建数组,但是一旦创建数组,就无法更改数组的大小。 + +2. **Collection** 保存单一的元素,而 **Map** 包含相关联的键值对。使用Java泛型,可以指定集合中保存的对象的类型,因此不能将错误类型的对象放入集合中,并且在从集合中获取元素时,不必进行类型转换。各种 **Collection** 和各种 **Map** 都可以在你向其中添加更多的元素时,自动调整其尺寸大小。集合不能保存基本类型,但自动装箱机制会负责执行基本类型和集合中保存的包装类型之间的双向转换。 + +3. 像数组一样, **List** 也将数字索引与对象相关联,因此,数组和 **List** 都是有序集合。 + +4. 如果要执行大量的随机访问,则使用 **ArrayList** ,如果要经常从表中间插入或删除元素,则应该使用 **LinkedList** 。 + +5. 队列和堆栈的行为是通过 **LinkedList** 提供的。 + +6. **Map** 是一种将对象(而非数字)与对象相关联的设计。 **HashMap** 专为快速访问而设计,而 **TreeMap** 保持键始终处于排序状态,所以没有 **HashMap** 快。 **LinkedHashMap** 按插入顺序保存其元素,但使用散列提供快速访问的能力。 + +7. **Set** 不接受重复元素。 **HashSet** 提供最快的查询速度,而 **TreeSet** 保持元素处于排序状态。 **LinkedHashSets** 按插入顺序保存其元素,但使用散列提供快速访问的能力。 + +8. 不要在新代码中使用遗留类 **Vector** , **Hashtable** 和 **Stack** 。 + +浏览一下Java集合的简图(不包含抽象类或遗留组件)会很有帮助。这里仅包括在一般情况下会碰到的接口和类。 + +![simple collection taxonomy](../images/simple-collection-taxonomy.png) + + +可以看到,实际上只有四个基本的集合组件: **Map** , **List** , **Set** 和 **Queue** ,它们各有两到三个实现版本(**Queue** 的 **java.util.concurrent** 实现未包含在此图中)。最常使用的集合用黑色粗线线框表示。 + +虚线框表示接口,实线框表示普通的(具体的)类。带有空心箭头的虚线表示特定的类实现了一个接口。实心箭头表示某个类可以生成箭头指向的类的对象。例如,任何 **Collection** 都可以生成 **Iterator** , **List** 可以生成 **ListIterator** (也能生成普通的 **Iterator** ,因为 **List** 继承自 **Collection** )。 + +下面的示例展示了各种不同的类在方法上的差异。实际代码来自[泛型]()章节,在这里只是调用它来产生输出。程序的输出还展示了在每个类或接口中所实现的接口: + +```java +// collections/CollectionDifferences.java +import onjava.*; + +public class CollectionDifferences { + public static void main(String[] args) { + CollectionMethodDifferences.main(args); + } +} +/* Output: +Collection: [add, addAll, clear, contains, containsAll, +equals, forEach, hashCode, isEmpty, iterator, +parallelStream, remove, removeAll, removeIf, retainAll, +size, spliterator, stream, toArray] +Interfaces in Collection: [Iterable] +Set extends Collection, adds: [] +Interfaces in Set: [Collection] +HashSet extends Set, adds: [] +Interfaces in HashSet: [Set, Cloneable, Serializable] +LinkedHashSet extends HashSet, adds: [] +Interfaces in LinkedHashSet: [Set, Cloneable, +Serializable] +TreeSet extends Set, adds: [headSet, +descendingIterator, descendingSet, pollLast, subSet, +floor, tailSet, ceiling, last, lower, comparator, +pollFirst, first, higher] +Interfaces in TreeSet: [NavigableSet, Cloneable, +Serializable] +List extends Collection, adds: [replaceAll, get, +indexOf, subList, set, sort, lastIndexOf, listIterator] +Interfaces in List: [Collection] +ArrayList extends List, adds: [trimToSize, +ensureCapacity] +Interfaces in ArrayList: [List, RandomAccess, +Cloneable, Serializable] +LinkedList extends List, adds: [offerFirst, poll, +getLast, offer, getFirst, removeFirst, element, +removeLastOccurrence, peekFirst, peekLast, push, +pollFirst, removeFirstOccurrence, descendingIterator, +pollLast, removeLast, pop, addLast, peek, offerLast, +addFirst] +Interfaces in LinkedList: [List, Deque, Cloneable, +Serializable] +Queue extends Collection, adds: [poll, peek, offer, +element] +Interfaces in Queue: [Collection] +PriorityQueue extends Queue, adds: [comparator] +Interfaces in PriorityQueue: [Serializable] +Map: [clear, compute, computeIfAbsent, +computeIfPresent, containsKey, containsValue, entrySet, +equals, forEach, get, getOrDefault, hashCode, isEmpty, +keySet, merge, put, putAll, putIfAbsent, remove, +replace, replaceAll, size, values] +HashMap extends Map, adds: [] +Interfaces in HashMap: [Map, Cloneable, Serializable] +LinkedHashMap extends HashMap, adds: [] +Interfaces in LinkedHashMap: [Map] +SortedMap extends Map, adds: [lastKey, subMap, +comparator, firstKey, headMap, tailMap] +Interfaces in SortedMap: [Map] +TreeMap extends Map, adds: [descendingKeySet, +navigableKeySet, higherEntry, higherKey, floorKey, +subMap, ceilingKey, pollLastEntry, firstKey, lowerKey, +headMap, tailMap, lowerEntry, ceilingEntry, +descendingMap, pollFirstEntry, lastKey, firstEntry, +floorEntry, comparator, lastEntry] +Interfaces in TreeMap: [NavigableMap, Cloneable, +Serializable] +*/ +``` + +除 **TreeSet** 之外的所有 **Set** 都具有与 **Collection** 完全相同的接口。**List** 和 **Collection** 存在着明显的不同,尽管 **List** 所要求的方法都在 **Collection** 中。另一方面,在 **Queue** 接口中的方法是独立的,在创建具有 **Queue** 功能的实现时,不需要使用 **Collection** 方法。最后, **Map** 和 **Collection** 之间唯一的交集是Map可以使用 **entrySet()** 和 **values()** 方法来产生 **Collection** 。 + +请注意,标记接口 **java.util.RandomAccess** 附加到了 **ArrayList** 上,但不附加到 **LinkedList** 上。这为根据特定 **List** 动态改变其行为的算法提供了信息。 + +从面向对象的继承层次结构来看,这种组织结构确实有些奇怪。但是,当了解了 **java.util** 中更多的有关集合的内容后(特别是在[附录:集合主题]()中的内容),就会发现出了继承结构有点奇怪外,还有更多的问题。集合类库一直以来都是设计难题——解决这些问题涉及到要去满足经常彼此之间互为牵制的各方面需求。所以要做好准备,在各处做出妥协。 + +尽管存在这些问题,但Java集合仍是在日常工作中使用的基本工具,它可以使程序更简洁、更强大、更有效。你可能需要一段时间才能熟悉集合类库的某些方面,但我想你很快就会找到自己的路子,来获得和使用这个类库中的类。 + +[^1]: 许多语言,例如Perl,Python和Ruby,都有集合的本地支持。 + +[^2]: 这里是操作符重载的用武之地,C++和C#的集合类都使用操作符重载生成了更简洁的语法。 + +[^3]: 在[泛型]()章节的末尾,有个关于这个问题是否很严重的讨论。但是,[泛型]()章节还将展示Java泛型远不止是类型安全的集合这么简单。 + +[^4]: **remove()** 是一个所谓的“可选”方法(还有一些其它的这种方法),这意味着并非所有的 **Iterator** 实现都必须实现该方法。这个问题将在[附录:集合主题]()中介绍。但是,标准Java库集合实现了 **remove()** ,因此在[附录:集合主题]()章节之前,都不必担心这个问题。 + +[^5]: 这实际上依赖于具体实现。优先级队列算法通常会按插入顺序排序(维护一个*堆*),但它们也可以在删除时选择最重要的元素。 如果对象的优先级在它在队列中等待时可以修改,那么算法的选择就显得很重要了。 + +[^6]: 有些人提倡这样一种自动创建机制,即对一个类中所有可能的方法组合都自动创建一个接口,有时候对于单个的类都是如此。 我相信接口的意义不应该仅限于方法组合的机械地复制,因此我在创建接口之前,总是要先看到增加接口带来的价值。 + +[^7]: 这在Java SE5之前是不可用的,因为该方法被认为与操作系统的耦合度过紧,因此违反“一次编写,处处运行”的原则。现在提供它这一事实表明,Java的设计者们更加务实了。 + -
\ No newline at end of file +
diff --git a/docs/book/Appendix-Low-Level-Concurrency.md b/docs/book/Appendix-Low-Level-Concurrency.md index 9de1eb6..e09e7be 100644 --- a/docs/book/Appendix-Low-Level-Concurrency.md +++ b/docs/book/Appendix-Low-Level-Concurrency.md @@ -3,18 +3,389 @@ # 附录:并发底层原理 +> 尽管不建议您自己编写底层 Java 并发代码,但是这样通常有助于了解它是如何工作的。 + +[并发编程](./24-Concurrent-Programming.md) 章节中介绍了一些用于高级并发的概念,包括为 Java 并发编程而最新提出的,更安全的概念( parallel Streams 和 CompletableFutures )。本附录则介绍在 Java 中底层并发概念,因此在阅读本篇时,您能有所了解掌握这些代码。您还会将进一步了解并发的普遍问题。 + +在 Java 的早期版本中, 底层并发概念是并发编程的重要组成部分。我们会着眼于围绕这些技巧的复杂性以及为何您应该避免它们而谈。 “并发编程” 章节展示最新的 Java 版本(尤其是 Java 8)所提供的改进技巧,这些技巧使得并发的使用,如果本来不容易使用,也会变得更容易些。 + + -## 线程 +## 什么是线程? +并发将程序划分成分离的,独立运行的任务。每个任务都由一个 *执行线程* 来驱动,我们通常将其简称为 *线程* 。而一个 *线程* 就是操作系统进程中单一顺序的控制流。因此,单个进程可以有多个并发执行的任务,但是你的程序使得每个任务都好像有自己的处理器一样。这线程模型为编程带来了便利,它简化了在单一程序中处理变戏法般的多任务过程。操作系统则从处理器上分配时间到您程序的所有线程中。 + +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 框架。 ## 异常捕获 +这可能会让你感到惊讶: + +```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 的改进方法进行比较。 ## 资源共享 +你可以将单线程程序看作一个孤独的实体,在你的问题空间中移动并一次只做一件事。因为只有一个实体,你永远不会想到两个实体试图同时使用相同资源的问题:问题犹如两个人试图同时停放在同一个空间,同时走过一扇门,甚至同时说话。 + +通过并发,事情不再孤单,但现在两个或更多任务可能会相互干扰。如果您不阻止这种冲突,您将有两个任务同时尝试访问同一个银行帐户,打印到同一个打印机,调整相同的阀门,等等。 + +### 资源竞争 + +### 解决资源竞争 + +### 同步多个生产者 ## volatile关键字 @@ -35,8 +406,28 @@ ## 本章小结 +本附录主要是为了让您在遇到底层并发代码时能对此有一定的了解,尽管本文还远没对这个主题进行全面的讨论。为此,你需要先从阅读由 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]: 这版本是我参与的;这可能在以后的标准中得到了修正
- diff --git a/docs/book/Appendix-Supplements.md b/docs/book/Appendix-Supplements.md index e5cc791..af505e6 100644 --- a/docs/book/Appendix-Supplements.md +++ b/docs/book/Appendix-Supplements.md @@ -3,18 +3,24 @@ # 附录:补充 +> 本书有许多补充内容,包括MindView网站提供的项目和服务。 + +本附录介绍了这些补充内容,你可以自行决定它们是否对你有所帮助。 ## 可下载的补充 +可以从 [https://github.com/BruceEckel/OnJava8-examples](https://github.com/BruceEckel/OnJava8-examples) 免费下载本书的代码。这里包括Gradle构建文件和其它一些必要的支持文件,以便成功构建和执行本书中所有的示例代码。 ## 通过Thinking-in-C来巩固Java基础 +在 [www.OnJava8.com](www.OnJava8.com) 上,可以免费下载*Thinking in C*的演示文稿。 此演示文稿由Chuck Allison创建,由MindView有限责任公司开发。这是一个电子演示文稿,介绍了Java语法所基于的C语法,运算符和函数。 -## 动手实践 +## Hand-On Java 电子演示文稿 +*Hand-On Java 电子演示文稿*(Hands-On Java eSeminar)是基于*Thinking in Java*第2版。对应于该书中的每一章,它附带有一个音频讲解和相应的幻灯片。我创建了这个电子演示文稿,并讲述了这些材料。这个资料是HTML5格式的,所以它应该可以在大多数现代浏览器上运行。该演示文稿将在[www.OnJava8.com](www.OnJava8.com)上发售,你可以在该网站上找到该产品的试用版演示。
\ No newline at end of file diff --git a/docs/book/Appendix-The-Positive-Legacy-of-C-plus-plus-and-Java.md b/docs/book/Appendix-The-Positive-Legacy-of-C-plus-plus-and-Java.md index fb26815..449e4a1 100644 --- a/docs/book/Appendix-The-Positive-Legacy-of-C-plus-plus-and-Java.md +++ b/docs/book/Appendix-The-Positive-Legacy-of-C-plus-plus-and-Java.md @@ -3,6 +3,29 @@ # 附录: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做的那样),要么就让竞争者茁壮成长。 +
- diff --git a/docs/book/Appendix-Understanding-equals-and-hashCode.md b/docs/book/Appendix-Understanding-equals-and-hashCode.md index 1dafbdc..e2bd8c3 100644 --- a/docs/book/Appendix-Understanding-equals-and-hashCode.md +++ b/docs/book/Appendix-Understanding-equals-and-hashCode.md @@ -1,11 +1,395 @@ [TOC] + + # 附录:理解equals和hashCode方法 +假设有一个容器使用hash函数,当你创建一个放到这个容器时,你必须定义 **hashCode()** 函数和 **equals()** 函数。这两个函数一起被用于hash容器中的查询操作。 -## 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()这个调用,没有必要重新发明它(因为你不总是有权限访问基类所有的必要字段) + + +### 不同子类的相等性 +继承意味着两个不同子类的对象当其向上转型的时候可以是相等的。假设你有一个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 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** 中。只有他们中的一个能存活。这强调了 **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 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** 中,无论 **equals()** 和 **hashCode()** 是如何定义的,但是对象不会在哈希数据结构中正常工作,除非这些函数能够被合理定义。不幸的是,**equals()** 不总是和 **hashCode()** 一起使用,这在你尝试为了某个特殊类型避免定义它的时候会让问题复杂化。并且这也是为什么遵循规范是有价值的。然而这会变得更加复杂,因为你不总是需要定义其中一个函数。 + @@ -16,4 +400,4 @@ ## 调整HashMap -
\ No newline at end of file +
diff --git a/docs/images/1562999314238.png b/docs/images/1562999314238.png new file mode 100644 index 0000000..16c137d Binary files /dev/null and b/docs/images/1562999314238.png differ diff --git a/docs/images/simple-collection-taxonomy.png b/docs/images/simple-collection-taxonomy.png new file mode 100644 index 0000000..8b7b6cd Binary files /dev/null and b/docs/images/simple-collection-taxonomy.png differ