mirror of
https://github.com/sjsdfg/effective-java-3rd-chinese.git
synced 2026-08-21 11:13:28 +08:00
Compare commits
71 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
78b92b4b18 | ||
|
|
111cdac7e7 | ||
|
|
2c052cec8f | ||
|
|
03078fe5a8 | ||
|
|
44b177fe4f | ||
|
|
32273113c0 | ||
|
|
18291cc8fc | ||
|
|
991ce8bc48 | ||
|
|
326c97043f | ||
|
|
69ee1d0ac2 | ||
|
|
eabddb9751 | ||
|
|
00581d7a8f | ||
|
|
34683114dd | ||
|
|
f911331fba | ||
|
|
a9e86c4a29 | ||
|
|
fd6b34977c | ||
|
|
b09a31df1e | ||
|
|
9a3c957a7c | ||
|
|
0057aaa203 | ||
|
|
0fbaba10f5 | ||
|
|
c46f61b357 | ||
|
|
c362daea33 | ||
|
|
1dc980f84e | ||
|
|
fc0f14364d | ||
|
|
83c1a31be1 | ||
|
|
650c961dc2 | ||
|
|
1e4dd8797d | ||
|
|
1d45e9e6b0 | ||
|
|
325766758b | ||
|
|
80f720fa9f | ||
|
|
aac63ecafe | ||
|
|
853f75d431 | ||
|
|
7b2bd2b15a | ||
|
|
5ef7049303 | ||
|
|
b882b056e4 | ||
|
|
543dacc4bf | ||
|
|
8b220000ea | ||
|
|
a718228e14 | ||
|
|
f932527b58 | ||
|
|
dea7c2c1e1 | ||
|
|
c1cbb7f072 | ||
|
|
0cfa1275ef | ||
|
|
c5fbb35f5c | ||
|
|
a71dbfd0da | ||
|
|
d9a539e643 | ||
|
|
1a7f221902 | ||
|
|
345f31851d | ||
|
|
175dde7754 | ||
|
|
b5df370624 | ||
|
|
1478ffd0ac | ||
|
|
dc9839e914 | ||
|
|
83b8ec9a6e | ||
|
|
9e9d80eb73 | ||
|
|
28e51ff3ca | ||
|
|
d6a294f38f | ||
|
|
7c69ce0de7 | ||
|
|
1ef48d01ff | ||
|
|
61ea173f86 | ||
|
|
46a3ce0480 | ||
|
|
eea845f1c0 | ||
|
|
ddf1ec699d | ||
|
|
5a2fcb9983 | ||
|
|
68acbcf2e1 | ||
|
|
7ec45f0974 | ||
|
|
157dde3c48 | ||
|
|
402b216416 | ||
|
|
30260c05b7 | ||
|
|
8364f91953 | ||
|
|
b170a5ac4d | ||
|
|
d2a537aa79 | ||
|
|
5b49011517 |
23
.github/ISSUE_TEMPLATE/contribute-md.md
vendored
Normal file
23
.github/ISSUE_TEMPLATE/contribute-md.md
vendored
Normal file
@@ -0,0 +1,23 @@
|
||||
---
|
||||
name: 贡献力量
|
||||
about: Describe this issue template's purpose here.
|
||||
title: "【翻译/错别字校对】条目xxx"
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
### 类型
|
||||
|
||||
1. 错别字
|
||||
2. 翻译校对
|
||||
|
||||
### 校正条目
|
||||
|
||||
- 第 xxx 条
|
||||
|
||||
### 校正内容
|
||||
|
||||
- 英语原文(翻译校对时使用):
|
||||
- 翻译原文:
|
||||
- 校对之后的内容:
|
||||
@@ -5,7 +5,7 @@
|
||||
现在全部章节已经更新完成
|
||||
- [github pages](https://sjsdfg.github.io/effective-java-3rd-chinese/#/):提供更好的在线阅读版本
|
||||
- [gitee pages](http://sjsdfg.gitee.io/effective-java-3rd-chinese/#/): 提供更快的访问速度
|
||||
- [pdf 下载](https://github.com/sjsdfg/effective-java-3rd-chinese/releases/download/v1.1/Effective.Java.pdf)
|
||||
- [pdf 下载](https://github.com/sjsdfg/effective-java-3rd-chinese/releases/download/v1.2/effective-java20190527.pdf)
|
||||
|
||||
## about this repository
|
||||
|
||||
@@ -17,6 +17,12 @@
|
||||
|
||||
markdown 文件以及英文版原版链接也都放在了 github 上面,希望小伙伴也可以中英文对照,给出一些意见。
|
||||
|
||||
## 一起来交流
|
||||
|
||||
Effective Java 第三版翻译校对群:[909059971](https://jq.qq.com/?_wv=1027&k=5tscKwN)
|
||||
|
||||

|
||||
|
||||
## 额外资源
|
||||
|
||||
* [effective-java-2nd 中文版 ](https://pan.baidu.com/s/1R6H9UHbFYubWWY9HrclZ2A)
|
||||
|
||||
@@ -10,6 +10,14 @@
|
||||
* [effective-java-3rd 英文版 ](https://pan.baidu.com/s/1mJx5ZrOD_RPjf3ghQnBV5g)
|
||||
* [effective-java-3rd 源代码](https://github.com/jbloch/effective-java-3e-source-code)
|
||||
|
||||
## 一起来校对翻译
|
||||
|
||||
Effective Java 第三版翻译校对群:[909059971](https://jq.qq.com/?_wv=1027&k=5tscKwN)
|
||||
|
||||

|
||||
|
||||
|
||||
|
||||
## 友情链接
|
||||
|
||||
- [On Java 8中文版 - 即 thinking in java 第五版](https://github.com/LingCoder/OnJava8)
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
// Copied from https://github.com/jeluard/prism-clojure
|
||||
Prism.languages.clojure = {
|
||||
comment: /;+.*/,
|
||||
string: /"(?:\\.|[^\\"\r\n])*"/,
|
||||
operator: /(?:::|[:|'])\b[a-z][\w*+!?-]*\b/i, //used for symbols and keywords
|
||||
keyword: {
|
||||
pattern: /([^\w+*'?-])(?:def|if|do|let|\.\.|quote|var|->>|->|fn|loop|recur|throw|try|monitor-enter|\.|new|set!|def\-|defn|defn\-|defmacro|defmulti|defmethod|defstruct|defonce|declare|definline|definterface|defprotocol|==|defrecord|>=|deftype|<=|defproject|ns|\*|\+|\-|\/|<|=|>|accessor|agent|agent-errors|aget|alength|all-ns|alter|and|append-child|apply|array-map|aset|aset-boolean|aset-byte|aset-char|aset-double|aset-float|aset-int|aset-long|aset-short|assert|assoc|await|await-for|bean|binding|bit-and|bit-not|bit-or|bit-shift-left|bit-shift-right|bit-xor|boolean|branch\?|butlast|byte|cast|char|children|class|clear-agent-errors|comment|commute|comp|comparator|complement|concat|conj|cons|constantly|cond|if-not|construct-proxy|contains\?|count|create-ns|create-struct|cycle|dec|deref|difference|disj|dissoc|distinct|doall|doc|dorun|doseq|dosync|dotimes|doto|double|down|drop|drop-while|edit|end\?|ensure|eval|every\?|false\?|ffirst|file-seq|filter|find|find-doc|find-ns|find-var|first|float|flush|for|fnseq|frest|gensym|get-proxy-class|get|hash-map|hash-set|identical\?|identity|if-let|import|in-ns|inc|index|insert-child|insert-left|insert-right|inspect-table|inspect-tree|instance\?|int|interleave|intersection|into|into-array|iterate|join|key|keys|keyword|keyword\?|last|lazy-cat|lazy-cons|left|lefts|line-seq|list\*|list|load|load-file|locking|long|loop|macroexpand|macroexpand-1|make-array|make-node|map|map-invert|map\?|mapcat|max|max-key|memfn|merge|merge-with|meta|min|min-key|name|namespace|neg\?|new|newline|next|nil\?|node|not|not-any\?|not-every\?|not=|ns-imports|ns-interns|ns-map|ns-name|ns-publics|ns-refers|ns-resolve|ns-unmap|nth|nthrest|or|parse|partial|path|peek|pop|pos\?|pr|pr-str|print|print-str|println|println-str|prn|prn-str|project|proxy|proxy-mappings|quot|rand|rand-int|range|re-find|re-groups|re-matcher|re-matches|re-pattern|re-seq|read|read-line|reduce|ref|ref-set|refer|rem|remove|remove-method|remove-ns|rename|rename-keys|repeat|replace|replicate|resolve|rest|resultset-seq|reverse|rfirst|right|rights|root|rrest|rseq|second|select|select-keys|send|send-off|seq|seq-zip|seq\?|set|short|slurp|some|sort|sort-by|sorted-map|sorted-map-by|sorted-set|special-symbol\?|split-at|split-with|str|string\?|struct|struct-map|subs|subvec|symbol|symbol\?|sync|take|take-nth|take-while|test|time|to-array|to-array-2d|tree-seq|true\?|union|up|update-proxy|val|vals|var-get|var-set|var\?|vector|vector-zip|vector\?|when|when-first|when-let|when-not|with-local-vars|with-meta|with-open|with-out-str|xml-seq|xml-zip|zero\?|zipmap|zipper)(?=[^\w+*'?-])/,
|
||||
lookbehind: true
|
||||
},
|
||||
boolean: /\b(?:true|false|nil)\b/,
|
||||
number: /\b[0-9A-Fa-f]+\b/,
|
||||
punctuation: /[{}\[\](),]/
|
||||
};
|
||||
File diff suppressed because it is too large
Load Diff
@@ -13,55 +13,55 @@ public static Boolean valueOf(boolean b) {
|
||||
|
||||
类可以为其客户端提供静态工厂方法,而不是公共构造方法。提供静态工厂方法而不是公共构造方法有优点也有缺点。
|
||||
|
||||
**静态工厂方法的一个优点是,不像构造方法,它们是有名字的。** 如果构造方法的参数本身并不描述被返回的对象,则具有精心选择名称的静态工厂更易于使用,并且生成的客户端代码更易于阅读。 例如,返回一个可能为素数的 `BigInteger` 的构造方法 `BigInteger(int,int,Random)` 可以更好地表示为名为 `BigInteger.probablePrime` 的静态工厂方法。 (这个方法是在 Java 1.4 中添加的。)
|
||||
**静态工厂方法的一个优点是,与构造方法不同,它们是有名字的。** 如果构造方法的参数本身并不描述被返回的对象,则具有精心选择名称的静态工厂更易于使用,并且生成的客户端代码更易于阅读。 例如,返回一个可能为素数的 `BigInteger` 的构造方法 `BigInteger(int,int,Random)` 可以更好地表示为名为 `BigInteger.probablePrime` 的静态工厂方法。 (这个方法是在 Java 1.4 中添加的。)
|
||||
|
||||
|
||||
一个类只能有一个给定签名的构造方法。 程序员知道通过提供两个构造方法来解决这个限制,这两个构造方法的参数列表只有它们的参数类型的顺序不同。 这是一个非常糟糕的主意。 这样的 API 用户将永远不会记得哪个构造方法是哪个,最终会错误地调用。 阅读使用这些构造方法的代码的人只有在参考类文档的情况下才知道代码的作用。
|
||||
|
||||
因为他们有名字,所以静态工厂方法不会受到上面讨论中的限制。在类中似乎需要具有相同签名的多个构造方法的情况下,用静态工厂方法替换构造方法,并仔细选择名称来突出它们的差异。
|
||||
|
||||
**静态工厂方法的第二个优点是,与构造方法不同,它们不需要每次调用时都创建一个新对象。** 这允许不可变的类 (详见第 17 条)使用预先构建的实例,或者在构造时缓存实例,并反复分配它们以避免创建不必要的重复对象。`Boolean.valueof(boolean)` 方法说明了这种方法:它从不创建对象。这种技术类似于 `Flyweight` 模式[Gamma95]。如果经常请求等价对象,那么它可以极大地提高性能,特别是如果在创建它们非常昂贵的情况下。
|
||||
**静态工厂方法的第二个优点是,与构造方法不同,它们不需要每次调用时都创建一个新对象。** 这允许不可变类 (详见第 17 条)使用预先构建的实例,或者在构造时缓存实例,并反复分配它们以避免创建不必要的重复对象。`Boolean.valueof(boolean)` 方法说明了这种方法:它从不创建对象。这种技术类似于 `Flyweight` 模式[Gamma95]。如果经常请求等价对象,那么它可以极大地提高性能,特别是在创建它们的代价非常昂贵的情况下。
|
||||
|
||||
静态工厂方法从重复调用返回相同对象的能力允许类保持在任何时候存在的实例的严格控制。这样做的类被称为实例控制(instance-controlled)。编写实例控制类的原因有很多。实例控制允许一个类来保证它是一个单例(详见第 3 条)项或不可实例化的(详见第 4 条)。同时,它允许一个不可变的值类(详见第 17 条)保证不存在两个相同的实例:当且仅当 `a == b` 时 `a.equals(b)`。这是享元模式的基础[Gamma95]。`Enum` 类型(详见第 34 条)提供了这个保证。
|
||||
静态工厂方法重复调用返回相同实例这个特点可以让类在任何时候都能对实例保持严格的控制。这样做的类被称为实例控制类( instance-controlled)。有很多理由足以让我们去我们编写实例控制类。实例控制可以保证一个类是单例 的(详见第 3 条) 或不可实例化的 (详见第 4 条)。同时,它允许一个不可变的值类 (详见第 17 条) 保证不存在两个相等但不相同的实例,也就是说当且仅当 `a == b` 时才有 `a.equals(b)`。这是`Flyweight`模式的基础[Gamma95]。`Enum` 类型 (详见第 34 条)可以做到这点。
|
||||
|
||||
**静态工厂方法的第三个优点是,与构造方法不同,它们可以返回其返回类型的任何子类型的对象。** 这为你在选择返回对象的类时提供了很大的灵活性。
|
||||
|
||||
这种灵活性的一个应用是 API 可以返回对象而不需要公开它的类。 以这种方式隐藏实现类会使 API 非常紧凑。 这种技术适用于基于接口的框架(详见第 20 条),其中接口为静态工厂方法提供自然返回类型。
|
||||
|
||||
在 Java 8 之前,接口不能有静态方法。根据约定,一个名为 `Type` 的接口的静态工厂方法被放入一个非实例化的伙伴类(companion class)(详见第 4 条)`Types` 类中。例如,Java 集合框架有 45 个接口的实用工具实现,提供不可修改的集合、同步集合等等。几乎所有这些实现都是通过静态工厂方法在一个非实例类 (`java .util. collections`) 中导出的。返回对象的类都是非公开的。
|
||||
在 Java 8 之前,接口不能有静态方法。根据约定,一个名为 `Type` 的接口的静态工厂方法被放入一个不可实例化的伙伴类(companion class)(详见第 4 条)`Types` 类中。例如,Java 集合框架有 45 个接口的实用工具实现,提供不可修改的集合、同步集合等等。几乎所有这些实现都是通过静态工厂方法在一个不可实例化的类 (`java .util. collections`) 中返回的。返回对象的类都隐藏的。
|
||||
|
||||
`Collections` 框架 API 的规模要比它之前输出的 45 个单独的公共类要小得多,每个类有个便利类的实现。不仅是 API 的大部分减少了,还包括概念上的权重:程序员必须掌握的概念的数量和难度,才能使用 API。程序员知道返回的对象恰好有其接口指定的 API,因此不需要为实现类读阅读额外的类文档。此外,使用这种静态工厂方法需要客户端通过接口而不是实现类来引用返回的对象,这通常是良好的实践(详见第 64 条)。
|
||||
`Collections` 框架 API 的规模要比它之前返回的 45 个单独的公共类要小得多,每个类在集合框架中都有一个便利的实现。不仅是 API 的大部分减少了,还包括概念上的权重:程序员要想使用 API必须掌握的概念的数量和难度。程序员知道返回的对象恰好有其接口指定的 API,因此不需要为实现类读阅读额外的类文档。此外,使用这种静态工厂方法需要客户端通过接口而不是实现类来引用返回的对象,这通常是良好的实践(详见第 64 条)。
|
||||
|
||||
从 Java 8 开始,接口不能包含静态方法的限制被取消了,所以通常没有理由为接口提供一个不可实例化的伴随类。 很多公开的静态成员应该放在这个接口本身。 但是,请注意,将这些静态方法的大部分实现代码放在单独的包私有类中仍然是必要的。 这是因为 Java 8 要求所有接口的静态成员都是公共的。 Java 9 允许私有静态方法,但静态字段和静态成员类仍然需要公开。
|
||||
|
||||
**静态工厂的第四个优点是返回对象的类可以根据输入参数的不同而不同。** 声明的返回类型的任何子类都是允许的。 返回对象的类也可以随每次发布而不同。
|
||||
|
||||
`EnumSet` 类(详见第 36 条)没有公共构造方法,只有静态工厂。 在 OpenJDK 实现中,它们根据底层枚举类型的大小返回两个子类中的一个的实例:如果大多数枚举类型具有 64 个或更少的元素,静态工厂将返回一个 `RegularEnumSet` 实例, 返回一个 `long` 类型;如果枚举类型具有六十五个或更多元素,则工厂将返回一个 `JumboEnumSet` 实例,返回一个 `long` 类型的数组。
|
||||
`EnumSet` 类(详见第 36 条)没有公共构造方法,只有静态工厂。 在 OpenJDK 实现中,它们根据底层枚举类型的大小返回两个子类中的一个的实例:大多数枚举类型具有 64 个或更少的元素,静态工厂将返回一个 `RegularEnumSet` 实例, 底层是`long` 类型;如果枚举类型具有六十五个或更多元素,则工厂将返回一个 `JumboEnumSet` 实例,底层是`long` 类型的数组。
|
||||
|
||||
这两个实现类的存在对于客户是不可见的。 如果 `RegularEnumSet` 不再为小枚举类型提供性能优势,则可以在未来版本中将其淘汰,而不会产生任何不良影响。 同样,未来的版本可能会添加 `EnumSet` 的第三个或第四个实现,如果它证明有利于性能。 客户既不知道也不关心他们从工厂返回的对象的类别; 他们只关心它是 `EnumSet` 的一些子类。
|
||||
这两个实现类的存在对于客户端而言是不可见的。 如果 `RegularEnumSet` 对于小的枚举类型不再具有性能优势,则可以在未来版本中将其淘汰,且不会产生任何不良影响。 同样,如果可以证明添加 `EnumSet` 的更多的实现可以提高性能,那么在未来的版本可能就会这样做。 客户既不知道也不关心他们从工厂返回的对象的类别; 他们只需要知道它是 `EnumSet` 的子类。
|
||||
|
||||
**静态工厂的第 5 个优点是,在编写包含该方法的类时,返回的对象的类不需要存在。** 这种灵活的静态工厂方法构成了服务提供者框架的基础,比如 Java 数据库连接 AP(JDBC)。服务提供者框架是提供者实现服务的系统,并且系统使得实现对客户端可用,从而将客户端从实现中分离出来。
|
||||
**静态工厂的第五个优点是,在编写包含该方法的类时,返回的对象的类不需要存在。** 这种灵活的静态工厂方法构成了服务提供者框架的基础,比如 Java 数据库连接 AP(JDBC)。服务提供者框架是提供者实现服务的系统,并且系统使得实现对客户端可用,从而将客户端从实现中分离出来。
|
||||
|
||||
服务提供者框架中有三个基本组:服务接口,它表示实现;提供者注册 API,提供者用来注册实现;以及服务访问 API,客户端使用该 API 获取服务的实例。服务访问 API 允许客户指定选择实现的标准。在缺少这样的标准的情况下,API 返回一个默认实现的实例,或者允许客户通过所有可用的实现进行遍历。服务访问 API 是灵活的静态工厂,它构成了服务提供者框架的基础。
|
||||
|
||||
服务提供者框架的一个可选的第四个组件是一个服务提供者接口,它描述了一个生成服务接口实例的工厂对象。在没有服务提供者接口的情况下,必须对实现进行反射实例化(详见第 65 条)。在 JDBC 的情况下,`Connection` 扮演服务接口的一部分,`DriverManager.registerDriver` 提供程序注册 API、`DriverManager.getConnection` 是服务访问 API,`Driver` 是服务提供者接口。
|
||||
|
||||
服务提供者框架模式有许多变种。 例如,服务访问 API 可以向客户端返回比提供者提供的更丰富的服务接口。 这是桥接模式[Gamma95]。 依赖注入框架(详见第 5 条)可以被看作是强大的服务提供者。 从 Java 6 开始,平台包含一个通用的服务提供者框架 `java.util.ServiceLoader`,所以你不需要,一般也不应该自己编写(条目 59)。 JDBC 不使用 `ServiceLoader`,因为前者早于后者。
|
||||
服务提供者框架模式有许多变种。 例如,服务访问 API 可以向客户端返回比提供者提供的更丰富的服务接口。 这是桥接模式[Gamma95]。 依赖注入框架(详见第 5 条)可以被看作是强大的服务提供者。 从 Java 6 开始,平台包含一个通用的服务提供者框架 `java.util.ServiceLoader`,所以你不需要,一般也不应该自己编写(详见第 59 条)。 JDBC 不使用 `ServiceLoader`,因为前者早于后者。
|
||||
|
||||
**只提供静态工厂方法的主要限制是,没有公共或受保护构造方法的类不能被子类化。** 例如,在 `Collections` 框架中不可能将任何方便实现类子类化。可以说,这可能是因祸得福,因为它鼓励程序员使用组合而不是继承(详见第 18 条),并且是不可变类型(详见第 17 条)。
|
||||
**只提供静态工厂方法的主要限制是,没有公共或受保护构造方法的类不能被子类化。** 例如,要想将 `Collections` 框架中任何遍历的实现类进行子类化,是不可能的。但是这样也会因祸得福,因为它鼓励程序员使用组合(composition)而不是继承(详见第 18 条),并且是不可变类型锁需要的(详见第 17 条)。
|
||||
|
||||
**静态工厂方法的第二个缺点是,程序员很难找到它们。** 它们不像构造方法那样在 API 文档中突出,因此很难找出如何实例化一个提供静态工厂方法而不是构造方法的类。Javadoc 工具可能有一天会引起对静态工厂方法的注意。与此同时,可以通过将注意力吸引到类或接口文档中的静态工厂以及遵守通用的命名约定来减少这个问题。下面是一些静态工厂方法的常用名称。以下清单并非完整:
|
||||
**静态工厂方法的第二个缺点是,程序员很难找到它们。** 它们不像构造方法那样在 API 文档中明确的标注出来。因此,对于提供了静态方法而不是构造器的类来说,想要查明如何实例化一个类是十分困难的。Javadoc 工具可能有一天会注意到静态工厂方法。与此同时,通过关注类或者接口的文档中静态方法,并且遵守标准的命名习惯,也可以弥补这一劣势。下面是一些静态工厂方法的常用名称。以下清单这是列出了其中的一小部分:
|
||||
|
||||
- from——A 类型转换方法,它接受单个参数并返回此类型的相应实例,例如:**Date d = Date.from(instant)**;
|
||||
- of——一个聚合方法,接受多个参数并返回该类型的实例,并把他们合并在一起,例如:**Set\<Rank\> faceCards = EnumSet.of(JACK, QUEEN, KING)**;
|
||||
- valueOf——from 和 to 更为详细的替代 方式,例如:**BigInteger prime = BigInteger.valueOf(Integer.MAX_VALUE)**;
|
||||
- instance 或 getinstance——返回一个由其参数 (如果有的话) 描述的实例,但不能说它具有相同的值,例如:**StackWalker luke = StackWalker.getInstance(options)**;
|
||||
- create 或 newInstance——与 instance 或 getInstance 类似,除了该方法保证每个调用返回一个新的实例,例如:**Object newArray = Array.newInstance(classObject, arrayLen)**;
|
||||
- getType——与 getInstance 类似,但是如果在工厂方法中不同的类中使用。**Type** 是工厂方法返回的对象类型,例如:**FileStore fs = Files.getFileStore(path)**;
|
||||
- newType——与 newInstance 类似,但是如果在工厂方法中不同的类中使用。Type 是工厂方法返回的对象类型,例如:**BufferedReader br = Files.newBufferedReader(path)**;
|
||||
- type—— getType 和 newType 简洁的替代方式,例如:**List\<Complaint\> litany = Collections.list(legacyLitany)**;
|
||||
- from —— 类型转换方法,它接受单个参数并返回此类型的相应实例,例如:**Date d = Date.from(instant)**;
|
||||
- of —— 聚合方法,接受多个参数并返回该类型的实例,并把他们合并在一起,例如:**Set\<Rank\> faceCards = EnumSet.of(JACK, QUEEN, KING)**;
|
||||
- valueOf —— from 和 to 更为详细的替代 方式,例如:**BigInteger prime = BigInteger.valueOf(Integer.MAX_VALUE)**;
|
||||
- instance 或 getinstance —— 返回一个由其参数 (如果有的话) 描述的实例,但不能说它具有相同的值,例如:**StackWalker luke = StackWalker.getInstance(options)**;
|
||||
- create 或 newInstance —— 与 instance 或 getInstance 类似,除此之外该方法保证每次调用返回一个新的实例,例如:**Object newArray = Array.newInstance(classObject, arrayLen)**;
|
||||
- getType —— 与 getInstance 类似,但是在工厂方法处于不同的类中的时候使用。**getType** 中的 **Type** 是工厂方法返回的对象类型,例如:**FileStore fs = Files.getFileStore(path)**;
|
||||
- newType —— 与 newInstance 类似,但是在工厂方法处于不同的类中的时候使用。**newType**中的 **Type** 是工厂方法返回的对象类型,例如:**BufferedReader br = Files.newBufferedReader(path)**;
|
||||
- type —— getType 和 newType 简洁的替代方式,例如:**List\<Complaint\> litany = Collections.list(legacyLitany)**;
|
||||
|
||||
总之,静态工厂方法和公共构造方法都有它们的用途,并且了解它们的相对优点是值得的。通常,静态工厂更可取,因此避免在没有考虑静态工厂的情况下提供公共构造方法。
|
||||
总之,静态工厂方法和公共构造方法都有它们的用途,并且了解它们的相对优点是值得的。通常,静态工厂更可取,因此避免在没有考虑静态工厂的情况下直接选择使用公共构造方法。
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
# 2. 当构造方法参数过多时使用 builder 模式
|
||||
|
||||
|
||||
静态工厂和构造方法都有一个限制:它们不能很好地扩展到很多可选参数的情景。请考虑一个代表包装食品上的营养成分标签的例子。这些标签有几个必需的属性——每次建议的摄入量,每罐的份量和每份卡路里 ,以及超过 20 个可选的属性——总脂肪、饱和脂肪、反式脂肪、胆固醇、钠等等。大多数产品都有非零值,只有少数几个可选属性。
|
||||
静态工厂和构造方法都有一个限制:它们不能很好地扩展到很多可选参数的情景。请考虑一个代表包装食品上的营养成分标签的例子。这些标签有几个必需的属性——每次建议的摄入量,每罐的份量和每份卡路里 ,以及超过 20 个可选的属性——总脂肪、饱和脂肪、反式脂肪、胆固醇、钠等等。大多数产品只有这些可选字段中的少数,且具有非零值。
|
||||
|
||||
应该为这样的类编写什么样的构造方法或静态工厂?传统上,程序员使用了可伸缩(telescoping constructor)构造方法模式,在这种模式中,只提供了一个只所需参数的构造函数,另一个只有一个可选参数,第三个有两个可选参数,等等,最终在构造函数中包含所有可选参数。这就是它在实践中的样子。为了简便起见,只显示了四个可选属性:
|
||||
应该为这样的类编写什么样的构造方法或静态工厂?传统上,程序员使用了可伸缩(telescoping constructor)构造方法模式,在这种模式中,首先提供一个只有必需参数的构造方法,接着提供增加了一个可选参数的构造函数,然后提供增加了两个可选参数的构造函数,等等,最终在构造函数中包含所有必需和可选参数。以下就是它在实践中的样子。为了简便起见,只显示了四个可选属性:
|
||||
|
||||
```java
|
||||
// Telescoping constructor pattern - does not scale well!
|
||||
|
||||
public class NutritionFacts {
|
||||
private final int servingSize; // (mL) required
|
||||
private final int servings; // (per container) required
|
||||
@@ -53,15 +52,14 @@ public class NutritionFacts {
|
||||
NutritionFacts cocaCola = new NutritionFacts(240, 8, 100, 0, 35, 27);
|
||||
```
|
||||
|
||||
通常情况下,这个构造方法的调用需要许多你不想设置的参数,但是你不得不为它们传递一个值。 在这种情况下,我们为 `fat` 属性传递了 0 值。「只有」六个参数可能看起来并不那么糟糕,但随着参数数量的增加,它会很快失控。
|
||||
通常情况下,这个构造方法的调用需要许多你不想设置的参数,但是你不得不为它们传递一个值。 在这种情况下,我们为 `fat` 属性传递了 0 值。「只有」六个参数可能看起来并不那么糟糕,但随着参数数量的增加,它很快就会失控。
|
||||
|
||||
简而言之,可伸缩构造方法模式是有效的,但是当有很多参数时,很难编写客户端代码,而且很难读懂它。读者不知道这些值是什么意思,并且必须仔细地计算参数才能找到答案。一长串相同类型的参数可能会导致一些细微的 bug。如果客户端意外地反转了两个这样的参数,编译器并不会抱怨,但是程序在运行时会出现错误行为 (条目 51)。
|
||||
简而言之,**可伸缩构造方法模式是有效的,但是当有很多参数时,很难编写客户端代码,而且很难读懂它。**读者不知道这些值是什么意思,并且必须仔细地去数参数才能找到答案。一长串相同类型的参数可能会导致一些细微的 bug。如果客户端不小心写反了两个这样的参数,编译器并不会报错,但是程序在运行时会出现错误行为 (详见第 51 条)。
|
||||
|
||||
当在构造方法中遇到许多可选参数时,另一种选择是 JavaBeans 模式,在这种模式中,调用一个无参数的构造函数来创建对象,然后调用 `setter` 方法来设置每个必需的参数和可选参数:
|
||||
当在构造方法中遇到许多可选参数时,另一种选择是 JavaBeans 模式,在这种模式中,调用一个无参的构造方法来创建对象,然后调用 `setter` 方法来设置每个必需的参数和可选参数:
|
||||
|
||||
```java
|
||||
// JavaBeans Pattern - allows inconsistency, mandates mutability
|
||||
|
||||
public class NutritionFacts {
|
||||
// Parameters initialized to default values (if any)
|
||||
private int servingSize = -1; // Required; no default value
|
||||
@@ -94,15 +92,14 @@ cocaCola.setSodium(35);
|
||||
cocaCola.setCarbohydrate(27);
|
||||
```
|
||||
|
||||
不幸的是,JavaBeans 模式本身有严重的缺陷。由于构造方法在多次调用中被分割,所以在构造过程中 JavaBean 可能处于不一致的状态。该类没有通过检查构造参数参数的有效性来执行一致性的选项。在不一致的状态下尝试使用对象可能会导致与包含 bug 的代码大相径庭的错误,因此很难调试。一个相关的缺点是,JavaBeans 模式排除了让类不可变的可能性(详见第 17 条),并且需要在程序员的部分增加工作以确保线程安全。
|
||||
不幸的是,JavaBeans 模式本身有严重的缺陷。由于构造方法被分割成了多次调用,所以在构造过程中 JavaBean 可能处于不一致的状态。该类没有通过检查构造参数参数的有效性来强制一致性的选项。在不一致的状态下尝试使用对象可能会导致一些错误,这些错误与平常代码的BUG很是不同,因此很难调试。一个相关的缺点是,JavaBeans 模式排除了让类不可变的可能性(详见第 17 条),并且需要程序员增加工作以确保线程安全。
|
||||
|
||||
通过在对象构建完成时手动「冻结」对象,并且不允许它在解冻之前使用,可以减少这些缺点,但是这种变体在实践中很难使用并且很少使用。 而且,在运行时会导致错误,因为编译器无法确保程序员在使用对象之前调用 `freeze` 方法。
|
||||
通过在对象构建完成时手动「冻结」对象,并且不允许它在解冻之前使用,可以减少这些缺点,但是这种变体在实践中很难使用并且很少使用。 而且,在运行时会导致错误,因为编译器无法确保程序员会在使用对象之前调用 `freeze` 方法。
|
||||
|
||||
幸运的是,还有第三种选择,它结合了可伸缩构造方法模式的安全性和 JavaBean 模式的可读性。 它是 Builder 模式[Gamma95] 的一种形式。客户端不直接调用所需的对象,而是调用构造方法 (或静态工厂),并使用所有必需的参数,并获得一个 builder 对象。然后,客户端调用 builder 对象的 `setter` 相似方法来设置每个可选参数。最后,客户端调用一个无参的 `build` 方法来生成对象,该对象通常是不可变的。Builder 通常是它所构建的类的一个静态成员类(详见第 24 条)。以下是它在实践中的示例:
|
||||
幸运的是,还有第三种选择,它结合了可伸缩构造方法模式的安全性和 JavaBean 模式的可读性。 它是 Builder 模式[Gamma95] 的一种形式。客户端不直接构造所需的对象,而是调用一个包含所有必需参数的构造方法 (或静态工厂)得到获得一个 builder 对象。然后,客户端调用 builder 对象的与 `setter` 相似方法来设置你想设置的可选参数。最后,客户端调用builder对象的一个无参的 `build` 方法来生成对象,该对象通常是不可变的。Builder 通常是它所构建的类的一个静态成员类(详见第 24 条)。以下是它在实践中的示例:
|
||||
|
||||
```java
|
||||
// Builder Pattern
|
||||
|
||||
public class NutritionFacts {
|
||||
private final int servingSize;
|
||||
private final int servings;
|
||||
@@ -163,22 +160,21 @@ public class NutritionFacts {
|
||||
}
|
||||
```
|
||||
|
||||
`NutritionFacts` 类是不可变的,所有的参数默认值都在一个地方。builder 的 setter 方法返回 builder 本身,这样调用就可以被链接起来,从而生成一个流畅的 API。下面是客户端代码的示例:
|
||||
`NutritionFacts` 类是不可变的,所有的参数默认值都在一个地方。builder 的 setter 方法返回 builder 本身,这样就可以进行链式调用,从而生成一个流畅的 API。下面是客户端代码的示例:
|
||||
|
||||
```java
|
||||
NutritionFacts cocaCola = new NutritionFacts.Builder(240, 8)
|
||||
.calories(100).sodium(35).carbohydrate(27).build();
|
||||
```
|
||||
|
||||
这个客户端代码很容易编写,更重要的是易于阅读。 Builder 模式模拟 Python 和 Scala 中的命名可选参数。
|
||||
这个客户端代码很容易编写,更重要的是易于阅读。 采用Builder 模式模拟实现的的可选参数可以在Python和Scala都可以找到。
|
||||
|
||||
为了简洁起见,省略了有效性检查。 要尽快检测无效参数,检查 builder 的构造方法和方法中的参数有效性。 在 `build` 方法调用的构造方法中检查包含多个参数的不变性。为了确保这些不变性不受攻击,在从 builder 复制参数后对对象属性进行检查(详见第 50 条)。 如果检查失败,则抛出 `IllegalArgumentException` 异常(详见第 72 条),其详细消息指示哪些参数无效(详见第 75 条)。
|
||||
|
||||
Builder 模式非常适合类层次结构。 使用平行层次的 builder,每个嵌套在相应的类中。 抽象类有抽象的 builder;具体的类有具体的 builder。 例如,考虑代表各种比萨饼的根层次结构的抽象类:
|
||||
Builder 模式非常适合类层次结构。 使用平行层次的 builder,每个builder嵌套在相应的类中。 抽象类有抽象的 builder;具体的类有具体的 builder。 例如,考虑代表各种比萨饼的根层次结构的抽象类:
|
||||
|
||||
```java
|
||||
// Builder pattern for class hierarchies
|
||||
|
||||
import java.util.EnumSet;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
@@ -207,7 +203,7 @@ public abstract class Pizza {
|
||||
}
|
||||
```
|
||||
|
||||
请注意,`Pizza.Builder` 是一个带有递归类型参数( recursive type parameter)(详见第 30 条)的泛型类型。 这与抽象的 `self` 方法一起,允许方法链在子类中正常工作,而不需要强制转换。 Java 缺乏自我类型的这种变通解决方法被称为模拟自我类型(simulated self-type)的习惯用法。
|
||||
请注意,`Pizza.Builder` 是一个带有递归类型参数( recursive type parameter)(详见第 30 条)的泛型类型。 这与抽象的 `self` 方法一起,允许方法链在子类中正常工作,而不需要强制转换。 Java 缺乏自我类型的这种变通解决方法被称为模拟自我类型(simulated self-type)。
|
||||
|
||||
这里有两个具体的 `Pizza` 的子类,其中一个代表标准的纽约风格的披萨,另一个是半圆形烤乳酪馅饼。前者有一个所需的尺寸参数,而后者则允许指定酱汁是否应该在里面或在外面:
|
||||
|
||||
@@ -282,6 +278,6 @@ Calzone calzone = new Calzone.Builder()
|
||||
|
||||
Builder 模式非常灵活。 单个 builder 可以重复使用来构建多个对象。 builder 的参数可以在构建方法的调用之间进行调整,以改变创建的对象。 builder 可以在创建对象时自动填充一些属性,例如每次创建对象时增加的序列号。
|
||||
|
||||
Builder 模式也有缺点。为了创建对象,首先必须创建它的 builder。虽然创建这个 builder 的成本在实践中不太可能被注意到,但在性能关键的情况下可能会出现问题。而且,builder 模式比伸缩构造方法模式更冗长,因此只有在有足够的参数时才值得使用它,比如四个或更多。但是请记住,如果希望在将来添加更多的参数。但是,如果从构造方法或静态工厂开始,并切换到 builder,当类演化到参数数量失控的时候,过时的构造方法或静态工厂就会面临尴尬的处境。因此,所以,最好从一开始就创建一个 builder。
|
||||
Builder 模式也有缺点。为了创建对象,首先必须创建它的 builder。虽然创建这个 builder 的成本在实践中不太可能被注意到,但在看中性能的场合下这可能就是一个问题。而且,builder 模式比伸缩构造方法模式更冗长,因此只有在有足够的参数时才值得使用它,比如四个或更多。但是请记住,你可能在以后会想要添加更多的参数。但是,如果你一开始是使用的构造方法或静态工厂,当类演化到参数数量失控的时候再转到Builder模式,过时的构造方法或静态工厂就会面临尴尬的处境。因此,通常最好从一开始就创建一个 builder。
|
||||
|
||||
总而言之,当设计类的构造方法或静态工厂的参数超过几个时,Builder 模式是一个不错的选择,特别是如果许多参数是可选的或相同类型的。客户端代码比使用伸缩构造方法(telescoping constructors)更容易读写,并且 builder 比 JavaBeans 更安全。
|
||||
总而言之,当设计类的构造方法或静态工厂的参数超过几个时,Builder 模式是一个不错的选择,特别是如果许多参数是可选的或相同类型的。builder模式客户端代码比使用伸缩构造方法(telescoping constructors)更容易读写,并且builder模式比 JavaBeans 更安全。
|
||||
|
||||
@@ -33,9 +33,9 @@ public class Elvis {
|
||||
|
||||
公共属性方法的主要优点是 API 明确表示该类是一个单例:公共静态属性是 final 的,所以它总是包含相同的对象引用。 第二个好处是它更简单。
|
||||
|
||||
静态工厂方法的一个优点是,它可以灵活地改变你的想法,无论该类是否为单例而不必更改其 API。 工厂方法返回唯一的实例,但是可以修改,比如,返回调用它的每个线程的单独实例。 第二个好处是,如果你的应用程序需要它,可以编写一个泛型单例工厂(generic singleton factory )(详见第30 条)。 使用静态工厂的最后一个优点是方法引用可以用 `supplier`,例如 `Elvis::instance` 等同于 `Supplier<Elvis>`。 除非与这些优点相关的,否则公共属性方法是可取的。
|
||||
静态工厂方法的优势之一在于,它提供了灵活性:在不改变其 API 的前提下,我们可以改变该类是否应该为单例的想法。工厂方法返回该类的唯一实例,但是,它很容易被修改,比如,改为每个调用该方法的线程返回一个唯一的实例。 第二个好处是,如果你的应用程序需要它,可以编写一个泛型单例工厂(generic singleton factory )(详见第30 条)。 使用静态工厂的最后一个优点是,可以通过方法引用(method reference)作为提供者,例如 `Elvis::instance` 等同于 `Supplier<Elvis>`。 除非满足以上任意一种优势,否则还是优先考虑公有域(public-field)的方法。
|
||||
|
||||
创建一个使用这两种方法的单例类(第 12 章),仅仅将 `implements Serializable` 添加到声明中是不够的。为了维护单例的保证,声明所有的实例属性为 `transient`,并提供一个 `readResolve` 方法(详见第 89条)。否则,每当序列化实例被反序列化时,就会创建一个新的实例,在我们的例子中,导致出现新的 Elvis 实例。为了防止这种情况发生,将这个 `readResolve` 方法添加到 Elvis 类:
|
||||
为了将上述方法中实现的单例类变成是可序列化的 (第 12 章),仅仅将 `implements Serializable` 添加到声明中是不够的。为了保证单例模式不被破坏,必须声明所有的实例字段为 `transient`,并提供一个 `readResolve` 方法(详见第 89 条)。否则,每当序列化的实例被反序列化时,就会创建一个新的实例,在我们的例子中,导致出现新的 Elvis 实例。为了防止这种情况发生,将如下的 `readResolve` 方法添加到 Elvis 类:
|
||||
|
||||
```java
|
||||
// readResolve method to preserve singleton property
|
||||
@@ -56,4 +56,4 @@ public enum Elvis {
|
||||
}
|
||||
```
|
||||
|
||||
这种方式类似于公共属性方法,但更简洁,提供了免费的序列化机制,并提供了针对多个实例化的坚固保证,即使是在复杂的序列化或反射攻击的情况下。这种方法可能感觉有点不自然,但是单一元素枚举类通常是实现单例的最佳方式。注意,如果单例必须继承 `Enum` 以外的父类 (尽管可以声明一个 `Enum` 来实现接口),那么就不能使用这种方法。
|
||||
这种方式类似于公共属性方法,但更简洁,无偿地提供了序列化机制,并提供了防止多个实例化的坚固保证,即使是在复杂的序列化或反射攻击的情况下。这种方法可能感觉有点不自然,但是 **单一元素枚举类通常是实现单例的最佳方式**。注意,如果单例必须继承 `Enum` 以外的父类 (尽管可以声明一个 `Enum` 来实现接口),那么就不能使用这种方法。
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
String s = new String("bikini"); // DON'T DO THIS!
|
||||
```
|
||||
|
||||
语句每次执行时都会创建一个新的 String 实例,而这些对象的创建都不是必需的。String 构造方法 `("bikini")` 的参数本身就是一个 `bikini` 实例,它与构造方法创建的所有对象的功能相同。如果这种用法发生在循环中,或者在频繁调用的方法中,就可以毫无必要地创建数百万个 String 实例。
|
||||
语句每次执行时都会创建一个新的 String 实例,而这些对象的创建都不是必需的。String 构造方法 `("bikini")` 的参数本身就是一个 `bikini` 实例,它与构造方法创建的所有对象的功能相同。如果这种用法发生在循环中,或者在频繁调用的方法中,就会毫无必要地创建数百万个 String 实例。
|
||||
|
||||
改进后的版本如下:
|
||||
```java
|
||||
@@ -68,7 +68,7 @@ private static long sum() {
|
||||
}
|
||||
```
|
||||
|
||||
这个程序的结果是正确的,但由于写错了一个字符,运行的结果要比实际慢很多。变量 `sum` 被声明成了 `Long` 而不是 `long`,这意味着程序构造了大约 231 不必要的 `Long` 实例(大约每次往 `Long` 类型的 `sum` 变量中增加一个 `long` 类型构造的实例),把 `sum` 变量的类型由 `Long` 改为 `long`,在我的机器上运行时间从 6.3 秒降低到 0.59 秒。这个教训很明显:**优先使用基本类型而不是装箱的基本类型,也要注意无意识的自动装箱。**
|
||||
这个程序的结果是正确的,但由于写错了一个字符,运行的结果要比实际慢很多。变量 `sum` 被声明成了 `Long` 而不是 `long`,这意味着程序构造了大约 2<sup>31</sup> 不必要的 `Long` 实例(大约每次往 `Long` 类型的 `sum` 变量中增加一个 `long` 类型构造的实例),把 `sum` 变量的类型由 `Long` 改为 `long`,在我的机器上运行时间从 6.3 秒降低到 0.59 秒。这个教训很明显:**优先使用基本类型而不是装箱的基本类型,也要注意无意识的自动装箱。**
|
||||
|
||||
这个条目不应该被误解为暗示对象创建是昂贵的,应该避免创建对象。 相反,使用构造方法创建和回收小的对象是非常廉价,构造方法只会做很少的显示工作,尤其是在现代 JVM 实现上。 创建额外的对象以增强程序的清晰度,简单性或功能性通常是件好事。
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
如果你从使用手动内存管理的语言(如 C 或 C++)切换到像 Java 这样的带有垃圾收集机制的语言,那么作为程序员的工作就会变得容易多了,因为你的对象在使用完毕以后就自动回收了。当你第一次体验它的时候,它就像魔法一样。这很容易让人觉得你不需要考虑内存管理,但这并不完全正确。
|
||||
|
||||
考虑以下简单的堆栈实现:
|
||||
考虑以下简单的栈实现:
|
||||
|
||||
```java
|
||||
// Can you spot the "memory leak"?
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# 10. 重写 equals 方法时遵守通用约定
|
||||
|
||||
|
||||
虽然 `Object` 是一个具体的类,但它主要是为继承而设计的。它的所有非 final 方法(equals、hashCode、toString、clone 和 finalize)都有清晰的通用约定( general contracts),因为它们被设计为被子类重写。任何类都有义务重写这些方法,以遵从他们的通用约定;如果不这样做,将会阻止其他依赖于约定的类 (例如 HashMap 和 HashSet) 与此类一起正常工作。
|
||||
虽然 `Object` 是一个具体的类,但它主要是为继承而设计的。它的所有非 final 方法(equals、hashCode、toString、clone 和 finalize)都有清晰的通用约定( general contracts),因为它们被设计为被子类重写。任何类要重写这些方法时,都有义务去遵从它们的通用约定;如果不这样做,将会阻止其他依赖于约定的类 (例如 HashMap 和 HashSet) 与此类一起正常工作。
|
||||
|
||||
本章论述何时以及如何重写 `Object` 类的非 final 的方法。这一章省略了 finalize 方法,因为它在条目 8 中进行了讨论。`Comparable.compareTo` 方法虽然不是 `Object` 中的方法,因为具有很多的相似性,所以也在这里讨论。
|
||||
|
||||
@@ -79,7 +79,6 @@ System.out.println(s.equals(cis)); // false
|
||||
|
||||
```java
|
||||
List<CaseInsensitiveString> list = new ArrayList<>();
|
||||
list.add(cis);List<CaseInsensitiveString> list = new ArrayList<>();
|
||||
list.add(cis);
|
||||
```
|
||||
|
||||
@@ -350,8 +349,8 @@ public final class PhoneNumber {
|
||||
以下是一些最后提醒:
|
||||
|
||||
1. **当重写 equals 方法时,同时也要重写 hashCode 方法(详见第 11 条)**。
|
||||
2. **不要让 equals 方法试图太聪明。**如果只是简单地测试用于相等的属性,那么要遵守 equals 约定并不困难。如果你在寻找相等方面过于激进,那么很容易陷入麻烦。一般来说,考虑到任何形式的别名通常是一个坏主意。例如,File 类不应该试图将引用的符号链接等同于同一文件对象。幸好 File 类并没这么做。
|
||||
3. **在 equal 时方法声明中,不要将参数 Object 替换成其他类型。**对于程序员来说,编写一个看起来像这样的 equals 方法并不少见,然后花上几个小时苦苦思索为什么它不能正常工作:在 equal 时方法声明中,不要将参数 Object 替换成其他类型。对于程序员来说,编写一个看起来像这样的 equals 方法并不少见,然后花上几个小时苦苦思索为什么它不能正常工作。
|
||||
2. **不要让 equals 方法试图太聪明。** 如果只是简单地测试用于相等的属性,那么要遵守 equals 约定并不困难。如果你在寻找相等方面过于激进,那么很容易陷入麻烦。一般来说,考虑到任何形式的别名通常是一个坏主意。例如,File 类不应该试图将引用的符号链接等同于同一文件对象。幸好 File 类并没这么做。
|
||||
3. **在 equal 时方法声明中,不要将参数 Object 替换成其他类型。** 对于程序员来说,编写一个看起来像这样的 equals 方法并不少见,然后花上几个小时苦苦思索为什么它不能正常工作:在 equal 时方法声明中,不要将参数 Object 替换成其他类型。对于程序员来说,编写一个看起来像这样的 equals 方法并不少见,然后花上几个小时苦苦思索为什么它不能正常工作。
|
||||
|
||||
```java
|
||||
// Broken - parameter type must be Object!
|
||||
|
||||
@@ -56,7 +56,7 @@ public interface Comparable<T> {
|
||||
public final class CaseInsensitiveString
|
||||
implements Comparable<CaseInsensitiveString> {
|
||||
public int compareTo(CaseInsensitiveString cis) {
|
||||
return String.CASE_INSENSITIVE_[ORDER.compare(s](http://ORDER.compare(s), cis.s);
|
||||
return String.CASE_INSENSITIVE_ORDER.compare(s, cis.s);
|
||||
}
|
||||
... // Remainder omitted
|
||||
}
|
||||
@@ -71,11 +71,11 @@ public final class CaseInsensitiveString
|
||||
```java
|
||||
// Multiple-field `Comparable` with primitive fields
|
||||
public int compareTo(PhoneNumber pn) {
|
||||
int result = [Short.compare(areaCode](http://Short.compare(areaCode), pn.areaCode);
|
||||
if (result == 0) {
|
||||
result = [Short.compare(prefix](http://Short.compare(prefix), pn.prefix);
|
||||
int result = Short.compare(areaCode, pn.areaCode);
|
||||
if (result == 0) {
|
||||
result = Short.compare(prefix, pn.prefix);
|
||||
if (result == 0)
|
||||
result = [Short.compare(lineNum](http://Short.compare(lineNum), pn.lineNum);
|
||||
result = Short.compare(lineNum, pn.lineNum);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# 15. 使类和成员的可访问性最小化
|
||||
|
||||
将设计良好的组件与设计不佳的组件区分开来的最重要的因素是,组件将其内部数据和其他组件的其他实现细节隐藏起来。一个设计良好的组件隐藏了它的所有实现细节,干净地将它的 API 与它的实现分离开来。然后,组件只通过它们的 API 进行通信,并且对彼此的内部工作一无所知。这一概念,被称为信息隐藏或封装,是软件设计的基本原则[Parnas72]。
|
||||
将设计良好的组件与设计不佳的组件区分开来的最重要的因素是,隐藏内部数据和其他实现细节的程度。一个设计良好的组件隐藏了它的所有实现细节,干净地将它的 API 与它的实现分离开来。然后,组件只通过它们的 API 进行通信,并且对彼此的内部工作一无所知。这一概念,被称为信息隐藏或封装,是软件设计的基本原则[Parnas72]。
|
||||
|
||||
信息隐藏很重要有很多原因,其中大部分来源于它将组成系统的组件分离开来,允许它们被独立地开发,测试,优化,使用,理解和修改。这加速了系统开发,因为组件可以并行开发。它减轻了维护的负担,因为可以更快速地理解组件,调试或更换组件,而不用担心损害其他组件。虽然信息隐藏本身并不会导致良好的性能,但它可以有效地进行性能调整:一旦系统完成并且分析确定了哪些组件导致了性能问题(条目 67),则可以优化这些组件,而不会影响别人的正确的组件。信息隐藏增加了软件重用,因为松耦合的组件通常在除开发它们之外的其他环境中证明是有用的。最后,隐藏信息降低了构建大型系统的风险,因为即使系统不能运行,各个独立的组件也可能是可用的。
|
||||
|
||||
@@ -8,18 +8,18 @@
|
||||
|
||||
经验法则很简单:**让每个类或成员尽可能地不可访问。** 换句话说,使用尽可能低的访问级别,与你正在编写的软件的对应功能保持一致。
|
||||
|
||||
对于顶层(非嵌套的)类和接口,只有两个可能的访问级别:包级私有(package-private)和公共的(public)。如果你使用 public 修饰符声明顶级类或接口,那么它是公开的;否则,它是包级私有的。如果一个顶层类或接口可以被做为包级私有,那么它应该是。通过将其设置为包级私有,可以将其作为实现的一部分,而不是导出的 API,你可以修改它、替换它,或者在后续版本中消除它,而不必担心损害现有的客户端。如果你把它公开,你就有义务永远地支持它,以保持兼容性。
|
||||
对于顶层(非嵌套的)类和接口,只有两个可能的访问级别:包级私有(package-private)和公共的(public)。如果你使用 public 修饰符声明顶级类或接口,那么它是公开的;否则,它是包级私有的。如果一个顶层类或接口可以被做为包级私有,那么它应该是。通过将其设置为包级私有,可以将其作为实现的一部分,而不是导出的 API,你可以修改它、替换它,或者在后续版本中消除它,而不必担心损害现有的客户端。如果你把它公开,你就有义务永远地支持它,以保持兼容性。
|
||||
|
||||
如果一个包级私有顶级类或接口只被一个类使用,那么可以考虑这个类作为使用它的唯一类的私有静态嵌套类 (条目 24)。这将它的可访问性从包级的所有类减少到使用它的一个类。但是,减少不必要的公共类的可访问性要比包级私有的顶级类更重要:公共类是包的 API 的一部分,而包级私有的顶级类已经是这个包实现的一部分了。
|
||||
如果一个包级私有顶级类或接口只被一个类使用,那么可以考虑这个类作为使用它的唯一类的私有静态嵌套类 (详见第 24 条)。这将它的可访问性从包级的所有类减少到使用它的一个类。但是,减少不必要的公共类的可访问性要比包级私有的顶级类更重要:公共类是包的 API 的一部分,而包级私有的顶级类已经是这个包实现的一部分了。
|
||||
|
||||
对于成员 (属性、方法、嵌套类和嵌套接口),有四种可能的访问级别,在这里,按照可访问性从小到大列出:
|
||||
对于成员(字段、方法、嵌套类和嵌套接口),有四种可能的访问级别,在这里,按照可访问性从小到大列出:
|
||||
|
||||
- private —— 该成员只能在声明它的顶级类内访问。
|
||||
- package-private —— 成员可以从被声明的包中的任何类中访问。从技术上讲,如果没有指定访问修饰符 (接口成员除外,它默认是公共的),这是默认访问级别。
|
||||
- protected —— 成员可以从被声明的类的子类中访问(受一些限制,JLS,6.6.2),以及它声明的包中的任何类。
|
||||
- package-private —— 成员可以从被声明的包中的任何类中访问。从技术上讲,如果没有指定访问修饰符(接口成员除外,它默认是公共的),这是默认访问级别。
|
||||
- protected —— 成员可以从被声明的类的子类中访问(会受一些限制 [JLS, 6.6.2]),以及它声明的包中的任何类。
|
||||
- public —— 该成员可以从任何地方被访问。
|
||||
|
||||
在仔细设计你的类的公共 API 之后,你的反应应该是让所有其他成员设计为私有的。 只有当同一个包中的其他类真的需要访问成员时,需要删除私有修饰符,从而使成员包成为包级私有的。 如果你发现自己经常这样做,你应该重新检查你的系统的设计,看看另一个分解可能产生更好的解耦的类。 也就是说,私有成员和包级私有成员都是类实现的一部分,通常不会影响其导出的 API。 但是,如果类实现 Serializable 接口(详见第 86 和 87 条),则这些属性可以「泄漏(leak)」到导出的 API 中。
|
||||
在仔细设计你的类的公共 API 之后,你的反应应该是让所有其他成员设计为私有的。 只有当同一个包中的其他类真的需要访问成员时,需要删除私有修饰符,从而使成员包成为包级私有的。 如果你发现自己经常这样做,你应该重新检查你的系统的设计,看看另一个分解可能产生更好的解耦的类。 也就是说,私有成员和包级私有成员都是类实现的一部分,通常不会影响其导出的 API。 但是,如果类实现 Serializable 接口(详见第 86 和 87 条),则这些字段可以「泄漏(leak)」到导出的 API 中。
|
||||
|
||||
对于公共类的成员,当访问级别从包私有到受保护级时,可访问性会大大增加。 受保护(protected)的成员是类导出的 API 的一部分,并且必须永远支持。 此外,导出类的受保护成员表示对实现细节的公开承诺(详见第 19 条)。 对受保护成员的需求应该相对较少。
|
||||
|
||||
@@ -27,25 +27,22 @@
|
||||
|
||||
为了便于测试你的代码,你可能会想要让一个类,接口或者成员更容易被访问。 这没问题。 为了测试将公共类的私有成员指定为包级私有是可以接受的,但是提高到更高的访问级别却是不可接受的。 换句话说,将类,接口或成员作为包级导出的 API 的一部分来促进测试是不可接受的。 幸运的是,这不是必须的,因为测试可以作为被测试包的一部分运行,从而获得对包私有元素的访问。
|
||||
|
||||
**公共类的实例属性很少公开(详见第 16 条)。** 如果一个实例属性是非 final 的,或者是对可变对象的引用,那么通过将其公开,你就放弃了限制可以存储在属性中的值的能力。这意味着你放弃了执行涉及该属性的不变量的能力。另外,当属性被修改时,就放弃了采取任何操作的能力,**因此公共可变属性的类通常不是线程安全的** 。即使属性是 final 的,并且引用了一个不可变的对象,通过使它公开,你就放弃切换到不存在属性的新的内部数据表示的灵活性。
|
||||
**公共类的实例字段很少情况下采用 public 修饰(详见第 16 条)。** 如果一个实例字段是非 final 的,或者是对可变对象的引用,那么通过将其公开,你就放弃了限制可以存储在字段中的值的能力。这意味着你放弃了执行涉及该字段的不变量的能力。另外,当字段被修改时,就放弃了采取任何操作的能力,**因此带有公共可变字段的类通常不是线程安全的** 。即使一个字段是 final 的,并且引用了一个不可变的对象,通过将其公开,你放弃了切换到一个新的内部数据表示的灵活性,而该字段并不存在。
|
||||
|
||||
同样的建议适用于静态属性,但有一个例外。 假设常量是类的抽象的一个组成部分,你可以通过 public static final 属性暴露常量。 按照惯例,这些属性的名字由大写字母组成,字母用下划线分隔(详见第 68 条)。 很重要的一点是,这些属性包含基本类型的值或对不可变对象的引用(详见第 17 条)。 包含对可变对象的引用的属性具有非 final 属性的所有缺点。 虽然引用不能被修改,但引用的对象可以被修改,并会带来灾难性的结果。
|
||||
同样的建议适用于静态字段,但有一个例外。 假设常量是类的抽象的一个组成部分,你可以通过 public static final 字段暴露常量。 按照惯例,这些字段的名字由大写字母组成,字母用下划线分隔(详见第 68 条)。 很重要的一点是,这些字段包含基本类型的值或对不可变对象的引用(详见第 17 条)。 包含对可变对象的引用的字段具有非 final 字段的所有缺点。 虽然引用不能被修改,但引用的对象可以被修改,并会带来灾难性的结果。
|
||||
|
||||
请注意,非零长度的数组总是可变的,**所以类具有公共静态 final 数组属性,或返回这样一个属性的访问器是错误的。** 如果一个类有这样的属性或访问方法,客户端将能够修改数组的内容。 这是安全漏洞的常见来源:
|
||||
请注意,非零长度的数组总是可变的,**所以类具有公共静态 final 数组字段,或返回这样一个字段的访问器是错误的。** 如果一个类有这样的字段或访问方法,客户端将能够修改数组的内容。 这是安全漏洞的常见来源:
|
||||
|
||||
```java
|
||||
// Potential security hole!
|
||||
public static final Thing[] VALUES = { ... };
|
||||
```
|
||||
|
||||
要小心这样的事实,一些 IDE 生成的访问方法返回对私有数组属性的引用,导致了这个问题。 有两种方法可以解决这个问题。 你可以使公共数组私有并添加一个公共的不可变列表:
|
||||
要小心这样的事实,一些 IDE 生成的访问方法返回对私有数组字段的引用,导致了这个问题。 有两种方法可以解决这个问题。 你可以使公共数组私有并添加一个公共的不可变列表:
|
||||
|
||||
```java
|
||||
private static final Thing[] PRIVATE_VALUES = { ... };
|
||||
|
||||
public static final List<Thing> VALUES =
|
||||
|
||||
Collections.unmodifiableList(Arrays.asList(PRIVATE_VALUES));
|
||||
public static final List<Thing> VALUES = Collections.unmodifiableList(Arrays.asList(PRIVATE_VALUES));
|
||||
```
|
||||
|
||||
或者,可以将数组设置为 private,并添加一个返回私有数组拷贝的公共方法:
|
||||
@@ -66,4 +63,4 @@ public static final Thing[] values() {
|
||||
|
||||
对于典型的 Java 程序员来说,不仅程序模块所提供的访问保护存在局限性,而且在本质上是很大程度上建议性的;为了利用它,你必须把你的包组合成模块,在模块声明中明确所有的依赖关系,重新安排你的源码树层级,并采取特殊的行动来适应你的模块内任何对非模块化包的访问[Reinhold, 3]。 现在说模块是否会在 JDK 之外得到广泛的使用还为时尚早。 与此同时,除非你有迫切的需要,否则似乎最好避免它们。
|
||||
|
||||
总而言之,应该尽可能地减少程序元素的可访问性(在合理范围内)。 在仔细设计一个最小化的公共 API 之后,你应该防止任何散乱的类,接口或成员成为 API 的一部分。 除了作为常量的公共静态 `final` 属性之外,公共类不应该有公共属性。 确保 `public static final` 属性引用的对象是不可变的。
|
||||
总而言之,应该尽可能地减少程序元素的可访问性(在合理范围内)。 在仔细设计一个最小化的公共 API 之后,你应该防止任何散乱的类,接口或成员成为 API 的一部分。 除了作为常量的公共静态 `final` 字段之外,公共类不应该有公共字段。 确保 `public static final` 字段引用的对象是不可变的。
|
||||
|
||||
@@ -39,7 +39,7 @@ class Point {
|
||||
|
||||
但是,**如果一个类是包级私有的,或者是一个私有的内部类,那么暴露它的数据属性就没有什么本质上的错误——假设它们提供足够描述该类提供的抽象。** 在类定义和使用它的客户端代码中,这种方法比访问方法产生更少的视觉混乱。 虽然客户端代码绑定到类的内部表示,但是这些代码仅限于包含该类的包。 如果类的内部表示是可取的,可以在不触碰包外的任何代码的情况下进行更改。 在私有内部类的情况下,更改作用范围进一步限制在封闭类中。
|
||||
|
||||
Java 平台类库中的几个类违反了公共类不应直接暴露属性的建议。 着名的例子包括 `java.awt` 包中的 `Point` 和 `Dimension` 类。 这些类别应该被视为警示性的示例,而不是模仿的例子。 如条目 67 所述,暴露 `Dimension` 的内部结构的决定是一个严重的性能问题,这个问题在今天仍然存在。
|
||||
Java 平台类库中的几个类违反了公共类不应直接暴露属性的建议。 著名的例子包括 `java.awt` 包中的 `Point` 和 `Dimension` 类。 这些类别应该被视为警示性的示例,而不是模仿的例子。 如条目 67 所述,时至今日,暴露 `Dimension` 的内部结构的决定仍然导致着严重的性能问题。
|
||||
|
||||
虽然公共类直接暴露属性并不是一个好主意,但是如果属性是不可变的,那么危害就不那么大了。当一个属性是只读的时候,除了更改类的 API 外,你不能改变类的内部表示形式,也不能采取一些辅助的行为,但是可以加强不变性。例如,下面的例子中保证每个实例表示一个有效的时间:
|
||||
|
||||
@@ -66,4 +66,4 @@ public final class Time {
|
||||
}
|
||||
```
|
||||
|
||||
总之,公共类不应该暴露可变属性。 公共累暴露不可变属性的危害虽然仍然存在问题,但其危害较小。 然而,有时需要包级私有或私有内部类来暴露属性,无论此类是否是可变的。
|
||||
总之,公共类不应该暴露可变属性。 公共类暴露不可变属性的危害虽然仍然存在问题,但其危害较小。 然而,有时需要包级私有或私有内部类来暴露属性,无论此类是否是可变的。
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# 18. 组合优于继承
|
||||
|
||||
继承是实现代码重用的有效方式,但并不总是最好的工具。使用不当,会导致脆弱的软件。 在包中使用继承是安全的,其中子类和父类的实现都在同一个程序员的控制之下。对应专门为了继承而设计的,并且有文档说明的类来说(详见第 19 条),使用继承也是安全的。 然而,从普通的具体类跨越包级边界继承,是危险的。 提醒一下,本书使用“继承”一词来表示实现继承(当一个类继承另一个类时)。 在这个项目中讨论的问题不适用于接口继承(当类实现接口或当接口继承另一个接口时)。
|
||||
继承是实现代码重用的有效方式,但并不总是最好的工具。使用不当,会导致脆弱的软件。 在包中使用继承是安全的,其中子类和父类的实现都在同一个程序员的控制之下。对应专门为了继承而设计的,并且有文档说明的类来说(详见第 19 条),使用继承也是安全的。 然而,从普通的具体类跨越包级边界继承,是危险的。 提醒一下,本书使用「继承」一词,其含义是*实现继承*(当一个类扩展另一个类时)。 在本条目中讨论的问题不适用于*接口继承*(当类实现接口,或者当接口继承另一个接口时)。
|
||||
|
||||
**与方法调用不同,继承打破了封装[Snyder86]。** 换句话说,一个子类依赖于其父类的实现细节来保证其正确的功能。 父类的实现可能会从发布版本不断变化,如果是这样,子类可能会被破坏,即使它的代码没有任何改变。 因此,一个子类必须与其超类一起更新而变化,除非父类的作者为了继承的目的而专门设计它,并对应有文档的说明。
|
||||
|
||||
@@ -44,7 +44,7 @@ s.addAll(List.of("Snap", "Crackle", "Pop"));
|
||||
|
||||
稍微好一点的做法是,重写 `addAll` 方法遍历指定集合,为每个元素调用 `add` 方法一次。 不管 `HashSet` 的 `addAll` 方法是否在其 `add` 方法上实现,都会保证正确的结果,因为 `HashSet` 的 `addAll` 实现将不再被调用。然而,这种技术并不能解决所有的问题。 这相当于重新实现了父类方法,这样的方法可能不能确定到底是否时自用(self-use)的,实现起来也是困难的,耗时的,容易出错的,并且可能会降低性能。 此外,这种方式并不能总是奏效,因为子类无法访问一些私有属性,所以有些方法就无法实现。
|
||||
|
||||
导致子类脆弱的一个相关原因是,它们的父类在后续的发布版本中可以添加新的方法。假设一个程序的安全性依赖于这样一个事实:所有被插入到集中的元素都满足一个先决条件。可以通过对集合进行子类化,然后并重写所有添加元素的方法,以确保在添加每个元素之前满足这个先决条件,来确保这一问题。如果在后续的版本中,父类没有新增添加元素的方法,那么这样做没有问题。但是,一旦父类增加了这样的新方法,则很有肯能由于调用了未被重写的新方法,将非法的元素添加到子类的实例中。这不是个纯粹的理论问题。在把 `Hashtable` 和 `Vector` 类加入到 `Collections` 框架中的时候,就修复了几个类似性质的安全漏洞。
|
||||
导致子类脆弱的一个相关原因是,它们的父类在后续的发布版本中可以添加新的方法。假设一个程序的安全性依赖于这样一个事实:所有被插入到集中的元素都满足一个先决条件。可以通过对集合进行子类化,然后并重写所有添加元素的方法,以确保在添加每个元素之前满足这个先决条件,来确保这一问题。如果在后续的版本中,父类没有新增添加元素的方法,那么这样做没有问题。但是,一旦父类增加了这样的新方法,则很有可能由于调用了未被重写的新方法,将非法的元素添加到子类的实例中。这不是个纯粹的理论问题。在把 `Hashtable` 和 `Vector` 类加入到 `Collections` 框架中的时候,就修复了几个类似性质的安全漏洞。
|
||||
|
||||
这两个问题都源于重写方法。 如果仅仅添加新的方法并且不要重写现有的方法,可能会认为继承一个类是安全的。 虽然这种扩展更为安全,但这并非没有风险。 如果父类在后续版本中添加了一个新的方法,并且你不幸给了子类一个具有相同签名和不同返回类型的方法,那么你的子类编译失败[JLS,8.4.8.3]。 如果已经为子类提供了一个与新的父类方法具有相同签名和返回类型的方法,那么你现在正在重写它,因此将遇到前面所述的问题。 此外,你的方法是否会履行新的父类方法的约定,这是值得怀疑的,因为在你编写子类方法时,这个约定还没有写出来。
|
||||
|
||||
@@ -177,11 +177,11 @@ static void walk(Set<Dog> dogs) {
|
||||
}
|
||||
```
|
||||
|
||||
`InstrumentedSet` 类被称为包装类,因为每个 `InstrumentedSet` 实例都包含(“包装”)另一个 Set 实例。 这也被称为装饰器模式[Gamma95],因为 `InstrumentedSet` 类通过添加计数功能来“装饰”一个集合。 有时组合和转发的结合被不精确地地称为委托(delegation)。 从技术上讲,除非包装对象把自身传递给被包装对象,否则不是委托[Lieberman86;Gamma95]。
|
||||
`InstrumentedSet` 类被称为包装类,因为每个 `InstrumentedSet` 实例都包含(“包装”)另一个 Set 实例。 这也被称为装饰器模式[Gamma95],因为 `InstrumentedSet` 类通过添加计数功能来“装饰”一个集合。 有时组合和转发的结合被不精确地地称为委托(delegation)。 从技术上讲,除非包装对象把自身传递给被包装对象,否则不是委托[Lieberman86; Gamma95]。
|
||||
|
||||
包装类的缺点很少。 一个警告是包装类不适合在回调框架(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 @@
|
||||
# 23. 类层次结构优于标签类
|
||||
|
||||
有时你可能会碰到一个类,它的实例有两个或更多的风格,并且包含一个标签属性(tag field),表示实例的风格。 例如,考虑这个类,它可以表示一个圆形或矩形:
|
||||
有时你可能会碰到一个类,它的实例有两个或更多的风格,并且包含一个标签字段(tag field),表示实例的风格。 例如,考虑这个类,它可以表示一个圆形或矩形:
|
||||
|
||||
```java
|
||||
// Tagged class - vastly inferior to a class hierarchy!
|
||||
@@ -43,13 +43,13 @@ class Figure {
|
||||
}
|
||||
```
|
||||
|
||||
这样的标签类具有许多缺点。 他们杂乱无章的样板代码,包括枚举声明,标签属性和 `switch` 语句。 可读性更差,因为多个实现在一个类中混杂在一起。 内存使用增加,因为实例负担属于其他风格不相关的领域。 属性不能成为 final,除非构造方法初始化不相关的属性,导致更多的样板代码。 构造方法在编译器的帮助下,必须设置标签属性并初始化正确的数据属性:如果初始化错误的属性,程序将在运行时失败。 除非可以修改其源文件,否则不能将其添加到标记的类中。 如果你添加一个风格,你必须记得给每个 `switch` 语句添加一个 `case`,否则这个类将在运行时失败。 最后,一个实例的数据类型没有提供任何关于风格的线索。 总之,**标签类是冗长的,容易出错的,而且效率低下。**
|
||||
这样的标签类具有许多缺点。 它们充斥着杂乱无章的样板代码,包括枚举声明,标签字段和 `switch` 语句。 可读性更差,因为多个实现在一个类中混杂在一起。 内存使用增加,因为实例负担属于其他风格不相关的领域。 字段不能成为 final,除非构造方法初始化不相关的字段,导致更多的样板代码。 构造方法在编译器的帮助下,必须设置标签字段并初始化正确的数据字段:如果初始化错误的字段,程序将在运行时失败。 除非可以修改其源文件,否则不能将其添加到标记的类中。 如果你添加一个风格,你必须记得给每个 `switch` 语句添加一个 `case`,否则这个类将在运行时失败。 最后,一个实例的数据类型没有提供任何关于风格的线索。 总之,**标签类是冗长的,容易出错的,而且效率低下。**
|
||||
|
||||
幸运的是,像 Java 这样的面向对象的语言为定义一个能够表示多种风格对象的单一数据类型提供了更好的选择:子类型化(subtyping)。标签类仅仅是一个类层次的简单的模仿。
|
||||
|
||||
要将标签类转换为类层次,首先定义一个包含抽象方法的抽象类,该标签类的行为取决于标签值。 在 `Figure` 类中,只有一个这样的方法,就是 `area` 方法。 这个抽象类是类层次的根。 如果有任何方法的行为不依赖于标签的值,把它们放在这个类中。 同样,如果有所有的方法使用的数据属性,把它们放在这个类。`Figure` 类中不存在这种与类型无关的方法或属性。
|
||||
要将标签类转换为类层次,首先定义一个包含抽象方法的抽象类,该标签类的行为取决于标签值。 在 `Figure` 类中,只有一个这样的方法,就是 `area` 方法。 这个抽象类是类层次的根。 如果有任何方法的行为不依赖于标签的值,把它们放在这个类中。 同样,如果有所有的方法使用的数据字段,把它们放在这个类。`Figure` 类中不存在这种与类型无关的方法或字段。
|
||||
|
||||
接下来,为原始标签类的每种类型定义一个根类的具体子类。 在我们的例子中,有两个类型:圆形和矩形。 在每个子类中包含特定于改类型的数据字段。 在我们的例子中,半径属性是属于圆的,长度和宽度属性都是矩形的。 还要在每个子类中包含根类中每个抽象方法的适当实现。 这里是对应于 `Figure` 类的类层次:
|
||||
接下来,为原始标签类的每种类型定义一个根类的具体子类。 在我们的例子中,有两个类型:圆形和矩形。 在每个子类中包含特定于改类型的数据字段。 在我们的例子中,半径字段是属于圆的,长度和宽度字段都是矩形的。 还要在每个子类中包含根类中每个抽象方法的适当实现。 这里是对应于 `Figure` 类的类层次:
|
||||
|
||||
```java
|
||||
// Class hierarchy replacement for a tagged class
|
||||
@@ -76,7 +76,7 @@ class Rectangle extends Figure {
|
||||
}
|
||||
```
|
||||
|
||||
这个类层次纠正了之前提到的标签类的每个缺点。 代码简单明了,不包含原文中的样板文件。 每种类型的实现都是由自己的类来分配的,而这些类都没有被无关的数据属性所占用。 所有的属性是 `final` 的。 编译器确保每个类的构造方法初始化其数据属性,并且每个类都有一个针对在根类中声明的每个抽象方法的实现。 这消除了由于缺少 `switch-case` 语句而导致的运行时失败的可能性。 多个程序员可以独立地继承类层次,并且可以相互操作,而无需访问根类的源代码。 每种类型都有一个独立的数据类型与之相关联,允许程序员指出变量的类型,并将变量和输入参数限制为特定的类型。
|
||||
这个类层次纠正了之前提到的标签类的每个缺点。 代码简单明了,不包含原文中的样板文件。 每种类型的实现都是由自己的类来分配的,而这些类都没有被无关的数据字段所占用。 所有的字段是 `final` 的。 编译器确保每个类的构造方法初始化其数据字段,并且每个类都有一个针对在根类中声明的每个抽象方法的实现。 这消除了由于缺少 `switch-case` 语句而导致的运行时失败的可能性。 多个程序员可以独立地继承类层次,并且可以相互操作,而无需访问根类的源代码。 每种类型都有一个独立的数据类型与之相关联,允许程序员指出变量的类型,并将变量和输入参数限制为特定的类型。
|
||||
|
||||
类层次的另一个优点是可以使它们反映类型之间的自然层次关系,从而提高了灵活性,并提高了编译时类型检查的效率。 假设原始示例中的标签类也允许使用正方形。 类层次可以用来反映一个正方形是一种特殊的矩形(假设它们是不可变的):
|
||||
|
||||
@@ -87,9 +87,9 @@ class Square extends Rectangle {
|
||||
}
|
||||
}
|
||||
```
|
||||
请注意,上述层中的属性是直接访问的,而不是访问方法。 这里是为了简洁起见,如果类层次是公开的(详见第 16 条),这将是一个糟糕的设计。
|
||||
请注意,上述层次结构中的字段是直接访问的,而不是通过访问器方法访问的。 这里是为了简洁起见,如果类层次是公开的(详见第 16 条),这将是一个糟糕的设计。
|
||||
|
||||
总之,标签类很少有适用的情况。 如果你想写一个带有明显标签属性的类,请考虑标签属性是否可以被删除,而类是否被类层次替换。 当遇到一个带有标签属性的现有类时,可以考虑将其重构为一个类层次中。
|
||||
总之,标签类很少有适用的情况。 如果你想写一个带有显式标签字段的类,请考虑标签字段是否可以被删除,并是否能被类层次结构替换。 当遇到一个带有标签字段的现有类时,可以考虑将其重构为一个类层次结构。
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ Set<Lark> exaltation = new HashSet<>();
|
||||
|
||||
但一些警告更难以消除。 本章充满了这种警告的例子。 当你收到需要进一步思考的警告时,坚持不懈! **尽可能地消除每一个未经检查的警告。** 如果你消除所有的警告,你可以放心,你的代码是类型安全的,这是一件非常好的事情。 这意味着在运行时你将不会得到一个 `ClassCastException` 异常,并且增加了你的程序将按照你的意图行事的信心。
|
||||
|
||||
**如果你不能消除警告,但你可以证明引发警告的代码是类型安全的,那么(并且只能这样)用 `@SuppressWarnings(“unchecked”)` 注解来抑制警告。** 如果你在没有首先证明代码是类型安全的情况下压制警告,那么你给自己一个错误的安全感。 代码可能会在不发出任何警告的情况下进行编译,但是它仍然可以在运行时抛出 `ClassCastException` 异常。 但是,如果你忽略了你认为是安全的未经检查的警告(而不是抑制它们),那么当一个新的警告出现时,你将不会注意到这是一个真正的问题。 新出现的警告就会淹没在所有的错误警告当中。
|
||||
**如果你不能消除警告,但你可以证明引发警告的代码是类型安全的,那么(并且只能这样)用 `@SuppressWarnings("unchecked")` 注解来抑制警告。** 如果你在没有首先证明代码是类型安全的情况下压制警告,那么你给自己一个错误的安全感。 代码可能会在不发出任何警告的情况下进行编译,但是它仍然可以在运行时抛出 `ClassCastException` 异常。 但是,如果你忽略了你认为是安全的未经检查的警告(而不是抑制它们),那么当一个新的警告出现时,你将不会注意到这是一个真正的问题。 新出现的警告就会淹没在所有的错误警告当中。
|
||||
|
||||
`SuppressWarnings` 注解可用于任何声明,从单个局部变量声明到整个类。 始终在尽可能最小的范围内使用 `SuppressWarnings` 注解。 通常这是一个变量声明或一个非常短的方法或构造方法。 切勿在整个类上使用 `SuppressWarnings` 注解。 这样做可能会掩盖重要的警告。
|
||||
|
||||
|
||||
@@ -130,4 +130,5 @@ public class Chooser<T> {
|
||||
|
||||
这个版本有些冗长,也许运行比较慢,但是值得一提的是,在运行时不会得到 `ClassCastException` 异常。
|
||||
|
||||
总之,数组和泛型具有非常不同的类型规则。 数组是协变和具体化的; 泛型是不变的,类型擦除的。 因此,数组提供运行时类型的安全性,但不提供编译时类型的安全性,反之亦然。 一般来说,数组和泛型不能很好地混合工作。 如果你发现把它们混合在一起,得到编译时错误或者警告,你的第一个冲动应该是用列表来替换数组。
|
||||
总之,数组和泛型具有非常不同的类型规则。 数组是协变和具体化的; 泛型是不变的,类型擦除的。 因此,数组提供运行时类型的安全性,但不提供编译时类型的安全性,反之亦然。 一般来说,数组和泛型不能很好地混合工作。 如果你发现把它们混合在一起,得到编译时错误或者警告,你的第一个冲动应该是用列表来替换数组。
|
||||
|
||||
|
||||
@@ -277,4 +277,5 @@ if (m.isAnnotationPresent(ExceptionTest.class)
|
||||
|
||||
这个项目中的测试框架只是一个演示,但它清楚地表明了注解相对于命名模式的优越性,而且它仅仅描绘了你可以用它们做什么的外观。 如果编写的工具要求程序员将信息添加到源代码中,请定义适当的注解类型。 **当可以使用注解代替时,没有理由使用命名模式。**
|
||||
|
||||
这就是说,除了特定的开发者(toolsmith)之外,大多数程序员都不需要定义注解类型。 **但所有程序员都应该使用 Java 提供的预定义注解类型**(详见第 27 和 40 条)。 另外,请考虑使用 IDE 或静态分析工具提供的注解。 这些注解可以提高这些工具提供的诊断信息的质量。 但请注意,这些注解尚未标准化,因此如果切换工具或标准出现,可能额外需要做一些工作。
|
||||
这就是说,除了特定的开发者(toolsmith)之外,大多数程序员都不需要定义注解类型。 **但所有程序员都应该使用 Java 提供的预定义注解类型**(详见第 27 和 40 条)。 另外,请考虑使用 IDE 或静态分析工具提供的注解。 这些注解可以提高这些工具提供的诊断信息的质量。 但请注意,这些注解尚未标准化,因此如果切换工具或标准出现,可能额外需要做一些工作。
|
||||
|
||||
|
||||
@@ -1,24 +1,22 @@
|
||||
# 41. 使用标记接口定义类型
|
||||
|
||||
标记接口(marker interface),不包含方法声明,只是指定(或「标记」)一个类实现了具有某些属性的接口。 例如,考虑 `Serializable` 接口(第 12 章)。通过实现这个接口,一个类表明它的实例可以写入 `ObjectOutputStream`(或「序列化」)。
|
||||
标记接口(marker interface),是不包含方法声明的接口,只是指定(或「标记」)一个类实现了具有某些属性的接口。 例如,考虑 `Serializable` 接口(第 12 章)。通过实现这个接口,一个类表明它的实例可以写入 `ObjectOutputStream`(或被「序列化」)。
|
||||
|
||||
你可能会听说过标记注解(详见第 39 条)标记一个接口是废弃过时的。 这个断言是不正确的。 标记接口与标记注解相比具有两个优点。
|
||||
你可能会听说过标记注解(详见第 39 条)使得标记接口过时了。 这个断言是不正确的。 标记接口与标记注解相比具有两个优点。 首先,也是最重要的一点,**标记接口定义了一个由标记类实例实现的类型;标记注解则没有定义这样的类型。** 标记接口类型的存在允许在编译时捕获错误,如果使用标记注解,则直到运行时才能捕获错误。
|
||||
|
||||
首先,**标记接口定义了一个由标记类实例实现的类型;标记注解则不会。** 标记接口类型的存在允许在编译时捕获错误,如果使用标记注解,则直到运行时才能捕获错误。
|
||||
Java 的序列化机制(第 6 章)使用 `Serializable` 标记接口来指示某个类型是可序列化的。 对传递给它的对象进行序列化的 `ObjectOutputStream.writeObject` 方法要求其参数可序列化。 如果此方法的参数是 `Serializable` 类型,则在编译时会检测到序列化不适当对象的尝试(通过类型检查)。 编译时错误检测是标记接口的意图,但不幸的是,`ObjectOutputStream.writeObject` API 没有利用 `Serializable` 接口:它的参数被声明为 `Object` 类型,所以尝试序列化一个不可序列化的对象直到运行时才会失败。
|
||||
|
||||
Java 的序列化机制(第 6 章)使用 `Serializable` 标记接口来指示某个类型是可序列化的。 对传递给它的对象进行序列化的 `ObjectOutputStream.writeObject` 方法要求其参数可序列化。 如果此方法的参数是 `Serializable` 类型,则在编译时会检测到序列化不适当对象的尝试(通过类型检查)。 编译时错误检测是标记接口的意图,但不幸的是,`ObjectOutputStream.write` API 没有利用 `Serializable` 接口:它的参数被声明为 `Object` 类型,所以尝试序列化一个不可序列化的对象直到运行时才会失败。
|
||||
|
||||
**标记接口对于标记注解的另一个优点是可以更精确地定位目标。** 如果使用目标 `ElementType.TYPE` 声明注解类型,它可以应用于任何类或接口。 假设有一个标记仅适用于特定接口的实现。 如果将其定义为标记接口,则可以扩展它适用的唯一接口,保证所有标记类型也是适用的唯一接口的子类型。
|
||||
**标记接口对于标记注解的另一个优点是可以更精确地定位目标。** 如果使用目标 `ElementType.TYPE` 声明注解类型,它就可以被应用于任何类或接口。 假设有一个标记仅适用于特定接口的实现。 如果将其定义为标记接口,则可以扩展它适用的唯一接口,保证所有标记类型也是适用的唯一接口的子类型。
|
||||
|
||||
可以说,`Set` 接口就是这样一个受限的标记接口。 它仅适用于 `Collection` 子类型,但不会添加超出 `Collection` 定义的方法。 它通常不被认为是标记接口,因为它改进了几个 `Collection` 方法的契约,包括 `add`,`equals` 和 `hashCode`。 但很容易想象一个标记接口,它仅适用于某些特定接口的子类型,并且不会改进任何接口方法的契约。 这样的标记接口可以描述整个对象的一些约束条件(invariant),或者说明实例有资格被某个其他类的方法处理(就像 `Serializable` 接口指示实例有资格被 `ObjectOutputStream` 处理的方式)。
|
||||
|
||||
标记注解优于标记接口的主要优点是它们是较大的注解工具的一部分。因此,标记注解允许在基于注解的框架中保持一致性。
|
||||
**标记注解优于标记接口的主要优点是它们是更大的注解工具的一部分。**因此,标记注解允许在基于注解的框架中保持一致性。
|
||||
|
||||
所以什么时候应该使用标记注解,什么时候应该使用标记接口?显然,如果标记适用于除类或接口以外的任何程序元素,则必须使用注解,因为只能使用类和接口来实现或扩展接口。如果标记仅适用于类和接口,那么问自己问题:“可能我想编写一个或多个只接受具有此标记的对象的方法呢?”如果是这样,则应该优先使用标记接口而不是注解。这将使你可以将接口用作所讨论方法的参数类型,这将带来编译时类型检查的好处。如果你能说服自己,永远不会想写一个只接受带有标记的对象的方法,那么最好使用标记注解。另外,如果标记是大量使用注解的框架的一部分,则标记注解是明确的选择。
|
||||
所以什么时候应该使用标记注解,什么时候应该使用标记接口?显然,如果标记是应用于除类或接口以外的任何程序元素,则必须使用注解,因为只能使用类和接口来实现或扩展接口。如果标记仅适用于类和接口,那么问自己问题:「可能我想编写一个或多个只接受具有此标记的对象的方法呢?」如果是这样,则应该优先使用标记接口而不是注解。这将使你可以将接口用作所讨论方法的参数类型,这将带来编译时类型检查的好处。如果你能说服自己,永远不会想写一个只接受带有标记的对象的方法,那么最好使用标记注解。另外,如果标记是大量使用注解的框架的一部分,则标记注解是明确的选择。
|
||||
|
||||
总之,标记接口和标记注释都有其用处。 如果你想定义一个没有任何关联的新方法的类型,一个标记接口是一种可行的方法。 如果要标记除类和接口以外的程序元素,或者将标记符合到已经大量使用注解类型的框架中,那么标记注解是正确的选择。 **如果发现自己正在编写目标为 `ElementType.TYPE` 的标记注解类型,请花点时间确定它是否应该是注释类型,是不是标记接口是否更合适。**
|
||||
总之,标记接口和标记注释都有其用处。 如果你想定义一个没有任何关联的新方法的类型,一个标记接口是一种可行的方法。 如果要标记除类和接口以外的程序元素,或者将标记符合到已经大量使用注解类型的框架中,那么标记注解是正确的选择。 **如果发现自己正在编写目标为 `ElementType.TYPE` 的标记注解类型,那么请花时间弄清楚究竟应该用注解类型,还是标记接口更合适。**
|
||||
|
||||
从某种意义来说,本条目与条目 22 的的意思正好相反,条目 22 的意思是:「如果你不想定义一个类型,不要使用接口」。本条目的意思是:如果想定义一个类型,一定要使用接口。
|
||||
从某种意义来说,本条目与条目 22 的的意思正好相反,条目 22 的意思是:「如果你不想定义一个类型,不要使用接口」。本条目的意思是:「如果想定义一个类型,一定要使用接口。」
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -64,4 +64,5 @@ private static void sort(long a[], int offset, int length) {
|
||||
|
||||
不要从本条目中推断出对参数的任意限制都是一件好事。 相反,你应该设计一些方法,使其尽可能通用。 假设方法可以对它接受的所有参数值做一些合理的操作,那么对参数的限制越少越好。 但是,通常情况下,某些限制是正在实现的抽象所固有的。
|
||||
|
||||
总而言之,每次编写方法或构造方法时,都应该考虑对其参数存在哪些限制。 应该记在这些限制,并在方法体的开头使用显式检查来强制执行这些限制。 养成这样做的习惯很重要。 在第一次有效性检查失败时,它所需要的少量工作将会得到对应的回报。
|
||||
总而言之,每次编写方法或构造方法时,都应该考虑对其参数存在哪些限制。 应该记在这些限制,并在方法体的开头使用显式检查来强制执行这些限制。 养成这样做的习惯很重要。 在第一次有效性检查失败时,它所需要的少量工作将会得到对应的回报。
|
||||
|
||||
|
||||
@@ -107,4 +107,5 @@ public Date end() {
|
||||
|
||||
包含方法或构造方法的类,这些方法或构造方法的调用指示控制权的转移,这些类无法防御恶意客户端。 只有当一个类和它的客户之间存在相互信任,或者当对类的不变量造成损害时,除了客户之外,任何人都不会受到损害。 后一种情况的一个例子是包装类模式(详见第 18 条)。 根据包装类的性质,客户端可以通过在包装后直接访问对象来破坏类的不变性,但这通常只会损害客户端。
|
||||
|
||||
总之,如果一个类有从它的客户端获取或返回的可变组件,那么这个类必须防御性地拷贝这些组件。如果拷贝的成本太高,并且类信任它的客户端不会不适当地修改组件,则可以用文档替换防御性拷贝,该文档概述了客户端不得修改受影响组件的责任。
|
||||
总之,如果一个类有从它的客户端获取或返回的可变组件,那么这个类必须防御性地拷贝这些组件。如果拷贝的成本太高,并且类信任它的客户端不会不适当地修改组件,则可以用文档替换防御性拷贝,该文档概述了客户端不得修改受影响组件的责任。
|
||||
|
||||
|
||||
@@ -22,4 +22,5 @@
|
||||
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` 的方法。
|
||||
|
||||
|
||||
@@ -148,4 +148,5 @@ public boolean contentEquals(StringBuffer sb) {
|
||||
|
||||
虽然 Java 类库在很大程度上遵循了这一条目中的建议,但是有一些类违反了它。例如,String 导出两个重载的静态工厂方法`valueOf(char[])`和`valueOf(Object)`,它们在传递相同的对象引用时执行完全不同的操作。对此没有任何正当的理由理由,它应该被视为一种异常现象,有可能造成真正的混乱。
|
||||
|
||||
总而言之,仅仅可以重载方法并不意味着应该这样做。通常,最好避免重载具有相同数量参数的多个签名的方法。在某些情况下,特别是涉及构造方法的情况下,可能无法遵循此建议。在这些情况下,至少应该避免通过添加强制转换将相同的参数集传递给不同的重载。如果这是无法避免的,例如,因为要对现有类进行改造以实现新接口,那么应该确保在传递相同的参数时,所有重载的行为都是相同的。如果做不到这一点,程序员将很难有效地使用重载方法或构造方法,也无法理解为什么它不能工作。
|
||||
总而言之,仅仅可以重载方法并不意味着应该这样做。通常,最好避免重载具有相同数量参数的多个签名的方法。在某些情况下,特别是涉及构造方法的情况下,可能无法遵循此建议。在这些情况下,至少应该避免通过添加强制转换将相同的参数集传递给不同的重载。如果这是无法避免的,例如,因为要对现有类进行改造以实现新接口,那么应该确保在传递相同的参数时,所有重载的行为都是相同的。如果做不到这一点,程序员将很难有效地使用重载方法或构造方法,也无法理解为什么它不能工作。
|
||||
|
||||
|
||||
@@ -64,4 +64,5 @@ public void foo(int a1, int a2, int a3, int... rest) { }
|
||||
|
||||
`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();
|
||||
@@ -24,7 +24,7 @@ if (cheeses != null && cheeses.contains(Cheese.STILTON))
|
||||
System.out.println("Jolly good, just the thing.");
|
||||
```
|
||||
|
||||
在几乎每次使用返回 null 来代替空集合或数组的方法时,都需要使用这种迂回的方式。 它容易出错,因为编写客户端的程序员可能忘记编写特殊情况代码来处理 null 返回。 多年来这种错误可能会被忽视,因为这种方法通常会返回一个或多个对象。 此外,返回 null 代替空容器会使返回容器的方法的实现变得复杂。
|
||||
在几乎每次使用返回 null 来代替空集合或数组的方法时,都需要使用这种迂回的方式。 这样做很容易出错,因为编写客户端的程序员可能忘记编写特殊情况代码来处理 null 返回。 多年来这种错误可能会被忽视,因为这种方法通常会返回一个或多个对象。 此外,返回 null 代替空容器会使返回容器的方法的实现变得复杂。
|
||||
|
||||
有时有人认为,null 返回值比空集合或数组更可取,因为它避免了分配空容器的开销。这个论点有两点是不成立的。首先,除非测量结果表明所讨论的分配是性能问题的真正原因,否则不宜担心此级别的性能(详见第 67 条)。第二,可以在不分配空集合和数组的情况下返回它们。下面是返回可能为空的集合的典型代码。通常,这就是你所需要的:
|
||||
|
||||
@@ -72,4 +72,5 @@ public Cheese[] getCheeses() {
|
||||
return cheesesInStock.toArray(new Cheese[cheesesInStock.size()]);
|
||||
```
|
||||
|
||||
总之,**永远不要返回 null 来代替空数组或集合**。它使你的 API 更难以使用,更容易出错,并且没有性能优势。
|
||||
总之,**永远不要返回 null 来代替空数组或集合**。它使你的 API 更难以使用,更容易出错,并且没有性能优势。
|
||||
|
||||
|
||||
@@ -118,4 +118,5 @@ streamOfOptionals.
|
||||
|
||||
这里留下了一个悬而未决的大问题。在实例中存储 Optional 属性是否合适吗?通常这是一种“不好的味道”:它建议你可能应该有一个包含 Optional 属性的子类。但有时这可能是合理的。考虑条目 2 中的 `NutritionFacts` 类的情况。`NutritionFacts` 实例包含许多不需要的属性。不可能为这些属性的每个可能组合都提供一个子类。此外,属性包含基本类型,这使得很难直接表示这种缺失。对于 `NutritionFacts` 最好的 API 将为每个 Optional 属性从 getter 方法返回一个 Optional,因此将这些 Optional 作为属性存储在对象中是很有意义的。
|
||||
|
||||
总之,如果发现自己编写的方法不能总是返回值,并且认为该方法的用户在每次调用时考虑这种可能性很重要,那么或许应该返回一个 Optional 的方法。但是,应该意识到,返回 Optional 会带来实际的性能后果;对于性能关键的方法,最好返回 null 或抛出异常。最后,除了作为返回值之外,不应该在任何其他地方中使用 Optional。
|
||||
总之,如果发现自己编写的方法不能总是返回值,并且认为该方法的用户在每次调用时考虑这种可能性很重要,那么或许应该返回一个 Optional 的方法。但是,应该意识到,返回 Optional 会带来实际的性能后果;对于性能关键的方法,最好返回 null 或抛出异常。最后,除了作为返回值之外,不应该在任何其他地方中使用 Optional。
|
||||
|
||||
|
||||
@@ -77,4 +77,5 @@ for (int i = 0, n = expensiveComputation(); i < n; i++) {
|
||||
|
||||
关于这个做法需要注意的重要一点是,它有两个循环变量,i 和 n,它们都具有完全相同的作用域。第二个变量 n 用于存储第一个变量的限定值,从而避免了每次迭代中冗余计算的代价。作为一个规则,如果循环测试涉及一个方法调用,并且保证在每次迭代中返回相同的结果,那么应该使用这种用法。
|
||||
|
||||
最小化局部变量作用域的最终技术是**保持方法小而集中**。 如果在同一方法中组合两个行为(activities),则与一个行为相关的局部变量可能会位于执行另一个行为的代码范围内。 为了防止这种情况发生,只需将方法分为两个:每个行为对应一个方法。
|
||||
最小化局部变量作用域的最终技术是**保持方法小而集中**。 如果在同一方法中组合两个行为(activities),则与一个行为相关的局部变量可能会位于执行另一个行为的代码范围内。 为了防止这种情况发生,只需将方法分为两个:每个行为对应一个方法。
|
||||
|
||||
|
||||
@@ -105,4 +105,5 @@ public interface Iterable<E> {
|
||||
|
||||
如果必须从头开始编写自己的 Iterator 实现,那么实现 Iterable 会有点棘手,但是如果你正在编写表示一组元素的类型,那么你应该强烈考虑让它实现 Iterable 接口,甚至可以选择不让它实现 Collection 接口。这允许用户使用 for-each 循环遍历类型,他们会永远感激不尽的。
|
||||
|
||||
总之,for-each 循环在清晰度,灵活性和错误预防方面提供了超越传统 for 循环的令人注目的优势,而且没有性能损失。 尽可能使用 for-each 循环优先于 for 循环。
|
||||
总之,for-each 循环在清晰度,灵活性和错误预防方面提供了超越传统 for 循环的令人注目的优势,而且没有性能损失。 尽可能使用 for-each 循环优先于 for 循环。
|
||||
|
||||
|
||||
@@ -48,10 +48,11 @@ public static void main(String[] args) throws IOException {
|
||||
}
|
||||
```
|
||||
|
||||
库太大,无法学习所有文档 [Java9-api],但是 **每个程序员都应该熟悉 java.lang、java.util 和 java.io 的基础知识及其子包。** 其他库的知识可以根据需要获得。概述库中的工具超出了本项目的范围,这些工具多年来已经发展得非常庞大。
|
||||
这些标准类库太庞大了,以致于不可能学完所有的文档 [Java9-api],但是 **每个程序员都应该熟悉 java.lang、java.util 和 java.io 的基础知识及其子包。** 其他库的知识可以根据需要获得。概述库中的工具超出了本条目的范围,这些工具多年来已经发展得非常庞大。
|
||||
|
||||
有几个图书馆值得一提。collections 框架和 streams 库(详见第 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]。如果你无法在任何适当的库中找到所需的功能,你可能别无选择,只能自己实现它。
|
||||
有时,类库工具可能无法满足你的需求。你的需求越特殊,发生这种情况的可能性就越大。虽然你的第一个思路应该是使用这些库,但是如果你已经了解了它们在某些领域提供的功能,而这些功能不能满足你的需求,那么可以使用另一种实现。任何有限的库集所提供的功能总是存在漏洞。如果你在 Java 平台库中找不到你需要的东西,你的下一个选择应该是寻找高质量的第三方库,比如谷歌的优秀的开源 Guava 库 [Guava]。如果你无法在任何适当的库中找到所需的功能,你可能别无选择,只能自己实现它。
|
||||
|
||||
总而言之,不要白费力气重新发明轮子。如果你需要做一些看起来相当常见的事情,那么库中可能已经有一个工具可以做你想做的事情。如果有,使用它;如果你不知道,检查一下。一般来说,库代码可能比你自己编写的代码更好,并且随着时间的推移可能会得到改进。这并不反映你作为一个程序员的能力。规模经济决定了库代码得到的关注要远远超过大多数开发人员所能承担的相同功能。
|
||||
|
||||
总而言之,不要白费力气重新发明轮子。如果你需要做一些看起来相当常见的事情,那么库中可能已经有一个工具可以做你想做的事情。如果有,使用它;如果你不知道,检查一下。一般来说,库代码可能比你自己编写的代码更好,并且随着时间的推移可能会得到改进。这并不反映你作为一个程序员的能力。规模经济决定了库代码得到的关注要远远超过大多数开发人员所能承担的相同功能。
|
||||
@@ -69,4 +69,5 @@ public static void main(String[] args) {
|
||||
}
|
||||
```
|
||||
|
||||
总之,对于任何需要精确答案的计算,不要使用 float 或 double 类型。如果希望系统来处理十进制小数点,并且不介意不使用基本类型带来的不便和成本,请使用 BigDecimal。使用 BigDecimal 的另一个好处是,它可以完全控制舍入,当执行需要舍入的操作时,可以从八种舍入模式中进行选择。如果你使用合法的舍入行为执行业务计算,这将非常方便。如果性能是最重要的,那么你不介意自己处理十进制小数点,而且数值不是太大,可以使用 int 或 long。如果数值不超过 9 位小数,可以使用 int;如果不超过 18 位,可以使用 long。如果数量可能超过 18 位,则使用 BigDecimal。
|
||||
总之,对于任何需要精确答案的计算,不要使用 float 或 double 类型。如果希望系统来处理十进制小数点,并且不介意不使用基本类型带来的不便和成本,请使用 BigDecimal。使用 BigDecimal 的另一个好处是,它可以完全控制舍入,当执行需要舍入的操作时,可以从八种舍入模式中进行选择。如果你使用合法的舍入行为执行业务计算,这将非常方便。如果性能是最重要的,那么你不介意自己处理十进制小数点,而且数值不是太大,可以使用 int 或 long。如果数值不超过 9 位小数,可以使用 int;如果不超过 18 位,可以使用 long。如果数量可能超过 18 位,则使用 BigDecimal。
|
||||
|
||||
|
||||
@@ -75,4 +75,5 @@ public final class ThreadLocal<T> {
|
||||
|
||||
粗略地说,这就是 `java.lang.ThreadLocal` 提供的 API,除了解决基于字符串的问题之外,它比任何基于键的 API 都更快、更优雅。
|
||||
|
||||
总之,当存在或可以编写更好的数据类型时,应避免将字符串用来表示对象。如果使用不当,字符串比其他类型更麻烦、灵活性更差、速度更慢、更容易出错。字符串经常被误用的类型包括基本类型、枚举和聚合类型。
|
||||
总之,当存在或可以编写更好的数据类型时,应避免将字符串用来表示对象。如果使用不当,字符串比其他类型更麻烦、灵活性更差、速度更慢、更容易出错。字符串经常被误用的类型包括基本类型、枚举和聚合类型。
|
||||
|
||||
|
||||
@@ -27,4 +27,5 @@ public String statement() {
|
||||
|
||||
自 Java 6 以来,为了使字符串连接更快,已经做了大量工作,但是这两个方法在性能上的差异仍然很大:如果 numItems 返回 100,lineForItem 返回 80 个字符串,那么第二个方法在我的机器上运行的速度是第一个方法的 6.5 倍。由于第一种方法在项目数量上是平方级的,而第二种方法是线性的,所以随着项目数量的增加,性能差异会变得越来越大。注意,第二个方法预先分配了一个足够大的 StringBuilder 来保存整个结果,从而消除了自动增长的需要。即使使用默认大小的 StringBuilder,它仍然比第一个方法快 5.5 倍。
|
||||
|
||||
道理很简单:**不要使用字符串连接操作符合并多个字符串**,除非性能无关紧要。否则使用 StringBuilder 的 append 方法。或者,使用字符数组,再或者一次只处理一个字符串,而不是组合它们。
|
||||
道理很简单:**不要使用字符串连接操作符合并多个字符串**,除非性能无关紧要。否则使用 StringBuilder 的 append 方法。或者,使用字符数组,再或者一次只处理一个字符串,而不是组合它们。
|
||||
|
||||
|
||||
@@ -34,4 +34,5 @@ Set<Son> sonSet = new HashSet<>();
|
||||
|
||||
没有合适接口类型的最后一种情况是,实现接口但同时提供接口中不存在的额外方法的类,例如,`PriorityQueue` 有一个在 `Queue` 接口上不存在的比较器方法。只有当程序依赖于额外的方法时,才应该使用这样的类来引用它的实例,这种情况应该非常少见。
|
||||
|
||||
这三种情况并不是面面俱到的,而仅仅是为了传达适合通过类引用对象的情况。在实际应用中,给定对象是否具有适当的接口应该是显而易见的。如果是这样,如果使用接口引用对象,程序将更加灵活和流行。**如果没有合适的接口,就使用类层次结构中提供所需功能的最底层的类**
|
||||
这三种情况并不是面面俱到的,而仅仅是为了传达适合通过类引用对象的情况。在实际应用中,给定对象是否具有适当的接口应该是显而易见的。如果是这样,如果使用接口引用对象,程序将更加灵活和流行。**如果没有合适的接口,就使用类层次结构中提供所需功能的最底层的类**
|
||||
|
||||
|
||||
@@ -68,4 +68,5 @@ private static void fatalError(String msg) {
|
||||
|
||||
反射的合法用途(很少)是管理类对运行时可能不存在的其他类、方法或字段的依赖关系。如果你正在编写一个包,并且必须针对其他包的多个版本运行,此时反射将非常有用。该技术是根据支持包所需的最小环境(通常是最老的版本)编译包,并反射性地访问任何较新的类或方法。如果你试图访问的新类或方法在运行时不存在,要使此工作正常进行,则必须采取适当的操作。适当的操作可能包括使用一些替代方法来完成相同的目标,或者使用简化的功能进行操作。
|
||||
|
||||
总之,反射是一种功能强大的工具,对于某些复杂的系统编程任务是必需的,但是它有很多缺点。如果编写的程序必须在编译时处理未知的类,则应该尽可能只使用反射实例化对象,并使用在编译时已知的接口或超类访问对象。
|
||||
总之,反射是一种功能强大的工具,对于某些复杂的系统编程任务是必需的,但是它有很多缺点。如果编写的程序必须在编译时处理未知的类,则应该尽可能只使用反射实例化对象,并使用在编译时已知的接口或超类访问对象。
|
||||
|
||||
|
||||
@@ -10,4 +10,5 @@
|
||||
|
||||
使用本地方法有严重的缺点。由于本地语言不安全(详见第 50 条),使用本地方法的应用程序不再能免受内存毁坏错误的影响。由于本地语言比 Java 更依赖于平台,因此使用本地方法的程序的可移植性较差。它们也更难调试。如果不小心,本地方法可能会降低性能,因为垃圾收集器无法自动跟踪本地内存使用情况(详见第 8 条),而且进出本地代码会产生相关的成本。最后,本地方法需要「粘合代码」,这很难阅读,而且编写起来很乏味。
|
||||
|
||||
总之,在使用本地方法之前要三思。一般很少需要使用它们来提高性能。如果必须使用本地方法来访问底层资源或本地库,请尽可能少地使用本地代码,并对其进行彻底的测试。本地代码中的一个错误就可以破坏整个应用程序。
|
||||
总之,在使用本地方法之前要三思。一般很少需要使用它们来提高性能。如果必须使用本地方法来访问底层资源或本地库,请尽可能少地使用本地代码,并对其进行彻底的测试。本地代码中的一个错误就可以破坏整个应用程序。
|
||||
|
||||
|
||||
@@ -46,4 +46,5 @@
|
||||
|
||||
自本条目首次编写以来的近 20 年里,Java 软件栈的每个组件都变得越来越复杂,从处理器到 vm 再到库,Java 运行的各种硬件都有了极大的增长。所有这些加在一起,使得 Java 程序的性能比 2001 年更难以预测,而对它进行度量的需求也相应增加。
|
||||
|
||||
总而言之,不要努力写快的程序,要努力写好程序;速度自然会提高。但是在设计系统时一定要考虑性能,特别是在设计API、线路层协议和持久数据格式时。当你完成了系统的构建之后,请度量它的性能。如果足够快,就完成了。如果没有,利用分析器找到问题的根源,并对系统的相关部分进行优化。第一步是检查算法的选择:再多的底层优化也不能弥补算法选择的不足。根据需要重复这个过程,在每次更改之后测量性能,直到你满意为止。
|
||||
总而言之,不要努力写快的程序,要努力写好程序;速度自然会提高。但是在设计系统时一定要考虑性能,特别是在设计API、线路层协议和持久数据格式时。当你完成了系统的构建之后,请度量它的性能。如果足够快,就完成了。如果没有,利用分析器找到问题的根源,并对系统的相关部分进行优化。第一步是检查算法的选择:再多的底层优化也不能弥补算法选择的不足。根据需要重复这个过程,在每次更改之后测量性能,直到你满意为止。
|
||||
|
||||
|
||||
@@ -48,4 +48,5 @@ if (car.speed() > 2 * SPEED_LIMIT)
|
||||
|
||||
字段名的语法约定没有类、接口和方法名的语法约定建立得好,也不那么重要,因为设计良好的 API 包含很少的公开字段。类型为 boolean 的字段的名称通常类似于 boolean 访问器方法,省略了初始值「is」,例如 initialized、composite。其他类型的字段通常用名词或名词短语来命名,如 height、digits 和 bodyStyle。局部变量的语法约定类似于字段的语法约定,但要求更少。
|
||||
|
||||
总之,将标准命名约定内在化,并将其作为第二性征来使用。排版习惯是直接的,而且在很大程度上是明确的;语法惯例更加复杂和松散。引用《The Java Language Specification》[JLS, 6.1] 中的话说,「如果长期以来的传统用法要求不遵循这些约定,就不应该盲目地遵循这些约定。」,应使用常识判断。
|
||||
总之,将标准命名约定内在化,并将其作为第二性征来使用。排版习惯是直接的,而且在很大程度上是明确的;语法惯例更加复杂和松散。引用《The Java Language Specification》[JLS, 6.1] 中的话说,「如果长期以来的传统用法要求不遵循这些约定,就不应该盲目地遵循这些约定。」,应使用常识判断。
|
||||
|
||||
|
||||
@@ -55,4 +55,5 @@ obj.action(args);
|
||||
|
||||
如果你怀疑这个简单的调用序列是否符合要求,这个 API 重构可能就是恰当的。这样重构之后的 API 在本质上等同于第 69 条中的「状态测试方法」,并且同样的告诫依然适用:如果对象将在缺少外部同步的情况下被并发访问,或者可被外界改变状态,这种重构就是不恰当的,因为在 actionPermitted 和 action 这两个调用的时间间隔之中,对象的状态有可能会发生变化。如果单独的 actionPermitted 方法必须重复 action 方法的工作,出于性能的考虑,这种 API 重构就不值得去做。
|
||||
|
||||
总而言之,在谨慎使用的前提之下,受检异常可以提升程序的可读性;如果过度使用,将会使 API 使用起来非常痛苦。如果调用者无法恢复失败,就应该抛出未受检异常。如果可以恢复,并且想要迫使调用者处理异常的条件,首选应该返回一个 optional 值。当且仅当万一失败时,这些无法提供足够的信息,才应该抛出受检异常。
|
||||
总而言之,在谨慎使用的前提之下,受检异常可以提升程序的可读性;如果过度使用,将会使 API 使用起来非常痛苦。如果调用者无法恢复失败,就应该抛出未受检异常。如果可以恢复,并且想要迫使调用者处理异常的条件,首选应该返回一个 optional 值。当且仅当万一失败时,这些无法提供足够的信息,才应该抛出受检异常。
|
||||
|
||||
|
||||
@@ -59,4 +59,5 @@ class HigherLevelException extends Exception {
|
||||
|
||||
如果无法阻止来自低层的异常,其次的做法是,让更高层来悄悄地处理这些异常,从而将高层方法的调用者与低层的问题隔离开来。在这种情况下,可以用某种适当的记录机制(如 java.util.logging)将异常记录下来。这样有助于管理员调查问题,同时又将客户端代码和最终用户与问题隔离开来。
|
||||
|
||||
总而言之,如果不能阻止或者处理来自更低层的异常,一般的做法是使用异常转译,只有在低层方法的规范碰巧可以保证“它所抛出的所有异常对于更高层也是合适的”情况下,才可以将异常从低层传播到高层。异常链对高层和低层异常都提供了最佳的功能:它允许抛出适当的高层异常,同时又能捕获低层的原因进行失败分析(详见第 75 条) 。
|
||||
总而言之,如果不能阻止或者处理来自更低层的异常,一般的做法是使用异常转译,只有在低层方法的规范碰巧可以保证“它所抛出的所有异常对于更高层也是合适的”情况下,才可以将异常从低层传播到高层。异常链对高层和低层异常都提供了最佳的功能:它允许抛出适当的高层异常,同时又能捕获低层的原因进行失败分析(详见第 75 条) 。
|
||||
|
||||
|
||||
@@ -14,4 +14,5 @@
|
||||
|
||||
**如果一个类中的许多方法出于同样的原因而抛出同一个异常,在该类的文档注释中对这个异常建立文档,这是可以接受的,** 而不是为每个方法单独建立文档。一个常见的例子是 `NullPointerException`。 若类的文档注释中有这样的描述:「All methods in this class throw a NullPointerException if a null object reference is passed in any parameter 」(如果 null 对象引用被传递到任何一个参数中,这个类中的所有方法都会抛出 `NullPointerException`),或者有其他类似的语句,这是可以的。
|
||||
|
||||
总而言之,要为你编写的每个方法所能抛出的每个异常建立文档。对于未受检异常和受检异常,以及抽象的方法和具体的方法一概如此。这个文档在文档注释中应当采用 `@throws` 标签的形式。要在方法的 throws 子句中为每个受检异常提供单独的声明,但是不要声明未受检的异常。如果没有为可以抛出的异常建立文档,其他人就很难或者根本不可能有效地使用你的类和接口。
|
||||
总而言之,要为你编写的每个方法所能抛出的每个异常建立文档。对于未受检异常和受检异常,以及抽象的方法和具体的方法一概如此。这个文档在文档注释中应当采用 `@throws` 标签的形式。要在方法的 throws 子句中为每个受检异常提供单独的声明,但是不要声明未受检的异常。如果没有为可以抛出的异常建立文档,其他人就很难或者根本不可能有效地使用你的类和接口。
|
||||
|
||||
|
||||
@@ -28,4 +28,5 @@ public Object pop() {
|
||||
|
||||
即使在可以实现失败原子性的场合,它也并不总是人们所期望的。对于某些操作,它会显著地增加开销或者复杂性。也就是说, 一旦了解了这个问题,获得失败原子性往往既简单又容易。
|
||||
|
||||
总而言之,作为方法规范的一部分,它产生的任何异常都应该让对象保持在调用该方法之前的状态。如果违反这条规则, API 文档就应该清楚地指明对象将会处于什么样的状态。遗憾的是,大量现有的 API 文档都未能做到这一点。
|
||||
总而言之,作为方法规范的一部分,它产生的任何异常都应该让对象保持在调用该方法之前的状态。如果违反这条规则, API 文档就应该清楚地指明对象将会处于什么样的状态。遗憾的是,大量现有的 API 文档都未能做到这一点。
|
||||
|
||||
|
||||
@@ -24,4 +24,5 @@ try {
|
||||
}
|
||||
```
|
||||
|
||||
本条目中的建议同样适用于受检异常和未受检异常。不管异常代表了可预见的异常条件,还是编程错误,用空的 catch 块忽略它,都将导致程序在遇到错误的情况下悄然地执行下去。然后,有可能在将来的某个点上,当程序不能再容忍与错误源明显相关的问题时,它就会失败。正确地处理异常能够彻底避免失败。只要将异常传播给外界,至少会导致程序迅速失败,从而保留了有助于调试该失败条件的信息。
|
||||
本条目中的建议同样适用于受检异常和未受检异常。不管异常代表了可预见的异常条件,还是编程错误,用空的 catch 块忽略它,都将导致程序在遇到错误的情况下悄然地执行下去。然后,有可能在将来的某个点上,当程序不能再容忍与错误源明显相关的问题时,它就会失败。正确地处理异常能够彻底避免失败。只要将异常传播给外界,至少会导致程序迅速失败,从而保留了有助于调试该失败条件的信息。
|
||||
|
||||
|
||||
@@ -128,4 +128,5 @@ public static long generateSerialNumber() {
|
||||
|
||||
让一个线程在短时间内修改一个数据对象,然后与其他线程共享,这是可以接受的,它只同步共享对象引用的动作。然后其他线程没有进一步的同步也可以读取对象,只要它没有再被修改。这种对象被称作高效不可变( effectively immutable ) [Goetz06, 3.5.4] 。将这种对象引用从一个线程传递到其他的线程被称作安全发布( safe publication) [Goetz06, 3.5.3] 。安全发布对象引用有许多种方法:可以将它保存在静态字段巾,作为类初始化的一部分;可以将它保存在 volatile 字段、final 字段或者通过正常锁定访问的字段中;或者可以将它放到并发的集合中(详见第 81 条)。
|
||||
|
||||
总而言之, **当多个线程共享可变数据的时候,每个读或者写数据的线程都必须执行同步。** 如果没有同步,就无法保证一个线程所做的修改可以被另一个线程获知。未能同步共享可变数据会造成程序的活跃性失败( liveness failure )和安全性失败( safety failure ) 。这样的失败是最难调试的。它们可能是间歇性的,且与时间相关,程序的行为在不同的虚拟机上可能根本不同。如果只需要线程之间的交互通信,而不需要互斥, volatile 修饰符就是一种可以接受的同步形式,但要正确地使用它可能需要一些技巧。
|
||||
总而言之, **当多个线程共享可变数据的时候,每个读或者写数据的线程都必须执行同步。** 如果没有同步,就无法保证一个线程所做的修改可以被另一个线程获知。未能同步共享可变数据会造成程序的活跃性失败( liveness failure )和安全性失败( safety failure ) 。这样的失败是最难调试的。它们可能是间歇性的,且与时间相关,程序的行为在不同的虚拟机上可能根本不同。如果只需要线程之间的交互通信,而不需要互斥, volatile 修饰符就是一种可以接受的同步形式,但要正确地使用它可能需要一些技巧。
|
||||
|
||||
|
||||
@@ -172,4 +172,5 @@ private void notifyElementAdded(E element) {
|
||||
|
||||
如果方法修改了静态字段,并且该方法很可能要被多个线程调用,那么也必须在内部同步对这个字段的访问(除非这个类能够容忍不确定的行为) 。多线程的客户端要在这种方法上执行外部同步是不可能的,因为其他不相关的客户端不需要同步也能调用该方法。字段本质上就是一个全局变量,即使是私有的也一样,因为它可以被不相关的客户端读取和修改。第 78 条中的 `generateSerialNumber` 方法使用的 `nextSerialNumber` 字段就是这样的一个例子。
|
||||
|
||||
总而言之,为了避免死锁和数据破坏,千万不要从同步区字段内部调用外来方法。更通俗地讲,要尽量将同步区字段内部的工作量限制到最少。当你在设计一个可变类的时候,要考虑一下它们是否应该自己完成同步操作。在如今这个多核的时代,这比永远不要过度同步来得更重要。只有当你有足够的理由一定要在内部同步类的时候,才应该这么做,同时还应该将这个决定清楚地写到文档中(详见第 82 条) 。
|
||||
总而言之,为了避免死锁和数据破坏,千万不要从同步区字段内部调用外来方法。更通俗地讲,要尽量将同步区字段内部的工作量限制到最少。当你在设计一个可变类的时候,要考虑一下它们是否应该自己完成同步操作。在如今这个多核的时代,这比永远不要过度同步来得更重要。只有当你有足够的理由一定要在内部同步类的时候,才应该这么做,同时还应该将这个决定清楚地写到文档中(详见第 82 条) 。
|
||||
|
||||
|
||||
@@ -113,4 +113,5 @@ synchronized (obj) {
|
||||
|
||||
即使这些前提条件都满足,也许还是有理由使用 `notifyAll` 方法而不是 `notify` 方法。就好像把 `wait` 方法调用放在一个循环中,以避免在公有可访问对象上的意外或恶意的通知一样,与此类似,使用 `notifyAll` 方法代替 `notify` 方法可以避免来自不相关线程的意外或恶意的等待。否则,这样的等待会“吞掉”一个关键的通知,使真正的接收线程无限地等待下去。
|
||||
|
||||
简而言之,直接使用 `wait` 方法和 `notify` 方法就像用“并发汇编语言”进行编程一样,而 `java.util.concurrent` 则提供了更高级的语言。 **没有理由在新代码中使用 `wait` 方法和 `notify` 方法,即使有,也是极少的。** 如果你在维护使用 `wait` 方法和 `notify` 方法的代码,务必确保始终是利用标准的模式从 `while` 循环内部调用 `wait` 方法。一般情况下,应该优先使用 `notifyAll` 方法,而不是使用 `notify` 方法。如果使用 `notify` 方法,请一定要小心,以确保程序的活性。
|
||||
简而言之,直接使用 `wait` 方法和 `notify` 方法就像用“并发汇编语言”进行编程一样,而 `java.util.concurrent` 则提供了更高级的语言。 **没有理由在新代码中使用 `wait` 方法和 `notify` 方法,即使有,也是极少的。** 如果你在维护使用 `wait` 方法和 `notify` 方法的代码,务必确保始终是利用标准的模式从 `while` 循环内部调用 `wait` 方法。一般情况下,应该优先使用 `notifyAll` 方法,而不是使用 `notify` 方法。如果使用 `notify` 方法,请一定要小心,以确保程序的活性。
|
||||
|
||||
|
||||
@@ -46,4 +46,5 @@ synchronized(m) { // Synchronizing on m, not s!
|
||||
|
||||
私有锁对象用法特别适合为继承而设计的类(详见第 19 条)。如果这样一个类要使用它的实例进行锁定,那么子类很容易在无意中干扰基类的操作,反之亦然。通过为不同的目的使用相同的锁,子类和基类最终可能「踩到对方的脚趾头」。这不仅仅是一个理论问题,它就发生在 `Thread` 类中 [Bloch05, Puzzle 77]。
|
||||
|
||||
总之,每个类都应该措辞严谨的描述或使用线程安全注解清楚地记录其线程安全属性。`synchronized` 修饰符在文档中没有任何作用。有条件的线程安全类必须记录哪些方法调用序列需要外部同步,以及在执行这些序列时需要获取哪些锁。如果你编写一个无条件线程安全的类,请考虑使用一个私有锁对象来代替同步方法。这将保护你免受客户端和子类的同步干扰,并为你提供更大的灵活性,以便在后续的版本中采用复杂的并发控制方式。
|
||||
总之,每个类都应该措辞严谨的描述或使用线程安全注解清楚地记录其线程安全属性。`synchronized` 修饰符在文档中没有任何作用。有条件的线程安全类必须记录哪些方法调用序列需要外部同步,以及在执行这些序列时需要获取哪些锁。如果你编写一个无条件线程安全的类,请考虑使用一个私有锁对象来代替同步方法。这将保护你免受客户端和子类的同步干扰,并为你提供更大的灵活性,以便在后续的版本中采用复杂的并发控制方式。
|
||||
|
||||
|
||||
@@ -74,4 +74,5 @@ private FieldType getField() {
|
||||
|
||||
如果您不关心每个线程是否都会重新计算字段的值,并且字段的类型是 long 或 double 之外的基本类型,那么您可以选择在单检查模式中从字段声明中删除 volatile 修饰符。这种变体称为原生单检查模式。它加快了某些架构上的字段访问速度,代价是需要额外的初始化(每个访问该字段的线程最多需要一个初始化)。这绝对是一种奇特的技术,不是日常使用的。
|
||||
|
||||
总之,您应该正常初始化大多数字段,而不是延迟初始化。如果必须延迟初始化字段以实现性能目标或为了破坏友有害的初始化循环,则使用适当的延迟初始化技术。对于字段,使用双重检查模式;对于静态字段,则应该使用the lazy initialization holder class idiom。例如,可以容忍重复初始化的实例字段,您还可以考虑单检查模式。
|
||||
总之,您应该正常初始化大多数字段,而不是延迟初始化。如果必须延迟初始化字段以实现性能目标或为了破坏友有害的初始化循环,则使用适当的延迟初始化技术。对于字段,使用双重检查模式;对于静态字段,则应该使用the lazy initialization holder class idiom。例如,可以容忍重复初始化的实例字段,您还可以考虑单检查模式。
|
||||
|
||||
|
||||
@@ -42,4 +42,5 @@ public class SlowCountDownLatch {
|
||||
|
||||
一个相关的技术是调整线程优先级,类似的警告也适用于此技术,即,线程优先级是 Java 中最不可移植的特性之一。通过调整线程优先级来调优应用程序的响应性并非不合理,但很少情况下是必要的,而且不可移植。试图通过调整线程优先级来解决严重的活性问题是不合理的。在找到并修复潜在原因之前,问题很可能会再次出现。
|
||||
|
||||
总之,不要依赖线程调度器来判断程序的正确性。生成的程序既不健壮也不可移植。因此,不要依赖 `Thread.yield` 或线程优先级。这些工具只是对调度器的提示。线程优先级可以少量地用于提高已经工作的程序的服务质量,但绝不应该用于「修复」几乎不能工作的程序。
|
||||
总之,不要依赖线程调度器来判断程序的正确性。生成的程序既不健壮也不可移植。因此,不要依赖 `Thread.yield` 或线程优先级。这些工具只是对调度器的提示。线程优先级可以少量地用于提高已经工作的程序的服务质量,但绝不应该用于「修复」几乎不能工作的程序。
|
||||
|
||||
|
||||
@@ -145,4 +145,5 @@ private static final long serialVersionUID = randomLongValue;
|
||||
|
||||
如果你希望创建一个新版本的类,它与现有版本不兼容,如果更改序列版本 UID 声明中的值,这将导致反序列化旧版本的序列化实例的操作引发 `InvalidClassException`。**不要更改序列版本 UID,除非你想破坏与现有序列化所有实例的兼容性。**
|
||||
|
||||
总而言之,如果你已经决定一个类应该是可序列化的(详见第 86 条),那么请仔细考虑一下序列化的形式应该是什么。只有在合理描述对象的逻辑状态时,才使用默认的序列化形式;否则,设计一个适合描述对象的自定义序列化形式。设计类的序列化形式应该和设计导出方法花的时间应该一样多,都应该严谨对待(详见第 51 条)。正如不能从未来版本中删除导出的方法一样,也不能从序列化形式中删除字段;必须永远保存它们,以确保序列化兼容性。选择错误的序列化形式可能会对类的复杂性和性能产生永久性的负面影响。
|
||||
总而言之,如果你已经决定一个类应该是可序列化的(详见第 86 条),那么请仔细考虑一下序列化的形式应该是什么。只有在合理描述对象的逻辑状态时,才使用默认的序列化形式;否则,设计一个适合描述对象的自定义序列化形式。设计类的序列化形式应该和设计导出方法花的时间应该一样多,都应该严谨对待(详见第 51 条)。正如不能从未来版本中删除导出的方法一样,也不能从序列化形式中删除字段;必须永远保存它们,以确保序列化兼容性。选择错误的序列化形式可能会对类的复杂性和性能产生永久性的负面影响。
|
||||
|
||||
|
||||
@@ -32,11 +32,11 @@ public final class Period {
|
||||
}
|
||||
```
|
||||
|
||||
假设决定要把这个类左晨可序列化的。因为 Period 对象的物理表示法正好反映了它的逻辑数据内容,所以,使用默认的序列化形式并没有什么不合理的(详见 87 条)。因此,为了使这个类成为可序列化的,似乎你所需要做的也就是在类的声明中增加 implements Serializable 字样。然而,如果你真的这么做,那么这个类就不保证它的关键约束了。
|
||||
假设你决定要把这个类成为可序列化的。因为 Period 对象的物理表示法正好反映了它的逻辑数据内容,所以,使用默认的序列化形式是合理的(详见 87 条)。因此,为了使这个类成为可序列化的,似乎你所需要做的也就是在类的声明中增加 implements Serializable 字样。然而,如果你真的这么做,那么这个类就不保证它的关键约束了。
|
||||
|
||||
问题在于 `readObject` 方法实际上相当于另外一个公有的构造器,它要求同其他构造器一样警惕所有的注意事项。构造器必须检查其参数的有效性(详见 49 条),并且在必要的时候对参数进行保护性拷贝(详见 50 条),同样的,`readObject` 方法也需要这样做。如果 `readObject` 方法无法做到这两者之一,对于攻击者来说要违反这个类的约束条件就相对容易很多。
|
||||
|
||||
不严格的说, `readObject` 方法是一个“用字节流作为唯一参数”的构造器。在正常使用的情况下,对一个正常构造的实例进行序列化可以产生字节流。但是,当面对一个人工仿造的字节流时, `readObject` 产生的对象会违反它所属类的约束条件,这时问题就产生了。这种字节流可以用来创建一个不可能的对象(impossible object),这时利用普通构造器无法创建的。
|
||||
不严格的说, `readObject` 方法是一个「用字节流作为唯一参数」的构造器。在正常使用的情况下,对一个正常构造的实例进行序列化可以产生字节流。但是,当面对一个人工仿造的字节流时, `readObject` 产生的对象会违反它所属类的约束条件,这时问题就产生了。这种字节流可以用来创建一个不可能的对象(impossible object),这时利用普通构造器无法创建的。
|
||||
|
||||
假设我们仅仅在 Period 类的声明加上了 `implements Serializable` 字样。那么这个丑陋的程序代码将会产生一个 Period 实例,他的结束时间比起始时间还早。对于高位 byte 值进行强制类型转换是 Java 缺少 byte 并且做出 byte 类型签名的不幸决定的后果:
|
||||
|
||||
@@ -76,7 +76,7 @@ public class BogusPeriod {
|
||||
}
|
||||
```
|
||||
|
||||
被用来初始化 serializedForm 的 byte 常量数组是这样产生的:首先对一个正常的 Period 实例进行序列化,然后对得到的字节流进行手工编辑。对于这个例子而言,字节流的细节并不重要,如果你对此十分好奇,可以在《Java Object Serialization Specification》[Serialization, 6] 中查到有关序列化字节流格式的描述信息。如果运行这个程序,它会打印出“ Fri Jan 01 12:00:00 PST 1999 - Sun Jan 01 12:00:00 PST 1984”。主要把 Period 类声明成为可序列化的,这会使我们创建出其违反类约束条件的对象。
|
||||
被用来初始化 serializedForm 的 byte 常量数组是这样产生的:首先对一个正常的 Period 实例进行序列化,然后对得到的字节流进行手工编辑。对于这个例子而言,字节流的细节并不重要,如果你对此十分好奇,可以在《Java Object Serialization Specification》[Serialization, 6] 中查到有关序列化字节流格式的描述信息。如果运行这个程序,它会打印出「Fri Jan 01 12:00:00 PST 1999 - Sun Jan 01 12:00:00 PST 1984」。主要把 Period 类声明成为可序列化的,这会使我们创建出其违反类约束条件的对象。
|
||||
|
||||
为了修整这个问题,可以为 `Period` 提供一个 `readObject` 方法,该方法首先调用 `defaultReadObject`,然后检查被反序列化之后的对象有效性。如果有效性检查失败,`readObject` 方法就会抛出一个 `InvalidObjectException` 异常,这使得反序列化过程不能成功的完成:
|
||||
|
||||
@@ -91,7 +91,7 @@ private void readObject(ObjectInputStream s)
|
||||
}
|
||||
```
|
||||
|
||||
尽管这样的修成避免了攻击者创建无效的 `Period` 实例,但是这里依旧隐藏着一个更为微妙的问题。通过伪造字节流,要想创建可变的 `Period` 实例仍是有可能的,做法事:字节流以一个有效的 `Period` 实例开头,然后附加上两个额外的引用,指向 `Period` 实例中两个私有的 `Date` 字段。攻击者从 `ObjectInputStream` 读取 `Period` 实例,然后读取附加在其后面的“恶意编制的对线引用”。这些对象引用使得攻击者能够访问到 `Period` 对象内部的私有 `Date` 字段所引用的对象。通过改变这些 `Date` 实例,攻击者可以改变 `Period` 实例。如下的类演示了这种攻击方式:
|
||||
尽管这样的修成避免了攻击者创建无效的 `Period` 实例,但是这里依旧隐藏着一个更为微妙的问题。通过伪造字节流,要想创建可变的 `Period` 实例仍是有可能的,做法事:字节流以一个有效的 `Period` 实例开头,然后附加上两个额外的引用,指向 `Period` 实例中两个私有的 `Date` 字段。攻击者从 `ObjectInputStream` 读取 `Period` 实例,然后读取附加在其后面的「恶意编制的对线引用」。这些对象引用使得攻击者能够访问到 `Period` 对象内部的私有 `Date` 字段所引用的对象。通过改变这些 `Date` 实例,攻击者可以改变 `Period` 实例。如下的类演示了这种攻击方式:
|
||||
|
||||
```java
|
||||
public class MutablePeriod {
|
||||
@@ -160,7 +160,7 @@ Wed Nov 22 00:21:29 PST 2017 - Wed Nov 22 00:21:29 PST 1978
|
||||
Wed Nov 22 00:21:29 PST 2017 - Sat Nov 22 00:21:29 PST 1969
|
||||
```
|
||||
|
||||
虽然 Period 实例被创建之后,他的约束条件没有被破坏。但是要随意修改它的内部组件仍然是有可能的。一旦攻击者获得了一个可变的 `Period` 实例,就可以将这个实例传递给一个“安全性依赖于 Period 的不可变性”的类,从而造成更大的危害。这种推断并不牵强:实际上,有许多类的安全性就是依赖于 String 的不可变性。
|
||||
虽然 Period 实例被创建之后,他的约束条件没有被破坏。但是要随意修改它的内部组件仍然是有可能的。一旦攻击者获得了一个可变的 `Period` 实例,就可以将这个实例传递给一个「安全性依赖于 Period 的不可变性」的类,从而造成更大的危害。这种推断并不牵强:实际上,有许多类的安全性就是依赖于 String 的不可变性。
|
||||
|
||||
问题的根源在于,`Period` 的 `readObject` 方法并没有完成足够的保护性拷贝。 **当一个对象被反序列化的时候,对于客户端不应该拥有的对象引用,如果那个字段包含了这样的对象引用,就必须做保护性拷贝,这是非常重要的。** 因此,对于每个可序列化的不可变类,如果它好汉了私有的可变字段,那么在它的 `readObject` 方法中,必须要对这些字段进行保护性拷贝。下面的这些 `readObject` 方法可以确保 `Period` 类的约束条件不会遭到破坏,以保持它的不可变性:
|
||||
|
||||
@@ -185,7 +185,7 @@ Wed Nov 22 00:23:41 PST 2017 - Wed Nov 22 00:23:41 PST 2017
|
||||
Wed Nov 22 00:23:41 PST 2017 - Wed Nov 22 00:23:41 PST 2017
|
||||
```
|
||||
|
||||
有一个简单的“石蕊”测试,可以用来确定默认的 `readObject` 方法是否可以被接受。测试方法:增加一个公有的构造器,其参数对应于该对象中每个非 transient 的字段,并且无论参数的值是什么,都是不进行检查就可以保存到相应的字段中。对于这样的做法,你是否会感到很舒适?如果你对这个问题的回答是否定的,就必须提供一个显式的 `readObject` 方法,并且它必须执行构造器所要求的所有有效性检查和保护性拷贝。另一种方法是,可以使用序列化代理模式(serialization proxy pattern),详见第 90 条。强烈建议使用这个模式,因为它分担了安全反序列化的部门工作。
|
||||
有一个简单的「石蕊」测试,可以用来确定默认的 `readObject` 方法是否可以被接受。测试方法:增加一个公有的构造器,其参数对应于该对象中每个非 transient 的字段,并且无论参数的值是什么,都是不进行检查就可以保存到相应的字段中。对于这样的做法,你是否会感到很舒适?如果你对这个问题的回答是否定的,就必须提供一个显式的 `readObject` 方法,并且它必须执行构造器所要求的所有有效性检查和保护性拷贝。另一种方法是,可以使用序列化代理模式(serialization proxy pattern),详见第 90 条。强烈建议使用这个模式,因为它分担了安全反序列化的部门工作。
|
||||
|
||||
对于非 final 的可序列化的类,在 `readObject` 方法和构造器之间还有其他类似的地方。与构造器一样,`readObject` 方法不可以调用可被覆盖的方法,无论是直接调用还是间接调用都不可以(详见 19 条)。如果违反了这条规则,并且覆盖了该方法,被覆盖的方法将在子类的状态被反序列化之前先运行。这个程序很可能会失败[Bloch05, Puzzle 91]。
|
||||
|
||||
|
||||
@@ -112,7 +112,7 @@ public class ElvisImpersonator {
|
||||
|
||||
通过将 `favoriteSongs` 字段声明为 `transient`,可以修复这个问题,但是最好把 `Elvis` 做成一个单元素的枚举类型(详见第 3 条)。就如 `ElvisStealer` 攻击所示范的,用 `readResolve` 方法防止“临时”被反序列化的实例收到攻击者的访问,这种方法十分脆弱需要万分谨慎。
|
||||
|
||||
如果将一个可序列化的实例受控的类编写为枚举,Java 就可以绝对保证出了所声明的常量之外,不会有其他实例,除非攻击者恶意的使用了享受特权的方法。如 `AccessibleObject.setAccessible`。能够做到这一点的任何一位攻击者,已经拥有了足够的特权来执行任意的本地代码,后果不堪设想。将 Elvis 写成枚举的例子如下所示:
|
||||
如果将一个可序列化的实例受控的类编写为枚举,Java 就可以绝对保证除了所声明的常量之外,不会有其他实例,除非攻击者恶意的使用了享受特权的方法。如 `AccessibleObject.setAccessible`。能够做到这一点的任何一位攻击者,已经拥有了足够的特权来执行任意的本地代码,后果不堪设想。将 Elvis 写成枚举的例子如下所示:
|
||||
```java
|
||||
// Enum singleton - the preferred approach
|
||||
public enum Elvis {
|
||||
@@ -130,4 +130,5 @@ public enum Elvis {
|
||||
|
||||
**readResolve 的可访问性(accessibility)十分重要。** 如果把 `readResolve` 方法放在一个 `final` 类上面,它应该是私有的。如果把 `readResolve` 方法放在一个非 `final` 类上,就必须认真考虑它的的访问性。如果它是私有的,就不适用于任何一个子类。如果它是包级私有的,就适用于同一个包内的子类。如果它是受保护的或者是公开的,并且子类没有覆盖它,对序列化的子类进行反序列化,就会产生一个超类实例,这样可能会导致 `ClassCastException` 异常。
|
||||
|
||||
总而言之,应该尽可能的使用枚举类型来实施实例控制的约束条件。如果做不到,同时又需要一个即可序列化又可以实例受控的类,就必须提供一个 `readResolve` 方法,并确保该类的所有实例化字段都被基本类型,或者是 `transient` 的。
|
||||
总而言之,应该尽可能的使用枚举类型来实施实例控制的约束条件。如果做不到,同时又需要一个即可序列化又可以实例受控的类,就必须提供一个 `readResolve` 方法,并确保该类的所有实例化字段都被基本类型,或者是 `transient` 的。
|
||||
|
||||
|
||||
@@ -94,4 +94,5 @@ private static class SerializationProxy<E extends Enum<E>>
|
||||
|
||||
最后一点,序列化代理模式所增强的功能和安全性不是没有代价。在我的机器上,通过序列化代理来序列化和反序列化 `Period` 实例的开销,比使用保护性拷贝增加了 14%。
|
||||
|
||||
总而言之,当你发现必须在一个不能被客户端拓展的类上面编写 `readObject` 或者 `writeObject` 方法时,就应该考虑使用序列化代理模式。想要稳健的将带有重要约束条件的对象序列化时,这种模式是最容易的方法。
|
||||
总而言之,当你发现必须在一个不能被客户端拓展的类上面编写 `readObject` 或者 `writeObject` 方法时,就应该考虑使用序列化代理模式。想要稳健的将带有重要约束条件的对象序列化时,这种模式是最容易的方法。
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user