mirror of
https://github.com/sjsdfg/effective-java-3rd-chinese.git
synced 2025-01-26 20:30:36 +08:00
更新排版
This commit is contained in:
parent
8d889908b4
commit
4ddf1e6521
@ -2,7 +2,7 @@
|
||||
|
||||
如果你是一个刚开始使用流的新手,那么很难掌握它们。仅仅将计算表示为流管道是很困难的。当你成功时,你的程序将运行,但对你来说可能没有意识到任何好处。流不仅仅是一个 API,它是基于函数式编程的范式(paradigm)。为了获得流提供的可表达性、速度和某些情况下的并行性,你必须采用范式和 API。
|
||||
|
||||
流范式中最重要的部分是将计算结构化为一系列转换,其中每个阶段的结果尽可能接近前一阶段结果的纯函数( pure function)。 纯函数的结果仅取决于其输入:它不依赖于任何可变状态,也不更新任何状态。 为了实现这一点,你传递给流操作的任何函数对象(中间操作和终结操作)都应该没有副作用。
|
||||
流范式中最重要的部分是将计算结构化为一系列转换,其中每个阶段的结果尽可能接近前一阶段结果的纯函数(pure function)。 纯函数的结果仅取决于其输入:它不依赖于任何可变状态,也不更新任何状态。 为了实现这一点,你传递给流操作的任何函数对象(中间操作和终结操作)都应该没有副作用。
|
||||
|
||||
有时,可能会看到类似于此代码片段的流代码,该代码构建了文本文件中单词的频率表:
|
||||
|
||||
@ -16,7 +16,7 @@ try (Stream<String> words = new Scanner(file).tokens()) {
|
||||
}
|
||||
```
|
||||
|
||||
这段代码出了什么问题? 毕竟,它使用了流,lambdas 和方法引用,并得到正确的答案。 简而言之,它根本不是流代码; 它是伪装成流代码的迭代代码。 它没有从流 API 中获益,并且它比相应的迭代代码更长,更难读,并且更难于维护。 问题源于这样一个事实:这个代码在一个终结操作 `forEach` 中完成所有工作,使用一个改变外部状态(频率表)的 lambda。`forEach` 操作除了表示由一个流执行的计算结果外,什么都不做,这是“代码中的臭味”,就像一个改变状态的 lambda 一样。那么这段代码应该是什么样的呢?
|
||||
这段代码出了什么问题? 毕竟,它使用了流,lambdas 和方法引用,并得到正确的答案。 简而言之,它根本不是流代码; 它是伪装成流代码的迭代代码。 它没有从流 API 中获益,并且它比相应的迭代代码更长,更难读,并且更难于维护。 问题源于这样一个事实:这个代码在一个终结操作 `forEach` 中完成所有工作,使用一个改变外部状态(频率表)的 lambda。`forEach` 操作除了表示由一个流执行的计算结果外,什么都不做,这是「代码中的臭味」,就像一个改变状态的 lambda 一样。那么这段代码应该是什么样的呢?
|
||||
|
||||
```java
|
||||
// Proper use of streams to initialize a frequency table
|
||||
@ -31,7 +31,7 @@ try (Stream<String> words = new Scanner(file).tokens()) {
|
||||
|
||||
改进后的代码使用了收集器(collector),这是使用流必须学习的新概念。`Collectors` 的 API 令人生畏:它有 39 个方法,其中一些方法有多达 5 个类型参数。好消息是,你可以从这个 API 中获得大部分好处,而不必深入研究它的全部复杂性。对于初学者来说,可以忽略收集器接口,将收集器看作是封装缩减策略(reduction strategy)的不透明对象。在此上下文中,reduction 意味着将流的元素组合为单个对象。 收集器生成的对象通常是一个集合(它代表名称收集器)。
|
||||
|
||||
将流的元素收集到真正的集合中的收集器非常简单。有三个这样的收集器:`toList()`、`toSet()` 和 `toCollection(collectionFactory)`。它们分别返回集合、列表和程序员指定的集合类型。有了这些知识,我们就可以编写一个流管道从我们的频率表中提取出现频率前 10 个单词的列表。
|
||||
将流的元素收集到真正的集合中的收集器非常简单。有三个这样的收集器:`toList()`、`toSet()` 和 `toCollection(collectionFactory)`。它们分别返回集合、列表和程序员指定的集合类型。有了这些知识,我们就可以编写一个流管道从我们的频率表中提取出现频率前 10 个单词的列表。
|
||||
|
||||
```java
|
||||
// Pipeline to get a top-ten list of words from a frequency table
|
||||
@ -43,11 +43,11 @@ List<String> topTen = freq.keySet().stream()
|
||||
|
||||
注意,我们没有对 `toList` 方法的类收集器进行限定。**静态导入收集器的所有成员是一种惯例和明智的做法,因为它使流管道更易于阅读。**
|
||||
|
||||
这段代码中唯一比较棘手的部分是我们把 `comparing(freq::get).reverse()` 传递给 `sort` 方法。comparing 是一种比较器构造方法 (条目 14),它具有一个 key 的提取方法。该函数接受一个单词,而“提取”实际上是一个表查找:绑定方法引用 `freq::get` 在 `frequency` 表中查找单词,并返回单词出现在文件中的次数。最后,我们在比较器上调用 `reverse` 方法,因此我们将单词从最频繁到最不频繁进行排序。然后,将流限制为 10 个单词并将它们收集到一个列表中就很简单了。
|
||||
这段代码中唯一比较棘手的部分是我们把 `comparing(freq::get).reverse()` 传递给 `sort` 方法。comparing 是一种比较器构造方法(详见第 14 条),它具有一个 key 的提取方法。该函数接受一个单词,而“提取”实际上是一个表查找:绑定方法引用 `freq::get` 在 `frequency` 表中查找单词,并返回单词出现在文件中的次数。最后,我们在比较器上调用 `reverse` 方法,因此我们将单词从最频繁到最不频繁进行排序。然后,将流限制为 10 个单词并将它们收集到一个列表中就很简单了。
|
||||
|
||||
前面的代码片段使用 `Scanner` 的 `stream` 方法在 `scanner` 实例上获取流。这个方法是在 Java 9 中添加的。如果正在使用较早的版本,可以使用类似于条目 47 中 (`streamOf(Iterable<E>)`) 的适配器将实现了 `Iterator` 的 `scanner` 序转换为流。
|
||||
|
||||
那么收集器中的其他 36 种方法呢?它们中的大多数都是用于将流收集到 map 中的,这比将流收集到真正的集合中要复杂得多。每个流元素都与一个键和一个值相关联,多个流元素可以与同一个键相关联。
|
||||
那么收集器中的其他 36 种方法呢?它们中的大多数都是用于将流收集到 map 中的,这比将流收集到真正的集合中要复杂得多。每个流元素都与一个键和一个值相关联,多个流元素可以与同一个键相关联。
|
||||
|
||||
最简单的映射收集器是 toMap(keyMapper、valueMapper),它接受两个函数,一个将流元素映射到键,另一个映射到值。在条目 34 中的 `fromString` 实现中,我们使用这个收集器从 enum 的字符串形式映射到 enum 本身:
|
||||
|
||||
@ -60,7 +60,7 @@ private static final Map<String, Operation> stringToEnum =
|
||||
|
||||
如果流中的每个元素都映射到唯一键,则这种简单的 `toMap` 形式是完美的。 如果多个流元素映射到同一个键,则管道将以 `IllegalStateException` 终止。
|
||||
|
||||
`toMap` 更复杂的形式,以及 `groupingBy` 方法,提供了处理此类冲突 (collisions) 的各种方法。一种方法是向 `toMap` 方法提供除键和值映射器 (mappers) 之外的 `merge` 方法。`merge` 方法是一个 `BinaryOperator<V>`,其中 V是 map 的值类型。与键关联的任何附加值都使用 `merge` 方法与现有值相结合,因此,例如,如果 merge 方法是乘法,那么最终得到的结果是是值 `mapper` 与键关联的所有值的乘积。
|
||||
`toMap` 更复杂的形式,以及 `groupingBy` 方法,提供了处理此类冲突 (collisions) 的各种方法。一种方法是向 `toMap` 方法提供除键和值映射器(mappers)之外的 `merge` 方法。`merge` 方法是一个 `BinaryOperator<V>`,其中 V是 map 的值类型。与键关联的任何附加值都使用 `merge` 方法与现有值相结合,因此,例如,如果 merge 方法是乘法,那么最终得到的结果是是值 `mapper` 与键关联的所有值的乘积。
|
||||
|
||||
`toMap` 的三个参数形式对于从键到与该键关联的选定元素的映射也很有用。例如,假设我们有一系列不同艺术家(artists)的唱片集(albums),我们想要一张从唱片艺术家到最畅销专辑的 map。这个收集器将完成这项工作。
|
||||
|
||||
@ -70,7 +70,7 @@ Map<Artist, Album> topHits = albums.collect(
|
||||
toMap(Album::artist, a->a, maxBy(comparing(Album::sales))));
|
||||
```
|
||||
|
||||
请注意,比较器使用静态工厂方法 `maxBy`,它是从 BinaryOperator 静态导入的。 此方法将 `Comparator<T>` 转换为 `BinaryOperator<T>`,用于计算指定比较器隐含的最大值。 在这种情况下,比较器由比较器构造方法 comparing 返回,它采用 key 提取器函数 `Album::sales`。 这可能看起来有点复杂,但代码可读性很好。 简而言之,它说,“将专辑(albums)流转换为地 map,将每位艺术家(artist)映射到销售量最佳的专辑。”这与问题陈述出奇得接近。
|
||||
请注意,比较器使用静态工厂方法 `maxBy`,它是从 BinaryOperator 静态导入的。 此方法将 `Comparator<T>` 转换为 `BinaryOperator<T>`,用于计算指定比较器隐含的最大值。 在这种情况下,比较器由比较器构造方法 comparing 返回,它采用 key 提取器函数 `Album::sales`。 这可能看起来有点复杂,但代码可读性很好。 简而言之,它说,「将专辑(albums)流转换为地 map,将每位艺术家(artist)映射到销售量最佳的专辑。」这与问题陈述出奇得接近。
|
||||
|
||||
`toMap` 的三个参数形式的另一个用途是产生一个收集器,当发生冲突时强制执行 last-write-wins 策略。 对于许多流,结果是不确定的,但如果映射函数可能与键关联的所有值都相同,或者它们都是可接受的,则此收集器的行为可能正是您想要的:
|
||||
|
||||
@ -83,7 +83,7 @@ toMap(keyMapper, valueMapper, (oldVal, newVal) ->newVal)
|
||||
|
||||
`toMap` 的前三个版本也有变体形式,名为 `toConcurrentMap`,它们并行高效运行并生成 `ConcurrentHashMap` 实例。
|
||||
|
||||
除了 toMap 方法之外,Collectors API 还提供了 `groupingBy` 方法,该方法返回收集器以生成基于分类器函数 (classifier function) 将元素分组到类别中的 map。 分类器函数接受一个元素并返回它所属的类别。 此类别来用作元素的 map 的键。 `groupingBy` 方法的最简单版本仅采用分类器并返回一个 map,其值是每个类别中所有元素的列表。 这是我们在条目 45 中的 `Anagram` 程序中使用的收集器,用于生成从按字母顺序排列的单词到单词列表的 map:
|
||||
除了 toMap 方法之外,Collectors API 还提供了 `groupingBy` 方法,该方法返回收集器以生成基于分类器函数 (classifier function)将元素分组到类别中的 map。 分类器函数接受一个元素并返回它所属的类别。 此类别来用作元素的 map 的键。 `groupingBy` 方法的最简单版本仅采用分类器并返回一个 map,其值是每个类别中所有元素的列表。 这是我们在条目 45 中的 `Anagram` 程序中使用的收集器,用于生成从按字母顺序排列的单词到单词列表的 map:
|
||||
|
||||
```java
|
||||
Map<String, Long> freq = words
|
||||
@ -93,7 +93,7 @@ Map<String, Long> freq = words
|
||||
|
||||
`groupingByConcurrent` 方法提供了 `groupingBy` 的所有三个重载的变体。 这些变体并行高效运行并生成 `ConcurrentHashMap` 实例。 还有一个很少使用的 grouping 的亲戚称为 `partitioningBy`。 代替分类器方法,它接受 `predicate` 并返回其键为布尔值的 map。 此方法有两种重载,除了 `predicate` 之外,其中一种方法还需要 `downstream` 收集器。
|
||||
|
||||
通过 `counting` 方法返回的收集器仅用作下游收集器。 Stream 上可以通过 count 方法直接使用相同的功能,因此没有理由说 `collect(counting())`。 此属性还有十五种收集器方法。 它们包括九个方法,其名称以 `summing`,`averaging` 和 `summarizing` 开头(其功能在相应的原始流类型上可用)。 它们还包括 `reduce` 方法的所有重载,以及 filter,`mapping`,`flatMapping` 和 `collectingAndThen` 方法。 大多数程序员可以安全地忽略大多数这些方法。 从设计的角度来看,这些收集器代表了尝试在收集器中部分复制流的功能,以便下游收集器可以充当“迷你流 (ministreams)”。
|
||||
通过 `counting` 方法返回的收集器仅用作下游收集器。 Stream 上可以通过 count 方法直接使用相同的功能,因此没有理由说 `collect(counting())`。 此属性还有十五种收集器方法。 它们包括九个方法,其名称以 `summing`,`averaging` 和 `summarizing` 开头(其功能在相应的原始流类型上可用)。 它们还包括 `reduce` 方法的所有重载,以及 filter,`mapping`,`flatMapping` 和 `collectingAndThen` 方法。 大多数程序员可以安全地忽略大多数这些方法。 从设计的角度来看,这些收集器代表了尝试在收集器中部分复制流的功能,以便下游收集器可以充当「迷你流(ministreams)」。
|
||||
|
||||
我们还有三种收集器方法尚未提及。 虽然他们在收 `Collectors` 类中,但他们不涉及集合。 前两个是 `minBy` 和 `maxBy`,它们取比较器并返回比较器确定的流中的最小或最大元素。 它们是 `Stream` 接口中 `min` 和 `max` 方法的次要总结,是 `BinaryOperator` 中类似命名方法返回的二元运算符的类似收集器。 回想一下,我们在最畅销的专辑中使用了 `BinaryOperator.maxBy` 方法。
|
||||
|
||||
|
@ -36,7 +36,7 @@ static Stream<BigInteger> primes() {
|
||||
|
||||
即使假设正在使用一个高效的可拆分的源流、一个可并行化的或廉价的终端操作以及非干扰的函数对象,也无法从并行化中获得良好的加速效果,除非管道做了足够的实际工作来抵消与并行性相关的成本。作为一个非常粗略的估计,流中的元素数量乘以每个元素执行的代码行数应该至少是 100,000 [Lea14]。
|
||||
|
||||
重要的是要记住并行化流是严格的性能优化。 与任何优化一样,必须在更改之前和之后测试性能,以确保它值得做(第 67 项)。 理想情况下,应该在实际的系统设置中执行测试。 通常,程序中的所有并行流管道都在公共 fork-join 池中运行。 单个行为不当的管道可能会损害系统中不相关部分的其他行为。
|
||||
重要的是要记住并行化流是严格的性能优化。 与任何优化一样,必须在更改之前和之后测试性能,以确保它值得做(详见第 67 条)。 理想情况下,应该在实际的系统设置中执行测试。 通常,程序中的所有并行流管道都在公共 fork-join 池中运行。 单个行为不当的管道可能会损害系统中不相关部分的其他行为。
|
||||
|
||||
如果在并行化流管道时,这种可能性对你不利,那是因为它们确实存在。一个认识的人,他维护一个数百万行代码库,大量使用流,他发现只有少数几个地方并行流是有效的。这并不意味着应该避免并行化流。**在适当的情况下,只需向流管道添加一个 `parallel` 方法调用,就可以实现处理器内核数量的近似线性加速。** 某些领域,如机器学习和数据处理,特别适合这些加速。
|
||||
|
||||
|
@ -4,7 +4,7 @@
|
||||
|
||||
大多数方法和构造方法对可以将哪些值传递到其对应参数中有一些限制。 例如,索引值必须是非负数,对象引用必须为非 `null`。 你应该清楚地在文档中记载所有这些限制,并在方法主体的开头用检查来强制执行。 应该尝试在错误发生后尽快检测到错误,这是一般原则的特殊情况。 如果不这样做,则不太可能检测到错误,并且一旦检测到错误就更难确定错误的来源。
|
||||
|
||||
如果将无效参数值传递给方法,并且该方法在执行之前检查其参数,则它抛出适当的异常然后快速且清楚地以失败结束。 如果该方法无法检查其参数,可能会发生一些事情。 在处理过程中,该方法可能会出现令人困惑的异常。 更糟糕的是,该方法可以正常返回,但默默地计算错误的结果。 最糟糕的是,该方法可以正常返回但是将某个对象置于受损状态,在将来某个未确定的时间在代码中的某些不相关点处导致错误。 换句话说,验证参数失败可能导致违反故障原子性(failure atomicity )(条目 76)。
|
||||
如果将无效参数值传递给方法,并且该方法在执行之前检查其参数,则它抛出适当的异常然后快速且清楚地以失败结束。 如果该方法无法检查其参数,可能会发生一些事情。 在处理过程中,该方法可能会出现令人困惑的异常。 更糟糕的是,该方法可以正常返回,但默默地计算错误的结果。 最糟糕的是,该方法可以正常返回但是将某个对象置于受损状态,在将来某个未确定的时间在代码中的某些不相关点处导致错误。 换句话说,验证参数失败可能导致违反故障原子性(failure atomicity)(详见第 76 条)。
|
||||
|
||||
对于公共方法和受保护方法,请使用 Java 文档`@throws`注解来记在在违反参数值限制时将引发的异常(条目 74)。 通常,生成的异常是`IllegalArgumentException`,`IndexOutOfBoundsException`或`NullPointerException`(条目 72)。 一旦记录了对方法参数的限制,并且记录了违反这些限制时将引发的异常,那么强制执行这些限制就很简单了。 这是一个典型的例子:
|
||||
|
||||
@ -27,7 +27,7 @@ public BigInteger mod(BigInteger m) {
|
||||
}
|
||||
```
|
||||
|
||||
请注意,文档注释没有说“如果 m 为 null,mod 抛出 `NullPointerException`”,尽管该方法正是这样做的,这是调用`m.sgn()`的副产品。这个异常记载在类级别文档注释中,用于包含的`BigInteger`类。类级别的注释应用于类的所有公共方法中的所有参数。这是避免在每个方法上分别记录每个`NullPointerException`的好方法。它可以与`@Nullable`或类似的注释结合使用,以表明某个特定参数可能为空,但这种做法不是标准的,为此使用了多个注解。
|
||||
请注意,文档注释没有说「如果 m 为 null,mod 抛出 `NullPointerException`」,尽管该方法正是这样做的,这是调用`m.sgn()`的副产品。这个异常记载在类级别文档注释中,用于包含的`BigInteger`类。类级别的注释应用于类的所有公共方法中的所有参数。这是避免在每个方法上分别记录每个`NullPointerException`的好方法。它可以与`@Nullable`或类似的注释结合使用,以表明某个特定参数可能为空,但这种做法不是标准的,为此使用了多个注解。
|
||||
|
||||
在 Java 7 中添加的`Objects.requireNonNull 方`法灵活方便,因此没有理由再手动执行空值检查。 如果愿意,可以指定自定义异常详细消息。 该方法返回其输入的值,因此可以在使用值的同时执行空检查:
|
||||
|
||||
@ -38,7 +38,7 @@ this.strategy = Objects.requireNonNull(strategy, "strategy");
|
||||
|
||||
你也可以忽略返回值,并使用`Objects.requireNonNull`作为满足需求的独立空值检查。
|
||||
|
||||
在 Java 9 中,java.util.Objects 类中添加了范围检查工具。 此工具包含三个方法:`checkFromIndexSize`,`checkFromToIndex`和`checkIndex`。 此工具不如空检查方法灵活。 它不允许指定自己的异常详细消息,它仅用于列表和数组索引。 它不处理闭合范围(包含两个端点)。 但如果它能满足你的需要,那就很方便了。
|
||||
在 Java 9 中,`java.util.Objects` 类中添加了范围检查工具。 此工具包含三个方法:`checkFromIndexSize`,`checkFromToIndex`和`checkIndex`。 此工具不如空检查方法灵活。 它不允许指定自己的异常详细消息,它仅用于列表和数组索引。 它不处理闭合范围(包含两个端点)。 但如果它能满足你的需要,那就很方便了。
|
||||
|
||||
对于未导出的方法,作为包的作者,控制调用方法的环境,这样就可以并且应该确保只传入有效的参数值。因此,非公共方法可以使用断言检查其参数,如下所示:
|
||||
|
||||
@ -58,7 +58,7 @@ private static void sort(long a[], int offset, int length) {
|
||||
|
||||
构造方法是这个原则的一个特例,你应该检查要存储起来供以后使用的参数的有效性。检查构造方法参数的有效性对于防止构造对象违反类不变性(class invariants)非常重要。
|
||||
|
||||
你应该在执行计算之前显式检查方法的参数,但这一规则也有例外。 一个重要的例外是有效性检查昂贵或不切实际的情况,并且在进行计算的过程中隐式执行检查。 例如,考虑一种对对象列表进行排序的方法,例如`Collections.sort(List)`。 列表中的所有对象必须是可相互比较的。 在对列表进行排序的过程中,列表中的每个对象都将与其他对象进行比较。 如果对象不可相互比较,则某些比较操作抛出 `ClassCastException` 异常,这正是`sort`方法应该执行的操作。 因此,提前检查列表中的元素是否具有可比性是没有意义的。 但请注意,不加选择地依赖隐式有效性检查会导致失败原子性(failure atomicity)的丢失(条目 76)。
|
||||
你应该在执行计算之前显式检查方法的参数,但这一规则也有例外。 一个重要的例外是有效性检查昂贵或不切实际的情况,并且在进行计算的过程中隐式执行检查。 例如,考虑一种对对象列表进行排序的方法,例如`Collections.sort(List)`。 列表中的所有对象必须是可相互比较的。 在对列表进行排序的过程中,列表中的每个对象都将与其他对象进行比较。 如果对象不可相互比较,则某些比较操作抛出 `ClassCastException` 异常,这正是`sort`方法应该执行的操作。 因此,提前检查列表中的元素是否具有可比性是没有意义的。 但请注意,不加选择地依赖隐式有效性检查会导致失败原子性(failure atomicity)的丢失(详见第 76 条)。
|
||||
|
||||
有时,计算会隐式执行必需的有效性检查,但如果检查失败则会抛出错误的异常。 换句话说,计算由于无效参数值而自然抛出的异常与文档记录方法抛出的异常不匹配。 在这些情况下,你应该使用条目 73 中描述的异常翻译(exception translation)习惯用法将自然异常转换为正确的异常。
|
||||
|
||||
|
@ -99,12 +99,12 @@ public Date end() {
|
||||
|
||||
在将内部组件返回给客户端之前进行防御性拷贝也是如此。无论你的类是否是不可变的,在返回对可拜年的内部组件的引用之前,都应该三思。可能的情况是,应该返回一个防御性拷贝。记住,非零长度数组总是可变的。因此,在将内部数组返回给客户端之前,应该始终对其进行防御性拷贝。或者,可以返回数组的不可变视图。这两项技术都记载于条目 15。
|
||||
|
||||
可以说,所有这些的真正教训是,在可能的情况下,应该使用不可变对象作为对象的组件,这样就不必担心防御性拷贝 (条目 17)。在我们的 `Period` 示例中,使用 `Instant`(或 `LocalDateTime` 或 `ZonedDateTime`),除非使用的是 Java 8 之前的版本。如果使用的是较早的版本,则一个选项是存储 `Date.getTime()` 返回的基本类型 long 来代替 Date 引用。
|
||||
可以说,所有这些的真正教训是,在可能的情况下,应该使用不可变对象作为对象的组件,这样就不必担心防御性拷贝(详见第 17 条)。在我们的 `Period` 示例中,使用 `Instant`(或 `LocalDateTime` 或 `ZonedDateTime`),除非使用的是 Java 8 之前的版本。如果使用的是较早的版本,则一个选项是存储 `Date.getTime()` 返回的基本类型 long 来代替 Date 引用。
|
||||
|
||||
可能存在与防御性拷贝相关的性能损失,并且它并不总是合理的。如果一个类信任它的调用者不修改内部组件,也许是因为这个类和它的客户端都是同一个包的一部分,那么它可能不需要防御性的拷贝。在这些情况下,类文档应该明确指出调用者不能修改受影响的参数或返回值。
|
||||
|
||||
即使跨越包边界,在将可变参数集成到对象之前对其进行防御性拷贝也并不总是合适的。有些方法和构造方法的调用指示参数引用的对象的显式切换。当调用这样的方法时,客户端承诺不再直接修改对象。希望获得客户端提供的可变对象的所有权的方法或构造方法必须在其文档中明确说明这一点。
|
||||
|
||||
包含方法或构造方法的类,这些方法或构造方法的调用指示控制权的转移,这些类无法防御恶意客户端。 只有当一个类和它的客户之间存在相互信任,或者当对类的不变量造成损害时,除了客户之外,任何人都不会受到损害。 后一种情况的一个例子是包装类模式(第 18 项)。 根据包装类的性质,客户端可以通过在包装后直接访问对象来破坏类的不变性,但这通常只会损害客户端。
|
||||
包含方法或构造方法的类,这些方法或构造方法的调用指示控制权的转移,这些类无法防御恶意客户端。 只有当一个类和它的客户之间存在相互信任,或者当对类的不变量造成损害时,除了客户之外,任何人都不会受到损害。 后一种情况的一个例子是包装类模式(详见第 18 条)。 根据包装类的性质,客户端可以通过在包装后直接访问对象来破坏类的不变性,但这通常只会损害客户端。
|
||||
|
||||
总之,如果一个类有从它的客户端获取或返回的可变组件,那么这个类必须防御性地拷贝这些组件。如果拷贝的成本太高,并且类信任它的客户端不会不适当地修改组件,则可以用文档替换防御性拷贝,该文档概述了客户端不得修改受影响组件的责任。
|
@ -2,9 +2,9 @@
|
||||
|
||||
这一条目是 API 设计提示的大杂烩,但它们本身并足以设立一个单独的条目。综合起来,这些设计提示将帮助你更容易地学习和使用 API,并且更不容易出错。
|
||||
|
||||
**仔细选择方法名名称**。名称应始终遵守标准命名约定 (条目 68)。你的主要目标应该是选择与同一包中的其他名称一致且易于理解的名称。其次是应该是选择与更广泛的共识一致的名称。避免使用较长的方法名。如果有疑问,可以从 Java 类库 API 中寻求指导。尽管类库中也存在许多不一致之处 (考虑到这些类库的规模和范围,这是不可避免的),也提供了相当客观的认可和共识。
|
||||
**仔细选择方法名名称**。名称应始终遵守标准命名约定(详见第 68 条)。你的主要目标应该是选择与同一包中的其他名称一致且易于理解的名称。其次是应该是选择与更广泛的共识一致的名称。避免使用较长的方法名。如果有疑问,可以从 Java 类库 API 中寻求指导。尽管类库中也存在许多不一致之处(考虑到这些类库的规模和范围,这是不可避免的),也提供了相当客观的认可和共识。
|
||||
|
||||
**不要过分地提供方便的方法**。每种方法都应该“尽其所能”。太多的方法使得类难以学习、使用、文档化、测试和维护。对于接口更是如此,在接口中,太多的方法使实现者和用户的工作变得复杂。对于类或接口支持的每个操作,提供一个功能完整的方法。只有在经常使用时,才考虑提供“快捷方式(shortcut)”。**如果有疑问,请将其删除**。
|
||||
**不要过分地提供方便的方法**。每种方法都应该“尽其所能”。太多的方法使得类难以学习、使用、文档化、测试和维护。对于接口更是如此,在接口中,太多的方法使实现者和用户的工作变得复杂。对于类或接口支持的每个操作,提供一个功能完整的方法。只有在经常使用时,才考虑提供「快捷方式(shortcut)」。**如果有疑问,请将其删除**。
|
||||
|
||||
**避免过长的参数列表**。目标是四个或更少的参数。大多数程序员不能记住更长的参数列表。如果你的许多方法超过了这个限制,如果未经常引用其文档的情况下,那么你的 API 将无法使用。现代 IDE 编辑器会提供帮助,但是使用简短的参数列表仍然会更好。**相同类型参数的长序列尤其有害**。用户不仅不能记住参数的顺序,而且当他们意外地弄错参数顺序时,他们的程序仍然会编译和运行。只是不会按照作者的意图去执行。
|
||||
|
||||
@ -12,9 +12,9 @@
|
||||
|
||||
缩短过长参数列表的第二种技术是创建辅助类来保存参数组。这些辅助类通常是静态成员类 (条目 24)。如果看到一个频繁出现的参数序列表示某个不同的实体,建议使用这种技术。例如,假设正在编写一个表示纸牌游戏的类,并且发现不断地传递一个由两个参数组成的序列,这些参数表示纸牌的点数和花色。如果添加一个辅助类来表示卡片,并用辅助类的单个参数替换参数序列的每次出现,那么 API 和类的内部结构可能会受益。
|
||||
|
||||
结合前两个方面的第三种技术是,从对象构造到方法调用采用 Builder 模式 (条目 2)。如果你有一个方法有许多参数,特别是其中一些是可选的,那么可以定义一个对象来表示所有的参数,并允许客户端在这个对象上进行多个 "setter" 调用,每次设置一个参数或较小相关的组。设置好所需的参数后,客户端调用对象的 "execute" 方法,该方法对参数进行最后的有效性检查,并执行实际的计算。
|
||||
结合前两个方面的第三种技术是,从对象构造到方法调用采用 Builder 模式 (条目 2)。如果你有一个方法有许多参数,特别是其中一些是可选的,那么可以定义一个对象来表示所有的参数,并允许客户端在这个对象上进行多个「setter」调用,每次设置一个参数或较小相关的组。设置好所需的参数后,客户端调用对象的「execute」方法,该方法对参数进行最后的有效性检查,并执行实际的计算。
|
||||
|
||||
**对于参数类型,优先选择接口而不是类**(条目 64)。如果有一个合适的接口来定义一个参数,那么使用它来支持一个实现该接口的类。例如,没有理由在编写方法时使用 HashMap 作为输入参数,相反,而是使用 Map 作为参数,这允许传入 HashMap、TreeMap、ConcurrentHashMap、TreeMap 的子 Map(submap)或任何尚未编写的 Map 实现。通过使用的类而不是接口,就把客户端限制在特定的实现中,如果输入数据碰巧以其他形式存在,则强制执行不必要的、代价高昂的复制操作。
|
||||
**对于参数类型,优先选择接口而不是类**(详见第 64 条)。如果有一个合适的接口来定义一个参数,那么使用它来支持一个实现该接口的类。例如,没有理由在编写方法时使用 HashMap 作为输入参数,相反,而是使用 Map 作为参数,这允许传入 HashMap、TreeMap、ConcurrentHashMap、TreeMap 的子 Map(submap)或任何尚未编写的 Map 实现。通过使用的类而不是接口,就把客户端限制在特定的实现中,如果输入数据碰巧以其他形式存在,则强制执行不必要的、代价高昂的复制操作。
|
||||
|
||||
**与布尔型参数相比,优先使用两个元素枚举类型**,除非布尔型参数的含义在方法名中是明确的。枚举类型使代码更容易阅读和编写。此外,它们还可以方便地在以后添加更多选项。例如,你可能有一个 `Thermometer` 类型的静态工厂方法,这个方法的签名是以下这个枚举:
|
||||
|
||||
@ -22,4 +22,4 @@
|
||||
public enum TemperatureScale { FAHRENHEIT, CELSIUS }
|
||||
```
|
||||
|
||||
`Thermometer.newInstance(TemperatureScale.CELSIUS)` 不仅比 `Thermometer.newInstance(true)` 更有意义,而且可以在将来的版本中将`KELVIN`添加到 `TemperatureScale` 中,而无需向 `Thermometer` 添加新的静态工厂。 此外,还可以将温度刻度(temperature-scale)依赖关系重构为枚举常量的方法(条目 34)。 例如,每个刻度常量可以有一个采用 double 值并将其转换为 `Celsius` 的方法。
|
||||
`Thermometer.newInstance(TemperatureScale.CELSIUS)` 不仅比 `Thermometer.newInstance(true)` 更有意义,而且可以在将来的版本中将`KELVIN`添加到 `TemperatureScale` 中,而无需向 `Thermometer` 添加新的静态工厂。 此外,还可以将温度刻度(temperature-scale)依赖关系重构为枚举常量的方法(详见第 34 条)。 例如,每个刻度常量可以有一个采用 double 值并将其转换为 `Celsius` 的方法。
|
@ -31,7 +31,7 @@ public class CollectionClassifier {
|
||||
}
|
||||
```
|
||||
|
||||
您可能希望此程序打印 Set,然后是 List 和 Unknown Collection 字符串,实际上并没有。 而是打印了三次 Unknown Collection 字符串。 为什么会这样? 因为`classify`方法被重载了,**在编译时选择要调用哪个重载方法**。 对于循环的所有三次迭代,参数的编译时类型是相同的:`Collection<?>`。 运行时类型在每次迭代中都不同,但这不会影响对重载方法的选择。 因为参数的编译时类型是`Collection<?>,`,所以唯一适用的重载是第三个`classify(Collection<?> c)`方法,并且在循环的每次迭代中调用这个重载。
|
||||
您可能希望此程序打印 Set,然后是 List 和 Unknown Collection 字符串,实际上并没有。 而是打印了三次 Unknown Collection 字符串。 为什么会这样? 因为`classify`方法被重载了,**在编译时选择要调用哪个重载方法**。 对于循环的所有三次迭代,参数的编译时类型是相同的:`Collection<?>`。 运行时类型在每次迭代中都不同,但这不会影响对重载方法的选择。 因为参数的编译时类型是`Collection<?>`,所以唯一适用的重载是第三个`classify(Collection<?> c)`方法,并且在循环的每次迭代中调用这个重载。
|
||||
|
||||
此程序的行为是违反直觉的,因为**重载(overloaded)方法之间的选择是静态的,而重写(overridden)方法之间的选择是动态的**。 根据调用方法的对象的运行时类型,在运行时选择正确版本的重写方法。 作为提醒,当子类包含与父类中具有相同签名的方法声明时,会重写此方法。 如果在子类中重写实例方法并且在子类的实例上调用,则无论子类实例的编译时类型如何,都会执行子类的重写方法。 为了具体说明,请考虑以下程序:
|
||||
|
||||
@ -59,8 +59,6 @@ public class Overriding {
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
|
||||
`name`方法在`Wine`类中声明,并在子类`SparklingWine`和`Champagne`中重写。 正如你所料,此程序打印出 wine,sparkling wine 和 champagne,即使实例的编译时类型在循环的每次迭代中都是`Wine`。 当调用重写方法时,对象的编译时类型对执行哪个方法没有影响; 总是会执行“最具体 (most specific)”的重写方法。 将此与重载进行比较,其中对象的运行时类型对执行的重载没有影响; 选择是在编译时完成的,完全基于参数的编译时类型。
|
||||
|
||||
在`CollectionClassifier`示例中,程序的目的是通过基于参数的运行时类型自动调度到适当的方法重载来辨别参数的类型,就像 Wine 类中的 name 方法一样。 方法重载根本不提供此功能。 假设需要一个静态方法,修复`CollectionClassifier`程序的最佳方法是用一个执行显式`instanceof`测试的方法替换`classify`的所有三个重载:
|
||||
@ -78,9 +76,9 @@ public class Overriding {
|
||||
|
||||
例如,考虑`ObjectOutputStream`类。对于每个基本类型和几个引用类型,它都有其`write`方法的变体。这些变体都有不同的名称,例如`writeBoolean(boolean)`、`writeInt(int)`和`writeLong(long)`,而不是重载`write`方法。与重载相比,这种命名模式的另一个好处是,可以为`read`方法提供相应的名称,例如`readBoolean()`、`readInt()`和`readLong()`。`ObjectInputStream`类实际上提供了这样的读取方法。
|
||||
|
||||
对于构造方法,无法使用不同的名称:类的多个构造函数总是被重载。 在许多情况下,可以选择导出静态工厂而不是构造方法(条目 1)。 此外,使用构造方法,不必担心重载和重写之间的影响,因为构造方法不能被重写。 你可能有机会导出具有相同数量参数的多个构造函数,因此知道如何安全地执行它是值得的。
|
||||
对于构造方法,无法使用不同的名称:类的多个构造函数总是被重载。 在许多情况下,可以选择导出静态工厂而不是构造方法(详见第 1 条)。 此外,使用构造方法,不必担心重载和重写之间的影响,因为构造方法不能被重写。 你可能有机会导出具有相同数量参数的多个构造函数,因此知道如何安全地执行它是值得的。
|
||||
|
||||
如果总是清楚哪个重载将应用于任何给定的实际参数集,那么用相同数量的参数导出多个重载不太可能让程序员感到困惑。在这种情况下,每对重载中至少有一个对应的形式参数在这两个重载中具有“完全不同的”类型。如果显然不可能将任何非空表达式强制转换为这两种类型,那么这两种类型是完全不同的。在这些情况下,应用于给定实际参数集的重载完全由参数的运行时类型决定,且不受其编译时类型的影响,因此消除了一个主要的混淆。例如,ArrayList 有一个接受 int 的构造方法和第二个接受 Collection 的构造方法。很难想象在任何情况下,这两个构造方法在调用时哪个会产生混淆。
|
||||
如果总是清楚哪个重载将应用于任何给定的实际参数集,那么用相同数量的参数导出多个重载不太可能让程序员感到困惑。在这种情况下,每对重载中至少有一个对应的形式参数在这两个重载中具有「完全不同的」类型。如果显然不可能将任何非空表达式强制转换为这两种类型,那么这两种类型是完全不同的。在这些情况下,应用于给定实际参数集的重载完全由参数的运行时类型决定,且不受其编译时类型的影响,因此消除了一个主要的混淆。例如,ArrayList 有一个接受 int 的构造方法和第二个接受 Collection 的构造方法。很难想象在任何情况下,这两个构造方法在调用时哪个会产生混淆。
|
||||
|
||||
在 Java 5 之前,所有基本类型都与引用类型完全不同,但在自动装箱存在的情况下,则并非如此,并且它已经造成了真正的麻烦。 考虑以下程序:
|
||||
|
||||
@ -105,8 +103,6 @@ public class SetList {
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
|
||||
首先,程序将从-3 到 2 的整数添加到有序集合和列表中。 然后,它在集合和列表上进行三次相同的`remove`方法调用。 如果你和大多数人一样,希望程序从集合和列表中删除非负值(0, 1 和 2)并打印[-3, -2, -1] [ - 3, -2, -1]。 实际上,程序从集合中删除非负值,从列表中删除奇数值,并打印[-3, -2, -1] [-2, 0, 2]。 称这种混乱的行为是一种保守的说法。
|
||||
|
||||
实际情况是:调用`set.remove(i)`选择重载`remove(E)`方法,其中`E`是`set (Integer)`的元素类型,将基本类型 i 由 int 自动装箱为 Integer 中。这是你所期望的行为,因此程序最终会从集合中删除正值。另一方面,对`list.remove(i)`的调用选择重载`remove(int i)`方法,它将删除列表中指定位置的元素。如果从列表\[-3, -2, -1, 0, 1, 2\] 开始,移除第 0 个元素,然后是第 1 个,然后是第二个,就只剩下\[-2, 0, 2\],谜底就解开了。若要修复此问题,请强制转换`list.remove`的参数为`Integer`类型,迫使选择正确的重载。或者,也可以调用`Integer.valueOf(i)`,然后将结果传递给`list.remove`方法。无论哪种方式,程序都会按预期打印\[-3, -2, -1\]\[-3, -2, -1\]:
|
||||
@ -131,9 +127,9 @@ exec.submit(System.out::println);
|
||||
```
|
||||
|
||||
虽然 Thread 构造方法调用和`submit`方法调用看起来很相似,但是前者编译而后者不编译。参数是相同的 (`System.out::println`),两者都有一个带有`Runnable`的重载。这里发生了什么?令人惊讶的答案是,`submit`方法有一个带有`Callable <T>`参数的重载,而`Thread`构造方法却没有。你可能认为这不会有什么区别,因为`println`方法的所有重载都会返回`void`,因此方法引用不可能是`Callable`
|
||||
。这很有道理,但重载解析算法不是这样工作的。也许同样令人惊讶的是,如果`println`方法没有被重载,那么`submit`方法调用是合法的。正是被引用的方法 (println) 的重载和被调用的方法 (submit) 相结合,阻止了重载解析算法按照你所期望的方式运行。
|
||||
。这很有道理,但重载解析算法不是这样工作的。也许同样令人惊讶的是,如果`println`方法没有被重载,那么`submit`方法调用是合法的。正是被引用的方法(println)的重载和被调用的方法(submit)相结合,阻止了重载解析算法按照你所期望的方式运行。
|
||||
|
||||
从技术上讲,问题是`System.out :: println`是一个不精确的方法引用[JLS,15.13.1],并且『包含隐式类型的 lambda 表达式或不精确的方法引用的某些参数表达式被适用性测试忽略,因为在选择目标类型之前无法确定它们的含义[JLS,15.12.2]。』如果你不理解这段话也不要担心; 它针对的是编译器编写者。 关键是在同一参数位置中具有不同功能接口的重载方法或构造方法会导致混淆。 因此,**不要在相同参数位置重载采用不同函数式接口的方法**。 在此条目的说法中,不同的函数式接口并没有根本不同。 如果传递命令行开关`-Xlint:overloads`,Java 编译器将警告这种有问题的重载。
|
||||
从技术上讲,问题是`System.out::println`是一个不精确的方法引用[JLS,15.13.1],并且「包含隐式类型的 lambda 表达式或不精确的方法引用的某些参数表达式被适用性测试忽略,因为在选择目标类型之前无法确定它们的含义[JLS,15.12.2]。」如果你不理解这段话也不要担心; 它针对的是编译器编写者。 关键是在同一参数位置中具有不同功能接口的重载方法或构造方法会导致混淆。 因此,**不要在相同参数位置重载采用不同函数式接口的方法**。 在此条目的说法中,不同的函数式接口并没有根本不同。 如果传递命令行开关`-Xlint:overloads`,Java 编译器将警告这种有问题的重载。
|
||||
|
||||
数组类型和 Object 以外的类是完全不同的。此外,除了`Serializable`和`Cloneable`之外,数组类型和其他接口类型也完全不同。如果两个不同的类都不是另一个类的后代[JLS, 5.5],则称它们是不相关的。例如,`String`和`Throwable`是不相关的。任何对象都不可能是两个不相关类的实例,所以不相关的类也是完全不同的。
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
# 53. 明智审慎地使用可变参数
|
||||
|
||||
可变参数方法正式名称称为可变的参数数量方法『variable arity methods』 [JLS, 8.4.1],接受零个或多个指定类型的参数。 可变参数机制首先创建一个数组,其大小是在调用位置传递的参数数量,然后将参数值放入数组中,最后将数组传递给方法。
|
||||
可变参数方法正式名称称为可变的参数数量方法「variable arity methods」 [JLS, 8.4.1],接受零个或多个指定类型的参数。 可变参数机制首先创建一个数组,其大小是在调用位置传递的参数数量,然后将参数值放入数组中,最后将数组传递给方法。
|
||||
|
||||
例如,这里有一个可变参数方法,它接受一系列 int 类型的参数并返回它们的总和。如你所料, `sum(1,2,3)` 的值为 6, `sum()` 的值为 0:
|
||||
|
||||
@ -60,8 +60,8 @@ public void foo(int a1, int a2, int a3) { }
|
||||
public void foo(int a1, int a2, int a3, int... rest) { }
|
||||
```
|
||||
|
||||
现在你知道,在所有参数数量超过 3 个的方法调用中,只有 5%的调用需要支付创建数组的成本。与大多数性能优化一样,这种技术通常不太合适,但一旦真正需要的时候,它是一个救星。
|
||||
现在你知道,在所有参数数量超过 3 个的方法调用中,只有 5% 的调用需要支付创建数组的成本。与大多数性能优化一样,这种技术通常不太合适,但一旦真正需要的时候,它是一个救星。
|
||||
|
||||
`EnumSet` 的静态工厂使用这种技术将创建枚举集合的成本降到最低。这是适当的,因为枚举集合为比特属性提供具有性能竞争力的替换(performance-competitive replacement for bit fields)是至关重要的 (条目 36)。
|
||||
`EnumSet` 的静态工厂使用这种技术将创建枚举集合的成本降到最低。这是适当的,因为枚举集合为比特属性提供具有性能竞争力的替换(performance-competitive replacement for bit fields)是至关重要的 (详见第 36 条)。
|
||||
|
||||
总之,当需要使用可变数量的参数定义方法时,可变参数非常有用。 在使用可变参数前加上任何必需的参数,并注意使用可变参数的性能后果。
|
@ -16,7 +16,7 @@ public List<Cheese> getCheeses() {
|
||||
}
|
||||
```
|
||||
|
||||
没有理由对没有奶酪 (Cheese) 可供购买的情况进行特殊处理。这样需要在客户端做额外的代码处理可能为 null 的返回值,例如:
|
||||
没有理由对没有奶酪(Cheese)可供购买的情况进行特殊处理。这样需要在客户端做额外的代码处理可能为 null 的返回值,例如:
|
||||
|
||||
```java
|
||||
List<Cheese> cheeses = shop.getCheeses();
|
||||
@ -35,7 +35,7 @@ public List<Cheese> getCheeses() {
|
||||
}
|
||||
```
|
||||
|
||||
如果有证据表明分配空集合会损害性能,可以通过重复返回相同的不可变空集合来避免分配,因为不可变对象可以自由共享 (条目 17)。下面的代码就是这样做的,使用了 `Collections.emptyList` 方法。如果你要返回一个 Set,可以使用 `Collections.emptySet` ;如果要返回 Map,则使用 `Collections.emptyMap`。但是请记住,这是一个优化,很少需要它。如果你认为你需要它,测量一下前后的性能表现,确保它确实有帮助:
|
||||
如果有证据表明分配空集合会损害性能,可以通过重复返回相同的不可变空集合来避免分配,因为不可变对象可以自由共享(详见第 17 条)。下面的代码就是这样做的,使用了 `Collections.emptyList` 方法。如果你要返回一个 Set,可以使用 `Collections.emptySet` ;如果要返回 Map,则使用 `Collections.emptyMap`。但是请记住,这是一个优化,很少需要它。如果你认为你需要它,测量一下前后的性能表现,确保它确实有帮助:
|
||||
|
||||
```java
|
||||
// Optimization - avoids allocating empty collections
|
||||
|
@ -1,6 +1,6 @@
|
||||
# 55. 明智审慎地返回 Optional
|
||||
|
||||
在 Java 8 之前,编写在特定情况下无法返回任何值的方法时,可以采用两种方法。要么抛出异常,要么返回 null(假设返回类型是对象是引用类型)。但这两种方法都不完美。应该为异常条件保留异常 (条目 69),并且抛出异常代价很高,因为在创建异常时捕获整个堆栈跟踪。返回 null 没有这些缺点,但是它有自己的缺陷。如果方法返回 null,客户端必须包含特殊情况代码来处理 null 返回的可能性,除非程序员能够证明 null 返回是不可能的。如果客户端忽略检查 null 返回并将 null 返回值存储在某个数据结构中,那么会在将来的某个时间在与这个问题不相关的代码位置上,抛出`NullPointerException`异常的可能性。
|
||||
在 Java 8 之前,编写在特定情况下无法返回任何值的方法时,可以采用两种方法。要么抛出异常,要么返回 null(假设返回类型是对象是引用类型)。但这两种方法都不完美。应该为异常条件保留异常 (详见第 69 条),并且抛出异常代价很高,因为在创建异常时捕获整个堆栈跟踪。返回 null 没有这些缺点,但是它有自己的缺陷。如果方法返回 null,客户端必须包含特殊情况代码来处理 null 返回的可能性,除非程序员能够证明 null 返回是不可能的。如果客户端忽略检查 null 返回并将 null 返回值存储在某个数据结构中,那么会在将来的某个时间在与这个问题不相关的代码位置上,抛出`NullPointerException`异常的可能性。
|
||||
|
||||
在 Java 8 中,还有第三种方法来编写可能无法返回任何值的方法。`Optional<T>`类表示一个不可变的容器,它可以包含一个非 null 的`T`引用,也可以什么都不包含。不包含任何内容的 Optional 被称为空(empty)。非空的包含值称的 Optional 被称为存在(present)。Optional 的本质上是一个不可变的集合,最多可以容纳一个元素。`Optional<T>`没有实现`Collection<T>`接口,但原则上是可以。
|
||||
|
||||
@ -51,7 +51,7 @@ public static <E extends Comparable<E>>
|
||||
}
|
||||
```
|
||||
|
||||
那么,如何选择返回 Optional 而不是返回 null 或抛出异常呢?`Optional`在本质上类似于检查异常(checked exceptions)(条目 71),因为它们迫使 API 的用户面对可能没有返回任何值的事实。抛出未检查的异常或返回 null 允许用户忽略这种可能性,从而带来潜在的可怕后果。但是,抛出一个检查异常需要在客户端中添加额外的样板代码。
|
||||
那么,如何选择返回 Optional 而不是返回 null 或抛出异常呢?`Optional`在本质上类似于检查异常(checked exceptions)(详见第 71 条),因为它们迫使 API 的用户面对可能没有返回任何值的事实。抛出未检查的异常或返回 null 允许用户忽略这种可能性,从而带来潜在的可怕后果。但是,抛出一个检查异常需要在客户端中添加额外的样板代码。
|
||||
|
||||
如果方法返回一个 Optional,则客户端可以选择在方法无法返回值时要采取的操作。 可以指定默认值:
|
||||
|
||||
@ -108,11 +108,11 @@ streamOfOptionals.
|
||||
.flatMap(Optional::stream)
|
||||
```
|
||||
|
||||
并不是所有的返回类型都能从 Optional 的处理中获益。**容器类型,包括集合、映射、Stream、数组和 Optional,不应该封装在 Optional 中**。与其返回一个空的`Optional<List<T>>`,不还如返回一个空的 `List<T>`(条目 54)。返回空容器将消除客户端代码处理 Optional 的需要。`ProcessHandle` 类确实有 `arguments` 方法,它返回`Optional<String[]>`,但是这个方法应该被视为一种异常,不该被效仿。
|
||||
并不是所有的返回类型都能从 Optional 的处理中获益。**容器类型,包括集合、映射、Stream、数组和 Optional,不应该封装在 Optional 中**。与其返回一个空的`Optional<List<T>>`,不还如返回一个空的 `List<T>`(详见第 54 条)。返回空容器将消除客户端代码处理 Optional 的需要。`ProcessHandle` 类确实有 `arguments` 方法,它返回`Optional<String[]>`,但是这个方法应该被视为一种异常,不该被效仿。
|
||||
|
||||
那么什么时候应该声明一个方法来返回 `Optional <T>` 而不是 `T` 呢? 通常,**如果可能无法返回结果,并且在没有返回结果,客户端还必须执行特殊处理的情况下,则应声明返回 Optional <T> 的方法**。也就是说,返回 `Optional <T>` 并非没有成本。 Optional 是必须分配和初始化的对象,从 Optional 中读取值需要额外的迂回。 这使得 Optional 不适合在某些性能关键的情况下使用。 特定方法是否属于此类别只能通过仔细测量来确定(详见第 67 条)。
|
||||
那么什么时候应该声明一个方法来返回 `Optional<T>` 而不是 `T` 呢? 通常,**如果可能无法返回结果,并且在没有返回结果,客户端还必须执行特殊处理的情况下,则应声明返回 Optional \<T\> 的方法**。也就是说,返回 `Optional<T>` 并非没有成本。 Optional 是必须分配和初始化的对象,从 Optional 中读取值需要额外的迂回。 这使得 Optional 不适合在某些性能关键的情况下使用。 特定方法是否属于此类别只能通过仔细测量来确定(详见第 67 条)。
|
||||
|
||||
与返回装箱的基本类型相比,返回包含已装箱基本类型的 Optional 的代价高得惊人,因为 Optional 有两个装箱级别,而不是零。因此,类库设计人员认为为基本类型 int、long 和 double 提供类似 `Option<T>` 是合适的。这些 Option 是 `OptionalInt`、`OptionalLong` 和 `OptionalDouble`。它们包含 `Optional<T>` 上的大多数方法,但不是所有方法。因此,除了“次要基本类型(minor primitive types)”Boolean,Byte,Character,Short 和 Float 之外,**永远不应该返回装箱的基本类型的 Optional**。
|
||||
与返回装箱的基本类型相比,返回包含已装箱基本类型的 Optional 的代价高得惊人,因为 Optional 有两个装箱级别,而不是零。因此,类库设计人员认为为基本类型 int、long 和 double 提供类似 `Option<T>` 是合适的。这些 Option 是 `OptionalInt`、`OptionalLong` 和 `OptionalDouble`。它们包含 `Optional<T>` 上的大多数方法,但不是所有方法。因此,除了「次要基本类型(minor primitive types)」Boolean,Byte,Character,Short 和 Float 之外,**永远不应该返回装箱的基本类型的 Optional**。
|
||||
|
||||
到目前为止,我们已经讨论了返回 Optional 并在返回后处理它们的方法。我们还没有讨论其他可能的用法,这是因为大多数其他 Optional 的用法都是可疑的。例如,永远不要将 Optional 用作映射值。如果这样做,则有两种方法可以表示键(key)在映射中逻辑上的缺失:键要么不在映射中,要么存在的话映射到一个空的 Optional。这反映了不必要的复杂性,很有可能导致混淆和错误。更通俗地说,在集合或数组中使用 Optional 的键、值或元素几乎都是不合适的。
|
||||
|
||||
|
@ -4,15 +4,15 @@
|
||||
|
||||
虽然文档注释约定不是 Java 语言的正式一部分,但它们构成了每个 Java 程序员都应该知道的事实上的 API。「如何编写文档注释(How to Write Doc Comments)」的网页[Javadoc-guide] 中介绍了这些约定。 虽然自 Java 4 发布以来该页面尚未更新,但它仍然是一个非常宝贵的资源。 Java 9 中添加了一个重要的文档标签,`{@ index}`; Java 8 中有一个,`{@implSpec}`;Java 5 中有两个,`{@literal}` 和 `{@code}`。 上述网页中缺少这些标签的介绍,但在此条目中进行讨论。
|
||||
|
||||
**要正确地记录 API,必须在每个导出的类、接口、构造方法、方法和属性声明之前加上文档注释**。如果一个类是可序列化的,还应该记录它的序列化形式 (条目 87)。在没有文档注释的情况下,Javadoc 可以做的最好的事情是将声明重现为受影响的 API 元素的唯一文档。使用缺少文档注释的 API 是令人沮丧和容易出错的。公共类不应该使用默认构造方法,因为无法为它们提供文档注释。要编写可维护的代码,还应该为大多数未导出的类、接口、构造方法、方法和属性编写文档注释,尽管这些注释不需要像导出 API 元素那样完整。
|
||||
**要正确地记录 API,必须在每个导出的类、接口、构造方法、方法和属性声明之前加上文档注释**。如果一个类是可序列化的,还应该记录它的序列化形式 (详见第 87 条)。在没有文档注释的情况下,Javadoc 可以做的最好的事情是将声明重现为受影响的 API 元素的唯一文档。使用缺少文档注释的 API 是令人沮丧和容易出错的。公共类不应该使用默认构造方法,因为无法为它们提供文档注释。要编写可维护的代码,还应该为大多数未导出的类、接口、构造方法、方法和属性编写文档注释,尽管这些注释不需要像导出 API 元素那样完整。
|
||||
|
||||
方法的文档注释应该简洁地描述方法与其客户端之间的契约。除了为继承而设计的类中的方法 (详见第 19 条)之外,契约应该说明方法做什么,而不是它如何工作的。文档注释应该列举方法的所有前置条件 (这些条件必须为真,以便客户端调用它们),以及后置条件 (这些条件是在调用成功完成后才为真)。通常,对于未检查的异常,前置条件由 `@throw` 标签隐式地描述;每个未检查异常对应于一个先决条件违反( precondition violation)。此外,可以在受影响的参数的 `@param` 标签中指定前置条件。
|
||||
方法的文档注释应该简洁地描述方法与其客户端之间的契约。除了为继承而设计的类中的方法 (详见第 19 条)之外,契约应该说明方法做什么,而不是它如何工作的。文档注释应该列举方法的所有前置条件 (这些条件必须为真,以便客户端调用它们),以及后置条件(这些条件是在调用成功完成后才为真)。通常,对于未检查的异常,前置条件由 `@throw` 标签隐式地描述;每个未检查异常对应于一个先决条件违反( precondition violation)。此外,可以在受影响的参数的 `@param` 标签中指定前置条件。
|
||||
|
||||
除了前置条件和后置条件之外,方法还应在文档中记录它的副作用(side effort)。 副作用是系统状态的可观察到的变化,这对于实现后置条件而言显然不是必需的。 例如,如果方法启动后台线程,则文档应记录它。
|
||||
|
||||
完整地描述方法的契约,文档注释应该为每个参数都有一个 `@param` 标签,一个 `@return` 标签 (除非方法有 void 返回类型),以及一个 `@throw` 标签 (无论是检查异常还是非检查异常)(条目 74)。如果 `@return` 标签中的文本与方法的描述相同,则可以忽略它,这取决于你所遵循的编码标准。
|
||||
完整地描述方法的契约,文档注释应该为每个参数都有一个 `@param` 标签,一个 `@return` 标签 (除非方法有 void 返回类型),以及一个 `@throw` 标签(无论是检查异常还是非检查异常)(详见第 74 条)。如果 `@return` 标签中的文本与方法的描述相同,则可以忽略它,这取决于你所遵循的编码标准。
|
||||
|
||||
按照惯例,`@param` 或 `@retur` 标签后面的文本应该是一个名词短语,描述参数或返回值所表示的值。 很少使用算术表达式代替名词短语; 请参阅 `BigInteger` 的示例。`@throw` 标签后面的文本应该包含单词“if”,后面跟着一个描述抛出异常的条件的子句。按照惯例,`@param` 、`@return` 或 `@throw` 标签后面的短语或子句不以句号结束。以下的文档注释说明了所有这些约定:
|
||||
按照惯例,`@param` 或 `@retur` 标签后面的文本应该是一个名词短语,描述参数或返回值所表示的值。 很少使用算术表达式代替名词短语; 请参阅 `BigInteger` 的示例。`@throw` 标签后面的文本应该包含单词「if」,后面跟着一个描述抛出异常的条件的子句。按照惯例,`@param` 、`@return` 或 `@throw` 标签后面的短语或子句不以句号结束。以下的文档注释说明了所有这些约定:
|
||||
|
||||
```java
|
||||
/**
|
||||
@ -33,11 +33,11 @@ E get(int index);
|
||||
|
||||
请注意在此文档注释(`<p>`和`<i>`)中使用 HTML 标记。 Javadoc 实用工具将文档注释转换为 HTML,文档注释中的任意 HTML 元素最终都会生成 HTML 文档。 有时候,程序员甚至会在他们的文档注释中嵌入 HTML 表格,尽管这种情况很少见。
|
||||
|
||||
还要注意在`@throw`子句中的代码片段周围使用 Javadoc 的 `{@code}`标签。这个标签有两个目的:它使代码片段以代码字体形式呈现,并且它抑制了代码片段中 HTML 标记和嵌套 Javadoc 标记的处理。后一个属性允许我们在代码片段中使用小于号 (<),即使它是一个 HTML 元字符。要在文档注释中包含多行代码示例,请使用包装在 HTML `<pre>`标记中的 Javadoc`{@code}`标签。换句话说,在代码示例前面加上字符`<pre>{@code,然后在代码后面加上}</pre>`。这保留了代码中的换行符,并消除了转义 HTML 元字符的需要,但不需要转义 at 符号 (@),如果代码示例使用注释,则必须转义 at 符号 (@)。
|
||||
还要注意在`@throw`子句中的代码片段周围使用 Javadoc 的 `{@code}`标签。这个标签有两个目的:它使代码片段以代码字体形式呈现,并且它抑制了代码片段中 HTML 标记和嵌套 Javadoc 标记的处理。后一个属性允许我们在代码片段中使用小于号(<),即使它是一个 HTML 元字符。要在文档注释中包含多行代码示例,请使用包装在 HTML `<pre>`标记中的 Javadoc`{@code}`标签。换句话说,在代码示例前面加上字符`<pre>{@code,然后在代码后面加上}</pre>`。这保留了代码中的换行符,并消除了转义 HTML 元字符的需要,但不需要转义 at 符号(@),如果代码示例使用注释,则必须转义 at 符号(@)。
|
||||
|
||||
最后,请注意文档注释中使用的单词「this list」。按照惯例,「this」指的是在实例方法的文档注释中,指向方法调用所在的对象。
|
||||
|
||||
正如条目 15 中提到的,当你为继承设计一个类时,必须记录它的自用模式( self-use patterns),以便程序员知道重写它的方法的语义。这些自用模式应该使用在 Java 8 中添加的`@implSpec` 标签来文档记录。回想一下,普通的问问昂注释描述了方法与其客户端之间的契约;相反,`@implSpec` 注释描述了方法与其子类之间的契约,如果它继承了方法或通过 super 调用方法,那么允许子类依赖于实现行为。下面是实际应用中的实例:
|
||||
正如条目 15 中提到的,当你为继承设计一个类时,必须记录它的自用模式(self-use patterns),以便程序员知道重写它的方法的语义。这些自用模式应该使用在 Java 8 中添加的`@implSpec` 标签来文档记录。回想一下,普通的问问昂注释描述了方法与其客户端之间的契约;相反,`@implSpec` 注释描述了方法与其子类之间的契约,如果它继承了方法或通过 super 调用方法,那么允许子类依赖于实现行为。下面是实际应用中的实例:
|
||||
|
||||
```java
|
||||
/**
|
||||
@ -61,9 +61,9 @@ public boolean isEmpty() { ... }
|
||||
|
||||
它会生成文档:「A geometric series converges if |r| < 1.」。`{@literal}`标签可能只放在小于号的位置,而不是整个不等式,并且生成的文档是一样的,但是文档注释在源代码中的可读性较差。 这说明了**文档注释在源代码和生成的文档中都应该是可读的通用原则**。 如果无法实现这两者,则生成的文档的可读性要胜过在源代码中的可读性。
|
||||
|
||||
每个文档注释的第一个「句子」(如下定义)成为注释所在元素的概要描述。 例如,第 255 页上的文档注释中的概要描述为:“返回此列表中指定位置的元素”。概要描述必须独立描述其概述元素的功能。 为避免混淆,**类或接口中的两个成员或构造方法不应具有相同的概要描述**。 要特别注意重载方法,为此通常使用相同的第一句话是自然的(但在文档注释中是不可接受的)。
|
||||
每个文档注释的第一个「句子」(如下定义)成为注释所在元素的概要描述。 例如,第 255 页上的文档注释中的概要描述为:「返回此列表中指定位置的元素」。概要描述必须独立描述其概述元素的功能。 为避免混淆,**类或接口中的两个成员或构造方法不应具有相同的概要描述**。 要特别注意重载方法,为此通常使用相同的第一句话是自然的(但在文档注释中是不可接受的)。
|
||||
|
||||
请小心,如果预期的概要描述包含句点,因为句点可能会提前终止描述。例如,以 “A college degree, such as B.S., M.S. or Ph.D.” 会导致概要描述为 “A college degree, such as B.S., M.S”。问题在于概要描述在第一个句点结束,然后是空格、制表符或行结束符 (或第一个块标签处)[Javadoc-ref]。这里是缩写 “M.S.” 中的第二个句号后面跟着一个空格。最好的解决方案是用`{@literal}`标签来包围不愉快的句点和任何相关的文本,这样源代码中的句点后面就不会有空格了:
|
||||
请小心,如果预期的概要描述包含句点,因为句点可能会提前终止描述。例如,以「A college degree, such as B.S., M.S. or Ph.D.」 会导致概要描述为「A college degree, such as B.S., M.S」。问题在于概要描述在第一个句点结束,然后是空格、制表符或行结束符(或第一个块标签处)[Javadoc-ref]。这里是缩写「M.S.」 中的第二个句号后面跟着一个空格。最好的解决方案是用`{@literal}`标签来包围不愉快的句点和任何相关的文本,这样源代码中的句点后面就不会有空格了:
|
||||
|
||||
```java
|
||||
/**
|
||||
@ -77,7 +77,7 @@ public class Degree { ... }
|
||||
- `ArrayList(int initialCapacity)` —— 构造具有指定初始容量的空列表。
|
||||
- `Collection.size()` —— 返回此集合中的元素个数。
|
||||
|
||||
如这些例子所示,使用第三人称陈述句时态 (“returns the number”) 而不是第二人称祈使句 (“return the number”)。
|
||||
如这些例子所示,使用第三人称陈述句时态 (“returns the number”)而不是第二人称祈使句(“return the number”)。
|
||||
|
||||
对于类,接口和属性,概要描述应该是描述由类或接口的实例或属性本身表示的事物的名词短语。 例如:
|
||||
|
||||
@ -145,7 +145,7 @@ public @interface ExceptionTest {
|
||||
}
|
||||
```
|
||||
|
||||
包级别文档注释应放在名为 package-info.java 的文件中。 除了这些注释之外,package-info.java 还必须包含一个包声明,并且可以在此声明中包含注解。 同样,如果使用模块化系统(条目 15),则应将模块级别注释放在 module-info.java 文件中。
|
||||
包级别文档注释应放在名为 package-info.java 的文件中。 除了这些注释之外,package-info.java 还必须包含一个包声明,并且可以在此声明中包含注解。 同样,如果使用模块化系统(详见第 15 条),则应将模块级别注释放在 module-info.java 文件中。
|
||||
|
||||
在文档中经常忽略的 API 的两个方面,分别是线程安全性和可序列化性。**无论类或静态方法是否线程安全,都应该在文档中描述其线程安全级别**,如条目 82 中所述。如果一个类是可序列化的,应该记录它的序列化形式,如条目 87 中所述。
|
||||
|
||||
|
@ -8,11 +8,11 @@
|
||||
|
||||
过早地声明局部变量可能导致其作用域不仅过早开始而且结束太晚。 局部变量的作用域从声明它的位置延伸到封闭块的末尾。 如果变量在使用它的封闭块之外声明,则在程序退出该封闭块后它仍然可见。如果在其预定用途区域之前或之后意外使用变量,则后果可能是灾难性的。
|
||||
|
||||
**几乎每个局部变量声明都应该包含一个初始化器**。如果还没有足够的信息来合理地初始化一个变量,那么应该推迟声明,直到认为可以这样做。这个规则的一个例外是 try-catch 语句。如果一个变量被初始化为一个表达式,该表达式的计算结果可以抛出一个已检查的异常,那么该变量必须在 try 块中初始化 (除非所包含的方法可以传播异常)。如果该值必须在 try 块之外使用,那么它必须在 try 块之前声明,此时它还不能被“合理地初始化”。例如,参照条目 65 中的示例。
|
||||
**几乎每个局部变量声明都应该包含一个初始化器**。如果还没有足够的信息来合理地初始化一个变量,那么应该推迟声明,直到认为可以这样做。这个规则的一个例外是 try-catch 语句。如果一个变量被初始化为一个表达式,该表达式的计算结果可以抛出一个已检查的异常,那么该变量必须在 try 块中初始化(除非所包含的方法可以传播异常)。如果该值必须在 try 块之外使用,那么它必须在 try 块之前声明,此时它还不能被「合理地初始化」。例如,参照条目 65 中的示例。
|
||||
|
||||
循环提供了一个特殊的机会来最小化变量的作用域。传统形式的 for 循环和 for-each 形式都允许声明循环变量,将其作用域限制在需要它们的确切区域。 (该区域由循环体和 for 关键字与正文之间的括号中的代码组成)。因此,如果循环终止后不需要循环变量的内容,那么**优先选择 for 循环而不是 while 循环**。
|
||||
|
||||
例如,下面是遍历集合的首选方式(条目 58):
|
||||
例如,下面是遍历集合的首选方式(详见第 58 条):
|
||||
|
||||
```java
|
||||
// Preferred idiom for iterating over a collection or array
|
||||
|
@ -21,7 +21,7 @@ for (int i = 0; i < a.length; i++) {
|
||||
|
||||
这些习惯用法比 while 循环更好(详见第 57 条),但是它们并不完美。迭代器和索引变量都很混乱——你只需要元素而已。此外,它们也代表了出错的机会。迭代器在每个循环中出现三次,索引变量出现四次,这使你有很多机会使用错误的变量。如果这样做,就不能保证编译器会发现到问题。最后,这两个循环非常不同,引起了对容器类型的不必要注意,并且增加了更改该类型的小麻烦。
|
||||
|
||||
for-each 循环 (官方称为“增强的 for 语句”) 解决了所有这些问题。它通过隐藏迭代器或索引变量来消除混乱和出错的机会。由此产生的习惯用法同样适用于集合和数组,从而简化了将容器的实现类型从一种转换为另一种的过程:
|
||||
for-each 循环(官方称为「增强的 for 语句」)解决了所有这些问题。它通过隐藏迭代器或索引变量来消除混乱和出错的机会。由此产生的习惯用法同样适用于集合和数组,从而简化了将容器的实现类型从一种转换为另一种的过程:
|
||||
|
||||
```java
|
||||
// The preferred idiom for iterating over collections and arrays
|
||||
@ -30,7 +30,7 @@ for (Element e : elements) {
|
||||
}
|
||||
```
|
||||
|
||||
当看到冒号 (:) 时,请将其读作“in”。因此,上面的循环读作“对于元素 elements 中的每个元素 e”。“使用 for-each 循环不会降低性能,即使对于数组也是如此:它们生成的代码本质上与手工编写的代码相同。
|
||||
当看到冒号(:) 时,请将其读作「in」。因此,上面的循环读作「对于元素 elements 中的每个元素 e」。使用 for-each 循环不会降低性能,即使对于数组也是如此:它们生成的代码本质上与手工编写的代码相同。
|
||||
|
||||
当涉及到嵌套迭代时,for-each 循环相对于传统 for 循环的优势甚至更大。下面是人们在进行嵌套迭代时经常犯的一个错误:
|
||||
|
||||
@ -49,7 +49,7 @@ for (Iterator<Suit> i = suits.iterator(); i.hasNext(); )
|
||||
deck.add(new Card(i.next(), j.next()));
|
||||
```
|
||||
|
||||
如果没有发现这个 bug,也不必感到难过。许多专业程序员都曾犯过这样或那样的错误。问题是,对于外部集合 (suit),next 方法在迭代器上调用了太多次。它应该从外部循环调用,因此每花色调用一次,但它是从内部循环调用的,因此每一张牌调用一次。在 suit 用完之后,循环抛出 `NoSuchElementException` 异常。
|
||||
如果没有发现这个 bug,也不必感到难过。许多专业程序员都曾犯过这样或那样的错误。问题是,对于外部集合(suit),next 方法在迭代器上调用了太多次。它应该从外部循环调用,因此每花色调用一次,但它是从内部循环调用的,因此每一张牌调用一次。在 suit 用完之后,循环抛出 `NoSuchElementException` 异常。
|
||||
|
||||
如果你真的不走运,外部集合的大小是内部集合大小的倍数——也许它们是相同的集合——循环将正常终止,但它不会做你想要的。 例如,考虑这种错误的尝试,打印一对骰子的所有可能的掷法:
|
||||
|
||||
@ -64,7 +64,7 @@ for (Iterator<Face> i = faces.iterator(); i.hasNext(); )
|
||||
System.out.println(i.next() + " " + j.next());
|
||||
```
|
||||
|
||||
该程序不会抛出异常,但它只打印 6 个重复的组合 (从“ONE ONE”到“SIX SIX”),而不是预期的 36 个组合。
|
||||
该程序不会抛出异常,但它只打印 6 个重复的组合(从“ONE ONE”到“SIX SIX”),而不是预期的 36 个组合。
|
||||
|
||||
要修复例子中的错误,必须在外部循环的作用域内添加一个变量来保存外部元素:
|
||||
|
||||
|
@ -50,7 +50,7 @@ public static void main(String[] args) throws IOException {
|
||||
|
||||
库太大,无法学习所有文档 [Java9-api],但是 **每个程序员都应该熟悉 java.lang、java.util 和 java.io 的基础知识及其子包。** 其他库的知识可以根据需要获得。概述库中的工具超出了本项目的范围,这些工具多年来已经发展得非常庞大。
|
||||
|
||||
有几个图书馆值得一提。collections 框架和 streams 库(可参看 Item 45-48)应该是每个程序员的基本工具包的一部分,`java.util.concurrent` 中的并发实用程序也应该是其中的一部分。这个包既包含高级的并发工具来简化多线程的编程任务,还包含低级别的并发基本类型,允许专家们自己编写更高级的并发抽象。`java.util.concurrent` 的高级部分,在第 80 条和第 81 条中讨论。
|
||||
有几个图书馆值得一提。collections 框架和 streams 库(详见第 45 到 48 条)应该是每个程序员的基本工具包的一部分,`java.util.concurrent` 中的并发实用程序也应该是其中的一部分。这个包既包含高级的并发工具来简化多线程的编程任务,还包含低级别的并发基本类型,允许专家们自己编写更高级的并发抽象。`java.util.concurrent` 的高级部分,在第 80 条和第 81 条中讨论。
|
||||
|
||||
有时,类库工具可能无法满足你的需求。你的需求越专门化,发生这种情况的可能性就越大。虽然你的第一个思路应该是使用这些库,但是如果你已经了解了它们在某些领域提供的功能,而这些功能不能满足你的需求,那么可以使用另一种实现。任何有限的库集所提供的功能总是存在漏洞。如果你在 Java 平台库中找不到你需要的东西,你的下一个选择应该是寻找高质量的第三方库,比如谷歌的优秀的开源 Guava 库 [Guava]。如果你无法在任何适当的库中找到所需的功能,你可能别无选择,只能自己实现它。
|
||||
|
||||
|
@ -31,7 +31,7 @@ for ( Mountain m : range )
|
||||
|
||||
这个例子的教训很简单:顾名思义,**异常应该只用于异常的情况下;他们永远不应该用于正常的程序控制流程。** 一般的,应该优先使用标准的、容易理解的模式,而不是那些声称可以提供更好性能的、弄巧成拙的方法。即使真的能够改进性能,面对平台的不断改进,这种模型的性能优势也不可能一直保持。然而这种过度聪明的模式带来的微妙 Bug 和维护的痛苦将依旧存在。
|
||||
|
||||
这条原则对于 API 设计也有启发**。设计良好的 API 不应该强迫它的客户端为了正常的控制流程而使用异常。**如果类中具有“状态相关”(state-dependent)的方法,即只有在特定的不可预知的条件下才可以被调用的方法,这个类往往也应该具有一个单独的“状态测试”(state-testing)方法,即表明是否可以调用这个状态相关的方法。比如 Iterator 接口含有状态相关的 next 方法,以及相应的状态测试方法 hasNext。这使得利用传统的 for 循环(以及 for-each 循环,在内部使用了 hasNext 方法)对集合进行迭代的标准模式成为可能。
|
||||
这条原则对于 API 设计也有启发**。设计良好的 API 不应该强迫它的客户端为了正常的控制流程而使用异常。**如果类中具有「状态相关」(state-dependent)的方法,即只有在特定的不可预知的条件下才可以被调用的方法,这个类往往也应该具有一个单独的「状态测试」(state-testing)方法,即表明是否可以调用这个状态相关的方法。比如 Iterator 接口含有状态相关的 next 方法,以及相应的状态测试方法 hasNext。这使得利用传统的 for 循环(以及 for-each 循环,在内部使用了 hasNext 方法)对集合进行迭代的标准模式成为可能。
|
||||
|
||||
```java
|
||||
for ( Iterator<Foo> i = collection.iterator(); i.hasNext(); ){
|
||||
@ -57,8 +57,8 @@ try {
|
||||
|
||||
这应该非常类似于本条目刚开始时对数据进行迭代的例子。除了代码繁琐令人误解之外,这个基于异常的模式可能执行起来也比标准模式更差,并且还可能掩盖系统中其他不相关部分的 Bug。
|
||||
|
||||
另外一种提供单独状态测试的做法是,如果“状态相关”方法无法执行想要的计算,就可以让它返回一个零长度的 optional 值(详见第 55 条),或者返回一个可被识别的返回值,比如 null。
|
||||
另外一种提供单独状态测试的做法是,如果「状态相关」方法无法执行想要的计算,就可以让它返回一个零长度的 optional 值(详见第 55 条),或者返回一个可被识别的返回值,比如 null。
|
||||
|
||||
对于“状态测试方法”和“optional 返回值或者可识别的返回值”这两种做法,有些指导原则可以帮助你在两者之间做出选择。如果对象将在缺少外部同步的情况下被并发访问,或者可被外界改变状态,就必须使用“optional 返回值或者可识别的返回值”,因为在调用“状态测试”方法和调用对应的“状态相关”方法的时间间隔之中,对象的状态有可能发生变化。如果单独的“状态测试”方法必须重复“状态相关”方法的工作,从性能的角度考虑,就必须使用可被识别的返回值。如果其他方面都是等同的,那么“状态测试”方法则优于可被识别的返回值。他提供了相对更高的可读性,对于使用不当的情形可能更加易于检测和改正:如果忘了去调用状态测试方法,状态相关的方法就会抛出异常,使得这个 Bug 变得很明显;如果忘了去检查可识别的返回值,这个 Bug 就很难被发现。optional 返回值不会有这方面的问题。
|
||||
对于「状态测试方法」和「optional 返回值或者可识别的返回值」这两种做法,有些指导原则可以帮助你在两者之间做出选择。如果对象将在缺少外部同步的情况下被并发访问,或者可被外界改变状态,就必须使用「optional 返回值或者可识别的返回值」,因为在调用「状态测试」方法和调用对应的「状态相关」方法的时间间隔之中,对象的状态有可能发生变化。如果单独的「状态测试」方法必须重复「状态相关」方法的工作,从性能的角度考虑,就必须使用可被识别的返回值。如果其他方面都是等同的,那么「状态测试」方法则优于可被识别的返回值。他提供了相对更高的可读性,对于使用不当的情形可能更加易于检测和改正:如果忘了去调用状态测试方法,状态相关的方法就会抛出异常,使得这个 Bug 变得很明显;如果忘了去检查可识别的返回值,这个 Bug 就很难被发现。optional 返回值不会有这方面的问题。
|
||||
|
||||
总而言之,异常是为了在异常情况下被设计和使用的。不要将它们勇于普通的控制流程,也不要编写迫使它们这么做的 API。
|
||||
|
@ -16,7 +16,7 @@
|
||||
|
||||
要想定义一个不是 `Exception`、`RuntimeException` 或者 `Error` 子类的 throwable,这也是有可能的。JLS 并没有直接规定这样的 throwable,而是隐式的指定了:从行为意义上讲,他们等同于普通的受检异常(即 `Exception` 的子类,但不是 `RuntimeException` 的子类)。那么什么时候应该使用这样的 throwable?一句话,永远也不会用到。它与普通的受检异常相比没有任何益处,还会困扰 API 的使用者。
|
||||
|
||||
API 的设计者往往会忘记,异常也是一个完全意义上的对象,可是在它上面定义任何的方法。这些方法的主要用途是捕获异常的代码提供额外信息,特别是关于引发这个异常条件的信息。如果没有这样的方法,程序员必须要懂的如何解析“该异常的字符串表示法”,以便获得这些额外信息。这是极为不好的做法(详见 12 条)。类很少会指定它们的字符串表示法中的细节,因此对于不同的实现及不同的版本,字符串表示法也会大相径庭。由此可见,“解析异常的字符串表示法”的代码可能是不可移植的,也是非常脆弱的。
|
||||
API 的设计者往往会忘记,异常也是一个完全意义上的对象,可是在它上面定义任何的方法。这些方法的主要用途是捕获异常的代码提供额外信息,特别是关于引发这个异常条件的信息。如果没有这样的方法,程序员必须要懂的如何解析「该异常的字符串表示法」,以便获得这些额外信息。这是极为不好的做法(详见 12 条)。类很少会指定它们的字符串表示法中的细节,因此对于不同的实现及不同的版本,字符串表示法也会大相径庭。由此可见,“解析异常的字符串表示法”的代码可能是不可移植的,也是非常脆弱的。
|
||||
|
||||
因为受检异常往往指明了可恢复的条件,所以对于这样的异常,提供一些辅助方法尤其重要,通过这种方法调用者可以获得一些有助于程序恢复的信息。例如,假设因为用户资金不足,当他企图购买一张礼品卡时导致失败,于是抛出受检异常。这个异常应该提供一个访问方法,以便允许客户查询所缺的费用金额,使得使用者可以将这个数值传递给用户。关于这个主题的更多详情,参见 75 条。
|
||||
|
||||
|
@ -25,7 +25,7 @@
|
||||
|
||||
除受检异常最容易的方法是,返回所要的结果类型的一个 optional(详见第 55 条)。这个方法不抛出受检异常,而只是返回一个零长度的 optional。这种方法的缺点是,方法无法返回任何额外的信息,来详细说明它无法执行你想要的计算。相反,异常则具有描述性的类型,并且能够导出方法,以提供额外的信息(详见第 70 条)。
|
||||
|
||||
“把受检异常变成未受检异常”的一种方法是,把这个抛出异常的方法分成两个方法,其中第一个方法返回一个 boolean 值,表明是否应该抛出异常。这种 API 重构,把下面的调用序列:
|
||||
「把受检异常变成未受检异常」的一种方法是,把这个抛出异常的方法分成两个方法,其中第一个方法返回一个 boolean 值,表明是否应该抛出异常。这种 API 重构,把下面的调用序列:
|
||||
|
||||
```java
|
||||
/* Invocation with checked exception */
|
||||
@ -53,6 +53,6 @@ if ( obj.actionPermitted( args ) ) {
|
||||
obj.action(args);
|
||||
```
|
||||
|
||||
如果你怀疑这个简单的调用序列是否符合要求,这个 API 重构可能就是恰当的。这样重构之后的 API 在本质上等同于第 69 条中的“状态测试方法”,并且同样的告诫依然适用:如果对象将在缺少外部同步的情况下被并发访问,或者可被外界改变状态,这种重构就是不恰当的,因为在 actionPermitted 和 action 这两个调用的时间间隔之中,对象的状态有可能会发生变化。如果单独的 actionPermitted 方法必须重复 action 方法的工作,出于性能的考虑,这种 API 重构就不值得去做。
|
||||
如果你怀疑这个简单的调用序列是否符合要求,这个 API 重构可能就是恰当的。这样重构之后的 API 在本质上等同于第 69 条中的「状态测试方法」,并且同样的告诫依然适用:如果对象将在缺少外部同步的情况下被并发访问,或者可被外界改变状态,这种重构就是不恰当的,因为在 actionPermitted 和 action 这两个调用的时间间隔之中,对象的状态有可能会发生变化。如果单独的 actionPermitted 方法必须重复 action 方法的工作,出于性能的考虑,这种 API 重构就不值得去做。
|
||||
|
||||
总而言之,在谨慎使用的前提之下,受检异常可以提升程序的可读性;如果过度使用,将会使 API 使用起来非常痛苦。如果调用者无法恢复失败,就应该抛出未受检异常。如果可以恢复,并且想要迫使调用者处理异常的条件,首选应该返回一个 optional 值。当且仅当万一失败时,这些无法提供足够的信息,才应该抛出受检异常。
|
@ -28,7 +28,7 @@
|
||||
| ConcurrentModificationException | 在禁止并发修改的情况下,检测到对象的并发修改 |
|
||||
| UnsupportedOperationException | 对象不支持用户请求的方法 |
|
||||
|
||||
然这些都是 Java 平台类库中迄今为止最常被重用的异常,但是,在条件许可的情况下,其他的异常也可以被重用。例如,如果要实现诸如复数或者有理数之类的算术对象,也可以重用 `ArithmeticException` 和 `NumberFormatException`。如果某个异常能够满足你的需要,就不要犹豫,使用就是,不过一定要确保抛出异常的条件与该异常的文档中描述的条件一致。这种重用必须建立在语义的基础上,而不是建立在名称的基础之上。而且,如果希望稍微增加更多的失败一捕获(failure-capture)信息(详见第 75 条),可以放心地子类化标准异常,但要记住异常是可序列化的(详见第 12 章)。这也正是“如果没有非常正当的理由,千万不要自己编写异常类”的原因。
|
||||
然这些都是 Java 平台类库中迄今为止最常被重用的异常,但是,在条件许可的情况下,其他的异常也可以被重用。例如,如果要实现诸如复数或者有理数之类的算术对象,也可以重用 `ArithmeticException` 和 `NumberFormatException`。如果某个异常能够满足你的需要,就不要犹豫,使用就是,不过一定要确保抛出异常的条件与该异常的文档中描述的条件一致。这种重用必须建立在语义的基础上,而不是建立在名称的基础之上。而且,如果希望稍微增加更多的失败一捕获(failure-capture)信息(详见第 75 条),可以放心地子类化标准异常,但要记住异常是可序列化的(详见第 12 章)。这也正是「如果没有非常正当的理由,千万不要自己编写异常类」的原因。
|
||||
|
||||
选择重用哪一种异常并非总是那么精确,因为上表中的“使用场合”并不是相互排斥的比如,以表示一副纸牌的对象为例。假设有一个处理发牌操作的方法,它的参数是发一手牌的纸牌张数。假设调用者在这个参数中传递的值大于整副纸牌的剩余张数。这种情形既可以被解释为 `IllegalArgumentException`( handSize 参数的值太大),也可以被解释为 `IllegalStateException`(纸牌对象包含的纸牌太少)。在这种情况下,**如果没有可用的参数值,就抛出 `IllegalStateException`,否则就抛出 `IllegalArgumentException`。**
|
||||
|
||||
|
@ -8,12 +8,12 @@
|
||||
/* Exception Translation */
|
||||
try {
|
||||
... /* Use lower-level abstraction to do our bidding */
|
||||
} catch ( LowerLevelException e ) {
|
||||
} catch (LowerLevelException e) {
|
||||
throw new HigherLevelException(...);
|
||||
}
|
||||
```
|
||||
|
||||
下面的异常转译例子取自于 `AbstractSequentialList` 类,该类是 List 接口的一个骨架实现 (skeletal implementation),详见第 20 条。在这个例子中,按照`List<E>`接口中 get 方法的规范要求,异常转译是必需的:
|
||||
下面的异常转译例子取自于 `AbstractSequentialList` 类,该类是 List 接口的一个骨架实现(skeletal implementation),详见第 20 条。在这个例子中,按照`List<E>`接口中 get 方法的规范要求,异常转译是必需的:
|
||||
|
||||
```java
|
||||
/**
|
||||
@ -21,12 +21,12 @@ try {
|
||||
* @throws IndexOutOfBoundsException if the index is out of range
|
||||
* ({@code index < 0 || index >= size()}).
|
||||
*/
|
||||
public E get( int index ) {
|
||||
ListIterator<E> i = listIterator( index );
|
||||
public E get(int index) {
|
||||
ListIterator<E> i = listIterator(index);
|
||||
try {
|
||||
return(i.next() );
|
||||
} catch ( NoSuchElementException e ) {
|
||||
throw new IndexOutOfBoundsException( "Index: " + index );
|
||||
} catch (NoSuchElementException e) {
|
||||
throw new IndexOutOfBoundsException("Index: " + index);
|
||||
}
|
||||
}
|
||||
```
|
||||
@ -42,7 +42,7 @@ try {
|
||||
}
|
||||
```
|
||||
|
||||
高层异常的构造器将原因传到支持链 (chaining-aware) 的超级构造器,因此它最终将被传给 Throw able 的其中一个运行异常链的构造器,例如 Throwable(Throwable) :
|
||||
高层异常的构造器将原因传到支持链(chaining-aware)的超级构造器,因此它最终将被传给 Throwable 的其中一个运行异常链的构造器,例如 Throwable(Throwable) :
|
||||
|
||||
```java
|
||||
/* Exception with chaining-aware constructor */
|
||||
|
@ -2,16 +2,16 @@
|
||||
|
||||
描述一个方法所抛出的异常,是正确使用这个方法时所需文档的重要组成部分。因此,花点时间仔细地为每个方法抛出的异常建立文档是特别重要的。
|
||||
|
||||
**始终要单独地声明受检异常,** 并且利用 Javadoc 的 `@throws` 标签, **准确地记录下抛出每个异常的条件。** 如果一个公有方法可能抛出多个异常类,则不要使用“快捷方式”声明它会抛出这些异常类的某个超类。永远不要声明一个公有方法直接“ throws Exception”,或者更糟糕的是声明它直接“ throws Throwable ”,这是非常极端的例子。这样的声明不仅没有为程序员提供关于“这个方法能够抛出哪些异常”的任何指导信息,而且大大地妨碍了该方法的使用,因为它实际上掩盖了该方法在同样的执行环境下可能抛出的任何其他异常。这条建议有一个例外,就是 main 方法它可以被安全地声明抛出 Exception ,因为它只通过虚拟机调用。
|
||||
**始终要单独地声明受检异常,** 并且利用 Javadoc 的 `@throws` 标签, **准确地记录下抛出每个异常的条件。** 如果一个公有方法可能抛出多个异常类,则不要使用“快捷方式”声明它会抛出这些异常类的某个超类。永远不要声明一个公有方法直接「throws Exception」,或者更糟糕的是声明它直接「throws Throwable」,这是非常极端的例子。这样的声明不仅没有为程序员提供关于“这个方法能够抛出哪些异常”的任何指导信息,而且大大地妨碍了该方法的使用,因为它实际上掩盖了该方法在同样的执行环境下可能抛出的任何其他异常。这条建议有一个例外,就是 main 方法它可以被安全地声明抛出 Exception ,因为它只通过虚拟机调用。
|
||||
|
||||
虽然 Java 语言本身并没有要求程序员为一个方法声明它可能会抛出的未受检异常,但是,如同受检异常一样,仔细地为它们建立文档是非常明智的。未受检异常通常代表编程上的错误(详见第 70 条),让程序员了解所有这些错误都有助于帮助他们避免犯同样的错误。对于方法可能抛出的未受检异常,如果将这些异常信息很好地组织成列表文档,就可以有效地描述出这个方法被成功执行的前提条件。每个方法的文档应该描述它的前提条件(详见第 56 条),这是很重要的,在文档中记录下未受检异常是满足前提条件的最佳做法。
|
||||
|
||||
对于接口中的方法,在文档中记录下它可能抛出的未受检异常显得尤为重要。这份文档构成了该接口的通用约定(general contract )的一部分,它指定了该接口的多个实现必须遵循的公共行为。
|
||||
对于接口中的方法,在文档中记录下它可能抛出的未受检异常显得尤为重要。这份文档构成了该接口的通用约定(general contract)的一部分,它指定了该接口的多个实现必须遵循的公共行为。
|
||||
|
||||
**使用 Javadoc 的 `@throws` 标签记录下一个方法可能抛出的每个未受检异常,但是不要使用 throws 关键字将未受检的异常包含在方法的声明中。** 使用 API 的程序员必须知道哪些异常是需要受检的,哪些是不需要受检的,因为他们有责任区分这两种情形。当缺少由 throws 声明产生的方法标头时,由 Javadoc 的 `@throws` 标签所产生的文档就会提供明显的提示信息,以帮助程序员区分受检异常和未受检异常。
|
||||
|
||||
应该注意的是,为每个方法可能抛出的所有未受检异常建立文档是很理想的,但是在实践中并非总能做到这一点。当类被修订之后,如果有个导出方法被修改了,它将会抛出额外的未受检异常,这不算违反源代码或者二进制兼容性。假设一个类调用了另一个独立编写的类中的方法。第一个类的编写者可能会为每个方法抛出的未受检异常仔细地建立文档,但是,如果第二个类被修订了,抛出了额外的未受检异常,很有可能第一个类(它并没有被修订)就会把新的未受检异常传播出去,尽管它并没有声明这些异常。
|
||||
|
||||
**如果一个类中的许多方法出于同样的原因而抛出同一个异常,在该类的文档注释中对这个异常建立文档,这是可以接受的,** 而不是为每个方法单独建立文档。一个常见的例子是 `NullPointerException`。 若类的文档注释中有这样的描述:"All methods in this class throw a NullPointerException if a null object reference is passed in any parameter ”(如果 null 对象引用被传递到任何一个参数中,这个类中的所有方法都会抛出 `NullPointerException`),或者有其他类似的语句,这是可以的。
|
||||
**如果一个类中的许多方法出于同样的原因而抛出同一个异常,在该类的文档注释中对这个异常建立文档,这是可以接受的,** 而不是为每个方法单独建立文档。一个常见的例子是 `NullPointerException`。 若类的文档注释中有这样的描述:「All methods in this class throw a NullPointerException if a null object reference is passed in any parameter 」(如果 null 对象引用被传递到任何一个参数中,这个类中的所有方法都会抛出 `NullPointerException`),或者有其他类似的语句,这是可以的。
|
||||
|
||||
总而言之,要为你编写的每个方法所能抛出的每个异常建立文档。对于未受检异常和受检异常,以及抽象的方法和具体的方法一概如此。这个文档在文档注释中应当采用 `@throws` 标签的形式。要在方法的 throws 子句中为每个受检异常提供单独的声明,但是不要声明未受检的异常。如果没有为可以抛出的异常建立文档,其他人就很难或者根本不可能有效地使用你的类和接口。
|
@ -2,7 +2,7 @@
|
||||
|
||||
当程序由于未被捕获的异常而失败的时’候,系统会自动地打印出该异常的堆栈轨迹。在堆栈轨迹中包含该异常的字符串表示法 (string representation),即它的 toString 方法的调用结果。它通常包含该异常的类名,紧随其后的是细节消息 (detail message)。通常,这只是程序员或者网站可靠性工程师在调查软件失败原因时必须检查的信息。如果失败的情形不容易重现,要想获得更多的信息会非常困难,甚至是不可能的。因此,异常类型的 toString 方法应该尽可能多地返回有关失败原因的信息,这一点特别重要。换句话说,异常的字符串表示法应该捕获失败,以便于后续进行分析。
|
||||
|
||||
**为了捕获失败,异常的细节信息应该包含“对该异常有贡献”的所有参数和字段的值。** 例如, `IndexOutOfBoundsException` 异常的细节消息应该包含下界、上界以及没有落在界内的下标值。该细节消息提供了许多关于失败的信息。这三个值中任何一个或者全部都有可能是错的。实际的下标值可能小于下界或等于上界(“越界错误”),或者它可能是个无效值,太小或太大。下界也有可能大于上界(严重违反内部约束条件的一种情况) 。每一种情形都代表了不同的问题,如果程序员知道应该去查找哪种错误,就可以极大地加速诊断过程。
|
||||
**为了捕获失败,异常的细节信息应该包含“对该异常有贡献”的所有参数和字段的值。** 例如, `IndexOutOfBoundsException` 异常的细节消息应该包含下界、上界以及没有落在界内的下标值。该细节消息提供了许多关于失败的信息。这三个值中任何一个或者全部都有可能是错的。实际的下标值可能小于下界或等于上界(「越界错误」),或者它可能是个无效值,太小或太大。下界也有可能大于上界(严重违反内部约束条件的一种情况) 。每一种情形都代表了不同的问题,如果程序员知道应该去查找哪种错误,就可以极大地加速诊断过程。
|
||||
|
||||
对安全敏感的信息有一条忠告。由于在诊断和修正软件问题的过程中,许多人都可以看见堆栈轨迹, **因此千万不要在细节消息中包含密码、密钥以及类似的信息!**
|
||||
|
||||
|
@ -2,7 +2,7 @@
|
||||
|
||||
延迟初始化是延迟字段的初始化,直到需要它的值。如果不需要该值,则不会初始化字段。这种技术既适用于静态字段,也适用于实例字段。虽然延迟初始化主要是一种优化,但是它也可以用于破坏类中的有害循环和实例初始化 [Bloch05, Puzzle 51]。
|
||||
|
||||
与大多数优化一样,延迟初始化的最佳建议是「除非需要,否则不要这样做」(详见第 67 条)。延迟初始化是一把双刃剑。它降低了初始化类或创建实例的成本,代价是增加了访问延迟初始化字段的成本。根据这些字段中最终需要初始化的部分、初始化它们的开销以及初始化后访问每个字段的频率,延迟初始化实际上会损害性能(就像许多「优化」一样)。
|
||||
与大多数优化一样,延迟初始化的最佳建议是「除非需要,否则不要这样做」(详见第 67 条)。延迟初始化是一把双刃剑。它降低了初始化类或创建实例的成本,代价是增加了访问延迟初始化字段的成本。根据这些字段中最终需要初始化的部分、初始化它们的开销以及初始化后访问每个字段的频率,延迟初始化实际上会损害性能(就像许多「优化」一样)。
|
||||
|
||||
延迟初始化也有它的用途。如果一个字段只在类的一小部分实例上访问,并且初始化该字段的代价很高,那么延迟初始化可能是值得的。唯一确定的方法是以使用和不使用延迟初始化的效果对比来度量类的性能。
|
||||
|
||||
|
@ -28,11 +28,11 @@ private Object readResolve() {
|
||||
|
||||
该方法忽略了被反序列化的对象,只返回类初始化创建的那个特殊的 `Elvis` 实例。因此 `Elvis` 实例的序列化形式不应该包含任何实际的数据;所有的实例字段都应该被声明为 `transient`。事实上,**如果依赖 `readResolve` 进行实例控制,带有对象引用类型的所有实例字段都必须声明为 `transient`。** 否则,那种破釜沉舟式的攻击者,就有可能在 `readResolve` 方法运行之前,保护指向反序列化对象的引用,采用的方式类似于在第 88 条中提到的 `MutablePeriod` 攻击。
|
||||
|
||||
这种攻击有点复杂,但是背后的思想十分简单。如果单例包含一个非 `transient` 的对象引用字段,这个字段的内容就可以在单例的 `readResolve` 方法之前被反序列化。当对象引用字段的内容被反序列化时,它就允许一个精心制作的流“盗用”指向最初被反序列化的单例对象引用。
|
||||
这种攻击有点复杂,但是背后的思想十分简单。如果单例包含一个非 `transient` 的对象引用字段,这个字段的内容就可以在单例的 `readResolve` 方法之前被反序列化。当对象引用字段的内容被反序列化时,它就允许一个精心制作的流「盗用」指向最初被反序列化的单例对象引用。
|
||||
|
||||
以下是它更详细的工作原理。首先编写一个“盗用者”类,它既有 `readResolve` 方法,又有实例字段,实例字段指向被序列化的单例的引用,“盗用者”就“潜伏”在其中。在序列化流中,用“盗用者”的 `readResolve` 方法运行时,它的实例字段仍然引用部分反序列化(并且还没有被解析)的 Singletion。
|
||||
以下是它更详细的工作原理。首先编写一个「盗用者」类,它既有 `readResolve` 方法,又有实例字段,实例字段指向被序列化的单例的引用,「盗用者」就「潜伏」在其中。在序列化流中,用「盗用者」的 `readResolve` 方法运行时,它的实例字段仍然引用部分反序列化(并且还没有被解析)的 Singletion。
|
||||
|
||||
“盗用者”的 `readResolve` 方法从它的实例字段中将引用复制到静态字段中,以便该引用可以在 `readResolve` 方法运行之后被访问到。然后这个方法为它所藏身的那个域返回一个正确的类型值。如果没有这么做,当序列化系统试着将“盗用者”引用保存到这个字段时,虚拟机就会抛出 `ClassCastException`。
|
||||
「盗用者」的 `readResolve` 方法从它的实例字段中将引用复制到静态字段中,以便该引用可以在 `readResolve` 方法运行之后被访问到。然后这个方法为它所藏身的那个域返回一个正确的类型值。如果没有这么做,当序列化系统试着将「盗用者」引用保存到这个字段时,虚拟机就会抛出 `ClassCastException`。
|
||||
|
||||
为了更具体的说明这一点,我们以如下这个单例模式为例:
|
||||
|
||||
@ -54,7 +54,7 @@ public class Elvis implements Serializable {
|
||||
}
|
||||
```
|
||||
|
||||
如下“盗用者”类,是根据以上描述构造的:
|
||||
如下「盗用者」类,是根据以上描述构造的:
|
||||
|
||||
```java
|
||||
public class ElvisStealer implements Serializable {
|
||||
@ -130,5 +130,4 @@ public enum Elvis {
|
||||
|
||||
**readResolve 的可访问性(accessibility)十分重要。** 如果把 `readResolve` 方法放在一个 `final` 类上面,它应该是私有的。如果把 `readResolve` 方法放在一个非 `final` 类上,就必须认真考虑它的的访问性。如果它是私有的,就不适用于任何一个子类。如果它是包级私有的,就适用于同一个包内的子类。如果它是受保护的或者是公开的,并且子类没有覆盖它,对序列化的子类进行反序列化,就会产生一个超类实例,这样可能会导致 `ClassCastException` 异常。
|
||||
|
||||
总而言之,应该尽可能的使用枚举类型来实施实例控制的约束条件。如果做不到,同时又需要一个即可序列化又可以实例受控的类,就必须提供一个 `readResolve` 方法,并确保该类的所有实例化字段都被基本类型,或者是
|
||||
`transient` 的。
|
||||
总而言之,应该尽可能的使用枚举类型来实施实例控制的约束条件。如果做不到,同时又需要一个即可序列化又可以实例受控的类,就必须提供一个 `readResolve` 方法,并确保该类的所有实例化字段都被基本类型,或者是 `transient` 的。
|
Loading…
Reference in New Issue
Block a user