mirror of
https://github.com/sjsdfg/effective-java-3rd-chinese.git
synced 2025-03-15 20:00:39 +08:00
格式校对到第 37 条
This commit is contained in:
parent
460dd1107e
commit
dc4ec6e5df
@ -1,7 +1,7 @@
|
||||
# 4. 使用私有构造方法执行非实例化
|
||||
|
||||
|
||||
偶尔你会想写一个只包含静态方法和静态属性的类。 这样的类获得了不好的名声,因为有些人滥用这些类从而避免以面向对象方式思考,但是它们确实有着特殊的用途。 它们可以用来按照 `java.lang.Math` 或 `java.util.Arrays` 的方式,把基本类型的值或数组类型上的相关方法组织起来。我们也可以通过 `java.util.Collections` 的方式,把实现特定接口上面的静态方法进行分组,也包括工厂方法(条目 1)。 (从 Java 8 开始,你也可以将这些方法放在接口中,假定是你编写的接口并可以进行修改。)最后,这样的类可以用于在 final 类上对方法进行分组,因为不能将它们放在子类中。
|
||||
偶尔你会想写一个只包含静态方法和静态属性的类。 这样的类获得了不好的名声,因为有些人滥用这些类从而避免以面向对象方式思考,但是它们确实有着特殊的用途。 它们可以用来按照 `java.lang.Math` 或 `java.util.Arrays` 的方式,把基本类型的值或数组类型上的相关方法组织起来。我们也可以通过 `java.util.Collections` 的方式,把实现特定接口上面的静态方法进行分组,也包括工厂方法(详见第 1 条)。 (从 Java 8 开始,你也可以将这些方法放在接口中,假定是你编写的接口并可以进行修改。)最后,这样的类可以用于在 final 类上对方法进行分组,因为不能将它们放在子类中。
|
||||
|
||||
这样的实用类(utility classes)不是设计用来被实例化的:一个实例是没有意义的。然而,在没有显式构造方法的情况下,编译器提供了一个公共的、无参的默认构造方法。对于用户来说,该构造方法与其他构造方法没有什么区别。在已发布的 API 中经常看到无意识的被实例的类。
|
||||
|
||||
|
@ -1,7 +1,7 @@
|
||||
# 7. 消除过期的对象引用
|
||||
|
||||
|
||||
如果你从使用手动内存管理的语言 (如 C 或 C++) 切换到像 Java 这样的带有垃圾收集机制的语言,那么作为程序员的工作就会变得容易多了,因为你的对象在使用完毕以后就自动回收了。当你第一次体验它的时候,它就像魔法一样。这很容易让人觉得你不需要考虑内存管理,但这并不完全正确。
|
||||
如果你从使用手动内存管理的语言(如 C 或 C++)切换到像 Java 这样的带有垃圾收集机制的语言,那么作为程序员的工作就会变得容易多了,因为你的对象在使用完毕以后就自动回收了。当你第一次体验它的时候,它就像魔法一样。这很容易让人觉得你不需要考虑内存管理,但这并不完全正确。
|
||||
|
||||
考虑以下简单的堆栈实现:
|
||||
|
||||
@ -37,9 +37,9 @@ public class Stack {
|
||||
}
|
||||
}
|
||||
```
|
||||
这个程序没有什么明显的错误(但是对于泛型版本,请参阅条目 29)。 你可以对它进行详尽的测试,它都会成功地通过每一项测试,但有一个潜在的问题。 笼统地说,程序有一个“内存泄漏”,由于垃圾回收器的活动的增加,或内存占用的增加,静默地表现为性能下降。 在极端的情况下,这样的内存泄漏可能会导致磁盘分页( disk paging),甚至导致内存溢出(OutOfMemoryError)的失败,但是这样的故障相对较少。
|
||||
这个程序没有什么明显的错误(但是对于泛型版本,请参阅条目 29)。 你可以对它进行详尽的测试,它都会成功地通过每一项测试,但有一个潜在的问题。 笼统地说,程序有一个“内存泄漏”,由于垃圾回收器的活动的增加,或内存占用的增加,静默地表现为性能下降。 在极端的情况下,这样的内存泄漏可能会导致磁盘分页(disk paging),甚至导致内存溢出(OutOfMemoryError)的失败,但是这样的故障相对较少。
|
||||
|
||||
那么哪里发生了内存泄漏? 如果一个栈增长后收缩,那么从栈弹出的对象不会被垃圾收集,即使使用栈的程序不再引用这些对象。 这是因为栈维护对这些对象的过期引用( obsolete references)。 过期引用简单来说就是永远不会解除的引用。 在这种情况下,元素数组“活动部分(active portion)”之外的任何引用都是过期的。 活动部分是由索引下标小于 size 的元素组成。
|
||||
那么哪里发生了内存泄漏? 如果一个栈增长后收缩,那么从栈弹出的对象不会被垃圾收集,即使使用栈的程序不再引用这些对象。 这是因为栈维护对这些对象的过期引用(obsolete references)。 过期引用简单来说就是永远不会解除的引用。 在这种情况下,元素数组“活动部分(active portion)”之外的任何引用都是过期的。 活动部分是由索引下标小于 size 的元素组成。
|
||||
|
||||
垃圾收集语言中的内存泄漏(更适当地称为无意的对象保留 unintentional object retentions)是隐蔽的。 如果无意中保留了对象引用,那么不仅这个对象排除在垃圾回收之外,而且该对象引用的任何对象也是如此。 即使只有少数对象引用被无意地保留下来,也可以阻止垃圾回收机制对许多对象的回收,这对性能产生很大的影响。
|
||||
|
||||
@ -56,9 +56,9 @@ public Object pop() {
|
||||
```
|
||||
取消过期引用的另一个好处是,如果它们随后被错误地引用,程序立即抛出 `NullPointerException` 异常,而不是悄悄地做继续做错误的事情。尽可能快地发现程序中的错误是有好处的。
|
||||
|
||||
当程序员第一次被这个问题困扰时,他们可能会在程序结束后立即清空所有对象引用。这既不是必要的,也不是可取的;它不必要地搞乱了程序。**清空对象引用应该是例外而不是规范。**消除过期引用的最好方法是让包含引用的变量超出范围。如果在最近的作用域范围内定义每个变量 (条目 57),这种自然就会出现这种情况。
|
||||
当程序员第一次被这个问题困扰时,他们可能会在程序结束后立即清空所有对象引用。这既不是必要的,也不是可取的;它不必要地搞乱了程序。**清空对象引用应该是例外而不是规范。**消除过期引用的最好方法是让包含引用的变量超出范围。如果在最近的作用域范围内定义每个变量 (详见第 57 条),这种自然就会出现这种情况。
|
||||
|
||||
那么什么时候应该清空一个引用呢?`Stack` 类的哪个方面使它容易受到内存泄漏的影响?简单地说,它管理自己的内存。存储池(storage pool)由 `elements` 数组的元素组成 (对象引用单元,而不是对象本身)。数组中活动部分的元素 (如前面定义的) 被分配,其余的元素都是空闲的。垃圾收集器没有办法知道这些;对于垃圾收集器来说,`elements` 数组中的所有对象引用都同样有效。只有程序员知道数组的非活动部分不重要。程序员可以向垃圾收集器传达这样一个事实,一旦数组中的元素变成非活动的一部分,就可以手动清空这些元素的引用。
|
||||
那么什么时候应该清空一个引用呢?`Stack` 类的哪个方面使它容易受到内存泄漏的影响?简单地说,它管理自己的内存。存储池(storage pool)由 `elements` 数组的元素组成(对象引用单元,而不是对象本身)。数组中活动部分的元素 (如前面定义的) 被分配,其余的元素都是空闲的。垃圾收集器没有办法知道这些;对于垃圾收集器来说,`elements` 数组中的所有对象引用都同样有效。只有程序员知道数组的非活动部分不重要。程序员可以向垃圾收集器传达这样一个事实,一旦数组中的元素变成非活动的一部分,就可以手动清空这些元素的引用。
|
||||
|
||||
一般来说,**当一个类自己管理内存时,程序员应该警惕内存泄漏问题。** 每当一个元素被释放时,元素中包含的任何对象引用都应该被清除。
|
||||
|
||||
|
@ -2,7 +2,7 @@
|
||||
|
||||
Finalizer 机制是不可预知的,往往是危险的,而且通常是不必要的。 它们的使用会导致不稳定的行为,糟糕的性能和移植性问题。 Finalizer 机制有一些特殊的用途,我们稍后会在这个条目中介绍,但是通常应该避免它们。 从 Java 9 开始,Finalizer 机制已被弃用,但仍被 Java 类库所使用。 Java 9 中 Cleaner 机制代替了 Finalizer 机制。 Cleaner 机制不如 Finalizer 机制那样危险,但仍然是不可预测,运行缓慢并且通常是不必要的。
|
||||
|
||||
提醒 C++程序员不要把 Java 中的 Finalizer 或 Cleaner 机制当成的 C++ 析构函数的等价物。 在 C++ 中,析构函数是回收对象相关资源的正常方式,是与构造方法相对应的。 在 Java 中,当一个对象变得不可达时,垃圾收集器回收与对象相关联的存储空间,不需要开发人员做额外的工作。 C++ 析构函数也被用来回收其他非内存资源。 在 Java 中,try-with-resources 或 try-finally 块用于此目的(条目 9)。
|
||||
提醒 C++程序员不要把 Java 中的 Finalizer 或 Cleaner 机制当成的 C++ 析构函数的等价物。 在 C++ 中,析构函数是回收对象相关资源的正常方式,是与构造方法相对应的。 在 Java 中,当一个对象变得不可达时,垃圾收集器回收与对象相关联的存储空间,不需要开发人员做额外的工作。 C++ 析构函数也被用来回收其他非内存资源。 在 Java 中,try-with-resources 或 try-finally 块用于此目的(详见第 9 条)。
|
||||
|
||||
Finalizer 和 Cleaner 机制的一个缺点是不能保证他们能够及时执行[JLS,12.6]。 在一个对象变得无法访问时,到 Finalizer 和 Cleaner 机制开始运行时,这期间的时间是任意长的。 这意味着你永远不应该 Finalizer 和 Cleaner 机制做任何时间敏感(time-critical)的事情。 例如,依赖于 Finalizer 和 Cleaner 机制来关闭文件是严重的错误,因为打开的文件描述符是有限的资源。 如果由于系统迟迟没有运行 Finalizer 和 Cleaner 机制而导致许多文件被打开,程序可能会失败,因为它不能再打开文件了。
|
||||
|
||||
@ -10,7 +10,7 @@
|
||||
|
||||
延迟终结(finalization)不只是一个理论问题。为一个类提供一个 Finalizer 机制可以任意拖延它的实例的回收。一位同事调试了一个长时间运行的 GUI 应用程序,这个应用程序正在被一个 OutOfMemoryError 错误神秘地死掉。分析显示,在它死亡的时候,应用程序的 Finalizer 机制队列上有成千上万的图形对象正在等待被终结和回收。不幸的是,Finalizer 机制线程的运行优先级低于其他应用程序线程,所以对象被回收的速度低于进入队列的速度。语言规范并不保证哪个线程执行 Finalizer 机制,因此除了避免使用 Finalizer 机制之外,没有轻便的方法来防止这类问题。在这方面, Cleaner 机制比 Finalizer 机制要好一些,因为 Java 类的创建者可以控制自己 cleaner 机制的线程,但 cleaner 机制仍然在后台运行,在垃圾回收器的控制下运行,但不能保证及时清理。
|
||||
|
||||
Java 规范不能保证 Finalizer 和 Cleaner 机制能及时运行;它甚至不能能保证它们是否会运行。当一个程序结束后,一些不可达对象上的 Finalizer 和 Cleaner 机制仍然没有运行。因此,不应该依赖于 Finalizer 和 Cleaner 机制来更新持久化状态。例如,依赖于 Finalizer 和 Cleaner 机制来释放对共享资源 (如数据库) 的持久锁,这是一个使整个分布式系统陷入停滞的好方法。
|
||||
Java 规范不能保证 Finalizer 和 Cleaner 机制能及时运行;它甚至不能能保证它们是否会运行。当一个程序结束后,一些不可达对象上的 Finalizer 和 Cleaner 机制仍然没有运行。因此,不应该依赖于 Finalizer 和 Cleaner 机制来更新持久化状态。例如,依赖于 Finalizer 和 Cleaner 机制来释放对共享资源(如数据库)的持久锁,这是一个使整个分布式系统陷入停滞的好方法。
|
||||
|
||||
不要相信 `System.gc` 和 `System.runFinalization` 方法。 他们可能会增加 Finalizer 和 Cleaner 机制被执行的几率,但不能保证一定会执行。 曾经声称做出这种保证的两个方法:`System.runFinalizersOnExit` 和它的孪生兄弟 `Runtime.runFinalizersOnExit`,包含致命的缺陷,并已被弃用了几十年[ThreadStop]。
|
||||
|
||||
|
@ -7,8 +7,8 @@
|
||||
1. **不要提供修改对象状态的方法(也称为 mutators)。**
|
||||
2. **确保这个类不能被继承。** 这可以防止粗心的或恶意的子类,假设对象的状态已经改变,从而破坏类的不可变行为。 防止子类化通常是通过 `final` 修饰类,但是我们稍后将讨论另一种方法。
|
||||
3. **把所有属性设置为 final。** 通过系统强制执行,清楚地表达了你的意图。 另外,如果一个新创建的实例的引用从一个线程传递到另一个线程而没有同步,就必须保证正确的行为,正如内存模型[JLS,17.5; Goetz06,16] 所述。
|
||||
4. **把所有的属性设置为 private。** 这可以防止客户端获得对属性引用的可变对象的访问权限并直接修改这些对象。 虽然技术上允许不可变类具有包含基本类型数值的公共 `final` 属性或对不可变对象的引用,但不建议这样做,因为它不允许在以后的版本中更改内部表示(条目 15 和 16)。
|
||||
5. **确保对任何可变组件的互斥访问。** 如果你的类有任何引用可变对象的属性,请确保该类的客户端无法获得对这些对象的引用。 切勿将这样的属性初始化为客户端提供的对象引用,或从访问方法返回属性。 在构造方法,访问方法和 `readObject` 方法(条目 88)中进行防御性拷贝(条目 50)。
|
||||
4. **把所有的属性设置为 private。** 这可以防止客户端获得对属性引用的可变对象的访问权限并直接修改这些对象。 虽然技术上允许不可变类具有包含基本类型数值的公共 `final` 属性或对不可变对象的引用,但不建议这样做,因为它不允许在以后的版本中更改内部表示(详见第 15 和 16 条)。
|
||||
5. **确保对任何可变组件的互斥访问。** 如果你的类有任何引用可变对象的属性,请确保该类的客户端无法获得对这些对象的引用。 切勿将这样的属性初始化为客户端提供的对象引用,或从访问方法返回属性。 在构造方法,访问方法和 `readObject` 方法(详见第 88 条)中进行防御性拷贝(详见第 50 条)。
|
||||
|
||||
|
||||
|
||||
@ -46,8 +46,8 @@ public final class Complex {
|
||||
public Complex times(Complex c) {
|
||||
return new Complex(re * c.re - im * c.im,
|
||||
re * c.im + im * c.re);
|
||||
|
||||
}
|
||||
|
||||
public Complex dividedBy(Complex c) {
|
||||
double tmp = c.re * c.re + c.im * c.im;
|
||||
return new Complex((re * c.re + im * c.im) / tmp,
|
||||
@ -95,15 +95,15 @@ public static final Complex ZERO = new Complex(0, 0);
|
||||
public static final Complex ONE = new Complex(1, 0);
|
||||
public static final Complex I = new Complex(0, 1);
|
||||
```
|
||||
这种方法可以更进一步。 一个不可变的类可以提供静态的工厂(条目 1)来缓存经常被请求的实例,以避免在现有的实例中创建新的实例。 所有基本类型的包装类和 `BigInteger` 类都是这样做的。 使用这样的静态工厂会使客户端共享实例而不是创建新实例,从而减少内存占用和垃圾回收成本。 在设计新类时,选择静态工厂代替公共构造方法,可以在以后增加缓存的灵活性,而不需要修改客户端。
|
||||
这种方法可以更进一步。 一个不可变的类可以提供静态的工厂(详见第 1 条)来缓存经常被请求的实例,以避免在现有的实例中创建新的实例。 所有基本类型的包装类和 `BigInteger` 类都是这样做的。 使用这样的静态工厂会使客户端共享实例而不是创建新实例,从而减少内存占用和垃圾回收成本。 在设计新类时,选择静态工厂代替公共构造方法,可以在以后增加缓存的灵活性,而不需要修改客户端。
|
||||
|
||||
不可变对象可以自由分享的结果是,你永远不需要做出防御性拷贝(defensive copies)(条目 50)。 事实上,永远不需要做任何拷贝,因为这些拷贝永远等于原始对象。 因此,你不需要也不应该在一个不可变的类上提供一个 clone 方法或拷贝构造方法(copy constructor)(条目 13)。 这一点在 Java 平台的早期阶段还不是很好理解,所以 `String` 类有一个拷贝构造方法,但是它应该尽量很少使用(条目 6)。
|
||||
不可变对象可以自由分享的结果是,你永远不需要做出防御性拷贝(defensive copies)(详见第 50 条)。 事实上,永远不需要做任何拷贝,因为这些拷贝永远等于原始对象。 因此,你不需要也不应该在一个不可变的类上提供一个 clone 方法或拷贝构造方法(copy constructor)(详见第 13 条)。 这一点在 Java 平台的早期阶段还不是很好理解,所以 `String` 类有一个拷贝构造方法,但是它应该尽量很少使用(详见第 6 条)。
|
||||
|
||||
**不仅可以共享不可变的对象,而且可以共享内部信息。** 例如,`BigInteger` 类在内部使用符号数值表示法。 符号用 `int` 值表示,数值用 `int` 数组表示。 `negate` 方法生成了一个数值相同但符号相反的新 `BigInteger` 实例。 即使它是可变的,也不需要复制数组;新创建的 `BigInteger` 指向与原始相同的内部数组。
|
||||
|
||||
**不可变对象为其他对象提供了很好的构件(building blocks)** ,无论是可变的还是不可变的。 如果知道一个复杂组件的内部对象不会发生改变,那么维护复杂对象的不变量就容易多了。这一原则的特例是,不可变对象可以构成 `Map` 对象的键和 `Set` 的元素,一旦不可变对象作为 `Map` 的键或 `Set` 里的元素,即使破坏了 `Map` 和 `Set` 的不可变性,但不用担心它们的值会发生变化。
|
||||
**不可变对象为其他对象提供了很好的构件(building blocks)**,无论是可变的还是不可变的。 如果知道一个复杂组件的内部对象不会发生改变,那么维护复杂对象的不变量就容易多了。这一原则的特例是,不可变对象可以构成 `Map` 对象的键和 `Set` 的元素,一旦不可变对象作为 `Map` 的键或 `Set` 里的元素,即使破坏了 `Map` 和 `Set` 的不可变性,但不用担心它们的值会发生变化。
|
||||
|
||||
**不可变对象提供了免费的原子失败机制(条目 76)。** 它们的状态永远不会改变,所以不可能出现临时的不一致。
|
||||
**不可变对象提供了免费的原子失败机制(详见第 76 条)。** 它们的状态永远不会改变,所以不可能出现临时的不一致。
|
||||
|
||||
**不可变类的主要缺点是对于每个不同的值都需要一个单独的对象。** 创建这些对象可能代价很高,特别是如果是大型的对象下。 例如,假设你有一个百万位的 `BigInteger` ,你想改变它的低位:
|
||||
|
||||
@ -118,15 +118,14 @@ moby = moby.flipBit(0);
|
||||
BitSet moby = ...;
|
||||
moby.flip(0);
|
||||
```
|
||||
如果执行一个多步操作,在每一步生成一个新对象,除最终结果之外丢弃所有对象,则性能问题会被放大。这里有两种方式来处理这个问题。第一种办法,先猜测一下会经常用到哪些多步的操作,然后讲它们作为基本类型提供。如果一个多步操作是作为一个基本类型提供的,那么不可变类就不必在每一步创建一个独立的对象。在内部,不可变的类可以是任意灵活的。 例如,`BigInteger` 有一个包级私有的可变的“伙伴类(companion class)”,它用来加速多步操作,比如模幂运算( modular exponentiation)。出于前面所述的所有原因,使用可变伙伴类比使用 `BigInteger` 要困难得多。 幸运的是,你不必使用它:`BigInteger` 类的实现者为你做了很多努力。
|
||||
如果执行一个多步操作,在每一步生成一个新对象,除最终结果之外丢弃所有对象,则性能问题会被放大。这里有两种方式来处理这个问题。第一种办法,先猜测一下会经常用到哪些多步的操作,然后讲它们作为基本类型提供。如果一个多步操作是作为一个基本类型提供的,那么不可变类就不必在每一步创建一个独立的对象。在内部,不可变的类可以是任意灵活的。 例如,`BigInteger` 有一个包级私有的可变的“伙伴类(companion class)”,它用来加速多步操作,比如模幂运算(modular exponentiation)。出于前面所述的所有原因,使用可变伙伴类比使用 `BigInteger` 要困难得多。 幸运的是,你不必使用它:`BigInteger` 类的实现者为你做了很多努力。
|
||||
|
||||
如果你可以准确预测客户端要在你的不可变类上执行哪些复杂的操作,那么包级私有可变伙伴类的方式可以正常工作。如果不是的话,那么最好的办法就是提供一个公开的可变伙伴类。 这种方法在 Java 平台类库中的主要例子是 `String` 类,它的可变伙伴类是 `StringBuilder`(及其过时的前身 `StringBuffer` 类)。
|
||||
|
||||
现在你已经知道如何创建一个不可改变类,并且了解不变性的优点和缺点,下面我们来讨论几个设计方案。 回想一下,为了保证不变性,一个类不得允许子类化。 这可以通过使类用 `final` 修饰,但是还有另外一个更灵活的选择。 而不是使不可变类设置为 `final`,可以使其所有的构造方法私有或包级私有,并添加公共静态工厂,而不是公共构造方法(条目 1)。 为了具体说明这种方法,下面以 `Complex` 为例,看看如何使用这种方法:
|
||||
现在你已经知道如何创建一个不可改变类,并且了解不变性的优点和缺点,下面我们来讨论几个设计方案。 回想一下,为了保证不变性,一个类不得允许子类化。 这可以通过使类用 `final` 修饰,但是还有另外一个更灵活的选择。 而不是使不可变类设置为 `final`,可以使其所有的构造方法私有或包级私有,并添加公共静态工厂,而不是公共构造方法(详见第 1 条)。 为了具体说明这种方法,下面以 `Complex` 为例,看看如何使用这种方法:
|
||||
|
||||
```java
|
||||
// Immutable class with static factories instead of constructors
|
||||
|
||||
public class Complex {
|
||||
|
||||
private final double re;
|
||||
@ -146,7 +145,7 @@ public class Complex {
|
||||
```
|
||||
这种方法往往是最好的选择。 这是最灵活的,因为它允许使用多个包级私有实现类。 对于驻留在包之外的客户端,不可变类实际上是 `final` 的,因为不可能继承来自另一个包的类,并且缺少公共或受保护的构造方法。 除了允许多个实现类的灵活性以外,这种方法还可以通过改进静态工厂的对象缓存功能来调整后续版本中类的性能。
|
||||
|
||||
当 `BigInteger` 和 `BigDecimal` 被写入时,不可变类必须是有效的 `final`,因此它们的所有方法都可能被重写。不幸的是,在保持向后兼容性的同时,这一事实无法纠正。如果你编写一个安全性取决于来自不受信任的客户端的 `BigInteger` 或 `BigDecimal` 参数的不变类时,则必须检查该参数是“真实的”`BigInteger` 还是 `BigDecimal`,而不应该是不受信任的子类的实例。如果是后者,则必须在假设可能是可变的情况下保护性拷贝(defensively copy)(条目 50):
|
||||
当 `BigInteger` 和 `BigDecimal` 被写入时,不可变类必须是有效的 `final`,因此它们的所有方法都可能被重写。不幸的是,在保持向后兼容性的同时,这一事实无法纠正。如果你编写一个安全性取决于来自不受信任的客户端的 `BigInteger` 或 `BigDecimal` 参数的不变类时,则必须检查该参数是“真实的”`BigInteger` 还是 `BigDecimal`,而不应该是不受信任的子类的实例。如果是后者,则必须在假设可能是可变的情况下保护性拷贝(defensively copy)(详见第 50 条):
|
||||
|
||||
```java
|
||||
public static BigInteger safeInstance(BigInteger val) {
|
||||
@ -157,11 +156,11 @@ public static BigInteger safeInstance(BigInteger val) {
|
||||
|
||||
在本条目开头关于不可变类的规则说明,没有方法可以修改对象,并且它的所有属性必须是 `final` 的。事实上,这些规则比实际需要的要强硬一些,其实可以有所放松来提高性能。 事实上,任何方法都不能在对象的状态中产生外部可见的变化。 然而,一些不可变类具有一个或多个非 `final` 属性,在第一次需要时将开销昂贵的计算结果缓存在这些属性中。 如果再次请求相同的值,则返回缓存的值,从而节省了重新计算的成本。 这个技巧的作用恰恰是因为对象是不可变的,这保证了如果重复的话,计算会得到相同的结果。
|
||||
|
||||
例如,`PhoneNumber` 类的 `hashCode` 方法(第 53 页的条目 11)在第一次调用改方法时计算哈希码,并在再次调用时对其进行缓存。 这种延迟初始化(条目 83)的一个例子,String 类也使用到了。
|
||||
例如,`PhoneNumber` 类的 `hashCode` 方法(第 53 页的条目 11)在第一次调用改方法时计算哈希码,并在再次调用时对其进行缓存。 这种延迟初始化(详见第 83 条)的一个例子,String 类也使用到了。
|
||||
|
||||
关于序列化应该加上一个警告。 如果你选择使您的不可变类实现 `Serializable` 接口,并且它包含一个或多个引用可变对象的属性,则必须提供显式的 `readObject` 或 `readResolve` 方法,或者使用 `ObjectOutputStream.writeUnshared` 和 `ObjectInputStream.readUnshared` 方法,即默认的序列化形式也是可以接受的。 否则攻击者可能会创建一个可变的类的实例。 这个主题会在条目 88 中会详细介绍。
|
||||
|
||||
总而言之,坚决不要为每个属性编写一个 get 方法后再编写一个对应的 set 方法。 **除非有充分的理由使类成为可变类,否则类应该是不可变的。** 不可变类提供了许多优点,唯一的缺点是在某些情况下可能会出现性能问题。 你应该始终使用较小的值对象(如 `PhoneNumber` 和 `Complex`),使其不可变。 (Java 平台类库中有几个类,如 `java.util.Date` 和 `java.awt.Point`,本应该是不可变的,但实际上并不是)。你应该认真考虑创建更大的值对象,例如 `String` 和 `BigInteger` ,设成不可改变的。 只有当你确认有必要实现令人满意的性能(条目 67)时,才应该为不可改变类提供一个公开的可变伙伴类。
|
||||
总而言之,坚决不要为每个属性编写一个 get 方法后再编写一个对应的 set 方法。 **除非有充分的理由使类成为可变类,否则类应该是不可变的。** 不可变类提供了许多优点,唯一的缺点是在某些情况下可能会出现性能问题。 你应该始终使用较小的值对象(如 `PhoneNumber` 和 `Complex`),使其不可变。 (Java 平台类库中有几个类,如 `java.util.Date` 和 `java.awt.Point`,本应该是不可变的,但实际上并不是)。你应该认真考虑创建更大的值对象,例如 `String` 和 `BigInteger` ,设成不可改变的。 只有当你确认有必要实现令人满意的性能(详见第 67 条)时,才应该为不可改变类提供一个公开的可变伙伴类。
|
||||
|
||||
对于一些类来说,不变性是不切实际的。**如果一个类不能设计为不可变类,那么也要尽可能地限制它的可变性** 。减少对象可以存在的状态数量,可以更容易地分析对象,以及降低出错的可能性。因此,除非有足够的理由把属性设置为非 `final` 的情况下,否则应该每个属性都设置为 `final` 的。把本条目的建议与条目 15 的建议结合起来,你自然的倾向就是:**除非有充分的理由不这样做,否则应该把每个属性声明为私有 final 的。**
|
||||
|
||||
@ -172,4 +171,4 @@ public static BigInteger safeInstance(BigInteger val) {
|
||||
在这个条目中,应该添加关于 `Complex` 类的最后一个注释。 这个例子只是为了说明不变性。 这不是一个工业强度复杂的复数实现。 它对复数使用了乘法和除法的标准公式,这些公式不正确会进行不正确的四舍五入,没有为复数的 `NaN` 和无穷大提供良好的语义[Kahan91,Smith62,Thomas94]。
|
||||
|
||||
|
||||
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
# 18. 组合优于继承
|
||||
|
||||
继承是实现代码重用的有效方式,但并不总是最好的工具。使用不当,会导致脆弱的软件。 在包中使用继承是安全的,其中子类和父类的实现都在同一个程序员的控制之下。对应专门为了继承而设计的,并且有文档说明的类来说(条目 19),使用继承也是安全的。 然而,从普通的具体类跨越包级边界继承,是危险的。 提醒一下,本书使用“继承”一词来表示实现继承(当一个类继承另一个类时)。 在这个项目中讨论的问题不适用于接口继承(当类实现接口或当接口继承另一个接口时)。
|
||||
继承是实现代码重用的有效方式,但并不总是最好的工具。使用不当,会导致脆弱的软件。 在包中使用继承是安全的,其中子类和父类的实现都在同一个程序员的控制之下。对应专门为了继承而设计的,并且有文档说明的类来说(详见第 19 条),使用继承也是安全的。 然而,从普通的具体类跨越包级边界继承,是危险的。 提醒一下,本书使用“继承”一词来表示实现继承(当一个类继承另一个类时)。 在这个项目中讨论的问题不适用于接口继承(当类实现接口或当接口继承另一个接口时)。
|
||||
|
||||
**与方法调用不同,继承打破了封装[Snyder86]。** 换句话说,一个子类依赖于其父类的实现细节来保证其正确的功能。 父类的实现可能会从发布版本不断变化,如果是这样,子类可能会被破坏,即使它的代码没有任何改变。 因此,一个子类必须与其超类一起更新而变化,除非父类的作者为了继承的目的而专门设计它,并对应有文档的说明。
|
||||
|
||||
@ -179,9 +179,9 @@ static void walk(Set<Dog> dogs) {
|
||||
|
||||
`InstrumentedSet` 类被称为包装类,因为每个 `InstrumentedSet` 实例都包含(“包装”)另一个 Set 实例。 这也被称为装饰器模式[Gamma95],因为 `InstrumentedSet` 类通过添加计数功能来“装饰”一个集合。 有时组合和转发的结合被不精确地地称为委托(delegation)。 从技术上讲,除非包装对象把自身传递给被包装对象,否则不是委托[Lieberman86;Gamma95]。
|
||||
|
||||
包装类的缺点很少。 一个警告是包装类不适合在回调框架(callback frameworks)中使用,其中对象将自我引用传递给其他对象以用于后续调用(“回调”)。 因为一个被包装的对象不知道它外面的包装对象,所以它传递一个指向自身的引用(this),回调时并不记得外面的包装对象。 这被称为 SELF 问题[Lieberman86]。 有些人担心转发方法调用的性能影响,以及包装对象对内存占用。 两者在实践中都没有太大的影响。 编写转发方法有些繁琐,但是只需为每个接口编写一次可重用的转发类,并且提供转发类。 例如,`Guava` 为所有的 `Collection` 接口提供转发类[Guava]。
|
||||
包装类的缺点很少。 一个警告是包装类不适合在回调框架(callback frameworks)中使用,其中对象将自我引用传递给其他对象以用于后续调用(「回调」)。 因为一个被包装的对象不知道它外面的包装对象,所以它传递一个指向自身的引用(this),回调时并不记得外面的包装对象。 这被称为 SELF 问题[Lieberman86]。 有些人担心转发方法调用的性能影响,以及包装对象对内存占用。 两者在实践中都没有太大的影响。 编写转发方法有些繁琐,但是只需为每个接口编写一次可重用的转发类,并且提供转发类。 例如,`Guava` 为所有的 `Collection` 接口提供转发类[Guava]。
|
||||
|
||||
只有在子类真的是父类的子类型的情况下,继承才是合适的。 换句话说,只有在两个类之间存在“is-a”关系的情况下,B 类才能继承 A 类。 如果你试图让 B 类继承 A 类时,问自己这个问题:每个 B 都是 A 吗? 如果你不能如实回答这个问题,那么 B 就不应该继承 A。如果答案是否定的,那么 B 通常包含一个 A 的私有实例,并且暴露一个不同的 API:A 不是 B 的重要部分 ,只是其实现细节。
|
||||
只有在子类真的是父类的子类型的情况下,继承才是合适的。 换句话说,只有在两个类之间存在「is-a」关系的情况下,B 类才能继承 A 类。 如果你试图让 B 类继承 A 类时,问自己这个问题:每个 B 都是 A 吗? 如果你不能如实回答这个问题,那么 B 就不应该继承 A。如果答案是否定的,那么 B 通常包含一个 A 的私有实例,并且暴露一个不同的 API:A 不是 B 的重要部分 ,只是其实现细节。
|
||||
|
||||
在 Java 平台类库中有一些明显的违反这个原则的情况。 例如,`stacks` 实例并不是 `vector` 实例,所以 `Stack` 类不应该继承 `Vector` 类。 同样,一个属性列表不是一个哈希表,所以 `Properties` 不应该继承 `Hashtable` 类。 在这两种情况下,组合方式更可取。
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
# 19. 要么设计继承并提供文档说明,要么禁用继承
|
||||
|
||||
条目 18 中提醒你注意继承没有设计和文档说明的“外来”类的子类化的危险。 那么为了继承而设计和文档说明一个类是什么意思呢?
|
||||
条目 18 中提醒你注意继承没有设计和文档说明的「外来」类的子类化的危险。 那么为了继承而设计和文档说明一个类是什么意思呢?
|
||||
|
||||
首先,这个类必须准确地描述重写这个方法带来的影响。 换句话说,该类必须文档说明可重写方法的自用性(self-use)。 对于每个公共或受保护的方法,文档必须指明方法调用哪些重写方法,以何种顺序以及每次调用的结果如何影响后续处理。 (重写方法,这里是指非 `final` 修饰的方法,无论是公开还是保护的。)更一般地说,一个类必须文档说明任何可能调用可重写方法的情况。 例如,后台线程或者静态初始化代码块可能会调用这样的方法。
|
||||
|
||||
@ -107,7 +107,7 @@ public final class Sub extends Super {
|
||||
|
||||
最后,如果你决定在为继承设计的类中实现 `Serializable` 接口,并且该类有一个 `readResolve` 或 `writeReplace` 方法,则必须使 `readResolve` 或 `writeReplace` 方法设置为受保护而不是私有。 如果这些方法是私有的,它们将被子类无声地忽略。 这是另一种情况,把实现细节成为类的 API 的一部分,以允许继承。
|
||||
|
||||
到目前为止,**设计一个继承类需要很大的努力,并且对这个类有很大的限制。** 这不是一个轻率的决定。 有些情况显然是正确的,比如抽象类,包括接口的骨架实现(skeletal implementations)(条目 20)。 还有其他的情况显然是错误的,比如不可变的类(条目 17)。
|
||||
到目前为止,**设计一个继承类需要很大的努力,并且对这个类有很大的限制。** 这不是一个轻率的决定。 有些情况显然是正确的,比如抽象类,包括接口的骨架实现(skeletal implementations)(详见第 20 条)。 还有其他的情况显然是错误的,比如不可变的类(详见第 17 条)。
|
||||
|
||||
但是普通的具体类呢? 传统上,它们既不是 `final` 的,也不是为了子类化而设计和文档说明的,但是这种情况是危险的。每次修改这样的类,则继承此类的子类将被破坏。 这不仅仅是一个理论问题。 在修改非 `final` 的具体类的内部之后,接收与子类相关的错误报告并不少见,这些类没有为继承而设计和文档说明。
|
||||
|
||||
|
@ -29,7 +29,7 @@ public interface SingerSongwriter extends Singer, Songwriter {
|
||||
|
||||
你并不总是需要这种灵活性,但是当你这样做的时候,接口是一个救星。 另一种方法是对于每个受支持的属性组合,包含一个单独的类的臃肿类层级结构。 如果类型系统中有 n 个属性,则可能需要支持 2n 种可能的组合。 这就是所谓的组合爆炸(combinatorial explosion)。 臃肿的类层级结构可能会导致具有许多方法的臃肿类,这些方法仅在参数类型上有所不同,因为类层级结构中没有类型来捕获通用行为。
|
||||
|
||||
接口通过包装类模式确保安全的,强大的功能增强成为可能(条目 18)。 如果使用抽象类来定义类型,那么就让程序员想要添加功能,只能继承。 生成的类比包装类更弱,更脆弱。
|
||||
接口通过包装类模式确保安全的,强大的功能增强成为可能(详见第 18 条)。 如果使用抽象类来定义类型,那么就让程序员想要添加功能,只能继承。 生成的类比包装类更弱,更脆弱。
|
||||
|
||||
当其他接口方法有明显的接口方法实现时,可以考虑向程序员提供默认形式的方法实现帮助。 有关此技术的示例,请参阅第 104 页的 `removeIf` 方法。如果提供默认方法,请确保使用`@implSpec` Javadoc 标记(条目 19)将它们文档说明为继承。
|
||||
|
||||
@ -67,7 +67,7 @@ static List<Integer> intArrayAsList(int[] a) {
|
||||
}
|
||||
```
|
||||
|
||||
当你考虑一个 `List` 实现为你做的所有事情时,这个例子是一个骨架实现的强大的演示。 顺便说一句,这个例子是一个适配器(Adapter)[Gamma95],它允许一个 `int` 数组被看作 `Integer` 实例列表。 由于 `int` 值和整数实例(装箱和拆箱)之间的来回转换,其性能并不是非常好。 请注意,实现采用匿名类的形式(条目 24)。
|
||||
当你考虑一个 `List` 实现为你做的所有事情时,这个例子是一个骨架实现的强大的演示。 顺便说一句,这个例子是一个适配器(Adapter)[Gamma95],它允许一个 `int` 数组被看作 `Integer` 实例列表。 由于 `int` 值和整数实例(装箱和拆箱)之间的来回转换,其性能并不是非常好。 请注意,实现采用匿名类的形式(详见第 24 条)。
|
||||
|
||||
骨架实现类的优点在于,它们提供抽象类的所有实现的帮助,而不会强加抽象类作为类型定义时的严格约束。对于具有骨架实现类的接口的大多数实现者来说,继承这个类是显而易见的选择,但它不是必需的。如果一个类不能继承骨架的实现,这个类可以直接实现接口。该类仍然受益于接口本身的任何默认方法。此外,骨架实现类仍然可以协助接口的实现。实现接口的类可以将接口方法的调用转发给继承骨架实现的私有内部类的包含实例。这种被称为模拟多重继承的技术与条目 18 讨论的包装类模式密切相关。它提供了多重继承的许多好处,同时避免了缺陷。
|
||||
|
||||
|
@ -2,7 +2,7 @@
|
||||
|
||||
在 Java 8 之前,不可能在不破坏现有实现的情况下为接口添加方法。 如果向接口添加了一个新方法,现有的实现通常会缺少该方法,从而导致编译时错误。 在 Java 8 中,添加了默认方法(default method)构造[JLS 9.4],目的是允许将方法添加到现有的接口。 但是增加新的方法到现有的接口是充满风险的。
|
||||
|
||||
默认方法的声明包含一个默认实现,该方法允许实现接口的类直接使用,而不必实现默认方法。 虽然在 Java 中添加默认方法可以将方法添加到现有接口,但不能保证这些方法可以在所有已有的实现中使用。 默认的方法被“注入(injected)”到现有的实现中,没有经过实现类的知道或同意。 在 Java 8 之前,这些实现是用默认的接口编写的,它们的接口永远不会获得任何新的方法。
|
||||
默认方法的声明包含一个默认实现,该方法允许实现接口的类直接使用,而不必实现默认方法。 虽然在 Java 中添加默认方法可以将方法添加到现有接口,但不能保证这些方法可以在所有已有的实现中使用。 默认的方法被「注入(injected)」到现有的实现中,没有经过实现类的知道或同意。 在 Java 8 之前,这些实现是用默认的接口编写的,它们的接口永远不会获得任何新的方法。
|
||||
|
||||
许多新的默认方法被添加到 Java 8 的核心集合接口中,主要是为了方便使用 lambda 表达式(第 6 章)。 Java 类库的默认方法是高质量的通用实现,在大多数情况下,它们工作正常。 **但是,编写一个默认方法并不总是可能的,它保留了每个可能的实现的所有不变量。**
|
||||
|
||||
@ -31,7 +31,7 @@ default boolean removeIf(Predicate<? super E> filter) {
|
||||
|
||||
**在默认方法的情况下,接口的现有实现类可以在没有错误或警告的情况下编译,但在运行时会失败。** 虽然不是非常普遍,但这个问题也不是一个孤立的事件。 在 Java 8 中添加到集合接口的一些方法已知是易受影响的,并且已知一些现有的实现会受到影响。
|
||||
|
||||
应该避免使用默认方法向现有的接口添加新的方法,除非这个需要是关键的,在这种情况下,你应该仔细考虑,以确定现有的接口实现是否会被默认的方法实现所破坏。然而,默认方法对于在创建接口时提供标准的方法实现非常有用,以减轻实现接口的任务(条目 20)。
|
||||
应该避免使用默认方法向现有的接口添加新的方法,除非这个需要是关键的,在这种情况下,你应该仔细考虑,以确定现有的接口实现是否会被默认的方法实现所破坏。然而,默认方法对于在创建接口时提供标准的方法实现非常有用,以减轻实现接口的任务(详见第 20 条)。
|
||||
|
||||
还值得注意的是,默认方法不是被用来设计,来支持从接口中移除方法或者改变现有方法的签名的目的。在不破坏现有客户端的情况下,这些接口都不可能发生更改。
|
||||
|
||||
|
@ -18,11 +18,11 @@ public interface PhysicalConstants {
|
||||
}
|
||||
```
|
||||
|
||||
**常量接口模式是对接口的糟糕使用。** 类在内部使用一些常量,完全属于实现细节。实现一个常量接口会导致这个实现细节泄漏到类的导出 API 中。对类的用户来说,类实现一个常量接口是没有意义的。事实上,它甚至可能使他们感到困惑。更糟糕的是,它代表了一个承诺:如果在将来的版本中修改了类,不再需要使用常量,那么它仍然必须实现接口,以确保二进制兼容性。如果一个非 final 类实现了常量接口,那么它的所有子类的命名空间都会被接口中的常量所污染。
|
||||
**常量接口模式是对接口的糟糕使用。** 类在内部使用一些常量,完全属于实现细节。实现一个常量接口会导致这个实现细节泄漏到类的导出 API 中。对类的用户来说,类实现一个常量接口是没有意义的。事实上,它甚至可能使他们感到困惑。更糟糕的是,它代表了一个承诺:如果在将来的版本中修改了类,不再需要使用常量,那么它仍然必须实现接口,以确保二进制兼容性。如果一个非 final 类实现了常量接口,那么它的所有子类的命名空间都会被接口中的常量所污染。
|
||||
|
||||
Java 平台类库中有多个常量接口,如 `java.io.ObjectStreamConstants。` 这些接口应该被视为不规范的,不应该被效仿。
|
||||
|
||||
如果你想导出常量,有几个合理的选择方案。 如果常量与现有的类或接口紧密相关,则应将其添加到该类或接口中。 例如,所有数字基本类型的包装类,如 `Integer` 和 `Double`,都会导出 `MIN_VALUE` 和 `MAX_VALUE` 常量。 如果常量最好被看作枚举类型的成员,则应该使用枚举类型(条目 34)导出它们。 否则,你应该用一个不可实例化的工具类来导出常量(条目 4)。 下是前面所示的 `PhysicalConstants` 示例的工具类的版本:
|
||||
如果你想导出常量,有几个合理的选择方案。 如果常量与现有的类或接口紧密相关,则应将其添加到该类或接口中。 例如,所有数字基本类型的包装类,如 `Integer` 和 `Double`,都会导出 `MIN_VALUE` 和 `MAX_VALUE` 常量。 如果常量最好被看作枚举类型的成员,则应该使用枚举类型(详见第 34 条)导出它们。 否则,你应该用一个不可实例化的工具类来导出常量(详见第 4 条)。 下是前面所示的 `PhysicalConstants` 示例的工具类的版本:
|
||||
|
||||
|
||||
```java
|
||||
|
@ -87,7 +87,7 @@ class Square extends Rectangle {
|
||||
}
|
||||
}
|
||||
```
|
||||
请注意,上述层中的属性是直接访问的,而不是访问方法。 这里是为了简洁起见,如果类层次是公开的(条目 16),这将是一个糟糕的设计。
|
||||
请注意,上述层中的属性是直接访问的,而不是访问方法。 这里是为了简洁起见,如果类层次是公开的(详见第 16 条),这将是一个糟糕的设计。
|
||||
|
||||
总之,标签类很少有适用的情况。 如果你想写一个带有明显标签属性的类,请考虑标签属性是否可以被删除,而类是否被类层次替换。 当遇到一个带有标签属性的现有类时,可以考虑将其重构为一个类层次中。
|
||||
|
||||
|
@ -4,7 +4,7 @@
|
||||
|
||||
静态成员类是最简单的嵌套类。 最好把它看作是一个普通的类,恰好在另一个类中声明,并且可以访问所有宿主类的成员,甚至是那些被声明为私有类的成员。 静态成员类是其宿主类的静态成员,并遵循与其他静态成员相同的可访问性规则。 如果它被声明为 `private`,则只能在宿主类中访问,等等。
|
||||
|
||||
静态成员类的一个常见用途是作为公共帮助类,仅在与其外部类一起使用时才有用。 例如,考虑一个描述计算器支持的操作的枚举类型(条目 34)。 `Operation` 枚举应该是 `Calculator` 类的公共静态成员类。 `Calculator` 客户端可以使用 `Calculator.Operation.PLUS` 和 `Calculator.Operation.MINUS` 等名称来引用操作。
|
||||
静态成员类的一个常见用途是作为公共帮助类,仅在与其外部类一起使用时才有用。 例如,考虑一个描述计算器支持的操作的枚举类型(详见第 34 条)。 `Operation` 枚举应该是 `Calculator` 类的公共静态成员类。 `Calculator` 客户端可以使用 `Calculator.Operation.PLUS` 和 `Calculator.Operation.MINUS` 等名称来引用操作。
|
||||
|
||||
在语法上,静态成员类和非静态成员类之间的唯一区别是静态成员类在其声明中具有 `static` 修饰符。 尽管句法相似,但这两种嵌套类是非常不同的。 非静态成员类的每个实例都隐含地与其包含的类的宿主实例相关联。 在非静态成员类的实例方法中,可以调用宿主实例上的方法,或者使用限定的构造[JLS,15.8.4] 获得对宿主实例的引用。 如果嵌套类的实例可以与其宿主类的实例隔离存在,那么嵌套类必须是静态成员类:不可能在没有宿主实例的情况下创建非静态成员类的实例。
|
||||
|
||||
@ -29,7 +29,7 @@ public class MySet<E> extends AbstractSet<E> {
|
||||
}
|
||||
```
|
||||
|
||||
**如果你声明了一个不需要访问宿主实例的成员类,总是把 static 修饰符放在它的声明中,使它成为一个静态成员类,而不是非静态的成员类。** 如果你忽略了这个修饰符,每个实例都会有一个隐藏的外部引用给它的宿主实例。 如前所述,存储这个引用需要占用时间和空间。 更严重的是,并且会导致即使宿主类在满足垃圾回收的条件时却仍然驻留在内存中(条目 7)。 由此产生的内存泄漏可能是灾难性的。 由于引用是不可见的,所以通常难以检测到。
|
||||
**如果你声明了一个不需要访问宿主实例的成员类,总是把 static 修饰符放在它的声明中,使它成为一个静态成员类,而不是非静态的成员类。** 如果你忽略了这个修饰符,每个实例都会有一个隐藏的外部引用给它的宿主实例。 如前所述,存储这个引用需要占用时间和空间。 更严重的是,并且会导致即使宿主类在满足垃圾回收的条件时却仍然驻留在内存中(详见第 7 条)。 由此产生的内存泄漏可能是灾难性的。 由于引用是不可见的,所以通常难以检测到。
|
||||
|
||||
私有静态成员类的常见用法是表示由它们的宿主类表示的对象的组件。 例如,考虑将键与值相关联的 `Map` 实例。 许多 `Map` 实现对于映射中的每个键值对都有一个内部的 `Entry` 对象。 当每个 `entry` 都与 `Map` 关联时,`entry` 上的方法 (`getKey`,`getValue` 和 `setValue`) 不需要访问 `Map`。 因此,使用非静态成员类来表示 `entry` 将是浪费的:私有静态成员类是最好的。 如果意外地忽略了 `entry` 声明中的 `static` 修饰符,`Map` 仍然可以工作,但是每个 `entry` 都会包含对 `Map` 的引用,浪费空间和时间。
|
||||
|
||||
@ -39,7 +39,7 @@ public class MySet<E> extends AbstractSet<E> {
|
||||
|
||||
匿名类的适用性有很多限制。 除了在声明的时候之外,不能实例化它们。 你不能执行 `instanceof` 方法测试或者做任何其他需要你命名的类。 不能声明一个匿名类来实现多个接口,或者继承一个类并同时实现一个接口。 匿名类的客户端不能调用除父类型继承的成员以外的任何成员。 因为匿名类在表达式中出现,所以它们必须保持简短 —— 约十行或更少 —— 否则可读性将受到影响。
|
||||
|
||||
在将 lambda 表达式添加到 Java(第 6 章)之前,匿名类是创建小函数对象和处理对象的首选方法,但 lambda 表达式现在是首选(条目 42)。 匿名类的另一个常见用途是实现静态工厂方法(请参阅条目 20 中的 `intArrayAsList`)。
|
||||
在将 lambda 表达式添加到 Java(第 6 章)之前,匿名类是创建小函数对象和处理对象的首选方法,但 lambda 表达式现在是首选(详见第 42 条)。 匿名类的另一个常见用途是实现静态工厂方法(请参阅条目 20 中的 `intArrayAsList`)。
|
||||
|
||||
局部类是四种嵌套类中使用最少的。 一个局部类可以在任何可以声明局部变量的地方声明,并遵守相同的作用域规则。 局部类与其他类型的嵌套类具有共同的属性。 像成员类一样,他们有名字,可以重复使用。 就像匿名类一样,只有在非静态上下文中定义它们时,它们才会包含实例,并且它们不能包含静态成员。 像匿名类一样,应该保持简短,以免损害可读性。
|
||||
|
||||
|
@ -43,7 +43,7 @@ class Dessert {
|
||||
|
||||
如果使用命令 `javac Main.java` 或 `javac Main.java Utensil.java` 编译程序,它的行为与在编写 `Dessert.java` 文件(即打印 `pancake`)之前的行为相同。 但是,如果使用命令 `javac Dessert.java Main.java 编译程序`,它将打印 `potpie`。 程序的行为因此受到源文件传递给编译器的顺序的影响,这显然是不可接受的。
|
||||
|
||||
解决这个问题很简单,将顶层类(如我们的例子中的 `Utensil` 和 `Dessert`)分割成单独的源文件。 如果试图将多个顶级类放入单个源文件中,请考虑使用静态成员类(条目 24)作为将类拆分为单独的源文件的替代方法。 如果这些类从属于另一个类,那么将它们变成静态成员类通常是更好的选择,因为它提高了可读性,并且可以通过声明它们为私有(条目 15)来减少类的可访问性。下面是我们的例子看起来如何使用静态成员类:
|
||||
解决这个问题很简单,将顶层类(如我们的例子中的 `Utensil` 和 `Dessert`)分割成单独的源文件。 如果试图将多个顶级类放入单个源文件中,请考虑使用静态成员类(详见第 24 条)作为将类拆分为单独的源文件的替代方法。 如果这些类从属于另一个类,那么将它们变成静态成员类通常是更好的选择,因为它提高了可读性,并且可以通过声明它们为私有(详见第 15 条)来减少类的可访问性。下面是我们的例子看起来如何使用静态成员类:
|
||||
|
||||
```java
|
||||
// Static member classes instead of multiple top-level classes
|
||||
|
@ -4,9 +4,9 @@
|
||||
|
||||
# 26. 不要使用原始类型
|
||||
|
||||
首先,有几个术语。一个类或接口,它的声明有一个或多个类型参数(type parameters ),被称之为泛型类或泛型接口[JLS,8.1.2,9.1.2]。 例如,`List` 接口具有单个类型参数 E,表示其元素类型。 接口的全名是 `List<E>`(读作“E”的列表),但是人们经常称它为 `List`。 泛型类和接口统称为泛型类型(generic types)。
|
||||
首先,有几个术语。一个类或接口,它的声明有一个或多个类型参数(type parameters ),被称之为泛型类或泛型接口[JLS,8.1.2,9.1.2]。 例如,`List` 接口具有单个类型参数 E,表示其元素类型。 接口的全名是 `List<E>`(读作「E」的列表),但是人们经常称它为 `List`。 泛型类和接口统称为泛型类型(generic types)。
|
||||
|
||||
每个泛型定义了一组参数化类型(parameterized types),它们由类或接口名称组成,后跟一个与泛型类型的形式类型参数[JLS,4.4,4.5] 相对应的实际类型参数的尖括号“<>”列表。 例如,`List<String>`(读作“字符串列表”)是一个参数化类型,表示其元素类型为 String 的列表。 (`String` 是与形式类型参数 `E` 相对应的实际类型参数)。
|
||||
每个泛型定义了一组参数化类型(parameterized types),它们由类或接口名称组成,后跟一个与泛型类型的形式类型参数[JLS,4.4,4.5] 相对应的实际类型参数的尖括号「<>」列表。 例如,`List<String>`(读作「字符串列表」)是一个参数化类型,表示其元素类型为 String 的列表。 (`String` 是与形式类型参数 `E` 相对应的实际类型参数)。
|
||||
|
||||
最后,每个泛型定义了一个原始类型(raw type),它是没有任何类型参数的泛型类型的名称[JLS,4.8]。 例如,对应于 `List<E>` 的原始类型是 `List`。 原始类型的行为就像所有的泛型类型信息都从类型声明中被清除一样。 它们的存在主要是为了与没有泛型之前的代码相兼容。
|
||||
|
||||
@ -35,7 +35,7 @@ for (Iterator i = stamps.iterator(); i.hasNext(); )
|
||||
stamp.cancel();
|
||||
```
|
||||
|
||||
正如本书所提到的,在编译完成之后尽快发现错误是值得的,理想情况是在编译时。 在这种情况下,直到运行时才发现错误,在错误发生后的很长一段时间,以及可能远离包含错误的代码的代码中。 一旦看到 `ClassCastException`,就必须搜索代码类库,查找将 `coin` 实例放入 `stamp` 集合的方法调用。 编译器不能帮助你,因为它不能理解那个说“仅包含 `stamp` 实例”的注释。
|
||||
正如本书所提到的,在编译完成之后尽快发现错误是值得的,理想情况是在编译时。 在这种情况下,直到运行时才发现错误,在错误发生后的很长一段时间,以及可能远离包含错误的代码的代码中。 一旦看到 `ClassCastException`,就必须搜索代码类库,查找将 `coin` 实例放入 `stamp` 集合的方法调用。 编译器不能帮助你,因为它不能理解那个说「仅包含 `stamp` 实例」的注释。
|
||||
|
||||
对于泛型,类型声明包含的信息,而不是注释:
|
||||
|
||||
@ -44,7 +44,7 @@ for (Iterator i = stamps.iterator(); i.hasNext(); )
|
||||
private final Collection<Stamp> stamps = ... ;
|
||||
```
|
||||
|
||||
从这个声明中,编译器知道 `stamps` 集合应该只包含 `Stamp` 实例,并保证它是 `true`,假设你的整个代码类库编译时不发出(或者抑制;参见条目 27)任何警告。 当使用参数化类型声明声明 `stamps` 时,错误的插入会生成一个编译时错误消息,告诉你到底发生了什么错误:
|
||||
从这个声明中,编译器知道 `stamps` 集合应该只包含 `Stamp` 实例,并保证它是 `true`,假设你的整个代码类库编译时不发出(或者抑制;参见条目 27)任何警告。 当使用参数化类型声明声明 `stamps` 时,错误的插入会生成一个编译时错误消息,告诉你到底发生了什么错误:
|
||||
|
||||
```java
|
||||
Test.java:9: error: incompatible types: Coin cannot be converted
|
||||
@ -55,7 +55,7 @@ to Stamp
|
||||
|
||||
当从集合中检索元素时,编译器会为你插入不可见的强制转换,并保证它们不会失败(再假设你的所有代码都不会生成或禁止任何编译器警告)。 虽然意外地将 `coin` 实例插入 `stamp` 集合的预期可能看起来很牵强,但这个问题是真实的。 例如,很容易想象将 `BigInteger` 放入一个只包含 `BigDecimal` 实例的集合中。
|
||||
|
||||
如前所述,使用原始类型(没有类型参数的泛型)是合法的,但是你不应该这样做。 **如果你使用原始类型,则会丧失泛型的所有安全性和表达上的优势。** 鉴于你不应该使用它们,为什么语言设计者首先允许原始类型呢? 答案是为了兼容性。 泛型被添加时,Java 即将进入第二个十年,并且有大量的代码没有使用泛型。 所有这些代码都是合法的,并且与使用泛型的新代码进行交互操作被认为是至关重要的。 将参数化类型的实例传递给为原始类型设计的方法必须是合法的,反之亦然。 这个需求,被称为迁移兼容性,驱使决策支持原始类型,并使用擦除来实现泛型(条目 28)。
|
||||
如前所述,使用原始类型(没有类型参数的泛型)是合法的,但是你不应该这样做。 **如果你使用原始类型,则会丧失泛型的所有安全性和表达上的优势。** 鉴于你不应该使用它们,为什么语言设计者首先允许原始类型呢? 答案是为了兼容性。 泛型被添加时,Java 即将进入第二个十年,并且有大量的代码没有使用泛型。 所有这些代码都是合法的,并且与使用泛型的新代码进行交互操作被认为是至关重要的。 将参数化类型的实例传递给为原始类型设计的方法必须是合法的,反之亦然。 这个需求,被称为迁移兼容性,驱使决策支持原始类型,并使用擦除来实现泛型(详见第 28 条)。
|
||||
|
||||
虽然不应使用诸如 `List` 之类的原始类型,但可以使用参数化类型来允许插入任意对象(如 `List<Object>`)。 原始类型 List 和参数化类型 `List<Object>` 之间有什么区别? 松散地说,前者已经选择了泛型类型系统,而后者明确地告诉编译器,它能够保存任何类型的对象。 虽然可以将 `List<String>` 传递给 `List` 类型的参数,但不能将其传递给 `List<Object>` 类型的参数。 泛型有子类型的规则,`List<String>` 是原始类型 `List` 的子类型,但不是参数化类型 `List<Object>` 的子类型(条目 28)。 因此,如果使用诸如 `List` 之类的原始类型,则会丢失类型安全性,但是如果使用参数化类型(例如 `List<Object>`)则不会。
|
||||
|
||||
@ -105,8 +105,7 @@ static int numElementsInCommon(Set s1, Set s2) {
|
||||
}
|
||||
```
|
||||
|
||||
这种方法可以工作,但它使用原始类型,这是危险的。 安全替代方式是使用无限制通配符类型(unbounded wildcard types)。 如果要使用泛型类型,但不知道或关心实际类型参数是什么,则可以使用问号来代替。 例如,泛型类型 `Set<E>` 的无限制通配符类型是 `Set<?>`(读取“某种类型的集合”)。 它是最通用的参数化的 `Set` 类型,能够保持任何集合。 下面是 `numElementsInCommon` 方法使用无限制通配符类型声明的情况:
|
||||
。
|
||||
这种方法可以工作,但它使用原始类型,这是危险的。 安全替代方式是使用无限制通配符类型(unbounded wildcard types)。 如果要使用泛型类型,但不知道或关心实际类型参数是什么,则可以使用问号来代替。 例如,泛型类型 `Set<E>` 的无限制通配符类型是 `Set<?>`(读取「某种类型的集合」)。 它是最通用的参数化的 `Set` 类型,能够保持任何集合。 下面是 `numElementsInCommon` 方法使用无限制通配符类型声明的情况:
|
||||
|
||||
```java
|
||||
// Uses unbounded wildcard type - typesafe and flexible
|
||||
@ -124,7 +123,7 @@ converted to CAP#1
|
||||
CAP#1 extends Object from capture of ?
|
||||
```
|
||||
|
||||
不可否认的是,这个错误信息留下了一些需要的东西,但是编译器已经完成了它的工作,不管它的元素类型是什么,都不会破坏集合的类型不变性。 你不仅可以将任何元素(除 `null` 以外)放入一个 `Collection<?>` 中,但是不能保证你所得到的对象的类型。 如果这些限制是不可接受的,可以使用泛型方法(条目 30)或有限制配符类型(条目 31)。
|
||||
不可否认的是,这个错误信息留下了一些需要的东西,但是编译器已经完成了它的工作,不管它的元素类型是什么,都不会破坏集合的类型不变性。 你不仅可以将任何元素(除 `null` 以外)放入一个 `Collection<?>` 中,但是不能保证你所得到的对象的类型。 如果这些限制是不可接受的,可以使用泛型方法(详见第 30 条)或有限制配符类型(详见第 31 条)。
|
||||
|
||||
对于不应该使用原始类型的规则,有一些小例外。 **你必须在类字面值(class literals)中使用原始类型。** 规范中不允许使用参数化类型(尽管它允许数组类型和基本类型)[JLS,15.8.2]。 换句话说,`List.class`,`String[].class` 和 `int.class` 都是合法的,但 `List<String>.class` 和 `List<?>.class` 不是合法的。
|
||||
|
||||
|
@ -18,7 +18,7 @@ Venery.java:4: warning: [unchecked] unchecked conversion
|
||||
found: HashSet。
|
||||
```
|
||||
|
||||
然后可以进行指示修正,让警告消失。 请注意,实际上并不需要指定类型参数,只是为了表明它与 Java 7 中引入的钻石运算符("<>")一同出现。然后编译器会推断出正确的实际类型参数(在本例中为 `Lark`):
|
||||
然后可以进行指示修正,让警告消失。 请注意,实际上并不需要指定类型参数,只是为了表明它与 Java 7 中引入的钻石运算符(「<>」)一同出现。然后编译器会推断出正确的实际类型参数(在本例中为 `Lark`):
|
||||
|
||||
```java
|
||||
Set<Lark> exaltation = new HashSet<>();
|
||||
|
@ -1,6 +1,6 @@
|
||||
# 28. 列表优于数组
|
||||
|
||||
数组在两个重要方面与泛型不同。 首先,数组是协变的(covariant)。 这个吓人的单词意味着如果 Sub 是 Super 的子类型,则数组类型 `Sub[]` 是数组类型 `Super[]` 的子类型。 相比之下,泛型是不变的(invariant):对于任何两种不同的类型 `Type1` 和 `Type2`,`List<Type1>` 既不是 `List<Type2>` 的子类型也不是父类型。[JLS,4.10; Naftalin07,2.5]。 你可能认为这意味着泛型是不足的,但可以说是数组缺陷。 这段代码是合法的:
|
||||
数组在两个重要方面与泛型不同。 首先,数组是协变的(covariant)。 这个吓人的单词意味着如果 Sub 是 Super 的子类型,则数组类型 `Sub[]` 是数组类型 `Super[]` 的子类型。 相比之下,泛型是不变的(invariant):对于任何两种不同的类型 `Type1` 和 `Type2`,`List<Type1>` 既不是 `List<Type2>` 的子类型也不是父类型。[JLS,4.10; Naftalin07, 2.5]。 你可能认为这意味着泛型是不足的,但可以说是数组缺陷。 这段代码是合法的:
|
||||
|
||||
```java
|
||||
// Fails at runtime!
|
||||
@ -18,7 +18,7 @@ ol.add("I don't fit in");
|
||||
|
||||
无论哪种方式,你不能把一个 `String` 类型放到一个 `Long` 类型容器中,但是用一个数组,你会发现在运行时产生了一个错误;对于列表,可以在编译时就能发现错误。 当然,你宁愿在编译时找出错误。
|
||||
|
||||
数组和泛型之间的第二个主要区别是数组被具体化了(reified)[JLS,4.7]。 这意味着数组在运行时知道并强制执行它们的元素类型。 如前所述,如果尝试将一个 `String` 放入 `Long` 数组中,得到一个 `ArrayStoreException` 异常。 相反,泛型通过擦除(erasure)来实现[JLS,4.6]。 这意味着它们只在编译时执行类型约束,并在运行时丢弃(或擦除)它们的元素类型信息。 擦除是允许泛型类型与不使用泛型的遗留代码自由互操作(条目 26),从而确保在 Java 5 中平滑过渡到泛型。
|
||||
数组和泛型之间的第二个主要区别是数组被具体化了(reified)[JLS,4.7]。 这意味着数组在运行时知道并强制执行它们的元素类型。 如前所述,如果尝试将一个 `String` 放入 `Long` 数组中,得到一个 `ArrayStoreException` 异常。 相反,泛型通过擦除(erasure)来实现[JLS,4.6]。 这意味着它们只在编译时执行类型约束,并在运行时丢弃(或擦除)它们的元素类型信息。 擦除是允许泛型类型与不使用泛型的遗留代码自由互操作(详见第 26 条),从而确保在 Java 5 中平滑过渡到泛型。
|
||||
|
||||
由于这些基本差异,数组和泛型不能很好地在一起混合使用。 例如,创建泛型类型的数组,参数化类型的数组,以及类型参数的数组都是非法的。 因此,这些数组创建表达式都不合法:`new List<E>[]`,`new List<String>[]`,`new E[]`。 所有将在编译时导致泛型数组创建错误。
|
||||
|
||||
@ -37,9 +37,9 @@ String s = stringLists[0].get(0); // (5)
|
||||
|
||||
让我们假设第 1 行创建一个泛型数组是合法的。第 2 行创建并初始化包含单个元素的 `List<Integer>`。第 3 行将 `List<String>` 数组存储到 Object 数组变量中,这是合法的,因为数组是协变的。第 4 行将 `List<Integer>` 存储在 Object 数组的唯一元素中,这是因为泛型是通过擦除来实现的:`List<Integer>` 实例的运行时类型仅仅是 `List`,而 `List<String>[]` 实例是 `List[]`,所以这个赋值不会产生 `ArrayStoreException` 异常。现在我们遇到了麻烦。将一个 `List<Integer>` 实例存储到一个声明为仅保存 `List<String>` 实例的数组中。在第 5 行中,我们从这个数组的唯一列表中检索唯一的元素。编译器自动将检索到的元素转换为 `String`,但它是一个 `Integer`,所以我们在运行时得到一个 `ClassCastException` 异常。为了防止发生这种情况,第 1 行(创建一个泛型数组)必须产生一个编译时错误。
|
||||
|
||||
类型 `E`,`List<E>` 和 `List<String>` 等在技术上被称为不可具体化的类型(nonreifiable types)[JLS,4.7]。 直观地说,不可具体化的类型是其运行时表示包含的信息少于其编译时表示的类型。 由于擦除,可唯一确定的参数化类型是无限定通配符类型,如 `List<?>` 和 `Map<?, ?>`(条目 26)。 尽管很少有用,创建无限定通配符类型的数组是合法的。
|
||||
类型 `E`,`List<E>` 和 `List<String>` 等在技术上被称为不可具体化的类型(nonreifiable types)[JLS,4.7]。 直观地说,不可具体化的类型是其运行时表示包含的信息少于其编译时表示的类型。 由于擦除,可唯一确定的参数化类型是无限定通配符类型,如 `List<?>` 和 `Map<?, ?>`(详见第 26 条)。 尽管很少有用,创建无限定通配符类型的数组是合法的。
|
||||
|
||||
禁止泛型数组的创建可能会很恼人的。 这意味着,例如,泛型集合通常不可能返回其元素类型的数组(但是参见条目 33 中的部分解决方案)。 这也意味着,当使用可变参数方法(条目 53)和泛型时,会产生令人困惑的警告。 这是因为每次调用可变参数方法时,都会创建一个数组来保存可变参数。 如果此数组的元素类型不可确定,则会收到警告。 `SafeVarargs` 注解可以用来解决这个问题(条目 32)。
|
||||
禁止泛型数组的创建可能会很恼人的。 这意味着,例如,泛型集合通常不可能返回其元素类型的数组(但是参见条目 33 中的部分解决方案)。 这也意味着,当使用可变参数方法(详见第 53 条)和泛型时,会产生令人困惑的警告。 这是因为每次调用可变参数方法时,都会创建一个数组来保存可变参数。 如果此数组的元素类型不可确定,则会收到警告。 `SafeVarargs` 注解可以用来解决这个问题(详见第 32 条)。
|
||||
|
||||
当你在强制转换为数组类型时,得到泛型数组创建错误,或是未经检查的强制转换警告时,最佳解决方案通常是使用集合类型 `List<E>` 而不是数组类型 `E[]`。 这样可能会牺牲一些简洁性或性能,但作为交换,你会获得更好的类型安全性和互操作性。
|
||||
|
||||
@ -106,7 +106,7 @@ Chooser.java:9: warning: [unchecked] unchecked cast
|
||||
T extends Object declared in class Chooser
|
||||
```
|
||||
|
||||
编译器告诉你在运行时不能保证强制转换的安全性,因为程序不会知道 `T` 代表什么类型——记住,元素类型信息在运行时会被泛型删除。 该程序可以正常工作吗? 是的,但编译器不能证明这一点。 你可以证明这一点,在注释中提出证据,并用注解来抑制警告,但最好是消除警告的原因(条目 27)。
|
||||
编译器告诉你在运行时不能保证强制转换的安全性,因为程序不会知道 `T` 代表什么类型——记住,元素类型信息在运行时会被泛型删除。 该程序可以正常工作吗? 是的,但编译器不能证明这一点。 你可以证明这一点,在注释中提出证据,并用注解来抑制警告,但最好是消除警告的原因(详见第 27 条)。
|
||||
|
||||
要消除未经检查的强制转换警告,请使用列表而不是数组。 下面是另一个版本的 `Chooser` 类,编译时没有错误或警告:
|
||||
|
||||
|
@ -39,7 +39,7 @@ public class Stack {
|
||||
}
|
||||
```
|
||||
|
||||
这个类应该已经被参数化了,但是由于事实并非如此,我们可以对它进行泛型化。 换句话说,我们可以参数化它,而不会损害原始非参数化版本的客户端。 就目前而言,客户端必须强制转换从堆栈中弹出的对象,而这些强制转换可能会在运行时失败。 泛型化类的第一步是在其声明中添加一个或多个类型参数。 在这种情况下,有一个类型参数,表示堆栈的元素类型,这个类型参数的常规名称是 E(条目 68)。
|
||||
这个类应该已经被参数化了,但是由于事实并非如此,我们可以对它进行泛型化。 换句话说,我们可以参数化它,而不会损害原始非参数化版本的客户端。 就目前而言,客户端必须强制转换从堆栈中弹出的对象,而这些强制转换可能会在运行时失败。 泛型化类的第一步是在其声明中添加一个或多个类型参数。 在这种情况下,有一个类型参数,表示堆栈的元素类型,这个类型参数的常规名称是 E(详见第 68 条)。
|
||||
|
||||
下一步是用相应的类型参数替换所有使用的 `Object` 类型,然后尝试编译生成的程序:
|
||||
|
||||
@ -137,7 +137,7 @@ public E pop() {
|
||||
}
|
||||
```
|
||||
|
||||
两种消除泛型数组创建的技术都有其追随者。 第一个更可读:数组被声明为 `E[]` 类型,清楚地表明它只包含 `E` 实例。 它也更简洁:在一个典型的泛型类中,你从代码中的许多点读取数组; 第一种技术只需要一次转换(创建数组的地方),而第二种技术每次读取数组元素都需要单独转换。 因此,第一种技术是优选的并且在实践中更常用。 但是,它确实会造成堆污染(heap pollution)(条目 32):数组的运行时类型与编译时类型不匹配(除非 `E` 碰巧是 `Object`)。 这使得一些程序员非常不安,他们选择了第二种技术,尽管在这种情况下堆的污染是无害的。
|
||||
两种消除泛型数组创建的技术都有其追随者。 第一个更可读:数组被声明为 `E[]` 类型,清楚地表明它只包含 `E` 实例。 它也更简洁:在一个典型的泛型类中,你从代码中的许多点读取数组; 第一种技术只需要一次转换(创建数组的地方),而第二种技术每次读取数组元素都需要单独转换。 因此,第一种技术是优选的并且在实践中更常用。 但是,它确实会造成堆污染(heap pollution)(详见第 32 条):数组的运行时类型与编译时类型不匹配(除非 `E` 碰巧是 `Object`)。 这使得一些程序员非常不安,他们选择了第二种技术,尽管在这种情况下堆的污染是无害的。
|
||||
|
||||
下面的程序演示了泛型 `Stack` 类的使用。 该程序以相反的顺序打印其命令行参数,并将其转换为大写。 对从堆栈弹出的元素调用 `String` 的 `toUpperCase` 方法不需要显式强制转换,而自动生成的强制转换将保证成功:
|
||||
|
||||
@ -155,7 +155,7 @@ public static void main(String[] args) {
|
||||
|
||||
上面的例子似乎与条目 28 相矛盾,条目 28 中鼓励使用列表优先于数组。 在泛型类型中使用列表并不总是可行或可取的。 Java 本身生来并不支持列表,所以一些泛型类型(如 `ArrayList`)必须在数组上实现。 其他的泛型类型,比如 `HashMap`,是为了提高性能而实现的。
|
||||
|
||||
绝大多数泛型类型就像我们的 `Stack` 示例一样,它们的类型参数没有限制:可以创建一个 `Stack<Object>,Stack<int[]>`,`Stack<List<String>>` 或者其他任何对象的 `Stack` 引用类型。 请注意,不能创建基本类型的堆栈:尝试创建 `Stack<int>` 或 `Stack<double>` 将导致编译时错误。 这是 Java 泛型类型系统的一个基本限制。 可以使用基本类型的包装类(条目 61)来解决这个限制。
|
||||
绝大多数泛型类型就像我们的 `Stack` 示例一样,它们的类型参数没有限制:可以创建一个 `Stack<Object>,Stack<int[]>`,`Stack<List<String>>` 或者其他任何对象的 `Stack` 引用类型。 请注意,不能创建基本类型的堆栈:尝试创建 `Stack<int>` 或 `Stack<double>` 将导致编译时错误。 这是 Java 泛型类型系统的一个基本限制。 可以使用基本类型的包装类(详见第 61 条)来解决这个限制。
|
||||
|
||||
有一些泛型类型限制了它们类型参数的允许值。 例如,考虑 `java.util.concurrent.DelayQueue`,它的声明如下所示:
|
||||
|
||||
|
@ -6,13 +6,9 @@
|
||||
|
||||
```java
|
||||
// Uses raw types - unacceptable! [Item 26]
|
||||
|
||||
public static Set union(Set s1, Set s2) {
|
||||
|
||||
Set result = new HashSet(s1);
|
||||
|
||||
result.addAll(s2);
|
||||
|
||||
return result;
|
||||
}
|
||||
```
|
||||
@ -30,7 +26,7 @@ addAll(Collection<? extends E>) as a member of raw type Set
|
||||
^
|
||||
```
|
||||
|
||||
要修复这些警告并使方法类型安全,请修改其声明以声明表示三个集合(两个参数和返回值)的元素类型的类型参数,并在整个方法中使用此类型参数。 声明类型参数的类型参数列表位于方法的修饰符和返回类型之间。 在这个例子中,类型参数列表是 `<E>`,返回类型是 `Set<E>`。 类型参数的命名约定对于泛型方法和泛型类型是相同的(条目 29 和 68):
|
||||
要修复这些警告并使方法类型安全,请修改其声明以声明表示三个集合(两个参数和返回值)的元素类型的类型参数,并在整个方法中使用此类型参数。 声明类型参数的类型参数列表位于方法的修饰符和返回类型之间。 在这个例子中,类型参数列表是 `<E>`,返回类型是 `Set<E>`。 类型参数的命名约定对于泛型方法和泛型类型是相同的(详见第 29 和 68 条):
|
||||
|
||||
```java
|
||||
// Generic method
|
||||
@ -56,11 +52,11 @@ public static void main(String[] args) {
|
||||
|
||||
当运行这个程序时,它会打印`[Moe, Tom, Harry, Larry, Curly, Dick]`(输出中元素的顺序依赖于具体实现。)
|
||||
|
||||
`union` 方法的一个限制是所有三个集合(输入参数和返回值)的类型必须完全相同。 通过使用限定通配符类型( bounded wildcard types)(条目 31),可以使该方法更加灵活。
|
||||
`union` 方法的一个限制是所有三个集合(输入参数和返回值)的类型必须完全相同。 通过使用限定通配符类型( bounded wildcard types)(详见第 31 条),可以使该方法更加灵活。
|
||||
|
||||
有时,需要创建一个不可改变但适用于许多不同类型的对象。 因为泛型是通过擦除来实现的(条目 28),所以可以使用单个对象进行所有必需的类型参数化,但是需要编写一个静态工厂方法来重复地为每个请求的类型参数化分配对象。 这种称为泛型单例工厂(generic singleton factory)的模式用于方法对象( function objects)(条目 42),比如 `Collections.reverseOrder` 方法,偶尔也用于 `Collections.emptySet` 之类的集合。
|
||||
有时,需要创建一个不可改变但适用于许多不同类型的对象。 因为泛型是通过擦除来实现的(详见第 28 条),所以可以使用单个对象进行所有必需的类型参数化,但是需要编写一个静态工厂方法来重复地为每个请求的类型参数化分配对象。 这种称为泛型单例工厂(generic singleton factory)的模式用于方法对象(function objects)(详见第 42 条),比如 `Collections.reverseOrder` 方法,偶尔也用于 `Collections.emptySet` 之类的集合。
|
||||
|
||||
假设你想写一个恒等方法分配器( identity function dispenser)。 类库提供了 `Function.identity` 方法,所以没有理由编写你自己的实现(条目 59),但它是有启发性的。 如果每次要求的时候都去创建一个新的恒等方法对象是浪费的,因为它是无状态的。 如果 Java 的泛型被具体化,那么每个类型都需要一个恒等方法,但是由于它们被擦除以后,所以泛型的单例就足够了。 以下是它的实例:
|
||||
假设你想写一个恒等方法分配器( identity function dispenser)。 类库提供了 `Function.identity` 方法,所以没有理由编写你自己的实现(详见第 59 条),但它是有启发性的。 如果每次要求的时候都去创建一个新的恒等方法对象是浪费的,因为它是无状态的。 如果 Java 的泛型被具体化,那么每个类型都需要一个恒等方法,但是由于它们被擦除以后,所以泛型的单例就足够了。 以下是它的实例:
|
||||
|
||||
```java
|
||||
// Generic singleton factory pattern
|
||||
@ -94,7 +90,7 @@ public static void main(String[] args) {
|
||||
}
|
||||
```
|
||||
|
||||
虽然相对较少,类型参数受涉及该类型参数本身的某种表达式限制是允许的。 这就是所谓的递归类型限制(recursive type bound)。 递归类型限制的常见用法与 `Comparable` 接口有关,它定义了一个类型的自然顺序(条目 14)。 这个接口如下所示:
|
||||
虽然相对较少,类型参数受涉及该类型参数本身的某种表达式限制是允许的。 这就是所谓的递归类型限制(recursive type bound)。 递归类型限制的常见用法与 `Comparable` 接口有关,它定义了一个类型的自然顺序(详见第 14 条)。 这个接口如下所示:
|
||||
|
||||
```java
|
||||
public interface Comparable<T> {
|
||||
@ -111,7 +107,7 @@ public interface Comparable<T> {
|
||||
public static <E extends Comparable<E>> E max(Collection<E> c);
|
||||
```
|
||||
|
||||
限定的类型 `<E extends Comparable <E >>` 可以理解为“任何可以与自己比较的类型 E”,这或多或少精确地对应于相互可比性的概念。
|
||||
限定的类型 `<E extends Comparable <E >>` 可以理解为「任何可以与自己比较的类型 E」,这或多或少精确地对应于相互可比性的概念。
|
||||
|
||||
这里有一个与前面的声明相匹配的方法。它根据其元素的自然顺序来计算集合中的最大值,并编译没有错误或警告:
|
||||
|
||||
@ -133,11 +129,11 @@ public static <E extends Comparable<E>> E max(Collection<E> c) {
|
||||
}
|
||||
```
|
||||
|
||||
请注意,如果列表为空,则此方法将引发 `IllegalArgumentException` 异常。 更好的选择是返回一个 `Optional<E>`(条目 55)。
|
||||
请注意,如果列表为空,则此方法将引发 `IllegalArgumentException` 异常。 更好的选择是返回一个 `Optional<E>`(详见第 55 条)。
|
||||
|
||||
递归类型限制可能变得复杂得多,但幸运的是他们很少这样做。 如果你理解了这个习惯用法,它的通配符变体(条目 31)和模拟的自我类型用法(条目 2),你将能够处理在实践中遇到的大多数递归类型限制。
|
||||
递归类型限制可能变得复杂得多,但幸运的是他们很少这样做。 如果你理解了这个习惯用法,它的通配符变体(详见第 31 条)和模拟的自我类型用法(详见第 2 条),你将能够处理在实践中遇到的大多数递归类型限制。
|
||||
|
||||
总之,像泛型类型一样,泛型方法比需要客户端对输入参数和返回值进行显式强制转换的方法更安全,更易于使用。 像类型一样,你应该确保你的方法可以不用强制转换,这通常意味着它们是泛型的。 应该泛型化现有的方法,其使用需要强制转换。 这使得新用户的使用更容易,而不会破坏现有的客户端(条目 26)。
|
||||
总之,像泛型类型一样,泛型方法比需要客户端对输入参数和返回值进行显式强制转换的方法更安全,更易于使用。 像类型一样,你应该确保你的方法可以不用强制转换,这通常意味着它们是泛型的。 应该泛型化现有的方法,其使用需要强制转换。 这使得新用户的使用更容易,而不会破坏现有的客户端(详见第 26 条)。
|
||||
|
||||
|
||||
|
||||
|
@ -46,7 +46,7 @@ cannot be converted to Iterable<Number>
|
||||
^
|
||||
```
|
||||
|
||||
幸运的是,有对应的解决方法。 该语言提供了一种特殊的参数化类型来调用一个限定通配符类型来处理这种情况。 `pushAll` 的输入参数的类型不应该是“`E` 的 `Iterable` 接口”,而应该是“`E` 的某个子类型的 `Iterable` 接口”,并且有一个通配符类型,这意味着:`Iterable<? extends E>`。 (关键字 `extends` 的使用有点误导:回忆条目 29 中,子类型被定义为每个类型都是它自己的子类型,即使它本身没有继承。)让我们修改 `pushAll` 来使用这个类型:
|
||||
幸运的是,有对应的解决方法。 该语言提供了一种特殊的参数化类型来调用一个限定通配符类型来处理这种情况。 `pushAll` 的输入参数的类型不应该是「`E` 的 `Iterable` 接口」,而应该是「`E` 的某个子类型的 `Iterable` 接口」,并且有一个通配符类型,这意味着:`Iterable<? extends E>`。 (关键字 `extends` 的使用有点误导:回忆条目 29 中,子类型被定义为每个类型都是它自己的子类型,即使它本身没有继承。)让我们修改 `pushAll` 来使用这个类型:
|
||||
|
||||
```java
|
||||
// Wildcard type for a parameter that serves as an E producer
|
||||
@ -78,7 +78,7 @@ Collection<Object> objects = ... ;
|
||||
numberStack.popAll(objects);
|
||||
```
|
||||
|
||||
如果尝试将此客户端代码与之前显示的 `popAll` 版本进行编译,则会得到与我们的第一版 `pushAll` 非常类似的错误:`Collection<Object>` 不是 `Collection<Number>` 的子类型。 通配符类型再一次提供了一条出路。 `popAll` 的输入参数的类型不应该是“E 的集合”,而应该是“E 的某个父类型的集合”(其中父类型被定义为 `E` 是它自己的父类型[JLS,4.10])。 再次,有一个通配符类型,正是这个意思:`Collection<? super E>`。 让我们修改 `popAll` 来使用它:
|
||||
如果尝试将此客户端代码与之前显示的 `popAll` 版本进行编译,则会得到与我们的第一版 `pushAll` 非常类似的错误:`Collection<Object>` 不是 `Collection<Number>` 的子类型。 通配符类型再一次提供了一条出路。 `popAll` 的输入参数的类型不应该是「E 的集合」,而应该是「E 的某个父类型的集合」(其中父类型被定义为 `E` 是它自己的父类型[JLS,4.10])。 再次,有一个通配符类型,正是这个意思:`Collection<? super E>`。 让我们修改 `popAll` 来使用它:
|
||||
|
||||
```java
|
||||
// Wildcard type for parameter that serves as an E consumer
|
||||
@ -94,7 +94,7 @@ public void popAll(Collection<? super E> dst) {
|
||||
|
||||
这里有一个助记符来帮助你记住使用哪种通配符类型: **PECS 代表: producer-extends,consumer-super。**
|
||||
|
||||
换句话说,如果一个参数化类型代表一个 `T` 生产者,使用 `<? extends T>`;如果它代表 `T` 消费者,则使用 `<? super T>`。 在我们的 `Stack` 示例中,`pushAll` 方法的 `src` 参数生成栈使用的 `E` 实例,因此 `src` 的合适类型为 `Iterable<? extends E>`;`popAll` 方法的 `dst` 参数消费 `Stack` 中的 `E` 实例,因此 `dst` 的合适类型是 C`ollection <? super E>`。 PECS 助记符抓住了使用通配符类型的基本原则。 Naftalin 和 Wadler 称之为获取和放置原则( Get and Put Principle )[Naftalin07,2.4]。
|
||||
换句话说,如果一个参数化类型代表一个 `T` 生产者,使用 `<? extends T>`;如果它代表 `T` 消费者,则使用 `<? super T>`。 在我们的 `Stack` 示例中,`pushAll` 方法的 `src` 参数生成栈使用的 `E` 实例,因此 `src` 的合适类型为 `Iterable<? extends E>`;`popAll` 方法的 `dst` 参数消费 `Stack` 中的 `E` 实例,因此 `dst` 的合适类型是 C`ollection <? super E>`。 PECS 助记符抓住了使用通配符类型的基本原则。 Naftalin 和 Wadler 称之为获取和放置原则(Get and Put Principle)[Naftalin07,2.4]。
|
||||
|
||||
记住这个助记符之后,让我们来看看本章中以前项目的一些方法和构造方法声明。 条目 28 中的 `Chooser` 类构造方法有这样的声明:
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
# 32. 合理地结合泛型和可变参数
|
||||
|
||||
在 Java 5 中,可变参数方法(条目 53)和泛型都被添加到平台中,所以你可能希望它们能够正常交互; 可悲的是,他们并没有。 可变参数的目的是允许客户端将一个可变数量的参数传递给一个方法,但这是一个脆弱的抽象(leaky abstraction):当你调用一个可变参数方法时,会创建一个数组来保存可变参数;那个应该是实现细节的数组是可见的。 因此,当可变参数具有泛型或参数化类型时,会导致编译器警告混淆。
|
||||
在 Java 5 中,可变参数方法(详见第 53 条)和泛型都被添加到平台中,所以你可能希望它们能够正常交互; 可悲的是,他们并没有。 可变参数的目的是允许客户端将一个可变数量的参数传递给一个方法,但这是一个脆弱的抽象(leaky abstraction):当你调用一个可变参数方法时,会创建一个数组来保存可变参数;那个应该是实现细节的数组是可见的。 因此,当可变参数具有泛型或参数化类型时,会导致编译器警告混淆。
|
||||
|
||||
回顾条目 28,非具体化(non-reifiable)的类型是其运行时表示比其编译时表示具有更少信息的类型,并且几乎所有泛型和参数化类型都是不可具体化的。 如果某个方法声明其可变参数为非具体化的类型,则编译器将在该声明上生成警告。 如果在推断类型不可确定的可变参数参数上调用该方法,那么编译器也会在调用中生成警告。 警告看起来像这样:
|
||||
|
||||
@ -110,7 +110,7 @@ static <T> List<T> flatten(List<List<? extends T>> lists) {
|
||||
|
||||
这种方法的优点是编译器可以证明这种方法是类型安全的。 不必使用 `@SafeVarargs` 注解来证明其安全性,也不用担心在确定安全性时可能会犯错。 主要缺点是客户端代码有点冗长,运行可能会慢一些。
|
||||
|
||||
这个技巧也可以用在不可能写一个安全的可变参数方法的情况下,就像第 147 页的 `toArray` 方法那样。它的列表模拟是 `List.of` 方法,所以我们甚至不必编写它; Java 类库作者已经为我们完成了这项工作。 `pickTwo` 方法然后变成这样:
|
||||
这个技巧也可以用在不可能写一个安全的可变参数方法的情况下,就像第 147 页的 `toArray` 方法那样。它的列表模拟是 `List.of` 方法,所以我们甚至不必编写它;Java 类库作者已经为我们完成了这项工作。 `pickTwo` 方法然后变成这样:
|
||||
|
||||
|
||||
```java
|
||||
|
@ -86,11 +86,11 @@ public<T> void putFavorite(Class<T> type, T instance) {
|
||||
|
||||
`java.util.Collections` 中有一些集合包装类,可以发挥相同的诀窍。 它们被称为 `checkedSet`,`checkedList`,`checkedMap` 等等。 他们的静态工厂除了一个集合(或 `Map`)之外还有一个 `Class` 对象(或两个)。 静态工厂是泛型方法,确保 `Class` 对象和集合的编译时类型匹配。 包装类为它们包装的集合添加了具体化。 例如,如果有人试图将 `Coin` 放入你的 `Collection<Stamp>` 中,则包装类在运行时会抛出 `ClassCastException`。 这些包装类对于追踪在混合了泛型和原始类型的应用程序中添加不正确类型的元素到集合的客户端代码很有用。
|
||||
|
||||
Favorites 类的第二个限制是它不能用于不可具体化的(non-reifiable)类型(条目 28)。 换句话说,你可以保存你最喜欢的 `String` 或 `String[]`,但不能保存 `List<String>`。 如果你尝试保存你最喜欢的 `List<String>`,程序将不能编译。 原因是无法获取 `List<String>` 的 `Class` 对象。 `List<String>.class` 是语法错误,也是一件好事。 `List<String>` 和 `List<Integer>` 共享一个 `Class` 对象,即 `List.class`。 如果“字面类型(type literals)”`List<String> .clas`s 和 `List<Integer>.class` 合法并返回相同的对象引用,那么它会对 `Favorites` 对象的内部造成严重破坏。 对于这种限制,没有完全令人满意的解决方法。
|
||||
Favorites 类的第二个限制是它不能用于不可具体化的(non-reifiable)类型(详见第 28 条)。 换句话说,你可以保存你最喜欢的 `String` 或 `String[]`,但不能保存 `List<String>`。 如果你尝试保存你最喜欢的 `List<String>`,程序将不能编译。 原因是无法获取 `List<String>` 的 `Class` 对象。 `List<String>.class` 是语法错误,也是一件好事。 `List<String>` 和 `List<Integer>` 共享一个 `Class` 对象,即 `List.class`。 如果“字面类型(type literals)”`List<String> .clas`s 和 `List<Integer>.class` 合法并返回相同的对象引用,那么它会对 `Favorites` 对象的内部造成严重破坏。 对于这种限制,没有完全令人满意的解决方法。
|
||||
|
||||
`Favorites` 使用的类型令牌 type tokens) 是无限制的:`getFavorite` 和 `putFavorite` 接受任何 `Class` 对象。 有时你可能需要限制可传递给方法的类型。 这可以通过一个有限定的类型令牌来实现,该令牌只是一个类型令牌,它使用限定的类型参数(条目 30)或限定的通配符(条目 31)来放置可以表示的类型的边界。
|
||||
`Favorites` 使用的类型令牌 type tokens) 是无限制的:`getFavorite` 和 `putFavorite` 接受任何 `Class` 对象。 有时你可能需要限制可传递给方法的类型。 这可以通过一个有限定的类型令牌来实现,该令牌只是一个类型令牌,它使用限定的类型参数(详见第 30 条)或限定的通配符(详见第 31 条)来放置可以表示的类型的边界。
|
||||
|
||||
注解 API(条目 39)广泛使用限定类型的令牌。 例如,以下是在运行时读取注解的方法。 此方法来自 `AnnotatedElement` 接口,该接口由表示类,方法,属性和其他程序元素的反射类型实现:
|
||||
注解 API(详见第 39 条)广泛使用限定类型的令牌。 例如,以下是在运行时读取注解的方法。 此方法来自 `AnnotatedElement` 接口,该接口由表示类,方法,属性和其他程序元素的反射类型实现:
|
||||
|
||||
```java
|
||||
public <T extends Annotation>
|
||||
@ -99,7 +99,7 @@ public <T extends Annotation>
|
||||
|
||||
参数 `annotationType` 是表示注解类型的限定类型令牌。 该方法返回该类型的元素的注解(如果它有一个);如果没有,则返回 `null`。 本质上,注解元素是一个类型安全的异构容器,其键是注解类型。
|
||||
|
||||
假设有一个 `Class<?>` 类型的对象,并且想要将它传递给需要限定类型令牌(如 `getAnnotation`)的方法。 可以将对象转换为 `Class<? extends Annotation>`,但是这个转换没有被检查,所以它会产生一个编译时警告(条目 27)。 幸运的是,`Class` 类提供了一种安全(动态)执行这种类型转换的实例方法。 该方法被称为 `asSubclass`,并且它转换所调用的 `Class` 对象来表示由其参数表示的类的子类。 如果转换成功,该方法返回它的参数;如果失败,则抛出 `ClassCastException` 异常。
|
||||
假设有一个 `Class<?>` 类型的对象,并且想要将它传递给需要限定类型令牌(如 `getAnnotation`)的方法。 可以将对象转换为 `Class<? extends Annotation>`,但是这个转换没有被检查,所以它会产生一个编译时警告(详见第 52 条)。 幸运的是,`Class` 类提供了一种安全(动态)执行这种类型转换的实例方法。 该方法被称为 `asSubclass`,并且它转换所调用的 `Class` 对象来表示由其参数表示的类的子类。 如果转换成功,该方法返回它的参数;如果失败,则抛出 `ClassCastException` 异常。
|
||||
|
||||
以下是如何使用 `asSubclass` 方法在编译时读取类型未知的注解。 此方法编译时没有错误或警告:
|
||||
|
||||
|
@ -37,13 +37,13 @@ public enum Orange { NAVEL, TEMPLE, BLOOD }
|
||||
|
||||
从表面上看,这些枚举类型可能看起来与其他语言类似,比如 C,C++和 C#,但事实并非如此。 Java 的枚举类型是完整的类,比其他语言中的其他语言更强大,其枚举本质本上是 `int` 值。
|
||||
|
||||
Java 枚举类型背后的基本思想很简单:它们是通过公共静态 `final` 属性为每个枚举常量导出一个实例的类。 由于没有可访问的构造方法,枚举类型实际上是 `final` 的。 由于客户既不能创建枚举类型的实例也不能继承它,除了声明的枚举常量外,不能有任何实例。 换句话说,枚举类型是实例控制的(第 6 页)。 它们是单例(条目 3)的泛型化,基本上是单元素的枚举。
|
||||
Java 枚举类型背后的基本思想很简单:它们是通过公共静态 `final` 属性为每个枚举常量导出一个实例的类。 由于没有可访问的构造方法,枚举类型实际上是 `final` 的。 由于客户既不能创建枚举类型的实例也不能继承它,除了声明的枚举常量外,不能有任何实例。 换句话说,枚举类型是实例控制的(第 6 页)。 它们是单例(详见第 3 条)的泛型化,基本上是单元素的枚举。
|
||||
|
||||
枚举提供了编译时类型的安全性。 如果声明一个参数为 `Apple` 类型,则可以保证传递给该参数的任何非空对象引用是三个有效 `Apple` 值中的一个。 尝试传递错误类型的值将导致编译时错误,因为会尝试将一个枚举类型的表达式分配给另一个类型的变量,或者使用 `==` 运算符来比较不同枚举类型的值。
|
||||
|
||||
具有相同名称常量的枚举类型可以和平共存,因为每种类型都有其自己的名称空间。 可以在枚举类型中添加或重新排序常量,而无需重新编译其客户端,因为导出常量的属性在枚举类型与其客户端之间提供了一层隔离:常量值不会编译到客户端,因为它们位于 `int` 枚举模式中。 最后,可以通过调用其 `toString` 方法将枚举转换为可打印的字符串。
|
||||
|
||||
除了纠正 `int` 枚举的缺陷之外,枚举类型还允许添加任意方法和属性并实现任意接口。 它们提供了所有 `Object` 方法的高质量实现(第 3 章),它们实现了 Compa`此处输入代码`rable(条目 14)和 `Serializable`(第 12 章),并针对枚举类型的可任意改变性设计了序列化方式。
|
||||
除了纠正 `int` 枚举的缺陷之外,枚举类型还允许添加任意方法和属性并实现任意接口。 它们提供了所有 `Object` 方法的高质量实现(第 3 章),它们实现了 `Comparable`(详见第 14 条)和 `Serializable`(第 12 章),并针对枚举类型的可任意改变性设计了序列化方式。
|
||||
|
||||
那么,为什么你要添加方法或属性到一个枚举类型? 对于初学者,可能想要将数据与其常量关联起来。 例如,我们的 `Apple` 和 `Orange` 类型可能会从返回水果颜色的方法或返回水果图像的方法中受益。 还可以使用任何看起来合适的方法来增强枚举类型。 枚举类型可以作为枚举常量的简单集合,并随着时间的推移而演变为全功能抽象。
|
||||
|
||||
@ -84,7 +84,7 @@ public enum Planet {
|
||||
}
|
||||
```
|
||||
|
||||
编写一个丰富的枚举类型比如 `Planet` 很容易。 **要将数据与枚举常量相关联,请声明实例属性并编写一个构造方法,构造方法带有数据并将数据保存在属性中。** 枚举本质上是不变的,所以所有的属性都应该是 `final` 的(条目 17)。 属性可以是公开的,但最好将它们设置为私有并提供公共访问方法(条目 16)。 在 `Planet` 的情况下,构造方法还计算和存储表面重力,但这只是一种优化。 每当重力被 `SurfaceWeight` 方法使用时,它可以从质量和半径重新计算出来,该方法返回它在由常数表示的行星上的重量。
|
||||
编写一个丰富的枚举类型比如 `Planet` 很容易。 **要将数据与枚举常量相关联,请声明实例属性并编写一个构造方法,构造方法带有数据并将数据保存在属性中。** 枚举本质上是不变的,所以所有的属性都应该是 `final` 的(详见第 17 条)。 属性可以是公开的,但最好将它们设置为私有并提供公共访问方法(详见第 16 条)。 在 `Planet` 的情况下,构造方法还计算和存储表面重力,但这只是一种优化。 每当重力被 `SurfaceWeight` 方法使用时,它可以从质量和半径重新计算出来,该方法返回它在由常数表示的行星上的重量。
|
||||
|
||||
虽然 `Planet` 枚举很简单,但它的功能非常强大。 这是一个简短的程序,它将一个物体在地球上的重量(任何单位),打印一个漂亮的表格,显示该物体在所有八个行星上的重量(以相同单位):
|
||||
|
||||
@ -113,11 +113,11 @@ Weight on URANUS is 167.398264
|
||||
Weight on NEPTUNE is 210.208751
|
||||
```
|
||||
|
||||
直到 2006 年,在 Java 中加入枚举两年之后,冥王星不再是一颗行星。 这引发了一个问题:“当你从枚举类型中移除一个元素时会发生什么?”答案是,任何不引用移除元素的客户端程序都将继续正常工作。 所以,举例来说,我们的 `WeightTable` 程序只需要打印一行少一行的表格。 什么是客户端程序引用删除的元素(在这种情况下,`Planet.Pluto`)? 如果重新编译客户端程序,编译将会失败并在引用前一个星球的行处提供有用的错误消息; 如果无法重新编译客户端,它将在运行时从此行中引发有用的异常。 这是你所希望的最好的行为,远远好于你用 `int` 枚举模式得到的结果。
|
||||
直到 2006 年,在 Java 中加入枚举两年之后,冥王星不再是一颗行星。 这引发了一个问题:「当你从枚举类型中移除一个元素时会发生什么?」答案是,任何不引用移除元素的客户端程序都将继续正常工作。 所以,举例来说,我们的 `WeightTable` 程序只需要打印一行少一行的表格。 什么是客户端程序引用删除的元素(在这种情况下,`Planet.Pluto`)? 如果重新编译客户端程序,编译将会失败并在引用前一个星球的行处提供有用的错误消息; 如果无法重新编译客户端,它将在运行时从此行中引发有用的异常。 这是你所希望的最好的行为,远远好于你用 `int` 枚举模式得到的结果。
|
||||
|
||||
一些与枚举常量相关的行为只需要在定义枚举的类或包中使用。 这些行为最好以私有或包级私有方式实现。 然后每个常量携带一个隐藏的行为集合,允许包含枚举的类或包在与常量一起呈现时作出适当的反应。 与其他类一样,除非你有一个令人信服的理由将枚举方法暴露给它的客户端,否则将其声明为私有的,如果需要的话将其声明为包级私有(条目 15)。
|
||||
一些与枚举常量相关的行为只需要在定义枚举的类或包中使用。 这些行为最好以私有或包级私有方式实现。 然后每个常量携带一个隐藏的行为集合,允许包含枚举的类或包在与常量一起呈现时作出适当的反应。 与其他类一样,除非你有一个令人信服的理由将枚举方法暴露给它的客户端,否则将其声明为私有的,如果需要的话将其声明为包级私有(详见第 15 条)。
|
||||
|
||||
如果一个枚举是广泛使用的,它应该是一个顶级类; 如果它的使用与特定的顶级类绑定,它应该是该顶级类的成员类(条目 24)。 例如,`java.math.RoundingMode` 枚举表示小数部分的舍入模式。 `BigDecimal` 类使用了这些舍入模式,但它们提供了一种有用的抽象,它并不与 `BigDecimal` 有根本的联系。 通过将 `RoundingMode` 设置为顶层枚举,类库设计人员鼓励任何需要舍入模式的程序员重用此枚举,从而提高跨 `API` 的一致性。
|
||||
如果一个枚举是广泛使用的,它应该是一个顶级类; 如果它的使用与特定的顶级类绑定,它应该是该顶级类的成员类(详见第 24 条)。 例如,`java.math.RoundingMode` 枚举表示小数部分的舍入模式。 `BigDecimal` 类使用了这些舍入模式,但它们提供了一种有用的抽象,它并不与 `BigDecimal` 有根本的联系。 通过将 `RoundingMode` 设置为顶层枚举,类库设计人员鼓励任何需要舍入模式的程序员重用此枚举,从而提高跨 `API` 的一致性。
|
||||
|
||||
```java
|
||||
// Enum type that switches on its own value - questionable
|
||||
@ -218,9 +218,9 @@ public static Optional<Operation> fromString(String symbol) {
|
||||
}
|
||||
```
|
||||
|
||||
请注意,`Operation` 枚举常量被放在 `stringToEnum` 的 `map` 中,它来自于创建枚举常量后运行的静态属性初始化。前面的代码在 `values()` 方法返回的数组上使用流(第 7 章);在 Java 8 之前,我们创建一个空的 `hashMap` 并遍历值数组,将字符串到枚举映射插入到 `map` 中,如果愿意,仍然可以这样做。但请注意,尝试让每个常量都将自己放入来自其构造方法的 `map` 中不起作用。这会导致编译错误,这是好事,因为如果它是合法的,它会在运行时导致 `NullPointerException`。除了编译时常量属性(条目 34)之外,枚举构造方法不允许访问枚举的静态属性。此限制是必需的,因为静态属性在枚举构造方法运行时尚未初始化。这种限制的一个特例是枚举常量不能从构造方法中相互访问。
|
||||
请注意,`Operation` 枚举常量被放在 `stringToEnum` 的 `map` 中,它来自于创建枚举常量后运行的静态属性初始化。前面的代码在 `values()` 方法返回的数组上使用流(第 7 章);在 Java 8 之前,我们创建一个空的 `hashMap` 并遍历值数组,将字符串到枚举映射插入到 `map` 中,如果愿意,仍然可以这样做。但请注意,尝试让每个常量都将自己放入来自其构造方法的 `map` 中不起作用。这会导致编译错误,这是好事,因为如果它是合法的,它会在运行时导致 `NullPointerException`。除了编译时常量属性(详见第 34 条)之外,枚举构造方法不允许访问枚举的静态属性。此限制是必需的,因为静态属性在枚举构造方法运行时尚未初始化。这种限制的一个特例是枚举常量不能从构造方法中相互访问。
|
||||
|
||||
另请注意,`fromString` 方法返回一个 `Optional<String>`。 这允许该方法指示传入的字符串不代表有效的操作,并且强制客户端面对这种可能性(条目 55)。
|
||||
另请注意,`fromString` 方法返回一个 `Optional<String>`。 这允许该方法指示传入的字符串不代表有效的操作,并且强制客户端面对这种可能性(详见第 55 条)。
|
||||
|
||||
特定于常量的方法实现的一个缺点是它们使得难以在枚举常量之间共享代码。 例如,考虑一个代表工资包中的工作天数的枚举。 该枚举有一个方法,根据工人的基本工资(每小时)和当天工作的分钟数计算当天工人的工资。 在五个工作日内,任何超过正常工作时间的工作都会产生加班费; 在两个周末的日子里,所有工作都会产生加班费。 使用 `switch` 语句,通过将多个 `case` 标签应用于两个代码片段中的每一个,可以轻松完成此计算:
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
# 36. 使用 EnumSet 替代位属性
|
||||
|
||||
如果枚举类型的元素主要用于集合中,一般来说使用 int 枚举模式(条目 34),下面将 2 的不同倍数赋值给每个常量:
|
||||
如果枚举类型的元素主要用于集合中,一般来说使用 int 枚举模式(详见第 34 条),下面将 2 的不同倍数赋值给每个常量:
|
||||
|
||||
```java
|
||||
// Bit field enumeration constants - OBSOLETE!
|
||||
@ -43,7 +43,7 @@ public class Text {
|
||||
text.applyStyles(EnumSet.of(Style.BOLD, Style.ITALIC));
|
||||
```
|
||||
|
||||
请注意,`applyStyles` 方法采用`Set<Style>`而不是`EnumSet<Style>`参数。 尽管所有客户端都可能会将 `EnumSet` 传递给该方法,但接受接口类型而不是实现类型通常是很好的做法(条目 64)。 这允许一个不寻常的客户端通过其他 Set 实现的可能性。
|
||||
请注意,`applyStyles` 方法采用`Set<Style>`而不是`EnumSet<Style>`参数。 尽管所有客户端都可能会将 `EnumSet` 传递给该方法,但接受接口类型而不是实现类型通常是很好的做法(详见第 64 条)。 这允许一个不寻常的客户端通过其他 Set 实现的可能性。
|
||||
|
||||
总之,仅仅因为枚举类型将被用于集合中,所以没有理由用位属性来表示它。 `EnumSet` 类将位属性的简洁性和性能与条目 34 中所述的枚举类型的所有优点相结合。`EnumSet` 的一个真正缺点是,它不像 Java 9 那样创建一个不可变的 `EnumSet`,但是在即将发布的版本中可能会得到补救。 同时,你可以用 `Collections.unmodifiableSet` 封装一个 `EnumSet`,但是简洁性和性能会受到影响。
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
# 37. 使用 EnumMap 替代序数索引
|
||||
|
||||
有时可能会看到使用 `ordinal` 方法(条目 35)来索引到数组或列表的代码。 例如,考虑一下这个简单的类来代表一种植物:
|
||||
有时可能会看到使用 `ordinal` 方法(详见第 35 条)来索引到数组或列表的代码。 例如,考虑一下这个简单的类来代表一种植物:
|
||||
|
||||
```java
|
||||
class Plant {
|
||||
@ -39,7 +39,7 @@ for (int i = 0; i < plantsByLifeCycle.length; i++) {
|
||||
}
|
||||
```
|
||||
|
||||
这种方法是有效的,但充满了问题。 因为数组不兼容泛型(条目 28),程序需要一个未经检查的转换,并且不会干净地编译。 由于该数组不知道索引代表什么,因此必须手动标记索引输出。 但是这种技术最严重的问题是,当你访问一个由枚举序数索引的数组时,你有责任使用正确的 `int` 值; `int` 不提供枚举的类型安全性。 如果你使用了错误的值,程序会默默地做错误的事情,如果你幸运的话,抛出一个 `ArrayIndexOutOfBoundsException` 异常。
|
||||
这种方法是有效的,但充满了问题。 因为数组不兼容泛型(详见第 28 条),程序需要一个未经检查的转换,并且不会干净地编译。 由于该数组不知道索引代表什么,因此必须手动标记索引输出。 但是这种技术最严重的问题是,当你访问一个由枚举序数索引的数组时,你有责任使用正确的 `int` 值; `int` 不提供枚举的类型安全性。 如果你使用了错误的值,程序会默默地做错误的事情,如果你幸运的话,抛出一个 `ArrayIndexOutOfBoundsException` 异常。
|
||||
|
||||
有一个更好的方法来达到同样的效果。 该数组有效地用作从枚举到值的映射,因此不妨使用 `Map`。 更具体地说,有一个非常快速的 `Map` 实现,设计用于枚举键,称为 `java.util.EnumMap`。 下面是当程序重写为使用 `EnumMap` 时的样子:
|
||||
|
||||
@ -59,7 +59,7 @@ System.out.println(plantsByLifeCycle);
|
||||
|
||||
这段程序更简短,更清晰,更安全,运行速度与原始版本相当。 没有不安全的转换; 无需手动标记输出,因为 `map` 键是知道如何将自己转换为可打印字符串的枚举; 并且不可能在计算数组索引时出错。 `EnumMap` 与序数索引数组的速度相当,其原因是 `EnumMap` 内部使用了这样一个数组,但它对程序员的隐藏了这个实现细节,将 Map 的丰富性和类型安全性与数组的速度相结合。 请注意,`EnumMap` 构造方法接受键类`Class`型的 Class 对象:这是一个有限定的类型令牌(bounded type token),它提供运行时的泛型类型信息(条目 33)。
|
||||
|
||||
通过使用 `stream`(条目 45)来管理 `Map`,可以进一步缩短以前的程序。 以下是最简单的基于 `stream` 的代码,它们在很大程度上重复了前面示例的行为:
|
||||
通过使用 `stream`(详见第 45 条)来管理 `Map`,可以进一步缩短以前的程序。 以下是最简单的基于 `stream` 的代码,它们在很大程度上重复了前面示例的行为:
|
||||
|
||||
```java
|
||||
// Naive stream-based approach - unlikely to produce an EnumMap!
|
||||
@ -80,7 +80,7 @@ System.out.println(Arrays.stream(garden)
|
||||
|
||||
基于 `stream` 版本的行为与 `EmumMap` 版本的行为略有不同。 `EnumMap` 版本总是为每个工厂生命周期生成一个嵌套 `map` 类,而如果花园包含一个或多个具有该生命周期的植物时,则基于流的版本才会生成嵌套 `map` 类。 因此,例如,如果花园包含一年生和多年生植物但没有两年生的植物,`plantByLifeCycle` 的大小在 `EnumMap` 版本中为三个,在两个基于流的版本中为两个。
|
||||
|
||||
你可能会看到数组索引 (两次) 的数组,用序数来表示从两个枚举值的映射。例如,这个程序使用这样一个数组来映射两个阶段到一个阶段转换(phase transition)(液体到固体表示凝固,液体到气体表示沸腾等等):
|
||||
你可能会看到数组索引(两次)的数组,用序数来表示从两个枚举值的映射。例如,这个程序使用这样一个数组来映射两个阶段到一个阶段转换(phase transition)(液体到固体表示凝固,液体到气体表示沸腾等等):
|
||||
|
||||
```java
|
||||
// Using ordinal() to index array of arrays - DON'T DO THIS!
|
||||
@ -139,7 +139,7 @@ public enum Phase {
|
||||
}
|
||||
```
|
||||
|
||||
初始化阶段转换的 `map` 的代码有点复杂。`map` 的类型是 `Map<Phase, Map<Phase, Transition>>`,意思是“从(源)阶段映射到从(目标)阶段到阶段转换映射。”这个 `map` 的 `map` 使用两个收集器的级联序列进行初始化。 第一个收集器按源阶段对转换进行分组,第二个收集器使用从目标阶段到转换的映射创建一个 `EnumMap`。 第二个收集器 `((x, y) -> y))` 中的合并方法未使用;仅仅因为我们需要指定一个 `map` 工厂才能获得一个 `EnumMap`,并且 `Collectors` 提供伸缩式工厂,这是必需的。 本书的前一版使用显式迭代来初始化阶段转换 `map`。 代码更详细,但可以更容易理解。
|
||||
初始化阶段转换的 `map` 的代码有点复杂。`map` 的类型是 `Map<Phase, Map<Phase, Transition>>`,意思是「从(源)阶段映射到从(目标)阶段到阶段转换映射。」这个 `map` 的 `map` 使用两个收集器的级联序列进行初始化。 第一个收集器按源阶段对转换进行分组,第二个收集器使用从目标阶段到转换的映射创建一个 `EnumMap`。 第二个收集器 `((x, y) -> y))` 中的合并方法未使用;仅仅因为我们需要指定一个 `map` 工厂才能获得一个 `EnumMap`,并且 `Collectors` 提供伸缩式工厂,这是必需的。 本书的前一版使用显式迭代来初始化阶段转换 `map`。 代码更详细,但可以更容易理解。
|
||||
|
||||
现在假设想为系统添加一个新阶段:等离子体或电离气体。 这个阶段只有两个转变:电离,将气体转化为等离子体; 和去离子,将等离子体转化为气体。 要更新基于数组的程序,必须将一个新的常量添加到 `Phase`,将两个两次添加到 `Phase.Transition`,并用新的十六个元素版本替换原始的九元素阵列数组。 如果向数组中添加太多或太少的元素或者将元素乱序放置,那么如果运气不佳:程序将会编译,但在运行时会失败。 要更新基于 `EnumMap` 的版本,只需将 `PLASMA` 添加到阶段列表中,并将 `IONIZE(GAS, PLASMA)` 和 `DEIONIZE(PLASMA, GAS)` 添加到阶段转换列表中:
|
||||
|
||||
@ -161,9 +161,9 @@ public enum Phase {
|
||||
|
||||
该程序会处理所有其他事情,并且几乎不会出现错误。 在内部,`map` 的 `map` 是通过数组的数组实现的,因此在空间或时间上花费很少,以增加清晰度,安全性和易于维护。
|
||||
|
||||
为了简便起见,上面的示例使用 `null` 来表示状态更改的缺失 (其从目标到源都是相同的)。这不是很好的实践,很可能在运行时导致 `NullPointerException`。为这个问题设计一个干净、优雅的解决方案是非常棘手的,而且结果程序足够长,以至于它们会偏离这个条目的主要内容。
|
||||
为了简便起见,上面的示例使用 `null` 来表示状态更改的缺失(其从目标到源都是相同的)。这不是很好的实践,很可能在运行时导致 `NullPointerException`。为这个问题设计一个干净、优雅的解决方案是非常棘手的,而且结果程序足够长,以至于它们会偏离这个条目的主要内容。
|
||||
|
||||
**总之,使用序数来索引数组很不合适:改用 EnumMap。** 如果你所代表的关系是多维的,请使用 `EnumMap <...,EnumMap <... >>`。 应用程序员应该很少使用 `Enum.ordinal`(条目 35),如果使用了,也是一般原则的特例。
|
||||
**总之,使用序数来索引数组很不合适:改用 EnumMap。** 如果你所代表的关系是多维的,请使用 `EnumMap <...,EnumMap <... >>`。 应用程序员应该很少使用 `Enum.ordinal`(详见第 35 条),如果使用了,也是一般原则的特例。
|
||||
|
||||
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user