mirror of
https://github.com/sjsdfg/effective-java-3rd-chinese.git
synced 2025-01-27 12:50:07 +08:00
update markdown code statement
```Java -> ```java
This commit is contained in:
parent
06eb34f7e4
commit
c95d1e401c
@ -3,7 +3,7 @@
|
||||
|
||||
一个类允许客户端获取其实例的传统方式是提供一个公共构造方法。 其实还有另一种技术应该成为每个程序员工具箱的一部分。 一个类可以提供一个公共静态工厂方法,它只是一个返回类实例的静态方法。 下面是一个 `Boolean` 简单的例子(`boolean` 基本类型的包装类)。 此方法将 `boolean` 基本类型转换为 `Boolean` 对象引用:
|
||||
|
||||
```Java
|
||||
```java
|
||||
public static Boolean valueOf(boolean b) {
|
||||
return b ? Boolean.TRUE : Boolean.FALSE;
|
||||
}
|
||||
|
@ -5,7 +5,7 @@
|
||||
|
||||
应该为这样的类编写什么样的构造方法或静态工厂?传统上,程序员使用了可伸缩(telescoping constructor)构造方法模式,在这种模式中,只提供了一个只所需参数的构造函数,另一个只有一个可选参数,第三个有两个可选参数,等等,最终在构造函数中包含所有可选参数。这就是它在实践中的样子。为了简便起见,只显示了四个可选属性:
|
||||
|
||||
```Java
|
||||
```java
|
||||
// Telescoping constructor pattern - does not scale well!
|
||||
|
||||
public class NutritionFacts {
|
||||
@ -49,7 +49,7 @@ public class NutritionFacts {
|
||||
|
||||
当想要创建一个实例时,可以使用包含所有要设置的参数的最短参数列表的构造方法:
|
||||
|
||||
```Java
|
||||
```java
|
||||
NutritionFacts cocaCola = new NutritionFacts(240, 8, 100, 0, 35, 27);
|
||||
```
|
||||
|
||||
@ -59,7 +59,7 @@ NutritionFacts cocaCola = new NutritionFacts(240, 8, 100, 0, 35, 27);
|
||||
|
||||
当在构造方法中遇到许多可选参数时,另一种选择是 JavaBeans 模式,在这种模式中,调用一个无参数的构造函数来创建对象,然后调用 `setter` 方法来设置每个必需的参数和可选参数:
|
||||
|
||||
```Java
|
||||
```java
|
||||
// JavaBeans Pattern - allows inconsistency, mandates mutability
|
||||
|
||||
public class NutritionFacts {
|
||||
@ -85,7 +85,7 @@ public class NutritionFacts {
|
||||
|
||||
这种模式没有伸缩构造方法模式的缺点。有点冗长,但创建实例很容易,并且易于阅读所生成的代码:
|
||||
|
||||
```Java
|
||||
```java
|
||||
NutritionFacts cocaCola = new NutritionFacts();
|
||||
cocaCola.setServingSize(240);
|
||||
cocaCola.setServings(8);
|
||||
@ -100,7 +100,7 @@ cocaCola.setCarbohydrate(27);
|
||||
|
||||
幸运的是,还有第三种选择,它结合了可伸缩构造方法模式的安全性和 JavaBean 模式的可读性。 它是 Builder 模式[Gamma95] 的一种形式。客户端不直接调用所需的对象,而是调用构造方法 (或静态工厂),并使用所有必需的参数,并获得一个 builder 对象。然后,客户端调用 builder 对象的 `setter` 相似方法来设置每个可选参数。最后,客户端调用一个无参的 `build` 方法来生成对象,该对象通常是不可变的。Builder 通常是它所构建的类的一个静态成员类 (条目 24)。以下是它在实践中的示例:
|
||||
|
||||
```Java
|
||||
```java
|
||||
// Builder Pattern
|
||||
|
||||
public class NutritionFacts {
|
||||
@ -165,7 +165,7 @@ public class NutritionFacts {
|
||||
|
||||
`NutritionFacts` 类是不可变的,所有的参数默认值都在一个地方。builder 的 setter 方法返回 builder 本身,这样调用就可以被链接起来,从而生成一个流畅的 API。下面是客户端代码的示例:
|
||||
|
||||
```Java
|
||||
```java
|
||||
NutritionFacts cocaCola = new NutritionFacts.Builder(240, 8)
|
||||
.calories(100).sodium(35).carbohydrate(27).build();
|
||||
```
|
||||
@ -176,7 +176,7 @@ NutritionFacts cocaCola = new NutritionFacts.Builder(240, 8)
|
||||
|
||||
Builder 模式非常适合类层次结构。 使用平行层次的 builder,每个嵌套在相应的类中。 抽象类有抽象的 builder; 具体的类有具体的 builder。 例如,考虑代表各种比萨饼的根层次结构的抽象类:
|
||||
|
||||
```Java
|
||||
```java
|
||||
// Builder pattern for class hierarchies
|
||||
|
||||
import java.util.EnumSet;
|
||||
@ -211,7 +211,7 @@ public abstract class Pizza {
|
||||
|
||||
这里有两个具体的 `Pizza` 的子类,其中一个代表标准的纽约风格的披萨,另一个是半圆形烤乳酪馅饼。前者有一个所需的尺寸参数,而后者则允许指定酱汁是否应该在里面或在外面:
|
||||
|
||||
```Java
|
||||
```java
|
||||
import java.util.Objects;
|
||||
|
||||
public class NyPizza extends Pizza {
|
||||
@ -271,7 +271,7 @@ public class Calzone extends Pizza {
|
||||
|
||||
这些“分层 builder”的客户端代码基本上与简单的 `NutritionFacts` builder 的代码相同。为了简洁起见,下面显示的示例客户端代码假设枚举常量的静态导入:
|
||||
|
||||
```Java
|
||||
```java
|
||||
NyPizza pizza = new NyPizza.Builder(SMALL)
|
||||
.addTopping(SAUSAGE).addTopping(ONION).build();
|
||||
Calzone calzone = new Calzone.Builder()
|
||||
|
@ -5,7 +5,7 @@
|
||||
|
||||
有两种常见的方法来实现单例。两者都基于保持构造方法私有和导出公共静态成员以提供对唯一实例的访问。在第一种方法中,成员是 `final` 修饰的属性:
|
||||
|
||||
```Java
|
||||
```java
|
||||
// Singleton with public final field
|
||||
public class Elvis {
|
||||
public static final Elvis INSTANCE = new Elvis();
|
||||
@ -18,7 +18,7 @@ public class Elvis {
|
||||
|
||||
在第二个实现单例的方法中,公共成员是一个静态的工厂方法:
|
||||
|
||||
```Java
|
||||
```java
|
||||
// Singleton with static factory
|
||||
public class Elvis {
|
||||
private static final Elvis INSTANCE = new Elvis();
|
||||
@ -37,7 +37,7 @@ public class Elvis {
|
||||
|
||||
创建一个使用这两种方法的单例类 (第 12 章),仅仅将 `implements Serializable` 添加到声明中是不够的。为了维护单例的保证,声明所有的实例属性为 `transient`,并提供一个 `readResolve` 方法 (条目 89)。否则,每当序列化实例被反序列化时,就会创建一个新的实例,在我们的例子中,导致出现新的 Elvis 实例。为了防止这种情况发生,将这个 `readResolve` 方法添加到 Elvis 类:
|
||||
|
||||
```Java
|
||||
```java
|
||||
// readResolve method to preserve singleton property
|
||||
private Object readResolve() {
|
||||
// Return the one true Elvis and let the garbage collector
|
||||
@ -47,7 +47,7 @@ private Object readResolve() {
|
||||
```
|
||||
实现一个单例的第三种方法是声明单一元素的枚举类:
|
||||
|
||||
```Java
|
||||
```java
|
||||
// Enum singleton - the preferred approach
|
||||
public enum Elvis {
|
||||
INSTANCE;
|
||||
|
@ -7,7 +7,7 @@
|
||||
|
||||
**试图通过创建抽象类来强制执行非实例化是行不通的。** 该类可以被子类化,子类可以被实例化。此外,它误导用户认为该类是为继承而设计的 (条目 19)。不过,有一个简单的方法来确保非实例化。只有当类不包含显式构造方法时,才会生成一个默认构造方法,**因此可以通过包含一个私有构造方法来实现类的非实例化:**
|
||||
|
||||
```Java
|
||||
```java
|
||||
// Noninstantiable utility class
|
||||
public class UtilityClass {
|
||||
// Suppress default constructor for noninstantiability
|
||||
|
@ -3,7 +3,7 @@
|
||||
|
||||
许多类依赖于一个或多个底层资源。例如,拼写检查器依赖于字典。将此类类实现为静态实用工具类并不少见 (条目 4):
|
||||
|
||||
```Java
|
||||
```java
|
||||
// Inappropriate use of static utility - inflexible & untestable!
|
||||
public class SpellChecker {
|
||||
private static final Lexicon dictionary = ...;
|
||||
@ -18,7 +18,7 @@ public class SpellChecker {
|
||||
同样地,将它们实现为单例也并不少见 (条目 3):
|
||||
|
||||
|
||||
```Java
|
||||
```java
|
||||
// Inappropriate use of singleton - inflexible & untestable!
|
||||
public class SpellChecker {
|
||||
private final Lexicon dictionary = ...;
|
||||
@ -38,7 +38,7 @@ public class SpellChecker {
|
||||
所需要的是能够支持类的多个实例 (在我们的示例中,即 `SpellChecker`),每个实例都使用客户端所期望的资源 (在我们的例子中是 `dictionary`)。满足这一需求的简单模式是在创建新实例时将资源传递到构造方法中。这是依赖项注入(dependency injection)的一种形式:字典是拼写检查器的一个依赖项,当它创建时被注入到拼写检查器中。
|
||||
|
||||
|
||||
```Java
|
||||
```java
|
||||
// Dependency injection provides flexibility and testability
|
||||
public class SpellChecker {
|
||||
private final Lexicon dictionary;
|
||||
|
@ -5,14 +5,14 @@
|
||||
|
||||
作为一个不应该这样做的极端例子,请考虑以下语句:
|
||||
|
||||
```Java
|
||||
```java
|
||||
String s = new String("bikini"); // DON'T DO THIS!
|
||||
```
|
||||
|
||||
语句每次执行时都会创建一个新的 String 实例,而这些对象的创建都不是必需的。String 构造方法 `("bikini")` 的参数本身就是一个 `bikini` 实例,它与构造方法创建的所有对象的功能相同。如果这种用法发生在循环中,或者在频繁调用的方法中,就可以毫无必要地创建数百万个 String 实例。
|
||||
|
||||
改进后的版本如下:
|
||||
```Java
|
||||
```java
|
||||
String s = "bikini";
|
||||
```
|
||||
|
||||
@ -23,7 +23,7 @@ String s = "bikini";
|
||||
一些对象的创建比其他对象的创建要昂贵得多。 如果要重复使用这样一个“昂贵的对象”,建议将其缓存起来以便重复使用。 不幸的是,当创建这样一个对象时并不总是很直观明显的。 假设你想写一个方法来确定一个字符串是否是一个有效的罗马数字。 以下是使用正则表达式完成此操作时最简单方法:
|
||||
|
||||
|
||||
```Java
|
||||
```java
|
||||
// Performance can be greatly improved!
|
||||
static boolean isRomanNumeral(String s) {
|
||||
return s.matches("^(?=.)M*(C[MD]|D?C{0,3})"
|
||||
@ -35,7 +35,7 @@ static boolean isRomanNumeral(String s) {
|
||||
|
||||
为了提高性能,作为类初始化的一部分,将正则表达式显式编译为一个 `Pattern` 实例(不可变),缓存它,并在 `isRomanNumeral` 方法的每个调用中重复使用相同的实例:
|
||||
|
||||
```Java
|
||||
```java
|
||||
// Reusing expensive object for improved performance
|
||||
public class RomanNumerals {
|
||||
private static final Pattern ROMAN = Pattern.compile(
|
||||
@ -58,7 +58,7 @@ public class RomanNumerals {
|
||||
|
||||
另一种创建不必要的对象的方法是自动装箱(autoboxing),它允许程序员混用基本类型和包装的基本类型,根据需要自动装箱和拆箱。 自动装箱模糊不清,但不会消除基本类型和装箱基本类型之间的区别。 有微妙的语义区别和不那么细微的性能差异(条目 61)。 考虑下面的方法,它计算所有正整数的总和。 要做到这一点,程序必须使用 `long` 类型,因为 `int` 类型不足以保存所有正整数的总和:
|
||||
|
||||
```Java
|
||||
```java
|
||||
// Hideously slow! Can you spot the object creation?
|
||||
private static long sum() {
|
||||
Long sum = 0L;
|
||||
|
@ -5,7 +5,7 @@
|
||||
|
||||
考虑以下简单的堆栈实现:
|
||||
|
||||
```Java
|
||||
```java
|
||||
// Can you spot the "memory leak"?
|
||||
public class Stack {
|
||||
private Object[] elements;
|
||||
@ -45,7 +45,7 @@ public class Stack {
|
||||
|
||||
这类问题的解决方法很简单:一旦对象引用过期,将它们设置为 null。 在我们的 `Stack` 类的情景下,只要从栈中弹出,元素的引用就设置为过期。 `pop` 方法的修正版本如下所示:
|
||||
|
||||
```Java
|
||||
```java
|
||||
public Object pop() {
|
||||
if (size == 0)
|
||||
throw new EmptyStackException();
|
||||
|
@ -28,7 +28,7 @@
|
||||
|
||||
Cleaner 机制使用起来有点棘手。下面是演示该功能的一个简单的 `Room` 类。假设 `Room` 对象必须在被回收前清理干净。`Room` 类实现 `AutoCloseable` 接口;它的自动清理安全网使用的是一个 Cleaner 机制,这仅仅是一个实现细节。与 Finalizer 机制不同,Cleaner 机制不污染一个类的公共 API:
|
||||
|
||||
```Java
|
||||
```java
|
||||
// An autocloseable class using a cleaner as a safety net
|
||||
public class Room implements AutoCloseable {
|
||||
private static final Cleaner cleaner = Cleaner.create();
|
||||
@ -72,7 +72,7 @@ public class Room implements AutoCloseable {
|
||||
|
||||
就像我们之前说的,`Room` 的 Cleaner 机制仅仅被用作一个安全网。如果客户将所有 `Room` 的实例放在 try-with-resource 块中,则永远不需要自动清理。行为良好的客户端如下所示:
|
||||
|
||||
```Java
|
||||
```java
|
||||
public class Adult {
|
||||
public static void main(String[] args) {
|
||||
try (Room myRoom = new Room(7)) {
|
||||
@ -83,7 +83,7 @@ public class Adult {
|
||||
```
|
||||
正如你所预料的,运行 `Adult` 程序会打印 `Goodbye` 字符串,随后打印 `Cleaning room` 字符串。但是如果时不合规矩的程序,它从来不清理它的房间会是什么样的?
|
||||
|
||||
```Java
|
||||
```java
|
||||
public class Teenager {
|
||||
public static void main(String[] args) {
|
||||
new Room(99);
|
||||
|
@ -4,7 +4,7 @@
|
||||
|
||||
从以往来看,try-finally 语句是保证资源正确关闭的最佳方式,即使是在程序抛出异常或返回的情况下:
|
||||
|
||||
```Java
|
||||
```java
|
||||
// try-finally - No longer the best way to close resources!
|
||||
static String firstLineOfFile(String path) throws IOException {
|
||||
BufferedReader br = new BufferedReader(new FileReader(path));
|
||||
@ -18,7 +18,7 @@ static String firstLineOfFile(String path) throws IOException {
|
||||
|
||||
这可能看起来并不坏,但是当添加第二个资源时,情况会变得更糟:
|
||||
|
||||
```Java
|
||||
```java
|
||||
// try-finally is ugly when used with more than one resource!
|
||||
static void copy(String src, String dst) throws IOException {
|
||||
InputStream in = new FileInputStream(src);
|
||||
@ -45,7 +45,7 @@ static void copy(String src, String dst) throws IOException {
|
||||
|
||||
以下是我们的第一个使用 try-with-resources 的示例:
|
||||
|
||||
```Java
|
||||
```java
|
||||
// try-with-resources - the the best way to close resources!
|
||||
static String firstLineOfFile(String path) throws IOException {
|
||||
try (BufferedReader br = new BufferedReader(
|
||||
@ -57,7 +57,7 @@ static String firstLineOfFile(String path) throws IOException {
|
||||
|
||||
以下是我们的第二个使用 try-with-resources 的示例:
|
||||
|
||||
```Java
|
||||
```java
|
||||
// try-with-resources on multiple resources - short and sweet
|
||||
static void copy(String src, String dst) throws IOException {
|
||||
try (InputStream in = new FileInputStream(src);
|
||||
@ -74,7 +74,7 @@ static void copy(String src, String dst) throws IOException {
|
||||
可以在 try-with-resources 语句中添加 catch 子句,就像在常规的 try-finally 语句中一样。这允许你处理异常,而不会在另一层嵌套中污染代码。作为一个稍微有些做作的例子,这里有一个版本的 `firstLineOfFile` 方法,它不会抛出异常,但是如果它不能打开或读取文件,则返回默认值:
|
||||
|
||||
|
||||
```Java
|
||||
```java
|
||||
// try-with-resources with a catch clause
|
||||
static String firstLineOfFile(String path, String defaultVal) {
|
||||
try (BufferedReader br = new BufferedReader(
|
||||
|
@ -12,7 +12,7 @@
|
||||
- 父类已经重写了 equals 方法,则父类行为完全适合于该子类。例如,大多数 Set 从 AbstractSet 继承了 equals 实现、List 从 AbstractList 继承了 equals 实现,Map 从 AbstractMap 的 Map 继承了 equals 实现。
|
||||
- 类是私有的或包级私有的,可以确定它的 equals 方法永远不会被调用。如果你非常厌恶风险,可以重写 equals 方法,以确保不会被意外调用:
|
||||
|
||||
```Java
|
||||
```java
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
throw new AssertionError(); // Method is never called
|
||||
@ -42,7 +42,7 @@ equals 方法实现了一个等价关系(equivalence relation)。它有以
|
||||
|
||||
**对称性(Symmetry)**——第二个要求是,任何两个对象必须在是否相等的问题上达成一致。与第一个要求不同的是,我们不难想象在无意中违反了这一要求。例如,考虑下面的类,它实现了不区分大小写的字符串。字符串被 toString 保存,但在 equals 比较中被忽略:
|
||||
|
||||
```Java
|
||||
```java
|
||||
import java.util.Objects;
|
||||
|
||||
public final class CaseInsensitiveString {
|
||||
@ -67,7 +67,7 @@ public final class CaseInsensitiveString {
|
||||
```
|
||||
|
||||
上面类中的 equals 试图与正常的字符串进行操作,假设我们有一个不区分大小写的字符串和一个正常的字符串:
|
||||
```Java
|
||||
```java
|
||||
CaseInsensitiveString cis = new CaseInsensitiveString("Polish");
|
||||
String s = "polish";
|
||||
|
||||
@ -77,7 +77,7 @@ System.out.println(s.equals(cis)); // false
|
||||
|
||||
正如所料,`cis.equals(s)` 返回 true。 问题是,尽管 `CaseInsensitiveString` 类中的 equals 方法知道正常字符串,但 String 类中的 equals 方法却忽略了不区分大小写的字符串。 因此,`s.equals(cis)` 返回 false,明显违反对称性。 假设把一个不区分大小写的字符串放入一个集合中:
|
||||
|
||||
```Java
|
||||
```java
|
||||
List<CaseInsensitiveString> list = new ArrayList<>();
|
||||
list.add(cis);List<CaseInsensitiveString> list = new ArrayList<>();
|
||||
list.add(cis);
|
||||
@ -87,7 +87,7 @@ list.add(cis);
|
||||
|
||||
要消除这个问题,只需删除 equals 方法中与 String 类相互操作的恶意尝试。这样做之后,可以将该方法重构为单个返回语句:
|
||||
|
||||
```Java
|
||||
```java
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
return o instanceof CaseInsensitiveString &&
|
||||
@ -96,7 +96,7 @@ public boolean equals(Object o) {
|
||||
```
|
||||
**传递性(Transitivity)**——equals 约定的第三个要求是,如果第一个对象等于第二个对象,第二个对象等于第三个对象,那么第一个对象必须等于第三个对象。同样,也不难想象,无意中违反了这一要求。考虑子类的情况, 将新值组件( value component)添加到其父类中。换句话说,子类添加了一个信息,它影响了 equals 方法比较。让我们从一个简单不可变的二维整数类型 Point 类开始:
|
||||
|
||||
```Java
|
||||
```java
|
||||
public class Point {
|
||||
private final int x;
|
||||
private final int y;
|
||||
@ -120,7 +120,7 @@ public class Point {
|
||||
|
||||
假设想继承这个类,将表示颜色的 Color 类添加到 Point 类中:
|
||||
|
||||
```Java
|
||||
```java
|
||||
public class ColorPoint extends Point {
|
||||
private final Color color;
|
||||
|
||||
@ -136,7 +136,7 @@ public class ColorPoint extends Point {
|
||||
equals 方法应该是什么样子?如果完全忽略,则实现是从 Point 类上继承的,颜色信息在 equals 方法比较中被忽略。虽然这并不违反 equals 约定,但这显然是不可接受的。假设你写了一个 equals 方法,它只在它的参数是另一个具有相同位置和颜色的 ColorPoint 实例时返回 true:
|
||||
|
||||
|
||||
```Java
|
||||
```java
|
||||
// Broken - violates symmetry!
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
@ -148,14 +148,14 @@ public boolean equals(Object o) {
|
||||
|
||||
当你比较 Point 对象和 ColorPoint 对象时,可以会得到不同的结果,反之亦然。前者的比较忽略了颜色属性,而后者的比较会一直返回 false,因为参数的类型是错误的。为了让问题更加具体,我们创建一个 Point 对象和 ColorPoint 对象:
|
||||
|
||||
```Java
|
||||
```java
|
||||
Point p = new Point(1, 2);
|
||||
ColorPoint cp = new ColorPoint(1, 2, Color.RED);
|
||||
```
|
||||
|
||||
p.equals(cp) 返回 true,但是 cp.equals(p) 返回 false。你可能想使用 ColorPoint.equals 通过混合比较的方式来解决这个问题。
|
||||
|
||||
```Java
|
||||
```java
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (!(o instanceof Point))
|
||||
@ -172,7 +172,7 @@ public boolean equals(Object o) {
|
||||
|
||||
这种方法确实提供了对称性,但是丧失了传递性:
|
||||
|
||||
```Java
|
||||
```java
|
||||
ColorPoint p1 = new ColorPoint(1, 2, Color.RED);
|
||||
Point p2 = new Point(1, 2);
|
||||
ColorPoint p3 = new ColorPoint(1, 2, Color.BLUE);
|
||||
@ -186,7 +186,7 @@ ColorPoint p3 = new ColorPoint(1, 2, Color.BLUE);
|
||||
|
||||
你可能听说过,可以继承一个可实例化的类并添加一个值组件,同时通过在 equals 方法中使用一个 getClass 测试代替 instanceof 测试来保留 equals 约定:
|
||||
|
||||
```Java
|
||||
```java
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (o == null || o.getClass() != getClass())
|
||||
@ -198,7 +198,7 @@ public boolean equals(Object o) {
|
||||
|
||||
只有当对象具有相同的实现类时,才会产生相同的效果。这看起来可能不是那么糟糕,但是结果是不可接受的:一个 Point 类子类的实例仍然是一个 Point 的实例,它仍然需要作为一个 Point 来运行,但是如果你采用这个方法,就会失败!假设我们要写一个方法来判断一个 Point 对象是否在 unitCircle 集合中。我们可以这样做:
|
||||
|
||||
```Java
|
||||
```java
|
||||
private static final Set<Point> unitCircle = Set.of(
|
||||
new Point( 1, 0), new Point( 0, 1),
|
||||
new Point(-1, 0), new Point( 0, -1));
|
||||
@ -210,7 +210,7 @@ public static boolean onUnitCircle(Point p) {
|
||||
|
||||
虽然这可能不是实现功能的最快方法,但它可以正常工作。假设以一种不添加值组件的简单方式继承 Point 类,比如让它的构造方法跟踪记录创建了多少实例:
|
||||
|
||||
```Java
|
||||
```java
|
||||
public class CounterPoint extends Point {
|
||||
private static final AtomicInteger counter =
|
||||
new AtomicInteger();
|
||||
@ -230,7 +230,7 @@ public class CounterPoint extends Point {
|
||||
|
||||
虽然没有令人满意的方法来继承一个可实例化的类并添加一个值组件,但是有一个很好的变通方法:按照条目 18 的建议,“优先使用组合而不是继承”。取代继承 Point 类的 ColorPoint 类,可以在 ColorPoint 类中定义一个私有 Point 属性,和一个公共的试图(view)(条目 6)方法,用来返回具有相同位置的 ColorPoint 对象。
|
||||
|
||||
```Java
|
||||
```java
|
||||
// Adds a value component without violating the equals contract
|
||||
public class ColorPoint {
|
||||
private final Point point;
|
||||
@ -270,7 +270,7 @@ public class ColorPoint {
|
||||
|
||||
**非空性(Non-nullity)**——最后 equals 约定的要求没有官方的名称,所以我冒昧地称之为“非空性”。意思是说说所有的对象都必须不等于 null。虽然很难想象在调用 `o.equals(null)` 的响应中意外地返回 true,但不难想象不小心抛出 `NullPointerException` 异常的情况。通用的约定禁止抛出这样的异常。许多类中的 equals 方法都会明确阻止对象为 null 的情况:
|
||||
|
||||
```Java
|
||||
```java
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (o == null)
|
||||
@ -281,7 +281,7 @@ public boolean equals(Object o) {
|
||||
|
||||
这个判断是不必要的。 为了测试它的参数是否相等,equals 方法必须首先将其参数转换为合适类型,以便调用访问器或允许访问的属性。 在执行类型转换之前,该方法必须使用 instanceof 运算符来检查其参数是否是正确的类型:
|
||||
|
||||
```Java
|
||||
```java
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (!(o instanceof MyType))
|
||||
@ -312,7 +312,7 @@ public boolean equals(Object o) {
|
||||
|
||||
在下面这个简单的 `PhoneNumber` 类中展示了根据之前的配方构建的 equals 方法:
|
||||
|
||||
```Java
|
||||
```java
|
||||
public final class PhoneNumber {
|
||||
|
||||
private final short areaCode, prefix, lineNum;
|
||||
@ -353,7 +353,7 @@ public final class PhoneNumber {
|
||||
2. **不要让 equals 方法试图太聪明。**如果只是简单地测试用于相等的属性,那么要遵守 equals 约定并不困难。如果你在寻找相等方面过于激进,那么很容易陷入麻烦。一般来说,考虑到任何形式的别名通常是一个坏主意。例如,File 类不应该试图将引用的符号链接等同于同一文件对象。幸好 File 类并没这么做。
|
||||
3. **在 equal 时方法声明中,不要将参数 Object 替换成其他类型。**对于程序员来说,编写一个看起来像这样的 equals 方法并不少见,然后花上几个小时苦苦思索为什么它不能正常工作:在 equal 时方法声明中,不要将参数 Object 替换成其他类型。对于程序员来说,编写一个看起来像这样的 equals 方法并不少见,然后花上几个小时苦苦思索为什么它不能正常工作。
|
||||
|
||||
```Java
|
||||
```java
|
||||
// Broken - parameter type must be Object!
|
||||
public boolean equals(MyClass o) {
|
||||
…
|
||||
@ -363,7 +363,7 @@ public boolean equals(MyClass o) {
|
||||
问题在于这个方法并没有重写 Object.equals 方法,它的参数是 Object 类型的,这样写只是重载了 equals 方法(Item 52)。 即使除了正常的方法之外,提供这种“强类型”的 equals 方法也是不可接受的,因为它可能会导致子类中的 Override 注解产生误报,提供不安全的错觉。
|
||||
在这里,使用 Override 注解会阻止你犯这个错误 (条目 40)。这个 equals 方法不会编译,错误消息会告诉你到底错在哪里:
|
||||
|
||||
```Java
|
||||
```java
|
||||
// Still broken, but won’t compile
|
||||
@Override
|
||||
public boolean equals(MyClass o) {
|
||||
|
@ -10,7 +10,7 @@
|
||||
|
||||
举例说明,假设你使用条目 10 中的 `PhoneNumber` 类的实例做为 HashMap 的键(key):
|
||||
|
||||
```Java
|
||||
```java
|
||||
Map<PhoneNumber, String> m = new HashMap<>();
|
||||
|
||||
m.put(new PhoneNumber(707, 867, 5309), "Jenny");
|
||||
@ -21,7 +21,7 @@ m.put(new PhoneNumber(707, 867, 5309), "Jenny");
|
||||
解决这个问题很简单,只需要为 `PhoneNumber` 类重写一个合适的 hashCode 方法。hashCode 方法是什么样的?写一个不规范的方法的是很简单的。以下示例,虽然永远是合法的,但绝对不能这样使用:
|
||||
|
||||
|
||||
```Java
|
||||
```java
|
||||
// The worst possible legal hashCode implementation - never use!
|
||||
|
||||
@Override public int hashCode() { return 42; }
|
||||
@ -51,7 +51,7 @@ m.put(new PhoneNumber(707, 867, 5309), "Jenny");
|
||||
|
||||
让我们把上述办法应用到 `PhoneNumber` 类中:
|
||||
|
||||
```Java
|
||||
```java
|
||||
// Typical hashCode method
|
||||
|
||||
@Override
|
||||
@ -75,7 +75,7 @@ public int hashCode() {
|
||||
`Objects` 类有一个静态方法,它接受任意数量的对象并为它们返回一个哈希码。 这个名为 hash 的方法可以让你编写一行 hashCode 方法,其质量与根据这个项目中的上面编写的方法相当。 不幸的是,它们的运行速度更慢,因为它们需要创建数组以传递可变数量的参数,以及如果任何参数是基本类型,则进行装箱和取消装箱。 这种哈希函数的风格建议仅在性能不重要的情况下使用。 以下是使用这种技术编写的 `PhoneNumber` 的哈希函数:
|
||||
|
||||
|
||||
```Java
|
||||
```java
|
||||
// One-line hashCode method - mediocre performance
|
||||
|
||||
@Override
|
||||
@ -88,7 +88,7 @@ public int hashCode() {
|
||||
|
||||
如果一个类是不可变的,并且计算哈希码的代价很大,那么可以考虑在对象中缓存哈希码,而不是在每次请求时重新计算哈希码。 如果你认为这种类型的大多数对象将被用作哈希键,那么应该在创建实例时计算哈希码。 否则,可以选择在首次调用 hashCode 时延迟初始化(lazily initialize)哈希码。 需要注意确保类在存在延迟初始化属性的情况下保持线程安全(项目 83)。 `PhoneNumber` 类不适合这种情况,但只是为了展示它是如何完成的。 请注意,属性 hashCode 的初始值(在本例中为 0)不应该是通常创建的实例的哈希码:
|
||||
|
||||
```Java
|
||||
```java
|
||||
// hashCode method with lazily initialized cached hash code
|
||||
|
||||
private int hashCode; // Automatically initialized to 0
|
||||
|
@ -6,7 +6,7 @@
|
||||
|
||||
如果为 `PhoneNumber` 提供了一个很好的 toString 方法,那么生成一个有用的诊断消息就像下面这样简单:
|
||||
|
||||
```Java
|
||||
```java
|
||||
System.out.println("Failed to connect to " + phoneNumber);
|
||||
```
|
||||
|
||||
@ -15,7 +15,7 @@ System.out.println("Failed to connect to " + phoneNumber);
|
||||
实际上,toString 方法应该返回对象中包含的所有需要关注的信息,如电话号码示例中所示。 如果对象很大或者包含不利于字符串表示的状态,这是不切实际的。 在这种情况下,toString 应该返回一个摘要,如 `Manhattan residential phone directory (1487536 listings)` 或线程`[main,5,main]`。 理想情况下,字符串应该是不言自明的(线程示例并没有遵守这点)。 如果未能将所有对象的值得关注的信息包含在字符串表示中,则会导致一个特别烦人的处罚:测试失败报告如下所示:
|
||||
|
||||
|
||||
```Java
|
||||
```java
|
||||
Assertion failure: expected {abc, 123}, but was {abc, 123}.
|
||||
```
|
||||
|
||||
@ -25,7 +25,7 @@ Assertion failure: expected {abc, 123}, but was {abc, 123}.
|
||||
|
||||
无论是否决定指定格式,你都应该清楚地在文档中表明你的意图。如果指定了格式,则应该这样做。例如,这里有一个 toString 方法,该方法在条目 11 中使用 `PhoneNumber` 类:
|
||||
|
||||
```Java
|
||||
```java
|
||||
/**
|
||||
* Returns the string representation of this phone number.
|
||||
* The string consists of twelve characters whose format is
|
||||
@ -47,7 +47,7 @@ public String toString() {
|
||||
|
||||
如果你决定不指定格式,那么文档注释应该是这样的:
|
||||
|
||||
```Java
|
||||
```java
|
||||
/**
|
||||
* Returns a brief description of this potion. The exact details
|
||||
* of the representation are unspecified and subject to change,
|
||||
|
@ -18,7 +18,7 @@
|
||||
|
||||
假设你希望在一个类中实现 Cloneable 接口,它的父类提供了一个行为良好的 clone 方法。首先调用 super.clone。 得到的对象将是原始的完全功能的复制品。 在你的类中声明的任何属性将具有与原始属性相同的值。 如果每个属性包含原始值或对不可变对象的引用,则返回的对象可能正是你所需要的,在这种情况下,不需要进一步的处理。 例如,对于条目 11 中的 `PhoneNumber` 类,情况就是这样,但是请注意,不可变类永远不应该提供 clone 方法,因为这只会浪费复制。 有了这个警告,以下是 `PhoneNumber` 类的 clone 方法:
|
||||
|
||||
```Java
|
||||
```java
|
||||
// Clone method for class with no references to mutable state
|
||||
@Override public PhoneNumber clone() {
|
||||
try {
|
||||
@ -35,7 +35,7 @@
|
||||
|
||||
如果对象包含引用可变对象的属性,则前面显示的简单 clone 实现可能是灾难性的。 例如,考虑条目 7 中的 Stack 类:
|
||||
|
||||
```Java
|
||||
```java
|
||||
public class Stack {
|
||||
|
||||
private Object[] elements;
|
||||
@ -72,7 +72,7 @@ public class Stack {
|
||||
|
||||
这种情况永远不会发生,因为调用 Stack 类中的唯一构造方法。 实际上,clone 方法作为另一种构造方法; 必须确保它不会损坏原始对象,并且可以在克隆上正确建立不变量。 为了使 Stack 上的 clone 方法正常工作,它必须复制 stack 对象的内部。 最简单的方法是对元素数组递归调用 clone 方法:
|
||||
|
||||
```Java
|
||||
```java
|
||||
// Clone method for class with references to mutable state
|
||||
@Override public Stack clone() {
|
||||
try {
|
||||
@ -91,7 +91,7 @@ public class Stack {
|
||||
|
||||
仅仅递归地调用 clone 方法并不总是足够的。 例如,假设您正在为哈希表编写一个 clone 方法,其内部包含一个哈希桶数组,每个哈希桶都指向“键-值”对链表的第一项。 为了提高性能,该类实现了自己的轻量级单链表,而没有使用 java 内部提供的 java.util.LinkedList:
|
||||
|
||||
```Java
|
||||
```java
|
||||
public class HashTable implements Cloneable {
|
||||
private Entry[] buckets = ...;
|
||||
private static class Entry {
|
||||
@ -109,7 +109,7 @@ public class HashTable implements Cloneable {
|
||||
}
|
||||
```
|
||||
假设你只是递归地克隆哈希桶数组,就像我们为 Stack 所做的那样:
|
||||
```Java
|
||||
```java
|
||||
// Broken clone method - results in shared mutable state!
|
||||
@Override public HashTable clone() {
|
||||
try {
|
||||
@ -122,7 +122,7 @@ public class HashTable implements Cloneable {
|
||||
}
|
||||
```
|
||||
虽然被克隆的对象有自己的哈希桶数组,但是这个数组引用与原始数组相同的链表,这很容易导致克隆对象和原始对象中的不确定性行为。 要解决这个问题,你必须复制包含每个桶的链表。 下面是一种常见的方法:
|
||||
```Java
|
||||
```java
|
||||
// Recursive clone method for class with complex mutable state
|
||||
public class HashTable implements Cloneable {
|
||||
private Entry[] buckets = ...;
|
||||
@ -161,7 +161,7 @@ public class HashTable implements Cloneable {
|
||||
}
|
||||
```
|
||||
私有类 HashTable.Entry 已被扩充以支持“深度复制”方法。 HashTable 上的 clone 方法分配一个合适大小的新哈希桶数组,迭代原来哈希桶数组,深度复制每个非空的哈希桶。 Entry 上的 deepCopy 方法递归地调用它自己以复制由头节点开始的整个链表。 如果哈希桶不是太长,这种技术很聪明并且工作正常。但是,克隆链表不是一个好方法,因为它为列表中的每个元素消耗一个栈帧(stack frame)。 如果列表很长,这很容易导致堆栈溢出。 为了防止这种情况发生,可以用迭代来替换 deepCopy 中的递归:
|
||||
```Java
|
||||
```java
|
||||
// Iteratively copy the linked list headed by this Entry
|
||||
Entry deepCopy() {
|
||||
Entry result = new Entry(key, value, next);
|
||||
@ -177,7 +177,7 @@ Entry deepCopy() {
|
||||
Object 类的 clone 方法被声明为抛出 CloneNotSupportedException 异常,但重写方法时不需要。 公共 clone 方法应该省略 throws 子句,因为不抛出检查时异常的方法更容易使用(条目 71)。
|
||||
|
||||
在为继承设计一个类时(条目 19),通常有两种选择,但无论选择哪一种,都不应该实现 Clonable 接口。你可以选择通过实现正确运行的受保护的 clone 方法来模仿 Object 的行为,该方法声明为抛出 CloneNotSupportedException 异常。 这给了子类实现 Cloneable 接口的自由,就像直接继承 Object 一样。 或者,可以选择不实现工作的 clone 方法,并通过提供以下简并 clone 实现来阻止子类实现它:
|
||||
```Java
|
||||
```java
|
||||
// clone method for extendable class not supporting Cloneable
|
||||
@Override
|
||||
protected final Object clone() throws CloneNotSupportedException {
|
||||
@ -189,12 +189,12 @@ protected final Object clone() throws CloneNotSupportedException {
|
||||
回顾一下,实现 Cloneable 的所有类应该重写公共 clone 方法,而这个方法的返回类型是类本身。 这个方法应该首先调用 super.clone,然后修复任何需要修复的属性。 通常,这意味着复制任何包含内部“深层结构”的可变对象,并用指向新对象的引用来代替原来指向这些对象的引用。虽然这些内部拷贝通常可以通过递归调用 clone 来实现,但这并不总是最好的方法。 如果类只包含基本类型或对不可变对象的引用,那么很可能是没有属性需要修复的情况。 这个规则也有例外。 例如,表示序列号或其他唯一 ID 的属性即使是基本类型的或不可变的,也需要被修正。
|
||||
|
||||
这么复杂是否真的有必要?很少。 如果你继承一个已经实现了 Cloneable 接口的类,你别无选择,只能实现一个行为良好的 clone 方法。 否则,通常你最好提供另一种对象复制方法。 对象复制更好的方法是提供一个复制构造方法或复制工厂。 复制构造方法接受参数,其类型为包含此构造方法的类,例如:
|
||||
```Java
|
||||
```java
|
||||
// Copy constructor
|
||||
public Yum(Yum yum) { ... };
|
||||
```
|
||||
复制工厂类似于复制构造方法的静态工厂:
|
||||
```Java
|
||||
```java
|
||||
// Copy factory
|
||||
public static Yum newInstance(Yum yum) { ... };
|
||||
```
|
||||
|
@ -2,13 +2,13 @@
|
||||
|
||||
与本章讨论的其他方法不同,`compareTo` 方法并没有在 `Object` 类中声明。 相反,它是 ``Comparable`` 接口中的唯一方法。 它与 Object 类的 equals 方法在性质上是相似的,除了它允许在简单的相等比较之外的顺序比较,它是泛型的。 通过实现 `Comparable` 接口,一个类表明它的实例有一个自然顺序( natural ordering)。 对实现 `Comparable` 接口的对象数组排序非常简单,如下所示:
|
||||
|
||||
```Java
|
||||
```java
|
||||
Arrays.sort(a);
|
||||
```
|
||||
|
||||
它很容易查找,计算极端数值,以及维护 `Comparable` 对象集合的自动排序。例如,在下面的代码中,依赖于 String 类实现了 `Comparable` 接口,去除命令行参数输入重复的字符串,并按照字母顺序排序:
|
||||
|
||||
```Java
|
||||
```java
|
||||
public class WordList {
|
||||
|
||||
public static void main(String[] args) {
|
||||
@ -19,7 +19,7 @@ public class WordList {
|
||||
}
|
||||
```
|
||||
通过实现 `Comparable` 接口,可以让你的类与所有依赖此接口的通用算法和集合实现进行互操作。 只需少量的努力就可以获得巨大的能量。 几乎 Java 平台类库中的所有值类以及所有枚举类型(条目 34)都实现了 `Comparable` 接口。 如果你正在编写具有明显自然顺序(如字母顺序,数字顺序或时间顺序)的值类,则应该实现 `Comparable` 接口:
|
||||
```Java
|
||||
```java
|
||||
public interface Comparable<T> {
|
||||
int compareTo(T t);
|
||||
}
|
||||
@ -51,7 +51,7 @@ public interface Comparable<T> {
|
||||
|
||||
在 `compareTo` 方法中,比较属性的顺序而不是相等。 要比较对象引用属性,请递归调用 `compareTo` 方法。 如果一个属性没有实现 Comparable,或者你需要一个非标准的顺序,那么使用 `Comparator` 接口。 可以编写自己的比较器或使用现有的比较器,如在条目 10 中的 `CaseInsensitiveString` 类的 `compareTo` 方法中:
|
||||
|
||||
```Java
|
||||
```java
|
||||
// Single-field Comparable with object reference field
|
||||
public final class CaseInsensitiveString
|
||||
implements Comparable<CaseInsensitiveString> {
|
||||
@ -68,7 +68,7 @@ public final class CaseInsensitiveString
|
||||
|
||||
如果一个类有多个重要的属性,那么比较他们的顺序是至关重要的。 从最重要的属性开始,逐步比较所有的重要属性。 如果比较结果不是零(零表示相等),则表示比较完成; 只是返回结果。 如果最重要的字段是相等的,比较下一个重要的属性,依此类推,直到找到不相等的属性或比较剩余不那么重要的属性。 以下是条目 11 中 `PhoneNumber` 类的 `compareTo` 方法,演示了这种方法:
|
||||
|
||||
```Java
|
||||
```java
|
||||
// Multiple-field `Comparable` with primitive fields
|
||||
public int compareTo(PhoneNumber pn) {
|
||||
int result = [Short.compare(areaCode](http://Short.compare(areaCode), pn.areaCode);
|
||||
@ -83,7 +83,7 @@ public int compareTo(PhoneNumber pn) {
|
||||
|
||||
在 Java 8 中 `Comparator` 接口提供了一系列比较器方法,可以使比较器流畅地构建。 这些比较器可以用来实现 `compareTo` 方法,就像 `Comparable` 接口所要求的那样。 许多程序员更喜欢这种方法的简洁性,尽管它的性能并不出众:在我的机器上排序 PhoneNumber 实例的数组速度慢了大约 10%。 在使用这种方法时,考虑使用 Java 的静态导入,以便可以通过其简单名称来引用比较器静态方法,以使其清晰简洁。 以下是 PhoneNumber 的 `compareTo` 方法的使用方法:
|
||||
|
||||
```Java
|
||||
```java
|
||||
// Comparable with comparator construction methods
|
||||
private static final Comparator<PhoneNumber> COMPARATOR =
|
||||
comparingInt((PhoneNumber pn) -> pn.areaCode)
|
||||
@ -105,7 +105,7 @@ public int compareTo(PhoneNumber pn) {
|
||||
|
||||
有时,你可能会看到 `compareTo` 或 `compare` 方法依赖于两个值之间的差值,如果第一个值小于第二个值,则为负;如果两个值相等则为零,如果第一个值大于,则为正值。这是一个例子:
|
||||
|
||||
```Java
|
||||
```java
|
||||
// BROKEN difference-based comparator - violates transitivity!
|
||||
|
||||
static Comparator<Object> hashCodeOrder = new Comparator<>() {
|
||||
@ -117,7 +117,7 @@ static Comparator<Object> hashCodeOrder = new Comparator<>() {
|
||||
|
||||
不要使用这种技术!它可能会导致整数最大长度溢出和 IEEE 754 浮点运算失真的危险[JLS 15.20.1,15.21.1]。 此外,由此产生的方法不可能比使用上述技术编写的方法快得多。 使用静态 `compare` 方法:
|
||||
|
||||
```Java
|
||||
```java
|
||||
// Comparator based on static compare method
|
||||
static Comparator<Object> hashCodeOrder = new Comparator<>() {
|
||||
public int compare(Object o1, Object o2) {
|
||||
@ -128,7 +128,7 @@ static Comparator<Object> hashCodeOrder = new Comparator<>() {
|
||||
|
||||
或者使用 `Comparator` 的构建方法:
|
||||
|
||||
```Java
|
||||
```java
|
||||
// Comparator based on Comparator construction method
|
||||
static Comparator<Object> hashCodeOrder =
|
||||
Comparator.comparingInt(o -> o.hashCode());
|
||||
|
@ -33,14 +33,14 @@
|
||||
|
||||
请注意,非零长度的数组总是可变的,**所以类具有公共静态 final 数组属性,或返回这样一个属性的访问器是错误的。** 如果一个类有这样的属性或访问方法,客户端将能够修改数组的内容。 这是安全漏洞的常见来源:
|
||||
|
||||
```Java
|
||||
```java
|
||||
// Potential security hole!
|
||||
public static final Thing[] VALUES = { ... };
|
||||
```
|
||||
|
||||
要小心这样的事实,一些 IDE 生成的访问方法返回对私有数组属性的引用,导致了这个问题。 有两种方法可以解决这个问题。 你可以使公共数组私有并添加一个公共的不可变列表:
|
||||
|
||||
```Java
|
||||
```java
|
||||
private static final Thing[] PRIVATE_VALUES = { ... };
|
||||
|
||||
public static final List<Thing> VALUES =
|
||||
@ -50,7 +50,7 @@ Collections.unmodifiableList(Arrays.asList(PRIVATE_VALUES));
|
||||
|
||||
或者,可以将数组设置为 private,并添加一个返回私有数组拷贝的公共方法:
|
||||
|
||||
```Java
|
||||
```java
|
||||
private static final Thing[] PRIVATE_VALUES = { ... };
|
||||
|
||||
public static final Thing[] values() {
|
||||
|
@ -2,7 +2,7 @@
|
||||
|
||||
有时候,你可能会试图写一些退化的类([degenerate classes](https://stackoverflow.com/questions/6810982/what-is-a-degenerate-class)),除了集中实例属性之外别无用处:
|
||||
|
||||
```Java
|
||||
```java
|
||||
// Degenerate classes like this should not be public!
|
||||
class Point {
|
||||
public double x;
|
||||
@ -13,7 +13,7 @@ class Point {
|
||||
由于这些类的数据属性可以直接被访问,因此这些类不提供封装的好处(条目 15)。 如果不更改 API,则无法更改其表示形式,无法强制执行不变量,并且在访问属性时无法执行辅助操作。 坚持面向对象的程序员觉得这样的类是厌恶的,应该被具有私有属性和公共访问方法的类(getter)所取代,而对于可变类来说,它们应该被替换为 setter 设值方法:
|
||||
|
||||
|
||||
```Java
|
||||
```java
|
||||
// Encapsulation of data by accessor methods and mutators
|
||||
class Point {
|
||||
private double x;
|
||||
@ -44,7 +44,7 @@ class Point {
|
||||
虽然公共类直接暴露属性并不是一个好主意,但是如果属性是不可变的,那么危害就不那么大了。当一个属性是只读的时候,除了更改类的 API 外,你不能改变类的内部表示形式,也不能采取一些辅助的行为,但是可以加强不变性。例如,下面的例子中保证每个实例表示一个有效的时间:
|
||||
|
||||
|
||||
```Java
|
||||
```java
|
||||
// Public class with exposed immutable fields - questionable
|
||||
|
||||
public final class Time {
|
||||
|
@ -14,7 +14,7 @@
|
||||
|
||||
以前条目中的许多示例类都是不可变的。 其中这样的类是条目 11 中的 `PhoneNumber` 类,它具有每个属性的访问方法(accessors),但没有相应的设值方法(mutators)。 这是一个稍微复杂一点的例子:
|
||||
|
||||
```Java
|
||||
```java
|
||||
// Immutable complex number class
|
||||
|
||||
public final class Complex {
|
||||
@ -90,7 +90,7 @@ public final class Complex {
|
||||
|
||||
**不可变对象本质上是线程安全的; 它们不需要同步。** 被多个线程同时访问它们时并不会被破坏。 这是实现线程安全的最简单方法。 由于没有线程可以观察到另一个线程对不可变对象的影响,所以**不可变对象可以被自由地共享。** 因此,不可变类应鼓励客户端尽可能重用现有的实例。 一个简单的方法是为常用的值提供公共的静态 final 常量。 例如,`Complex` 类可能提供这些常量:
|
||||
|
||||
```Java
|
||||
```java
|
||||
public static final Complex ZERO = new Complex(0, 0);
|
||||
public static final Complex ONE = new Complex(1, 0);
|
||||
public static final Complex I = new Complex(0, 1);
|
||||
@ -107,14 +107,14 @@ public static final Complex I = new Complex(0, 1);
|
||||
|
||||
**不可变类的主要缺点是对于每个不同的值都需要一个单独的对象。** 创建这些对象可能代价很高,特别是如果是大型的对象下。 例如,假设你有一个百万位的 `BigInteger` ,你想改变它的低位:
|
||||
|
||||
```Java
|
||||
```java
|
||||
BigInteger moby = ...;
|
||||
moby = moby.flipBit(0);
|
||||
```
|
||||
|
||||
`flipBit` 方法创建一个新的 `BigInteger` 实例,也是一百万位长,与原始位置只有一位不同。 该操作需要与 `BigInteger` 大小成比例的时间和空间。 将其与 `java.util.BitSet` 对比。 像 `BigInteger` 一样,`BitSet` 表示一个任意长度的位序列,但与 `BigInteger` 不同,`BitSet` 是可变的。 `BitSet` 类提供了一种方法,允许你在固定时间内更改百万位实例中单个位的状态:
|
||||
|
||||
```Java
|
||||
```java
|
||||
BitSet moby = ...;
|
||||
moby.flip(0);
|
||||
```
|
||||
@ -124,7 +124,7 @@ moby.flip(0);
|
||||
|
||||
现在你已经知道如何创建一个不可改变类,并且了解不变性的优点和缺点,下面我们来讨论几个设计方案。 回想一下,为了保证不变性,一个类不得允许子类化。 这可以通过使类用 `final` 修饰,但是还有另外一个更灵活的选择。 而不是使不可变类设置为 `final`,可以使其所有的构造方法私有或包级私有,并添加公共静态工厂,而不是公共构造方法(条目 1)。 为了具体说明这种方法,下面以 `Complex` 为例,看看如何使用这种方法:
|
||||
|
||||
```Java
|
||||
```java
|
||||
// Immutable class with static factories instead of constructors
|
||||
|
||||
public class Complex {
|
||||
@ -148,7 +148,7 @@ public class Complex {
|
||||
|
||||
当 `BigInteger` 和 `BigDecimal` 被写入时,不可变类必须是有效的 `final`,因此它们的所有方法都可能被重写。不幸的是,在保持向后兼容性的同时,这一事实无法纠正。如果你编写一个安全性取决于来自不受信任的客户端的 `BigInteger` 或 `BigDecimal` 参数的不变类时,则必须检查该参数是“真实的”`BigInteger` 还是 `BigDecimal`,而不应该是不受信任的子类的实例。如果是后者,则必须在假设可能是可变的情况下保护性拷贝(defensively copy)(条目 50):
|
||||
|
||||
```Java
|
||||
```java
|
||||
public static BigInteger safeInstance(BigInteger val) {
|
||||
return val.getClass() == BigInteger.class ?
|
||||
val : new BigInteger(val.toByteArray());
|
||||
|
@ -6,7 +6,7 @@
|
||||
|
||||
为了具体说明,假设有一个使用 `HashSet` 的程序。 为了调整程序的性能,需要查询 `HashSet` ,从创建它之后已经添加了多少个元素(不要和当前的元素数量混淆,当元素被删除时数量也会下降)。 为了提供这个功能,编写了一个 `HashSet` 变体,它保留了尝试元素插入的数量,并导出了这个插入数量的一个访问方法。 `HashSet` 类包含两个添加元素的方法,分别是 `add` 和 `addAll`,所以我们重写这两个方法:
|
||||
|
||||
```Java
|
||||
```java
|
||||
// Broken - Inappropriate use of inheritance!
|
||||
public class InstrumentedHashSet<E> extends HashSet<E> {
|
||||
// The number of attempted element insertions
|
||||
@ -34,7 +34,7 @@ public class InstrumentedHashSet<E> extends HashSet<E> {
|
||||
|
||||
这个类看起来很合理,但是不能正常工作。 假设创建一个实例并使用 `addAll` 方法添加三个元素。 顺便提一句,请注意,下面代码使用在 Java 9 中添加的静态工厂方法 `List.of` 来创建一个列表;如果使用的是早期版本,请改为使用 `Arrays.asList`:
|
||||
|
||||
```Java
|
||||
```java
|
||||
InstrumentedHashSet<String> s = new InstrumentedHashSet<>();
|
||||
s.addAll(List.of("Snap", "Crackle", "Pop"));
|
||||
```
|
||||
@ -50,7 +50,7 @@ s.addAll(List.of("Snap", "Crackle", "Pop"));
|
||||
|
||||
幸运的是,有一种方法可以避免上述所有的问题。不要继承一个现有的类,而应该给你的新类增加一个私有属性,该属性是 现有类的实例引用,这种设计被称为组合(composition),因为现有的类成为新类的组成部分。新类中的每个实例方法调用现有类的包含实例上的相应方法并返回结果。这被称为转发(forwarding),而新类中的方法被称为转发方法。由此产生的类将坚如磐石,不依赖于现有类的实现细节。即使将新的方法添加到现有的类中,也不会对新类产生影响。为了具体说用,下面代码使用组合和转发方法替代 `InstrumentedHashSet` 类。请注意,实现分为两部分,类本身和一个可重用的转发类,其中包含所有的转发方法,没有别的方法:
|
||||
|
||||
```Java
|
||||
```java
|
||||
// Reusable forwarding class
|
||||
import java.util.Collection;
|
||||
import java.util.Iterator;
|
||||
@ -133,7 +133,7 @@ public class ForwardingSet<E> implements Set<E> {
|
||||
}
|
||||
```
|
||||
|
||||
```Java
|
||||
```java
|
||||
// Wrapper class - uses composition in place of inheritance
|
||||
import java.util.Collection;
|
||||
import java.util.Set;
|
||||
@ -163,14 +163,14 @@ public class InstrumentedSet<E> extends ForwardingSet<E> {
|
||||
```
|
||||
`InstrumentedSet` 类的设计是通过存在的 `Set` 接口来实现的,该接口包含 `HashSet` 类的功能特性。除了功能强大,这个设计是非常灵活的。`InstrumentedSet` 类实现了 `Set` 接口,并有一个构造方法,其参数也是 `Set` 类型的。本质上,这个类把 `Set` 转换为另一个类型 `Set`, 同时添加了计数的功能。与基于继承的方法不同,该方法仅适用于单个具体类,并且父类中每个需要支持构造方法,提供单独的构造方法,所以可以使用包装类来包装任何 `Set` 实现,并且可以与任何预先存在的构造方法结合使用:
|
||||
|
||||
```Java
|
||||
```java
|
||||
Set<Instant> times = new InstrumentedSet<>(new TreeSet<>(cmp));
|
||||
Set<E> s = new InstrumentedSet<>(new HashSet<>(INIT_CAPACITY));
|
||||
```
|
||||
|
||||
`InstrumentedSet` 类甚至可以用于临时替换没有计数功能下使用的集合实例:
|
||||
|
||||
```Java
|
||||
```java
|
||||
static void walk(Set<Dog> dogs) {
|
||||
InstrumentedSet<Dog> iDogs = new InstrumentedSet<>(dogs);
|
||||
... // Within this method use iDogs instead of dogs
|
||||
|
@ -6,7 +6,7 @@
|
||||
|
||||
调用可重写方法的方法在文档注释结束时包含对这些调用的描述。 这些描述在规范中特定部分,标记为“Implementation Requirements”,由 Javadoc 标签 `@implSpec` 生成。 本节介绍该方法的内部工作原理。 下面是从 `java.util.AbstractCollection` 类的规范中拷贝的例子:
|
||||
|
||||
```Java
|
||||
```java
|
||||
public boolean remove(Object o)
|
||||
Removes a single instance of the specified element from this collection, if it is present (optional operation). More formally, removes an element e such that Objects.equals(o, e), if this collection contains one or more such elements. Returns true if this collection contained the specified element (or equivalently, if this collection changed as a result of the call).
|
||||
|
||||
@ -25,7 +25,7 @@ Implementation Requirements: This implementation iterates over the collection lo
|
||||
|
||||
设计继承涉及的不仅仅是文档说明自用的模式。 为了让程序员能够写出有效的子类而不会带来不适当的痛苦,一个类可能以明智选择的受保护方法的形式提供内部工作,或者在罕见的情况下,提供受保护的属性。 例如,考虑 `java.util.AbstractList` 中的 `removeRange` 方法:
|
||||
|
||||
```Java
|
||||
```java
|
||||
protected void removeRange(int fromIndex, int toIndex)
|
||||
Removes from this list all of the elements whose index is between fromIndex, inclusive, and toIndex, exclusive. Shifts any succeeding elements to the left (reduces their index). This call shortens the list by (toIndex - fromIndex) elements. (If toIndex == fromIndex, this operation has no effect.)
|
||||
This method is called by the clear operation on this list and its sublists. Overriding this method to take advantage of the internals of the list implementation can substantially improve the performance of the clear operation on this list and its sublists.
|
||||
@ -58,7 +58,7 @@ toIndex index after last element to be removed.
|
||||
|
||||
还有一些类必须遵守允许继承的限制。 **构造方法绝不能直接或间接调用可重写的方法。** 如果违反这个规则,将导致程序失败。 父类构造方法在子类构造方法之前运行,所以在子类构造方法运行之前,子类中的重写方法被调用。 如果重写方法依赖于子类构造方法执行的任何初始化,则此方法将不会按预期运行。 为了具体说明,这是一个违反这个规则的类:
|
||||
|
||||
```Java
|
||||
```java
|
||||
public class Super {
|
||||
// Broken - constructor invokes an overridable method
|
||||
public Super() {
|
||||
@ -71,7 +71,7 @@ public class Super {
|
||||
|
||||
以下是一个重写 `overrideMe` 方法的子类,`Super` 类的唯一构造方法会错误地调用它:
|
||||
|
||||
```Java
|
||||
```java
|
||||
public final class Sub extends Super {
|
||||
// Blank final, set by constructor
|
||||
private final Instant instant;
|
||||
|
@ -8,7 +8,7 @@
|
||||
|
||||
接口允许构建非层级类型的框架。 类型层级对于组织某些事物来说是很好的,但是其他的事物并不是整齐地落入严格的层级结构中。 例如,假设我们有一个代表歌手的接口,和另一个代表作曲家的接口:
|
||||
|
||||
```Java
|
||||
```java
|
||||
public interface Singer {
|
||||
AudioClip sing(Song s);
|
||||
}
|
||||
@ -20,7 +20,7 @@ public interface Songwriter {
|
||||
|
||||
在现实生活中,一些歌手也是作曲家。 因为我们使用接口而不是抽象类来定义这些类型,所以单个类实现歌手和作曲家两个接口是完全允许的。 事实上,我们可以定义一个继承歌手和作曲家的第三个接口,并添加适合于这个组合的新方法:
|
||||
|
||||
```Java
|
||||
```java
|
||||
public interface SingerSongwriter extends Singer, Songwriter {
|
||||
AudioClip strum();
|
||||
void actSensitive();
|
||||
@ -39,7 +39,7 @@ public interface SingerSongwriter extends Singer, Songwriter {
|
||||
|
||||
按照惯例,骨架实现类被称为 `AbstractInterface`,其中 `Interface` 是它们实现的接口的名称。 例如,集合框架( Collections Framework)提供了一个框架实现以配合每个主要集合接口:`AbstractCollection`,`AbstractSet`,`AbstractList` 和 `AbstractMap`。 可以说,将它们称为 `SkeletalCollection`,`SkeletalSet`,`SkeletalList` 和 `SkeletalMap` 是有道理的,但是现在已经确立了抽象约定。 如果设计得当,骨架实现(无论是单独的抽象类还是仅由接口上的默认方法组成)可以使程序员非常容易地提供他们自己的接口实现。 例如,下面是一个静态工厂方法,在 `AbstractList` 的顶层包含一个完整的功能齐全的 `List` 实现:
|
||||
|
||||
```Java
|
||||
```java
|
||||
// Concrete implementation built atop skeletal implementation
|
||||
static List<Integer> intArrayAsList(int[] a) {
|
||||
Objects.requireNonNull(a);
|
||||
@ -75,7 +75,7 @@ static List<Integer> intArrayAsList(int[] a) {
|
||||
|
||||
作为一个简单的例子,考虑一下 `Map.Entry` 接口。 显而易见的基本方法是 `getKey`,`getValue` 和(可选的)`setValue`。 接口指定了 `equals` 和 `hashCode` 的行为,并且在基本方面方面有一个 `toString` 的明显的实现。 由于不允许为 `Object` 类方法提供默认实现,因此所有实现均放置在骨架实现类中:
|
||||
|
||||
```Java
|
||||
```java
|
||||
// Skeletal implementation class
|
||||
public abstract class AbstractMapEntry<K,V>
|
||||
implements Map.Entry<K,V> {
|
||||
|
@ -8,7 +8,7 @@
|
||||
|
||||
例如,考虑在 Java 8 中添加到 `Collection` 接口的 `removeIf` 方法。此方法删除给定布尔方法(或 `Predicate` 函数式接口)返回 `true` 的所有元素。默认实现被指定为使用迭代器遍历集合,调用每个元素的谓词,并使用迭代器的 `remove` 方法删除谓词返回 `true` 的元素。 据推测,这个声明看起来像这样:默认实现被指定为使用迭代器遍历集合,调用每个元素的 `Predicate` 函数式接口,并使用迭代器的 `remove` 方法删除 `Predicate` 函数式接口返回 `true` 的元素。 根据推测,这个声明看起来像这样:
|
||||
|
||||
```Java
|
||||
```java
|
||||
// Default method added to the Collection interface in Java 8
|
||||
default boolean removeIf(Predicate<? super E> filter) {
|
||||
Objects.requireNonNull(filter);
|
||||
|
@ -4,7 +4,7 @@
|
||||
|
||||
一种失败的接口就是所谓的常量接口(constant interface)。 这样的接口不包含任何方法; 它只包含静态 final 属性,每个输出一个常量。 使用这些常量的类实现接口,以避免需要用类名限定常量名。 这里是一个例子:
|
||||
|
||||
```Java
|
||||
```java
|
||||
// Constant interface antipattern - do not use!
|
||||
public interface PhysicalConstants {
|
||||
// Avogadro's number (1/mol)
|
||||
@ -25,7 +25,7 @@ public interface PhysicalConstants {
|
||||
如果你想导出常量,有几个合理的选择方案。 如果常量与现有的类或接口紧密相关,则应将其添加到该类或接口中。 例如,所有数字基本类型的包装类,如 `Integer` 和 `Double`,都会导出 `MIN_VALUE` 和 `MAX_VALUE` 常量。 如果常量最好被看作枚举类型的成员,则应该使用枚举类型(条目 34)导出它们。 否则,你应该用一个不可实例化的工具类来导出常量(条目 4)。 下是前面所示的 `PhysicalConstants` 示例的工具类的版本:
|
||||
|
||||
|
||||
```Java
|
||||
```java
|
||||
// Constant utility class
|
||||
package com.effectivejava.science;
|
||||
|
||||
@ -42,7 +42,7 @@ public class PhysicalConstants {
|
||||
|
||||
通常,实用工具类要求客户端使用类名来限定常量名,例如 `PhysicalConstants.AVOGADROS_NUMBER`。 如果大量使用实用工具类导出的常量,则通过使用静态导入来限定具有类名的常量:
|
||||
|
||||
```Java
|
||||
```java
|
||||
// Use of static import to avoid qualifying constants
|
||||
import static com.effectivejava.science.PhysicalConstants.*;
|
||||
|
||||
|
@ -2,7 +2,7 @@
|
||||
|
||||
有时你可能会碰到一个类,它的实例有两个或更多的风格,并且包含一个标签属性(tag field),表示实例的风格。 例如,考虑这个类,它可以表示一个圆形或矩形:
|
||||
|
||||
```Java
|
||||
```java
|
||||
// Tagged class - vastly inferior to a class hierarchy!
|
||||
class Figure {
|
||||
enum Shape { RECTANGLE, CIRCLE };
|
||||
@ -51,7 +51,7 @@ class Figure {
|
||||
|
||||
接下来,为原始标签类的每种类型定义一个根类的具体子类。 在我们的例子中,有两个类型:圆形和矩形。 在每个子类中包含特定于改类型的数据字段。 在我们的例子中,半径属性是属于圆的,长度和宽度属性都是矩形的。 还要在每个子类中包含根类中每个抽象方法的适当实现。 这里是对应于 `Figure` 类的类层次:
|
||||
|
||||
```Java
|
||||
```java
|
||||
// Class hierarchy replacement for a tagged class
|
||||
abstract class Figure {
|
||||
abstract double area();
|
||||
@ -80,7 +80,7 @@ class Rectangle extends Figure {
|
||||
|
||||
类层次的另一个优点是可以使它们反映类型之间的自然层次关系,从而提高了灵活性,并提高了编译时类型检查的效率。 假设原始示例中的标签类也允许使用正方形。 类层次可以用来反映一个正方形是一种特殊的矩形(假设它们是不可变的):
|
||||
|
||||
```Java
|
||||
```java
|
||||
class Square extends Rectangle {
|
||||
Square(double side) {
|
||||
super(side, side);
|
||||
|
@ -13,7 +13,7 @@
|
||||
非静态成员类的一个常见用法是定义一个 `Adapter` [Gamma95],它允许将外部类的实例视为某个不相关类的实例。 例如,`Map` 接口的实现通常使用非静态成员类来实现它们的集合视图,这些视图由 `Map` 的 `keySet`,`entrySet` 和 `values` 方法返回。 同样,集合接口(如 `Set` 和 `List`)的实现通常使用非静态成员类来实现它们的迭代器:
|
||||
|
||||
|
||||
```Java
|
||||
```java
|
||||
// Typical use of a nonstatic member class
|
||||
public class MySet<E> extends AbstractSet<E> {
|
||||
... // Bulk of the class omitted
|
||||
|
@ -3,7 +3,7 @@
|
||||
虽然 Java 编译器允许在单个源文件中定义多个顶级类,但这样做没有任何好处,并且存在重大风险。 风险源于在源文件中定义多个顶级类使得为类提供多个定义成为可能。 使用哪个定义会受到源文件传递给编译器的顺序的影响。
|
||||
|
||||
为了具体说明,请考虑下面源文件,其中只包含一个引用其他两个顶级类(`Utensil` 和 `Dessert` 类)的成员的 Main 类:
|
||||
```Java
|
||||
```java
|
||||
public class Main {
|
||||
public static void main(String[] args) {
|
||||
System.out.println(Utensil.NAME + [Dessert.NAME](http://Dessert.NAME));
|
||||
@ -13,7 +13,7 @@ public class Main {
|
||||
|
||||
现在假设在 `Utensil.java` 的源文件中同时定义了 `Utensil` 和 `Dessert`:
|
||||
|
||||
```Java
|
||||
```java
|
||||
// Two classes defined in one file. Don't ever do this!
|
||||
class Utensil {
|
||||
static final String NAME = "pan";
|
||||
@ -28,7 +28,7 @@ class Dessert {
|
||||
|
||||
现在假设你不小心创建了另一个名为 `Dessert.java` 的源文件,它定义了相同的两个类:
|
||||
|
||||
```Java
|
||||
```java
|
||||
// Two classes defined in one file. Don't ever do this!
|
||||
class Utensil {
|
||||
static final String NAME = "pot";
|
||||
@ -45,7 +45,7 @@ class Dessert {
|
||||
|
||||
解决这个问题很简单,将顶层类(如我们的例子中的 `Utensil` 和 `Dessert`)分割成单独的源文件。 如果试图将多个顶级类放入单个源文件中,请考虑使用静态成员类(条目 24)作为将类拆分为单独的源文件的替代方法。 如果这些类从属于另一个类,那么将它们变成静态成员类通常是更好的选择,因为它提高了可读性,并且可以通过声明它们为私有(条目 15)来减少类的可访问性。下面是我们的例子看起来如何使用静态成员类:
|
||||
|
||||
```Java
|
||||
```java
|
||||
// Static member classes instead of multiple top-level classes
|
||||
public class Test {
|
||||
public static void main(String[] args) {
|
||||
|
@ -12,7 +12,7 @@
|
||||
|
||||
在泛型被添加到 Java 之前,这是一个典型的集合声明。 从 Java 9 开始,它仍然是合法的,但并不是典型的声明方式了:
|
||||
|
||||
```Java
|
||||
```java
|
||||
// Raw collection type - don't do this!
|
||||
|
||||
// My stamp collection. Contains only Stamp instances.
|
||||
@ -21,14 +21,14 @@ private final Collection stamps = ... ;
|
||||
|
||||
如果你今天使用这个声明,然后不小心把 `coin` 实例放入你的 `stamp` 集合中,错误的插入编译和运行没有错误(尽管编译器发出一个模糊的警告):
|
||||
|
||||
```Java
|
||||
```java
|
||||
// Erroneous insertion of coin into stamp collection
|
||||
stamps.add(new Coin( ... )); // Emits "unchecked call" warning
|
||||
```
|
||||
|
||||
直到您尝试从 `stamp` 集合中检索 `coin` 实例时才会发生错误:
|
||||
|
||||
```Java
|
||||
```java
|
||||
// Raw iterator type - don't do this!
|
||||
for (Iterator i = stamps.iterator(); i.hasNext(); )
|
||||
Stamp stamp = (Stamp) i.next(); // Throws ClassCastException
|
||||
@ -39,14 +39,14 @@ for (Iterator i = stamps.iterator(); i.hasNext(); )
|
||||
|
||||
对于泛型,类型声明包含的信息,而不是注释:
|
||||
|
||||
```Java
|
||||
```java
|
||||
// Parameterized collection type - typesafe
|
||||
private final Collection<Stamp> stamps = ... ;
|
||||
```
|
||||
|
||||
从这个声明中,编译器知道 `stamps` 集合应该只包含 `Stamp` 实例,并保证它是 `true`,假设你的整个代码类库编译时不发出(或者抑制;参见条目 27)任何警告。 当使用参数化类型声明声明 `stamps` 时,错误的插入会生成一个编译时错误消息,告诉你到底发生了什么错误:
|
||||
|
||||
```Java
|
||||
```java
|
||||
Test.java:9: error: incompatible types: Coin cannot be converted
|
||||
to Stamp
|
||||
c.add(new Coin());
|
||||
@ -61,7 +61,7 @@ to Stamp
|
||||
|
||||
为了具体说明,请考虑以下程序:
|
||||
|
||||
```Java
|
||||
```java
|
||||
// Fails at runtime - unsafeAdd method uses a raw type (List)!
|
||||
public static void main(String[] args) {
|
||||
List<String> strings = new ArrayList<>();
|
||||
@ -76,7 +76,7 @@ private static void unsafeAdd(List list, Object o) {
|
||||
|
||||
此程序可以编译,它使用原始类型列表,但会收到警告:
|
||||
|
||||
```Java
|
||||
```java
|
||||
Test.java:10: warning: [unchecked] unchecked call to add(E) as a
|
||||
member of the raw type List
|
||||
list.add(o);
|
||||
@ -87,14 +87,14 @@ member of the raw type List
|
||||
如果用 `unsafeAdd` 声明中的参数化类型 `List<Object>` 替换原始类型 `List`,并尝试重新编译该程序,则会发现它不再编译,而是发出错误消息:
|
||||
|
||||
|
||||
```Java
|
||||
```java
|
||||
Test.java:5: error: incompatible types: List<String> cannot be
|
||||
converted to List<Object>
|
||||
unsafeAdd(strings, Integer.valueOf(42));
|
||||
```
|
||||
你可能会试图使用原始类型来处理元素类型未知且无关紧要的集合。 例如,假设你想编写一个方法,它需要两个集合并返回它们共同拥有的元素的数量。 如果是泛型新手,那么您可以这样写:
|
||||
|
||||
```Java
|
||||
```java
|
||||
// Use of raw type for unknown element type - don't do this!
|
||||
static int numElementsInCommon(Set s1, Set s2) {
|
||||
int result = 0;
|
||||
@ -108,14 +108,14 @@ static int numElementsInCommon(Set s1, Set s2) {
|
||||
这种方法可以工作,但它使用原始类型,这是危险的。 安全替代方式是使用无限制通配符类型(unbounded wildcard types)。 如果要使用泛型类型,但不知道或关心实际类型参数是什么,则可以使用问号来代替。 例如,泛型类型 `Set<E>` 的无限制通配符类型是 `Set<?>`(读取“某种类型的集合”)。 它是最通用的参数化的 `Set` 类型,能够保持任何集合。 下面是 `numElementsInCommon` 方法使用无限制通配符类型声明的情况:
|
||||
。
|
||||
|
||||
```Java
|
||||
```java
|
||||
// Uses unbounded wildcard type - typesafe and flexible
|
||||
static int numElementsInCommon(Set<?> s1, Set<?> s2) { ... }
|
||||
```
|
||||
|
||||
无限制通配符 `Set<?>` 与原始类型 `Set` 之间有什么区别? 问号真的给你放任何东西吗? 这不是要点,但通配符类型是安全的,原始类型不是。 你可以将任何元素放入具有原始类型的集合中,轻易破坏集合的类型不变性(如第 119 页上的 `unsafeAdd` 方法所示); 你不能把任何元素(除 null 之外)放入一个 `Collection<?>` 中。 试图这样做会产生一个像这样的编译时错误消息:
|
||||
|
||||
```Java
|
||||
```java
|
||||
WildCard.java:13: error: incompatible types: String cannot be
|
||||
converted to CAP#1
|
||||
c.add("verboten");
|
||||
@ -130,7 +130,7 @@ converted to CAP#1
|
||||
|
||||
规则的第二个例外涉及 `instanceof` 操作符。 因为泛型类型信息在运行时被删除,所以在无限制通配符类型以外的参数化类型上使用 `instanceof` 运算符是非法的。 使用无限制通配符类型代替原始类型不会以任何方式影响 `instanceof` 运算符的行为。 在这种情况下,尖括号和问号就显得多余。 **以下是使用泛型类型的 `instanceof` 运算符的首选方法:**
|
||||
|
||||
```Java
|
||||
```java
|
||||
// Legitimate use of raw type - instanceof operator
|
||||
if (o instanceof Set) { // Raw type
|
||||
Set<?> s = (Set<?>) o; // Wildcard type
|
||||
|
@ -4,13 +4,13 @@
|
||||
|
||||
许多未经检查的警告很容易消除。 例如,假设你不小心写了以下声明:
|
||||
|
||||
```Java
|
||||
```java
|
||||
Set<Lark> exaltation = new HashSet();
|
||||
```
|
||||
|
||||
编译器会提醒你你做错了什么:
|
||||
|
||||
```Java
|
||||
```java
|
||||
Venery.java:4: warning: [unchecked] unchecked conversion
|
||||
Set<Lark> exaltation = new HashSet();
|
||||
^
|
||||
@ -20,7 +20,7 @@ Venery.java:4: warning: [unchecked] unchecked conversion
|
||||
|
||||
然后可以进行指示修正,让警告消失。 请注意,实际上并不需要指定类型参数,只是为了表明它与 Java 7 中引入的钻石运算符("<>")一同出现。然后编译器会推断出正确的实际类型参数(在本例中为 `Lark`):
|
||||
|
||||
```Java
|
||||
```java
|
||||
Set<Lark> exaltation = new HashSet<>();
|
||||
```
|
||||
|
||||
@ -33,7 +33,7 @@ Set<Lark> exaltation = new HashSet<>();
|
||||
如果你发现自己在长度超过一行的方法或构造方法上使用 `SuppressWarnings` 注解,则可以将其移到局部变量声明上。 你可能需要声明一个新的局部变量,但这是值得的。 例如,考虑这个来自 `ArrayList` 的 `toArray` 方法:
|
||||
|
||||
|
||||
```Java
|
||||
```java
|
||||
public <T> T[] toArray(T[] a) {
|
||||
if (a.length < size)
|
||||
return (T[]) Arrays.copyOf(elements, size, a.getClass());
|
||||
@ -54,7 +54,7 @@ ArrayList.java:305: warning: [unchecked] unchecked cast
|
||||
在返回语句中设置 `SuppressWarnings` 注解是非法的,因为它不是一个声明[JLS,9.7]。 你可能会试图把注释放在整个方法上,但是不要这要做。 相反,声明一个局部变量来保存返回值并标注它的声明,如下所示:
|
||||
|
||||
|
||||
```Java
|
||||
```java
|
||||
// Adding local variable to reduce scope of @SuppressWarnings
|
||||
public <T> T[] toArray(T[] a) {
|
||||
if (a.length < size) {
|
||||
|
@ -2,7 +2,7 @@
|
||||
|
||||
数组在两个重要方面与泛型不同。 首先,数组是协变的(covariant)。 这个吓人的单词意味着如果 Sub 是 Super 的子类型,则数组类型 `Sub[]` 是数组类型 `Super[]` 的子类型。 相比之下,泛型是不变的(invariant):对于任何两种不同的类型 `Type1` 和 `Type2`,`List<Type1>` 既不是 `List<Type2>` 的子类型也不是父类型。[JLS,4.10; Naftalin07,2.5]。 你可能认为这意味着泛型是不足的,但可以说是数组缺陷。 这段代码是合法的:
|
||||
|
||||
```Java
|
||||
```java
|
||||
// Fails at runtime!
|
||||
Object[] objectArray = new Long[1];
|
||||
objectArray[0] = "I don't fit in"; // Throws ArrayStoreException
|
||||
@ -10,7 +10,7 @@ objectArray[0] = "I don't fit in"; // Throws ArrayStoreException
|
||||
|
||||
但这个不是:
|
||||
|
||||
```Java
|
||||
```java
|
||||
// Won't compile!
|
||||
List<Object> ol = new ArrayList<Long>(); // Incompatible types
|
||||
ol.add("I don't fit in");
|
||||
@ -26,7 +26,7 @@ ol.add("I don't fit in");
|
||||
|
||||
为了具体说明,请考虑下面的代码片段:
|
||||
|
||||
```Java
|
||||
```java
|
||||
// Why generic array creation is illegal - won't compile!
|
||||
List<String>[] stringLists = new List<String>[1]; // (1)
|
||||
List<Integer> intList = List.of(42); // (2)
|
||||
@ -45,7 +45,7 @@ String s = stringLists[0].get(0); // (5)
|
||||
|
||||
例如,假设你想用带有集合的构造方法来编写一个 `Chooser` 类,并且有个方法返回随机选择的集合的一个元素。 根据传递给构造方法的集合,可以使用选择器作为游戏模具,魔术 8 球或数据源进行蒙特卡罗模拟。 这是一个没有泛型的简单实现:
|
||||
|
||||
```Java
|
||||
```java
|
||||
// Chooser - a class badly in need of generics!
|
||||
public class Chooser {
|
||||
private final Object[] choiceArray;
|
||||
@ -65,7 +65,7 @@ public class Chooser {
|
||||
|
||||
要使用这个类,每次调用方法时,都必须将 `Object` 的 `choose` 方法的返回值转换为所需的类型,如果类型错误,则转换在运行时失败。 我们先根据条目 29 的建议,试图修改 `Chooser` 类,使其成为泛型的。
|
||||
|
||||
```Java
|
||||
```java
|
||||
// A first cut at making Chooser generic - won't compile
|
||||
public class Chooser<T> {
|
||||
private final T[] choiceArray;
|
||||
@ -80,7 +80,7 @@ public class Chooser<T> {
|
||||
|
||||
如果你尝试编译这个类,会得到这个错误信息:
|
||||
|
||||
```Java
|
||||
```java
|
||||
Chooser.java:9: error: incompatible types: Object[] cannot be
|
||||
converted to T[]
|
||||
choiceArray = choices.toArray();
|
||||
@ -91,13 +91,13 @@ converted to T[]
|
||||
|
||||
没什么大不了的,将 `Object` 数组转换为 `T` 数组:
|
||||
|
||||
```Java
|
||||
```java
|
||||
choiceArray = (T[]) choices.toArray();
|
||||
```
|
||||
|
||||
这没有了错误,而是得到一个警告:
|
||||
|
||||
```Java
|
||||
```java
|
||||
Chooser.java:9: warning: [unchecked] unchecked cast
|
||||
choiceArray = (T[]) choices.toArray();
|
||||
^
|
||||
@ -110,7 +110,7 @@ T extends Object declared in class Chooser
|
||||
|
||||
要消除未经检查的强制转换警告,请使用列表而不是数组。 下面是另一个版本的 `Chooser` 类,编译时没有错误或警告:
|
||||
|
||||
```Java
|
||||
```java
|
||||
// List-based Chooser - typesafe
|
||||
public class Chooser<T> {
|
||||
private final List<T> choiceList;
|
||||
|
@ -4,7 +4,7 @@
|
||||
|
||||
考虑条目 7 中的简单堆栈实现:
|
||||
|
||||
```Java
|
||||
```java
|
||||
// Object-based collection - a prime candidate for generics
|
||||
public class Stack {
|
||||
private Object[] elements;
|
||||
@ -43,7 +43,7 @@ public class Stack {
|
||||
|
||||
下一步是用相应的类型参数替换所有使用的 `Object` 类型,然后尝试编译生成的程序:
|
||||
|
||||
```Java
|
||||
```java
|
||||
// Initial attempt to generify Stack - won't compile!
|
||||
public class Stack<E> {
|
||||
private E[] elements;
|
||||
@ -72,7 +72,7 @@ public class Stack<E> {
|
||||
|
||||
你通常会得到至少一个错误或警告,这个类也不例外。 幸运的是,这个类只产生一个错误:
|
||||
|
||||
```Java
|
||||
```java
|
||||
Stack.java:8: generic array creation
|
||||
elements = new E[DEFAULT_INITIAL_CAPACITY];
|
||||
^
|
||||
@ -81,7 +81,7 @@ Stack.java:8: generic array creation
|
||||
如条目 28 所述,你不能创建一个不可具体化类型的数组,例如类型 `E`。每当编写一个由数组支持的泛型时,就会出现此问题。 有两种合理的方法来解决它。 第一种解决方案直接规避了对泛型数组创建的禁用:创建一个 `Object` 数组并将其转换为泛型数组类型。 现在没有了错误,编译器会发出警告。 这种用法是合法的,但不是(一般)类型安全的:
|
||||
|
||||
|
||||
```Java
|
||||
```java
|
||||
Stack.java:8: warning: [unchecked] unchecked cast
|
||||
found: Object[], required: E[]
|
||||
elements = (E[]) new Object[DEFAULT_INITIAL_CAPACITY];
|
||||
@ -92,7 +92,7 @@ found: Object[], required: E[]
|
||||
|
||||
一旦证明未经检查的强制转换是安全的,请尽可能缩小范围(条目 27)。 在这种情况下,构造方法只包含未经检查的数组创建,所以在整个构造方法中抑制警告是合适的。 通过添加一个注解来执行此操作,`Stack` 可以干净地编译,并且可以在没有显式强制转换或担心 `ClassCastException` 异常的情况下使用它:
|
||||
|
||||
```Java
|
||||
```java
|
||||
// The elements array will contain only E instances from push(E).
|
||||
// This is sufficient to ensure type safety, but the runtime
|
||||
// type of the array won't be E[]; it will always be Object[]!
|
||||
@ -104,7 +104,7 @@ public Stack() {
|
||||
|
||||
消除 `Stack` 中的泛型数组创建错误的第二种方法是将属性元素的类型从 `E[]` 更改为 `Object[]`。 如果这样做,会得到一个不同的错误:
|
||||
|
||||
```Java
|
||||
```java
|
||||
Stack.java:19: incompatible types
|
||||
found: Object, required: E
|
||||
E result = elements[--size];
|
||||
@ -113,7 +113,7 @@ found: Object, required: E
|
||||
|
||||
可以通过将从数组中检索到的元素转换为 `E` 来将此错误更改为警告:
|
||||
|
||||
```Java
|
||||
```java
|
||||
Stack.java:19: warning: [unchecked] unchecked cast
|
||||
found: Object, required: E
|
||||
E result = (E) elements[--size];
|
||||
@ -122,7 +122,7 @@ found: Object, required: E
|
||||
|
||||
因为 `E` 是不可具体化的类型,编译器无法在运行时检查强制转换。 再一次,你可以很容易地向自己证明,不加限制的转换是安全的,所以可以适当地抑制警告。 根据条目 27 的建议,我们只在包含未经检查的强制转换的分配上抑制警告,而不是在整个 `pop` 方法上:
|
||||
|
||||
```Java
|
||||
```java
|
||||
// Appropriate suppression of unchecked warning
|
||||
public E pop() {
|
||||
if (size == 0)
|
||||
@ -142,7 +142,7 @@ public E pop() {
|
||||
下面的程序演示了泛型 `Stack` 类的使用。 该程序以相反的顺序打印其命令行参数,并将其转换为大写。 对从堆栈弹出的元素调用 `String` 的 `toUpperCase` 方法不需要显式强制转换,而自动生成的强制转换将保证成功:
|
||||
|
||||
|
||||
```Java
|
||||
```java
|
||||
// Little program to exercise our generic Stack
|
||||
public static void main(String[] args) {
|
||||
Stack<String> stack = new Stack<>();
|
||||
@ -159,7 +159,7 @@ public static void main(String[] args) {
|
||||
|
||||
有一些泛型类型限制了它们类型参数的允许值。 例如,考虑 `java.util.concurrent.DelayQueue`,它的声明如下所示:
|
||||
|
||||
```Java
|
||||
```java
|
||||
class DelayQueue<E extends Delayed> implements BlockingQueue<E>
|
||||
```
|
||||
|
||||
|
@ -4,7 +4,7 @@
|
||||
|
||||
编写泛型方法类似于编写泛型类型。 考虑这个方法,它返回两个集合的并集:
|
||||
|
||||
```Java
|
||||
```java
|
||||
// Uses raw types - unacceptable! [Item 26]
|
||||
|
||||
public static Set union(Set s1, Set s2) {
|
||||
@ -19,7 +19,7 @@ public static Set union(Set s1, Set s2) {
|
||||
|
||||
此方法可以编译但有两个警告:
|
||||
|
||||
```Java
|
||||
```java
|
||||
Union.java:5: warning: [unchecked] unchecked call to
|
||||
HashSet(Collection<? extends E>) as a member of raw type HashSet
|
||||
Set result = new HashSet(s1);
|
||||
@ -32,7 +32,7 @@ addAll(Collection<? extends E>) as a member of raw type Set
|
||||
|
||||
要修复这些警告并使方法类型安全,请修改其声明以声明表示三个集合(两个参数和返回值)的元素类型的类型参数,并在整个方法中使用此类型参数。 声明类型参数的类型参数列表位于方法的修饰符和返回类型之间。 在这个例子中,类型参数列表是 `<E>`,返回类型是 `Set<E>`。 类型参数的命名约定对于泛型方法和泛型类型是相同的(条目 29 和 68):
|
||||
|
||||
```Java
|
||||
```java
|
||||
// Generic method
|
||||
public static <E> Set<E> union(Set<E> s1, Set<E> s2) {
|
||||
Set<E> result = new HashSet<>(s1);
|
||||
@ -44,7 +44,7 @@ public static <E> Set<E> union(Set<E> s1, Set<E> s2) {
|
||||
|
||||
至少对于简单的泛型方法来说,就是这样。 此方法编译时不会生成任何警告,并提供类型安全性和易用性。 这是一个简单的程序来运行该方法。 这个程序不包含强制转换和编译时没有错误或警告:至少对于简单的泛型方法来说,就是这样。 此方法编译时不会生成任何警告,并提供类型安全性和易用性。 这是一个简单的程序来运行该方法。 这个程序不包含强制转换和编译时没有错误或警告:
|
||||
|
||||
```Java
|
||||
```java
|
||||
// Simple program to exercise generic method
|
||||
public static void main(String[] args) {
|
||||
Set<String> guys = Set.of("Tom", "Dick", "Harry");
|
||||
@ -62,7 +62,7 @@ public static void main(String[] args) {
|
||||
|
||||
假设你想写一个恒等方法分配器( identity function dispenser)。 类库提供了 `Function.identity` 方法,所以没有理由编写你自己的实现(条目 59),但它是有启发性的。 如果每次要求的时候都去创建一个新的恒等方法对象是浪费的,因为它是无状态的。 如果 Java 的泛型被具体化,那么每个类型都需要一个恒等方法,但是由于它们被擦除以后,所以泛型的单例就足够了。 以下是它的实例:
|
||||
|
||||
```Java
|
||||
```java
|
||||
// Generic singleton factory pattern
|
||||
private static UnaryOperator<Object> IDENTITY_FN = (t) -> t;
|
||||
|
||||
@ -76,7 +76,7 @@ public static <T> UnaryOperator<T> identityFunction() {
|
||||
|
||||
下面是一个示例程序,它使用我们的泛型单例作为 `UnaryOperator<String>` 和 `UnaryOperator<Number>`。 像往常一样,它不包含强制转化,编译时也没有错误和警告:
|
||||
|
||||
```Java
|
||||
```java
|
||||
// Sample program to exercise generic singleton
|
||||
public static void main(String[] args) {
|
||||
String[] strings = { "jute", "hemp", "nylon" };
|
||||
@ -96,7 +96,7 @@ public static void main(String[] args) {
|
||||
|
||||
虽然相对较少,类型参数受涉及该类型参数本身的某种表达式限制是允许的。 这就是所谓的递归类型限制(recursive type bound)。 递归类型限制的常见用法与 `Comparable` 接口有关,它定义了一个类型的自然顺序(条目 14)。 这个接口如下所示:
|
||||
|
||||
```Java
|
||||
```java
|
||||
public interface Comparable<T> {
|
||||
int compareTo(T o);
|
||||
}
|
||||
@ -106,7 +106,7 @@ public interface Comparable<T> {
|
||||
|
||||
许多方法采用实现 `Comparable` 的元素的集合来对其进行排序,在其中进行搜索,计算其最小值或最大值等。 要做到这一点,要求集合中的每一个元素都可以与其中的每一个元素相比,换言之,这个元素是可以相互比较的。 以下是如何表达这一约束:
|
||||
|
||||
```Java
|
||||
```java
|
||||
// Using a recursive type bound to express mutual comparability
|
||||
public static <E extends Comparable<E>> E max(Collection<E> c);
|
||||
```
|
||||
@ -115,7 +115,7 @@ public static <E extends Comparable<E>> E max(Collection<E> c);
|
||||
|
||||
这里有一个与前面的声明相匹配的方法。它根据其元素的自然顺序来计算集合中的最大值,并编译没有错误或警告:
|
||||
|
||||
```Java
|
||||
```java
|
||||
// Returns max value in a collection - uses recursive type bound
|
||||
public static <E extends Comparable<E>> E max(Collection<E> c) {
|
||||
|
||||
|
@ -4,7 +4,7 @@
|
||||
|
||||
相对于提供的不可变的类型,有时你需要比此更多的灵活性。 考虑条目 29 中的 `Stack` 类。下面是它的公共 API:
|
||||
|
||||
```Java
|
||||
```java
|
||||
public class Stack<E> {
|
||||
|
||||
public Stack();
|
||||
@ -20,7 +20,7 @@ public class Stack<E> {
|
||||
|
||||
假设我们想要添加一个方法来获取一系列元素,并将它们全部推送到栈上。 以下是第一种尝试:
|
||||
|
||||
```Java
|
||||
```java
|
||||
// pushAll method without wildcard type - deficient!
|
||||
public void pushAll(Iterable<E> src) {
|
||||
for (E e : src)
|
||||
@ -31,7 +31,7 @@ public void pushAll(Iterable<E> src) {
|
||||
这种方法可以干净地编译,但不完全令人满意。 如果可遍历的 `src` 元素类型与栈的元素类型完全匹配,那么它工作正常。 但是,假设有一个 `Stack<Number>`,并调用 `push(intVal)`,其中 `intVal` 的类型是 `Integer`。 这是因为 `Integer` 是 `Number` 的子类型。 从逻辑上看,这似乎也应该起作用:
|
||||
|
||||
|
||||
```Java
|
||||
```java
|
||||
Stack<Number> numberStack = new Stack<>();
|
||||
Iterable<Integer> integers = ... ;
|
||||
numberStack.pushAll(integers);
|
||||
@ -39,7 +39,7 @@ numberStack.pushAll(integers);
|
||||
|
||||
但是,如果你尝试了,会得到这个错误消息,因为参数化类型是不变的:
|
||||
|
||||
```Java
|
||||
```java
|
||||
StackTest.java:7: error: incompatible types: Iterable<Integer>
|
||||
cannot be converted to Iterable<Number>
|
||||
numberStack.pushAll(integers);
|
||||
@ -48,7 +48,7 @@ cannot be converted to Iterable<Number>
|
||||
|
||||
幸运的是,有对应的解决方法。 该语言提供了一种特殊的参数化类型来调用一个限定通配符类型来处理这种情况。 `pushAll` 的输入参数的类型不应该是“`E` 的 `Iterable` 接口”,而应该是“`E` 的某个子类型的 `Iterable` 接口”,并且有一个通配符类型,这意味着:`Iterable<? extends E>`。 (关键字 `extends` 的使用有点误导:回忆条目 29 中,子类型被定义为每个类型都是它自己的子类型,即使它本身没有继承。)让我们修改 `pushAll` 来使用这个类型:
|
||||
|
||||
```Java
|
||||
```java
|
||||
// Wildcard type for a parameter that serves as an E producer
|
||||
public void pushAll(Iterable<? extends E> src) {
|
||||
for (E e : src)
|
||||
@ -60,7 +60,7 @@ public void pushAll(Iterable<? extends E> src) {
|
||||
|
||||
现在假设你想写一个 `popAll` 方法,与 `pushAll` 方法相对应。 `popAll` 方法从栈中弹出每个元素并将元素添加到给定的集合中。 以下是第一次尝试编写 `popAll` 方法的过程:
|
||||
|
||||
```Java
|
||||
```java
|
||||
// popAll method without wildcard type - deficient!
|
||||
public void popAll(Collection<E> dst) {
|
||||
while (!isEmpty())
|
||||
@ -70,7 +70,7 @@ public void popAll(Collection<E> dst) {
|
||||
|
||||
同样,如果目标集合的元素类型与栈的元素类型完全匹配,则干净编译并且工作正常。 但是,这又不完全令人满意。 假设你有一个 `Stac<Number>` 和 `Object` 类型的变量。 如果从栈中弹出一个元素并将其存储在该变量中,它将编译并运行而不会出错。 所以你也不能这样做吗?
|
||||
|
||||
```Java
|
||||
```java
|
||||
Stack<Number> numberStack = new Stack<Number>();
|
||||
|
||||
Collection<Object> objects = ... ;
|
||||
@ -80,7 +80,7 @@ numberStack.popAll(objects);
|
||||
|
||||
如果尝试将此客户端代码与之前显示的 `popAll` 版本进行编译,则会得到与我们的第一版 `pushAll` 非常类似的错误:`Collection<Object>` 不是 `Collection<Number>` 的子类型。 通配符类型再一次提供了一条出路。 `popAll` 的输入参数的类型不应该是“E 的集合”,而应该是“E 的某个父类型的集合”(其中父类型被定义为 `E` 是它自己的父类型[JLS,4.10])。 再次,有一个通配符类型,正是这个意思:`Collection<? super E>`。 让我们修改 `popAll` 来使用它:
|
||||
|
||||
```Java
|
||||
```java
|
||||
// Wildcard type for parameter that serves as an E consumer
|
||||
public void popAll(Collection<? super E> dst) {
|
||||
while (!isEmpty())
|
||||
@ -98,13 +98,13 @@ public void popAll(Collection<? super E> dst) {
|
||||
|
||||
记住这个助记符之后,让我们来看看本章中以前项目的一些方法和构造方法声明。 条目 28 中的 `Chooser` 类构造方法有这样的声明:
|
||||
|
||||
```Java
|
||||
```java
|
||||
public Chooser(Collection<T> choices)
|
||||
```
|
||||
|
||||
这个构造方法只使用集合选择来生产类型 T 的值(并将它们存储起来以备后用),所以它的声明应该使用一个 extends T 的通配符类型。下面是得到的构造方法声明:
|
||||
|
||||
```Java
|
||||
```java
|
||||
// Wildcard type for parameter that serves as an T producer
|
||||
|
||||
public Chooser(Collection<? extends T> choices)
|
||||
@ -114,19 +114,19 @@ public Chooser(Collection<? extends T> choices)
|
||||
|
||||
现在看看条目 30 中的 `union` 方法。下是声明:
|
||||
|
||||
```Java
|
||||
```java
|
||||
public static <E> Set<E> union(Set<E> s1, Set<E> s2)
|
||||
```
|
||||
|
||||
两个参数 s1 和 s2 都是 `E` 的生产者,所以 PECS 助记符告诉我们该声明应该如下:
|
||||
|
||||
```Java
|
||||
```java
|
||||
public static <E> Set<E> union(Set<? extends E> s1, Set<? extends E> s2)
|
||||
```
|
||||
|
||||
请注意,返回类型仍然是 `Set<E>`。 不要使用限定通配符类型作为返回类型。除了会为用户提供额外的灵活性,还强制他们在客户端代码中使用通配符类型。 通过修改后的声明,此代码将清晰地编译:
|
||||
|
||||
```Java
|
||||
```java
|
||||
Set<Integer> integers = Set.of(1, 3, 5);
|
||||
|
||||
Set<Double> doubles = Set.of(2.0, 4.0, 6.0);
|
||||
@ -138,7 +138,7 @@ Set<Number> numbers = union(integers, doubles);
|
||||
|
||||
在 Java 8 之前,类型推断规则不够聪明,无法处理先前的代码片段,这要求编译器使用上下文指定的返回类型(或目标类型)来推断 `E` 的类型。`union` 方法调用的目标类型如前所示是 `Set<Number>`。 如果尝试在早期版本的 Java 中编译片段(以及适合的 `Set.of` 工厂替代版本),将会看到如此长的错综复杂的错误消息:
|
||||
|
||||
```Java
|
||||
```java
|
||||
Union.java:14: error: incompatible types
|
||||
Set<Number> numbers = union(integers, doubles);
|
||||
^
|
||||
@ -151,14 +151,14 @@ Union.java:14: error: incompatible types
|
||||
|
||||
幸运的是有办法来处理这种错误。 如果编译器不能推断出正确的类型,你可以随时告诉它使用什么类型的显式类型参数[JLS,15.12]。 甚至在 Java 8 中引入目标类型之前,这不是你必须经常做的事情,这很好,因为显式类型参数不是很漂亮。 通过添加显式类型参数,如下所示,代码片段在 Java 8 之前的版本中进行了干净编译:
|
||||
|
||||
```Java
|
||||
```java
|
||||
// Explicit type parameter - required prior to Java 8
|
||||
Set<Number> numbers = Union.<Number>union(integers, doubles);
|
||||
```
|
||||
|
||||
接下来让我们把注意力转向条目 30 中的 `max` 方法。这里是原始声明:
|
||||
|
||||
```Java
|
||||
```java
|
||||
public static <T extends Comparable<T>> T max(List<T> list)
|
||||
```
|
||||
|
||||
@ -166,7 +166,7 @@ public static <T extends Comparable<T>> T max(List<T> list)
|
||||
|
||||
修改后的 `max` 声明可能是本书中最复杂的方法声明。 增加的复杂性是否真的起作用了吗? 同样,它的确如此。 这是一个列表的简单例子,它被原始声明排除,但在被修改后的版本里是允许的:
|
||||
|
||||
```Java
|
||||
```java
|
||||
List<ScheduledFuture<?>> scheduledFutures = ... ;
|
||||
```
|
||||
|
||||
@ -174,7 +174,7 @@ List<ScheduledFuture<?>> scheduledFutures = ... ;
|
||||
|
||||
还有一个关于通配符相关的话题。 类型参数和通配符之间具有双重性,许多方法可以用一个或另一个声明。 例如,下面是两个可能的声明,用于交换列表中两个索引项目的静态方法。 第一个使用无限制类型参数(条目 30),第二个使用无限制通配符:
|
||||
|
||||
```Java
|
||||
```java
|
||||
// Two possible declarations for the swap method
|
||||
public static <E> void swap(List<E> list, int i, int j);
|
||||
public static void swap(List<?> list, int i, int j);
|
||||
@ -184,7 +184,7 @@ public static void swap(List<?> list, int i, int j);
|
||||
|
||||
第二个 `swap` 方法声明有一个问题。 这个简单的实现不会编译:
|
||||
|
||||
```Java
|
||||
```java
|
||||
public static void swap(List<?> list, int i, int j) {
|
||||
list.set(i, list.set(j, list.get(i)));
|
||||
}
|
||||
@ -192,7 +192,7 @@ public static void swap(List<?> list, int i, int j) {
|
||||
|
||||
试图编译它会产生这个不太有用的错误信息:
|
||||
|
||||
```Java
|
||||
```java
|
||||
Swap.java:5: error: incompatible types: Object cannot be
|
||||
converted to CAP#1
|
||||
list.set(i, list.set(j, list.get(i)));
|
||||
@ -203,7 +203,7 @@ converted to CAP#1
|
||||
|
||||
看起来我们不能把一个元素放回到我们刚刚拿出来的列表中。 问题是列表的类型是 `List<?>`,并且不能将除 `null` 外的任何值放入 `List<?>` 中。 幸运的是,有一种方法可以在不使用不安全的转换或原始类型的情况下实现此方法。 这个想法是写一个私有辅助方法来捕捉通配符类型。 辅助方法必须是泛型方法才能捕获类型。 以下是它的定义:
|
||||
|
||||
```Java
|
||||
```java
|
||||
public static void swap(List<?> list, int i, int j) {
|
||||
swapHelper(list, i, j);
|
||||
}
|
||||
|
@ -5,7 +5,7 @@
|
||||
回顾条目 28,非具体化(non-reifiable)的类型是其运行时表示比其编译时表示具有更少信息的类型,并且几乎所有泛型和参数化类型都是不可具体化的。 如果某个方法声明其可变参数为非具体化的类型,则编译器将在该声明上生成警告。 如果在推断类型不可确定的可变参数参数上调用该方法,那么编译器也会在调用中生成警告。 警告看起来像这样:
|
||||
|
||||
|
||||
```Java
|
||||
```java
|
||||
warning: [unchecked] Possible heap pollution from
|
||||
parameterized vararg type List<String>
|
||||
```
|
||||
@ -14,7 +14,7 @@ warning: [unchecked] Possible heap pollution from
|
||||
|
||||
例如,请考虑以下方法,该方法是第 127 页上的代码片段的一个不太明显的变体:
|
||||
|
||||
```Java
|
||||
```java
|
||||
// Mixing generics and varargs can violate type safety!
|
||||
static void dangerous(List<String>... stringLists) {
|
||||
List<Integer> intList = List.of(42);
|
||||
@ -35,7 +35,7 @@ static void dangerous(List<String>... stringLists) {
|
||||
值得注意的是,你可以违反类型安全性,即使不会在可变参数数组中存储任何内容。 考虑下面的泛型可变参数方法,它返回一个包含参数的数组。 乍一看,它可能看起来像一个方便的小工具:
|
||||
|
||||
|
||||
```Java
|
||||
```java
|
||||
// UNSAFE - Exposes a reference to its generic parameter array!
|
||||
static <T> T[] toArray(T... args) {
|
||||
return args;
|
||||
@ -46,7 +46,7 @@ static <T> T[] toArray(T... args) {
|
||||
|
||||
为了具体说明,请考虑下面的泛型方法,它接受三个类型 T 的参数,并返回一个包含两个参数的数组,随机选择:
|
||||
|
||||
```Java
|
||||
```java
|
||||
static <T> T[] pickTwo(T a, T b, T c) {
|
||||
switch(ThreadLocalRandom.current().nextInt(3)) {
|
||||
case 0: return toArray(a, b);
|
||||
@ -61,7 +61,7 @@ static <T> T[] pickTwo(T a, T b, T c) {
|
||||
|
||||
编译此方法时,编译器会生成代码以创建一个将两个 T 实例传递给 `toArray` 的可变参数数组。 这段代码分配了一个 `Object[]` 类型的数组,它是保证保存这些实例的最具体的类型,而不管在调用位置传递给 `pickTwo` 的对象是什么类型。 `toArray` 方法只是简单地将这个数组返回给 `pickTwo`,然后 `pickTwo` 将它返回给调用者,所以 `pickTwo` 总是返回一个 `Object[]` 类型的数组。
|
||||
|
||||
```Java
|
||||
```java
|
||||
public static void main(String[] args) {
|
||||
String[] attributes = pickTwo("Good", "Fast", "Cheap");
|
||||
}
|
||||
@ -74,7 +74,7 @@ public static void main(String[] args) {
|
||||
这里是安全使用泛型可变参数的典型示例。 此方法将任意数量的列表作为参数,并按顺序返回包含所有输入列表元素的单个列表。 由于该方法使用 `@SafeVarargs` 进行标注,因此在声明或其调用站位置上不会生成任何警告:
|
||||
|
||||
|
||||
```Java
|
||||
```java
|
||||
// Safe method with a generic varargs parameter
|
||||
@SafeVarargs
|
||||
static <T> List<T> flatten(List<? extends T>... lists) {
|
||||
@ -94,7 +94,7 @@ static <T> List<T> flatten(List<? extends T>... lists) {
|
||||
|
||||
使用 `SafeVarargs` 注解的替代方法是采用条目 28 的建议,并用 `List` 参数替换可变参数(这是一个变相的数组)。 下面是应用于我们的 `flatten` 方法时,这种方法的样子。 请注意,只有参数声明被更改了:
|
||||
|
||||
```Java
|
||||
```java
|
||||
// List as a typesafe alternative to a generic varargs parameter
|
||||
static <T> List<T> flatten(List<List<? extends T>> lists) {
|
||||
List<T> result = new ArrayList<>();
|
||||
@ -113,7 +113,7 @@ static <T> List<T> flatten(List<List<? extends T>> lists) {
|
||||
这个技巧也可以用在不可能写一个安全的可变参数方法的情况下,就像第 147 页的 `toArray` 方法那样。它的列表模拟是 `List.of` 方法,所以我们甚至不必编写它; Java 类库作者已经为我们完成了这项工作。 `pickTwo` 方法然后变成这样:
|
||||
|
||||
|
||||
```Java
|
||||
```java
|
||||
static <T> List<T> pickTwo(T a, T b, T c) {
|
||||
switch(rnd.nextInt(3)) {
|
||||
case 0: return List.of(a, b);
|
||||
@ -126,7 +126,7 @@ static <T> List<T> pickTwo(T a, T b, T c) {
|
||||
|
||||
`main` 方变成这样:
|
||||
|
||||
```Java
|
||||
```java
|
||||
public static void main(String[] args) {
|
||||
List<String> attributes = pickTwo("Good", "Fast", "Cheap");
|
||||
}
|
||||
|
@ -8,7 +8,7 @@
|
||||
|
||||
`Favorites` 类的 `API` 很简单。 它看起来就像一个简单 `Map` 类,除了该键是参数化的以外。 客户端在设置和获取 favorites 实例时呈现一个 `Class` 对象。 这里是 API:
|
||||
|
||||
```Java
|
||||
```java
|
||||
// Typesafe heterogeneous container pattern - API
|
||||
public class Favorites {
|
||||
public <T> void putFavorite(Class<T> type, T instance);
|
||||
@ -18,7 +18,7 @@ public class Favorites {
|
||||
|
||||
下面是一个演示 `Favorites` 类,保存,检索和打印喜欢的 `String`,`Integer` 和 `Class` 实例:
|
||||
|
||||
```Java
|
||||
```java
|
||||
// Typesafe heterogeneous container pattern - client
|
||||
public static void main(String[] args) {
|
||||
Favorites f = new Favorites();
|
||||
@ -40,7 +40,7 @@ public static void main(String[] args) {
|
||||
|
||||
Favorites 的实现非常小巧。 这是完整的代码:
|
||||
|
||||
```Java
|
||||
```java
|
||||
// Typesafe heterogeneous container pattern - implementation
|
||||
public class Favorites {
|
||||
private Map<Class<?>, Object> favorites = new HashMap<>();
|
||||
@ -67,7 +67,7 @@ public class Favorites {
|
||||
|
||||
那么这个 `cast` 方法为我们做了什么,因为它只是返回它的参数? `cast` 的签名充分利用了 `Class` 类是泛型的事实。 它的返回类型是 `Class` 对象的类型参数:
|
||||
|
||||
```Java
|
||||
```java
|
||||
public class Class<T> {
|
||||
T cast(Object obj);
|
||||
}
|
||||
@ -77,7 +77,7 @@ public class Class<T> {
|
||||
|
||||
`Favorites` 类有两个限制值得注意。 首先,恶意客户可以通过使用原始形式的 `Class` 对象,轻松破坏 `Favorites` 实例的类型安全。 但生成的客户端代码在编译时会生成未经检查的警告。 这与正常的集合实现(如 `HashSet` 和 `HashMap`)没有什么不同。 通过使用原始类型 `HashSet`(条目 26),可以轻松地将字符串放入 `HashSet<Integer>` 中。 也就是说,如果你愿意为此付出一点代价,就可以拥有运行时类型安全性。 确保 `Favorites` 永远不违反类型不变的方法是,使 putFavorite 方法检查该实例是否由 `type` 表示类型的实例,并且我们已经知道如何执行此操作。只需使用动态转换:
|
||||
|
||||
```Java
|
||||
```java
|
||||
// Achieving runtime type safety with a dynamic cast
|
||||
public<T> void putFavorite(Class<T> type, T instance) {
|
||||
favorites.put(type, type.cast(instance));
|
||||
@ -92,7 +92,7 @@ public<T> void putFavorite(Class<T> type, T instance) {
|
||||
|
||||
注解 API(条目 39)广泛使用限定类型的令牌。 例如,以下是在运行时读取注解的方法。 此方法来自 `AnnotatedElement` 接口,该接口由表示类,方法,属性和其他程序元素的反射类型实现:
|
||||
|
||||
```Java
|
||||
```java
|
||||
public <T extends Annotation>
|
||||
T getAnnotation(Class<T> annotationType);
|
||||
```
|
||||
@ -103,7 +103,7 @@ public <T extends Annotation>
|
||||
|
||||
以下是如何使用 `asSubclass` 方法在编译时读取类型未知的注解。 此方法编译时没有错误或警告:
|
||||
|
||||
```Java
|
||||
```java
|
||||
// Use of asSubclass to safely cast to a bounded type token
|
||||
static Annotation getAnnotation(AnnotatedElement element,
|
||||
String annotationTypeName) {
|
||||
|
@ -5,7 +5,7 @@
|
||||
|
||||
枚举是其合法值由一组固定的常量组成的一种类型,例如一年中的季节,太阳系中的行星或一副扑克牌中的套装。 在将枚举类型添加到该语言之前,表示枚举类型的常见模式是声明一组名为 `int` 的常量,每个类型的成员都有一个常量:
|
||||
|
||||
```Java
|
||||
```java
|
||||
// The int enum pattern - severely deficient!
|
||||
public static final int APPLE_FUJI = 0;
|
||||
public static final int APPLE_PIPPIN = 1;
|
||||
@ -16,7 +16,7 @@ public static final int ORANGE_TEMPLE = 1;
|
||||
public static final int ORANGE_BLOOD = 2;。
|
||||
```
|
||||
这种被称为 `int` 枚举模式的技术有许多缺点。 它没有提供类型安全的方式,也没有提供任何表达力。 如果你将一个 `Apple` 传递给一个需要 `Orange` 的方法,那么编译器不会出现警告,还会用 == 运算符比较 `Apple` 与 `Orange`,或者更糟糕的是:
|
||||
```Java
|
||||
```java
|
||||
// Tasty citrus flavored applesauce!
|
||||
int i = (APPLE_FUJI - ORANGE_TEMPLE) / APPLE_PIPPIN;
|
||||
```
|
||||
@ -30,7 +30,7 @@ int i = (APPLE_FUJI - ORANGE_TEMPLE) / APPLE_PIPPIN;
|
||||
|
||||
幸运的是,Java 提供了一种避免 `int` 和 `String` 枚举模式的所有缺点的替代方法,并提供了许多额外的好处。 它是枚举类型[JLS,8.9]。 以下是它最简单的形式:
|
||||
|
||||
```Java
|
||||
```java
|
||||
public enum Apple { FUJI, PIPPIN, GRANNY_SMITH }
|
||||
public enum Orange { NAVEL, TEMPLE, BLOOD }
|
||||
```
|
||||
@ -49,7 +49,7 @@ public enum Orange { NAVEL, TEMPLE, BLOOD }
|
||||
|
||||
对于丰富的枚举类型的一个很好的例子,考虑我们太阳系的八颗行星。 每个行星都有质量和半径,从这两个属性可以计算出它的表面重力。 从而在给定物体的质量下,计算出一个物体在行星表面上的重量。 下面是这个枚举类型。 每个枚举常量之后的括号中的数字是传递给其构造方法的参数。 在这种情况下,它们是地球的质量和半径:
|
||||
|
||||
```Java
|
||||
```java
|
||||
// Enum type with data and behavior
|
||||
public enum Planet {
|
||||
MERCURY(3.302e+23, 2.439e6),
|
||||
@ -88,7 +88,7 @@ public enum Planet {
|
||||
|
||||
虽然 `Planet` 枚举很简单,但它的功能非常强大。 这是一个简短的程序,它将一个物体在地球上的重量(任何单位),打印一个漂亮的表格,显示该物体在所有八个行星上的重量(以相同单位):
|
||||
|
||||
```Java
|
||||
```java
|
||||
public class WeightTable {
|
||||
public static void main(String[] args) {
|
||||
double earthWeight = Double.parseDouble(args[0]);
|
||||
@ -102,7 +102,7 @@ public class WeightTable {
|
||||
|
||||
请注意,`Planet` 和所有枚举一样,都有一个静态 `values` 方法,该方法以声明的顺序返回其值的数组。 另请注意,`toString` 方法返回每个枚举值的声明名称,使 `println` 和 `printf` 可以轻松打印。 如果你对此字符串表示形式不满意,可以通过重写 `toString` 方法来更改它。 这是使用命令行参数 185 运行 `WeightTable` 程序(不重写 `toString`)的结果:
|
||||
|
||||
```Java
|
||||
```java
|
||||
Weight on MERCURY is 69.912739
|
||||
Weight on VENUS is 167.434436
|
||||
Weight on EARTH is 185.000000
|
||||
@ -119,7 +119,7 @@ Weight on NEPTUNE is 210.208751
|
||||
|
||||
如果一个枚举是广泛使用的,它应该是一个顶级类; 如果它的使用与特定的顶级类绑定,它应该是该顶级类的成员类(条目 24)。 例如,`java.math.RoundingMode` 枚举表示小数部分的舍入模式。 `BigDecimal` 类使用了这些舍入模式,但它们提供了一种有用的抽象,它并不与 `BigDecimal` 有根本的联系。 通过将 `RoundingMode` 设置为顶层枚举,类库设计人员鼓励任何需要舍入模式的程序员重用此枚举,从而提高跨 `API` 的一致性。
|
||||
|
||||
```Java
|
||||
```java
|
||||
// Enum type that switches on its own value - questionable
|
||||
public enum Operation {
|
||||
PLUS, MINUS, TIMES, DIVIDE;
|
||||
@ -141,7 +141,7 @@ public enum Operation {
|
||||
|
||||
幸运的是,有一种更好的方法可以将不同的行为与每个枚举常量关联起来:在枚举类型中声明一个抽象的 apply 方法,并用常量特定的类主体中的每个常量的具体方法重写它。 这种方法被称为特定于常量(constant-specific)的方法实现:
|
||||
|
||||
```Java
|
||||
```java
|
||||
// Enum type with constant-specific method implementations
|
||||
public enum Operation {
|
||||
PLUS {public double apply(double x, double y){return x + y;}},
|
||||
@ -157,7 +157,7 @@ public enum Operation {
|
||||
|
||||
特定于常量的方法实现可以与特定于常量的数据结合使用。 例如,以下是 `Operation` 的一个版本,它重写 `toString` 方法以返回通常与该操作关联的符号:
|
||||
|
||||
```Java
|
||||
```java
|
||||
// Enum type with constant-specific class bodies and data
|
||||
public enum Operation {
|
||||
PLUS("+") {
|
||||
@ -185,7 +185,7 @@ public enum Operation {
|
||||
|
||||
显示的 `toString` 实现可以很容易地打印算术表达式,正如这个小程序所展示的那样:
|
||||
|
||||
```Java
|
||||
```java
|
||||
public static void main(String[] args) {
|
||||
double x = Double.parseDouble(args[0]);
|
||||
double y = Double.parseDouble(args[1]);
|
||||
@ -197,7 +197,7 @@ public static void main(String[] args) {
|
||||
|
||||
以 2 和 4 作为命令行参数运行此程序会生成以下输出:
|
||||
|
||||
```Java
|
||||
```java
|
||||
2.000000 + 4.000000 = 6.000000
|
||||
2.000000 - 4.000000 = -2.000000
|
||||
2.000000 * 4.000000 = 8.000000
|
||||
@ -206,7 +206,7 @@ public static void main(String[] args) {
|
||||
|
||||
枚举类型具有自动生成的 `valueOf(String)` 方法,该方法将常量名称转换为常量本身。 如果在枚举类型中重写 `toString` 方法,请考虑编写 `fromString` 方法将自定义字符串表示法转换回相应的枚举类型。 下面的代码(类型名称被适当地改变)将对任何枚举都有效,只要每个常量具有唯一的字符串表示形式:
|
||||
|
||||
```Java
|
||||
```java
|
||||
// Implementing a fromString method on an enum type
|
||||
private static final Map<String, Operation> stringToEnum =
|
||||
Stream.of(values()).collect(
|
||||
@ -224,7 +224,7 @@ public static Optional<Operation> fromString(String symbol) {
|
||||
|
||||
特定于常量的方法实现的一个缺点是它们使得难以在枚举常量之间共享代码。 例如,考虑一个代表工资包中的工作天数的枚举。 该枚举有一个方法,根据工人的基本工资(每小时)和当天工作的分钟数计算当天工人的工资。 在五个工作日内,任何超过正常工作时间的工作都会产生加班费; 在两个周末的日子里,所有工作都会产生加班费。 使用 `switch` 语句,通过将多个 `case` 标签应用于两个代码片段中的每一个,可以轻松完成此计算:
|
||||
|
||||
```Java
|
||||
```java
|
||||
// Enum that switches on its value to share code - questionable
|
||||
enum PayrollDay {
|
||||
MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY,
|
||||
@ -258,7 +258,7 @@ enum PayrollDay {
|
||||
|
||||
你真正想要的是每次添加枚举常量时被迫选择加班费策略。 幸运的是,有一个很好的方法来实现这一点。 这个想法是将加班费计算移入私有嵌套枚举中,并将此策略枚举的实例传递给 `PayrollDay` 枚举的构造方法。 然后,`PayrollDay` 枚举将加班工资计算委托给策略枚举,从而无需在 `PayrollDay` 中实现 `switch` 语句或特定于常量的方法实现。 虽然这种模式不如 `switch` 语句简洁,但它更安全,更灵活:
|
||||
|
||||
```Java
|
||||
```java
|
||||
// The strategy enum pattern
|
||||
enum PayrollDay {
|
||||
MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY,
|
||||
@ -300,7 +300,7 @@ enum PayrollDay {
|
||||
|
||||
如果对枚举的 `switch` 语句不是实现常量特定行为的好选择,那么它们有什么好处呢?枚举类型的 `switch` 有利于用常量特定的行为增加枚举类型。例如,假设 `Operation` 枚举不在你的控制之下,你希望它有一个实例方法来返回每个相反的操作。你可以用以下静态方法模拟效果:
|
||||
|
||||
```Java
|
||||
```java
|
||||
// Switch on an enum to simulate a missing method
|
||||
public static Operation inverse(Operation op) {
|
||||
switch(op) {
|
||||
|
@ -2,7 +2,7 @@
|
||||
|
||||
许多枚举通常与单个 `int` 值关联。所有枚举都有一个 `ordinal` 方法,它返回每个枚举常量类型的数值位置。你可能想从序数中派生一个关联的 `int` 值:
|
||||
|
||||
```Java
|
||||
```java
|
||||
// Abuse of ordinal to derive an associated value - DON'T DO THIS
|
||||
public enum Ensemble {
|
||||
SOLO, DUET, TRIO, QUARTET, QUINTET,
|
||||
@ -18,7 +18,7 @@ public enum Ensemble {
|
||||
|
||||
幸运的是,这些问题有一个简单的解决方案。 **永远不要从枚举的序号中得出与它相关的值; 请将其保存在实例属性中:**
|
||||
|
||||
```Java
|
||||
```java
|
||||
public enum Ensemble {
|
||||
SOLO(1), DUET(2), TRIO(3), QUARTET(4), QUINTET(5),
|
||||
SEXTET(6), SEPTET(7), OCTET(8), DOUBLE_QUARTET(8),
|
||||
|
@ -2,7 +2,7 @@
|
||||
|
||||
如果枚举类型的元素主要用于集合中,一般来说使用 int 枚举模式(条目 34),下面将 2 的不同倍数赋值给每个常量:
|
||||
|
||||
```Java
|
||||
```java
|
||||
// Bit field enumeration constants - OBSOLETE!
|
||||
public class Text {
|
||||
public static final int STYLE_BOLD = 1 << 0; // 1
|
||||
@ -17,7 +17,7 @@ public class Text {
|
||||
|
||||
这种表示方式允许你使用按位或(or)运算将几个常量合并到一个称为位属性(bit field)的集合中:
|
||||
|
||||
```Java
|
||||
```java
|
||||
text.applyStyles(STYLE_BOLD | STYLE_ITALIC);
|
||||
```
|
||||
|
||||
@ -27,7 +27,7 @@ text.applyStyles(STYLE_BOLD | STYLE_ITALIC);
|
||||
|
||||
下面是前一个使用枚举和枚举集合替代位属性的示例。 它更短,更清晰,更安全:
|
||||
|
||||
```Java
|
||||
```java
|
||||
// EnumSet - a modern replacement for bit fields
|
||||
public class Text {
|
||||
public enum Style { BOLD, ITALIC, UNDERLINE, STRIKETHROUGH }
|
||||
@ -39,7 +39,7 @@ public class Text {
|
||||
|
||||
这里是将 `EnumSet` 实例传递给 `applyStyles` 方法的客户端代码。 `EnumSet` 类提供了一组丰富的静态工厂,可以轻松创建集合,其中一个代码如下所示:
|
||||
|
||||
```Java
|
||||
```java
|
||||
text.applyStyles(EnumSet.of(Style.BOLD, Style.ITALIC));
|
||||
```
|
||||
|
||||
|
@ -2,7 +2,7 @@
|
||||
|
||||
有时可能会看到使用 `ordinal` 方法(条目 35)来索引到数组或列表的代码。 例如,考虑一下这个简单的类来代表一种植物:
|
||||
|
||||
```Java
|
||||
```java
|
||||
class Plant {
|
||||
enum LifeCycle { ANNUAL, PERENNIAL, BIENNIAL }
|
||||
final String name;
|
||||
@ -21,7 +21,7 @@ class Plant {
|
||||
|
||||
现在假设你有一组植物代表一个花园,想要列出这些由生命周期组织的植物 (一年生,多年生,或双年生)。为此,需要构建三个集合,每个生命周期作为一个,并遍历整个花园,将每个植物放置在适当的集合中。一些程序员可以通过将这些集合放入一个由生命周期序数索引的数组中来实现这一点:
|
||||
|
||||
```Java
|
||||
```java
|
||||
// Using ordinal() to index into an array - DON'T DO THIS!
|
||||
Set<Plant>[] plantsByLifeCycle =
|
||||
(Set<Plant>[]) new Set[Plant.LifeCycle.values().length];
|
||||
@ -43,7 +43,7 @@ for (int i = 0; i < plantsByLifeCycle.length; i++) {
|
||||
|
||||
有一个更好的方法来达到同样的效果。 该数组有效地用作从枚举到值的映射,因此不妨使用 `Map`。 更具体地说,有一个非常快速的 `Map` 实现,设计用于枚举键,称为 `java.util.EnumMap`。 下面是当程序重写为使用 `EnumMap` 时的样子:
|
||||
|
||||
```Java
|
||||
```java
|
||||
// Using an EnumMap to associate data with an enum
|
||||
Map<Plant.LifeCycle, Set<Plant>> plantsByLifeCycle =
|
||||
new EnumMap<>(Plant.LifeCycle.class);
|
||||
@ -61,7 +61,7 @@ System.out.println(plantsByLifeCycle);
|
||||
|
||||
通过使用 `stream`(条目 45)来管理 `Map`,可以进一步缩短以前的程序。 以下是最简单的基于 `stream` 的代码,它们在很大程度上重复了前面示例的行为:
|
||||
|
||||
```Java
|
||||
```java
|
||||
// Naive stream-based approach - unlikely to produce an EnumMap!
|
||||
System.out.println(Arrays.stream(garden)
|
||||
.collect(groupingBy(p -> p.lifeCycle)));
|
||||
@ -69,7 +69,7 @@ System.out.println(Arrays.stream(garden)
|
||||
|
||||
这个代码的问题在于它选择了自己的 `Map` 实现,实际上它不是 `EnumMap`,所以它不会与显式 `EnumMap` 的版本的空间和时间性能相匹配。 为了解决这个问题,使用 `Collectors.groupingBy` 的三个参数形式的方法,它允许调用者使用 mapFactory 参数指定 map 的实现:
|
||||
|
||||
```Java
|
||||
```java
|
||||
// Using a stream and an EnumMap to associate data with an enum
|
||||
System.out.println(Arrays.stream(garden)
|
||||
.collect(groupingBy(p -> p.lifeCycle,
|
||||
@ -82,7 +82,7 @@ System.out.println(Arrays.stream(garden)
|
||||
|
||||
你可能会看到数组索引 (两次) 的数组,用序数来表示从两个枚举值的映射。例如,这个程序使用这样一个数组来映射两个阶段到一个阶段转换(phase transition)(液体到固体表示凝固,液体到气体表示沸腾等等):
|
||||
|
||||
```Java
|
||||
```java
|
||||
// Using ordinal() to index array of arrays - DON'T DO THIS!
|
||||
public enum Phase {
|
||||
SOLID, LIQUID, GAS;
|
||||
@ -108,7 +108,7 @@ public enum Phase {
|
||||
|
||||
同样,可以用 `EnumMap` 做得更好。 因为每个阶段转换都由一对阶段枚举来索引,所以最好将关系表示为从一个枚举(from 阶段)到第二个枚举(to 阶段)到结果(阶段转换)的 `map`。 与阶段转换相关的两个阶段最好通过将它们与阶段转换枚举相关联来捕获,然后可以用它来初始化嵌套的 EnumMap:
|
||||
|
||||
```Java
|
||||
```java
|
||||
// Using a nested EnumMap to associate data with enum pairs
|
||||
public enum Phase {
|
||||
SOLID, LIQUID, GAS;
|
||||
@ -143,7 +143,7 @@ public enum Phase {
|
||||
|
||||
现在假设想为系统添加一个新阶段:等离子体或电离气体。 这个阶段只有两个转变:电离,将气体转化为等离子体; 和去离子,将等离子体转化为气体。 要更新基于数组的程序,必须将一个新的常量添加到 `Phase`,将两个两次添加到 `Phase.Transition`,并用新的十六个元素版本替换原始的九元素阵列数组。 如果向数组中添加太多或太少的元素或者将元素乱序放置,那么如果运气不佳:程序将会编译,但在运行时会失败。 要更新基于 `EnumMap` 的版本,只需将 `PLASMA` 添加到阶段列表中,并将 `IONIZE(GAS, PLASMA)` 和 `DEIONIZE(PLASMA, GAS)` 添加到阶段转换列表中:
|
||||
|
||||
```Java
|
||||
```java
|
||||
// Adding a new phase using the nested EnumMap implementation
|
||||
public enum Phase {
|
||||
|
||||
|
@ -6,7 +6,7 @@
|
||||
|
||||
幸运的是,使用枚举类型有一个很好的方法来实现这种效果。基本思想是利用枚举类型可以通过为 `opcode` 类型定义一个接口,并实现任意接口。例如,这里是来自条目 34 的 `Operation` 类型的可扩展版本:
|
||||
|
||||
```Java
|
||||
```java
|
||||
// Emulated extensible enum using an interface
|
||||
public interface Operation {
|
||||
double apply(double x, double y);
|
||||
@ -42,7 +42,7 @@ public enum BasicOperation implements Operation {
|
||||
|
||||
虽然枚举类型(`BasicOperation`)不可扩展,但接口类型(`Operation`)是可以扩展的,并且它是用于表示 API 中的操作的接口类型。 你可以定义另一个实现此接口的枚举类型,并使用此新类型的实例来代替基本类型。 例如,假设想要定义前面所示的操作类型的扩展,包括指数运算和余数运算。 你所要做的就是编写一个实现 `Operation` 接口的枚举类型:
|
||||
|
||||
```Java
|
||||
```java
|
||||
// Emulated extension enum
|
||||
public enum ExtendedOperation implements Operation {
|
||||
EXP("^") {
|
||||
@ -72,7 +72,7 @@ public enum ExtendedOperation implements Operation {
|
||||
|
||||
不仅可以在任何需要“基本枚举”的地方传递“扩展枚举”的单个实例,而且还可以传入整个扩展枚举类型,并使用其元素。 例如,这里是第 163 页上的一个测试程序版本,它执行之前定义的所有扩展操作:
|
||||
|
||||
```Java
|
||||
```java
|
||||
public static void main(String[] args) {
|
||||
double x = Double.parseDouble(args[0]);
|
||||
double y = Double.parseDouble(args[1]);
|
||||
@ -92,7 +92,7 @@ private static <T extends Enum<T> & Operation> void test(
|
||||
|
||||
第二种方式是传递一个 `Collection<? extends Operation>`,这是一个限定通配符类型(条目 31),而不是传递了一个 `class` 对象:
|
||||
|
||||
```Java
|
||||
```java
|
||||
public static void main(String[] args) {
|
||||
double x = Double.parseDouble(args[0]);
|
||||
double y = Double.parseDouble(args[1]);
|
||||
@ -111,7 +111,7 @@ private static void test(Collection<? extends Operation> opSet,
|
||||
|
||||
上面的两个程序在运行命令行输入参数 4 和 2 时生成以下输出:
|
||||
|
||||
```Java
|
||||
```java
|
||||
4.000000 ^ 2.000000 = 16.000000
|
||||
4.000000 % 2.000000 = 0.000000
|
||||
```
|
||||
|
@ -8,7 +8,7 @@
|
||||
|
||||
注解[JLS,9.7] 很好地解决了所有这些问题,`JUnit` 从第 4 版开始采用它们。在这个项目中,我们将编写我们自己的测试框架来显示注解的工作方式。 假设你想定义一个注解类型来指定自动运行的简单测试,并且如果它们抛出一个异常就会失败。 以下是名为 Test 的这种注解类型的定义:
|
||||
|
||||
```Java
|
||||
```java
|
||||
// Marker annotation type declaration
|
||||
import java.lang.annotation.*;
|
||||
|
||||
@ -29,7 +29,7 @@ public @interface Test {
|
||||
|
||||
以下是 `Test` 注解在实践中的应用。 它被称为标记注解,因为它没有参数,只是“标记”注解元素。 如果程序员错拼 `Test` 或将 `Test` 注解应用于程序元素而不是方法声明,则该程序将无法编译。
|
||||
|
||||
```Java
|
||||
```java
|
||||
// Program containing marker annotations
|
||||
public class Sample {
|
||||
|
||||
@ -63,7 +63,7 @@ public class Sample {
|
||||
|
||||
`Test` 注解对 `Sample` 类的语义没有直接影响。 他们只提供信息供相关程序使用。 更一般地说,注解不会改变注解代码的语义,但可以通过诸如这个简单的测试运行器等工具对其进行特殊处理:
|
||||
|
||||
```Java
|
||||
```java
|
||||
// Program to process marker annotations
|
||||
import java.lang.reflect.*;
|
||||
|
||||
@ -96,7 +96,7 @@ public class RunTests {
|
||||
|
||||
如果尝试通过反射调用测试方法会抛出除 `InvocationTargetException` 之外的任何异常,则表示编译时未捕获到没有使用的 `Test` 注解。 这些用法包括注解实例方法,具有一个或多个参数的方法或不可访问的方法。 测试运行器中的第二个 `catch` 块会捕获这些 `Test` 使用错误并显示相应的错误消息。 这是在 `RunTests` 在 `Sample` 上运行时打印的输出:
|
||||
|
||||
```Java
|
||||
```java
|
||||
public static void Sample.m3() failed: RuntimeException: Boom
|
||||
Invalid @Test: public void Sample.m5()
|
||||
public static void Sample.m7() failed: RuntimeException: Crash
|
||||
@ -105,7 +105,7 @@ Passed: 1, Failed: 3
|
||||
|
||||
现在,让我们添加对仅在抛出特定异常时才成功的测试的支持。 我们需要为此添加一个新的注解类型:
|
||||
|
||||
```Java
|
||||
```java
|
||||
// Annotation type with a parameter
|
||||
import java.lang.annotation.*;
|
||||
|
||||
@ -122,7 +122,7 @@ public @interface ExceptionTest {
|
||||
|
||||
此注解的参数类型是 `Class<? extends Throwable>`。毫无疑问,这种通配符是拗口的。 在英文中,它表示“扩展 `Throwable` 的某个类的 `Class` 对象”,它允许注解的用户指定任何异常(或错误)类型。 这个用法是一个限定类型标记的例子(条目 33)。 以下是注解在实践中的例子。 请注意,类名字被用作注解参数值:
|
||||
|
||||
```Java
|
||||
```java
|
||||
// Program containing annotations with a parameter
|
||||
public class Sample2 {
|
||||
|
||||
@ -145,7 +145,7 @@ public class Sample2 {
|
||||
|
||||
现在让我们修改测试运行器工具来处理新的注解。 这样将包括将以下代码添加到 `main` 方法中:
|
||||
|
||||
```Java
|
||||
```java
|
||||
if (m.isAnnotationPresent(ExceptionTest.class)) {
|
||||
tests++;
|
||||
try {
|
||||
@ -172,7 +172,7 @@ if (m.isAnnotationPresent(ExceptionTest.class)) {
|
||||
|
||||
将我们的异常测试示例进一步推进,可以设想一个测试,如果它抛出几个指定的异常中的任何一个,就会通过测试。 注解机制有一个便于支持这种用法的工具。 假设我们将 `ExceptionTest` 注解的参数类型更改为 `Class` 对象数组:
|
||||
|
||||
```Java
|
||||
```java
|
||||
// Annotation type with an array parameter
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Target(ElementType.METHOD)
|
||||
@ -183,7 +183,7 @@ public @interface ExceptionTest {
|
||||
|
||||
注解中数组参数的语法很灵活。 它针对单元素数组进行了优化。 所有以前的 `ExceptionTest` 注解仍然适用于 `ExceptionTest` 的新数组参数版本,并且会生成单元素数组。 要指定一个多元素数组,请使用花括号将这些元素括起来,并用逗号分隔它们:
|
||||
|
||||
```Java
|
||||
```java
|
||||
// Code containing an annotation with an array parameter
|
||||
@ExceptionTest({ IndexOutOfBoundsException.class,
|
||||
NullPointerException.class })
|
||||
@ -197,7 +197,7 @@ public static void doublyBad() {
|
||||
|
||||
修改测试运行器工具以处理新版本的 `ExceptionTest` 是相当简单的。 此代码替换原始版本:
|
||||
|
||||
```Java
|
||||
```java
|
||||
if (m.isAnnotationPresent(ExceptionTest.class)) {
|
||||
tests++;
|
||||
try {
|
||||
@ -222,7 +222,7 @@ if (m.isAnnotationPresent(ExceptionTest.class)) {
|
||||
|
||||
从 Java 8 开始,还有另一种方法来执行多值注解。 可以使用 `@Repeatable` 元注解来标示注解的声明,而不用使用数组参数声明注解类型,以指示注解可以重复应用于单个元素。 该元注解采用单个参数,该参数是包含注解类型的类对象,其唯一参数是注解类型[JLS,9.6.3] 的数组。 如果我们使用 `ExceptionTest` 注解采用这种方法,下面是注解的声明。 请注意,包含注解类型必须使用适当的保留策略和目标进行注解,否则声明将无法编译:
|
||||
|
||||
```Java
|
||||
```java
|
||||
// Repeatable annotation type
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Target(ElementType.METHOD)
|
||||
@ -240,7 +240,7 @@ public @interface ExceptionTestContainer {
|
||||
|
||||
下面是我们的 `doublyBad` 测试用一个重复的注解代替基于数组值注解的方式:
|
||||
|
||||
```Java
|
||||
```java
|
||||
// Code containing a repeated annotation
|
||||
@ExceptionTest(IndexOutOfBoundsException.class)
|
||||
@ExceptionTest(NullPointerException.class)
|
||||
@ -249,7 +249,7 @@ public static void doublyBad() { ... }
|
||||
|
||||
处理可重复的注解需要注意。重复注解会生成包含注解类型的合成注解。 `getAnnotationsByType` 方法掩盖了这一事实,可用于访问可重复注解类型和非重复注解。但 `isAnnotationPresent` 明确指出重复注解不是注解类型,而是包含注解类型。如果某个元素具有某种类型的重复注解,并且使用 `isAnnotationPresent` 方法检查元素是否具有该类型的注释,则会发现它没有。使用此方法检查注解类型的存在会因此导致程序默默忽略重复的注解。同样,使用此方法检查包含的注解类型将导致程序默默忽略不重复的注释。要使用 `isAnnotationPresent` 检测重复和非重复的注解,需要检查注解类型及其包含的注解类型。以下是 `RunTests` 程序的相关部分在修改为使用 `ExceptionTest` 注解的可重复版本时的例子:
|
||||
|
||||
```Java
|
||||
```java
|
||||
// Processing repeatable annotations
|
||||
if (m.isAnnotationPresent(ExceptionTest.class)
|
||||
|| m.isAnnotationPresent(ExceptionTestContainer.class)) {
|
||||
|
@ -2,7 +2,7 @@
|
||||
|
||||
Java 类库包含几个注解类型。对于典型的程序员来说,最重要的是 `@Override`。此注解只能在方法声明上使用,它表明带此注解的方法声明重写了父类的声明。如果始终使用这个注解,它将避免产生大量的恶意 bug。考虑这个程序,在这个程序中,类 `Bigram` 表示双字母组合,或者是有序的一对字母:
|
||||
|
||||
```Java
|
||||
```java
|
||||
// Can you spot the bug?
|
||||
public class Bigram {
|
||||
private final char first;
|
||||
@ -37,7 +37,7 @@ public class Bigram {
|
||||
|
||||
幸运的是,编译器可以帮助你找到这个错误,但只有当你通过告诉它你打算重写 `Object.equals` 来帮助你。 要做到这一点,用 `@Override` 注解 `Bigram.equals` 方法,如下所示:
|
||||
|
||||
```Java
|
||||
```java
|
||||
@Override public boolean equals(Bigram b) {
|
||||
return b.first == first && b.second == second;
|
||||
}
|
||||
@ -45,7 +45,7 @@ public class Bigram {
|
||||
|
||||
如果插入此注解并尝试重新编译该程序,编译器将生成如下错误消息:
|
||||
|
||||
```Java
|
||||
```java
|
||||
Bigram.java:10: method does not override or implement a method
|
||||
from a supertype
|
||||
@Override public boolean equals(Bigram b) {
|
||||
@ -54,7 +54,7 @@ from a supertype
|
||||
|
||||
你会立刻意识到你做错了什么,在额头上狠狠地打了一下,用一个正确的(条目 10)来替换出错的 `equals` 实现:
|
||||
|
||||
```Java
|
||||
```java
|
||||
@Override public boolean equals(Object o) {
|
||||
if (!(o instanceof Bigram))
|
||||
return false;
|
||||
|
@ -4,7 +4,7 @@
|
||||
|
||||
以往,使用单一抽象方法的接口(或者很少使用抽象类)被用作函数类型。 它们的实例(称为函数对象)表示函数(functions)或行动(actions)。 自从 JDK 1.1 于 1997 年发布以来,创建函数对象的主要手段就是匿名类(条目 24)。 下面是一段代码片段,按照字符串长度顺序对列表进行排序,使用匿名类创建排序的比较方法(强制排序顺序):
|
||||
|
||||
```Java
|
||||
```java
|
||||
// Anonymous class instance as a function object - obsolete!
|
||||
Collections.sort(words, new Comparator<String>() {
|
||||
public int compare(String s1, String s2) {
|
||||
@ -17,7 +17,7 @@ Collections.sort(words, new Comparator<String>() {
|
||||
|
||||
在 Java 8 中,语言形式化了这样的概念,即使用单个抽象方法的接口是特别的,应该得到特别的对待。 这些接口现在称为函数式接口,并且该语言允许你使用 **lambda** 表达式或简称 **lambda** 来创建这些接口的实例。 **Lambdas** 在功能上与匿名类相似,但更为简洁。 下面的代码使用 **lambdas** 替换上面的匿名类。 样板不见了,行为清晰明了:
|
||||
|
||||
```Java
|
||||
```java
|
||||
// Lambda expression as function object (replaces anonymous class)
|
||||
Collections.sort(words,
|
||||
(s1, s2) -> Integer.compare(s1.length(), s2.length()));
|
||||
@ -29,17 +29,17 @@ Collections.sort(words,
|
||||
|
||||
顺便提一句,如果使用比较器构造方法代替 **lambda**,则代码中的比较器可以变得更加简洁(条目 14,43):
|
||||
|
||||
```Java
|
||||
```java
|
||||
Collections.sort(words, comparingInt(String::length));
|
||||
```
|
||||
实际上,通过利用添加到 Java 8 中的 `List` 接口的 `sort` 方法,可以使片段变得更简短:
|
||||
```Java
|
||||
```java
|
||||
words.sort(comparingInt(String::length));
|
||||
```
|
||||
|
||||
将 **lambdas** 添加到该语言中,使得使用函数对象在以前没有意义的地方非常实用。例如,考虑条目 34 中的 `Operation` 枚举类型。由于每个枚举都需要不同的应用程序行为,所以我们使用了特定于常量的类主体,并在每个枚举常量中重写了 `apply` 方法。为了刷新你的记忆,下面是之前的代码:
|
||||
|
||||
```Java
|
||||
```java
|
||||
// Enum type with constant-specific class bodies & data
|
||||
public enum Operation {
|
||||
PLUS("+") {
|
||||
@ -71,7 +71,7 @@ public enum Operation {
|
||||
|
||||
第 34 条目说,枚举实例属性比常量特定的类主体更可取。 **Lambdas** 可以很容易地使用前者而不是后者来实现常量特定的行为。 仅仅将实现每个枚举常量行为的 **lambda** 传递给它的构造方法。 构造方法将 **lambda** 存储在实例属性中,`apply` 方法将调用转发给 **lambda**。 由此产生的代码比原始版本更简单,更清晰:
|
||||
|
||||
```Java
|
||||
```java
|
||||
public enum Operation {
|
||||
PLUS ("+", (x, y) -> x + y),
|
||||
MINUS ("-", (x, y) -> x - y),
|
||||
|
@ -3,7 +3,7 @@
|
||||
|
||||
lambda 优于匿名类的主要优点是它更简洁。Java 提供了一种生成函数对象的方法,比 lambda 还要简洁,那就是:方法引用( method references)。下面是一段程序代码片段,它维护一个从任意键到整数值的映射。如果将该值解释为键的实例个数,则该程序是一个多重集合的实现。该代码的功能是,根据键找到整数值,然后在此基础上加 1:
|
||||
|
||||
```Java
|
||||
```java
|
||||
map.merge(key, 1, (count, incr) -> count + incr);
|
||||
```
|
||||
|
||||
@ -11,7 +11,7 @@ map.merge(key, 1, (count, incr) -> count + incr);
|
||||
|
||||
代码很好读,但仍然有一些样板的味道。 参数 `count` 和 `incr` 不会增加太多价值,并且占用相当大的空间。 真的,所有的 lambda 都告诉你函数返回两个参数的和。 从 Java 8 开始,`Integer` 类(和所有其他包装数字基本类型)提供了一个静态方法总和,和它完全相同。 我们可以简单地传递一个对这个方法的引用,并以较少的视觉混乱得到相同的结果:
|
||||
|
||||
```Java
|
||||
```java
|
||||
map.merge(key, 1, Integer::sum);
|
||||
```
|
||||
|
||||
@ -21,13 +21,13 @@ map.merge(key, 1, Integer::sum);
|
||||
|
||||
如果你使用 IDE 编程,它将提供替换 lambda 的方法,并在任何地方使用方法引用。通常情况下,你应该接受这个提议。偶尔,lambda 会比方法引用更简洁。这种情况经常发生在方法与 lambda 相同的类中。例如,考虑这段代码,它被假定出现在一个名为 `GoshThisClassNameIsHumongous` 的类中:
|
||||
|
||||
```Java
|
||||
```java
|
||||
service.execute(GoshThisClassNameIsHumongous::action);
|
||||
```
|
||||
|
||||
这个 lambda 类似于等价于下面的代码:
|
||||
|
||||
```Java
|
||||
```java
|
||||
service.execute(() -> action());
|
||||
```
|
||||
|
||||
|
@ -4,7 +4,7 @@
|
||||
|
||||
考虑 `LinkedHashMap`。 可以通过重写其受保护的 `removeEldestEntry` 方法将此类用作缓存,每次将新的 key 值加入到 map 时都会调用该方法。 当此方法返回 true 时,map 将删除传递给该方法的最久条目。 以下代码重写允许 map 增长到一百个条目,然后在每次添加新 key 值时删除最老的条目,并保留最近的一百个条目:
|
||||
|
||||
```Java
|
||||
```java
|
||||
protected boolean removeEldestEntry(Map.Entry<K,V> eldest) {
|
||||
return size() > 100;
|
||||
}
|
||||
@ -12,7 +12,7 @@ protected boolean removeEldestEntry(Map.Entry<K,V> eldest) {
|
||||
|
||||
这种技术很有效,但是你可以用 lambdas 做得更好。如果 `LinkedHashMap` 是现在编写的,那么它将有一个静态的工厂或构造方法来获取函数对象。查看 `removeEldestEntry` 方法的声明,你可能会认为函数对象应该接受一个 `Map.Entry<K,V>` 并返回一个布尔值,但是这并不完全是这样:`removeEldestEntry` 方法调用 `size()` 方法来获取条目的数量,因为 `removeEldestEntry` 是 map 上的一个实例方法。传递给构造方法的函数对象不是 map 上的实例方法,无法捕获,因为在调用其工厂或构造方法时 map 还不存在。因此,map 必须将自己传递给函数对象,函数对象把 map 以及最就的条目作为输入参数。如果要声明这样一个功能接口,应该是这样的:
|
||||
|
||||
```Java
|
||||
```java
|
||||
// Unnecessary functional interface; use a standard one instead.
|
||||
@FunctionalInterface
|
||||
interface EldestEntryRemovalFunction<K,V>{
|
||||
|
@ -27,7 +27,7 @@
|
||||
考虑以下程序,该程序从字典文件中读取单词并打印其大小符合用户指定的最小值的所有变位词(anagram)组。如果两个单词由长度相通,不同顺序的相同字母组成,则它们是变位词。程序从用户指定的字典文件中读取每个单词并将单词放入 map 对象中。map 对象的键是按照字母排序的单词,因此『staple』的键是『aelpst』,『petals』的键也是『aelpst』:这两个单词就是同位词,所有的同位词共享相同的依字母顺序排列的形式(或称之为 alphagram)。map 对象的值是包含共享字母顺序形式的所有单词的列表。 处理完字典文件后,每个列表都是一个完整的同位词组。然后程序遍历 map 对象的 values() 的视图并打印每个大小符合阈值的列表:
|
||||
|
||||
|
||||
```Java
|
||||
```java
|
||||
// Prints all large anagram groups in a dictionary iteratively
|
||||
public class Anagrams {
|
||||
public static void main(String[] args) throws IOException {
|
||||
@ -59,7 +59,7 @@ public class Anagrams {
|
||||
|
||||
现在考虑以下程序,它解决了同样的问题,但大量过度使用了流。 请注意,整个程序(打开字典文件的代码除外)包含在单个表达式中。 在单独的表达式中打开字典文件的唯一原因是允许使用 `try-with-resources` 语句,该语句确保关闭字典文件:
|
||||
|
||||
```Java
|
||||
```java
|
||||
// Overuse of streams - don't do this!
|
||||
public class Anagrams {
|
||||
public static void main(String[] args) throws IOException {
|
||||
@ -84,7 +84,7 @@ public class Anagrams {
|
||||
|
||||
幸运的是,有一个折中的办法。下面的程序解决了同样的问题,使用流而不过度使用它们。其结果是一个比原来更短更清晰的程序:
|
||||
|
||||
```Java
|
||||
```java
|
||||
// Tasteful use of streams enhances clarity and conciseness
|
||||
public class Anagrams {
|
||||
|
||||
@ -111,7 +111,7 @@ public class Anagrams {
|
||||
|
||||
字母顺序方法可以使用流重新实现,但基于流的字母顺序方法本来不太清楚,更难以正确编写,并且可能更慢。 这些缺陷是由于 Java 缺乏对原始字符流的支持(这并不意味着 Java 应该支持 char 流;这样做是不可行的)。 要演示使用流处理 char 值的危害,请考虑以下代码:
|
||||
|
||||
```Java
|
||||
```java
|
||||
"Hello world!".chars().forEach(System.out::print);
|
||||
```
|
||||
|
||||
@ -120,7 +120,7 @@ public class Anagrams {
|
||||
|
||||
请注意,仔细选择 lambda 参数名称。 上面程序中参数 g 应该真正命名为 group,但是生成的代码行对于本书来说太宽了。 **在没有显式类型的情况下,仔细命名 lambda 参数对于流管道的可读性至关重要。**
|
||||
|
||||
```Java
|
||||
```java
|
||||
"Hello world!".chars().forEach(x -> System.out.print((char) x));
|
||||
```
|
||||
|
||||
@ -147,7 +147,7 @@ public class Anagrams {
|
||||
|
||||
例如,让我们编写一个程序来打印前 20 个梅森素数 (Mersenne primes)。 梅森素数是一个 2p − 1 形式的数字。如果 p 是素数,相应的梅森数可能是素数; 如果是这样的话,那就是梅森素数。 作为我们管道中的初始流,我们需要所有素数。 这里有一个返回该(无限)流的方法。 我们假设使用静态导入来轻松访问 `BigInteger` 的静态成员:
|
||||
|
||||
```Java
|
||||
```java
|
||||
static Stream<BigInteger> primes() {
|
||||
return Stream.iterate(TWO, BigInteger::nextProbablePrime);
|
||||
}
|
||||
@ -155,7 +155,7 @@ static Stream<BigInteger> primes() {
|
||||
|
||||
方法的名称(primes)是一个复数名词,描述了流的元素。 强烈建议所有返回流的方法使用此命名约定,因为它增强了流管道的可读性。 该方法使用静态工厂 `Stream.iterate`,它接受两个参数:流中的第一个元素,以及从前一个元素生成流中的下一个元素的函数。 这是打印前 20 个梅森素数的程序:
|
||||
|
||||
```Java
|
||||
```java
|
||||
public static void main(String[] args) {
|
||||
primes().map(p -> TWO.pow(p.intValueExact()).subtract(ONE))
|
||||
.filter(mersenne -> mersenne.isProbablePrime(50))
|
||||
@ -168,13 +168,13 @@ public static void main(String[] args) {
|
||||
|
||||
现在假设我们想在每个梅森素数前面加上它的指数 (p),这个值只出现在初始流中,因此在终结操作中不可访问,而终结操作将输出结果。幸运的是通过反转第一个中间操作中发生的映射,可以很容易地计算出 `Mersenne` 数的指数。 指数是二进制表示中的位数,因此该终结操作会生成所需的结果:
|
||||
|
||||
```Java
|
||||
```java
|
||||
.forEach(mp -> System.out.println(mp.bitLength() + ": " + mp));
|
||||
```
|
||||
|
||||
有很多任务不清楚是使用流还是迭代。例如,考虑初始化一副新牌的任务。假设 `Card` 是一个不可变的值类,它封装了 `Rank` 和 `Suit`,它们都是枚举类型。这个任务代表任何需要计算可以从两个集合中选择的所有元素对。数学家们称它为两个集合的笛卡尔积。下面是一个迭代实现,它有一个嵌套的 `for-each` 循环,你应该非常熟悉:
|
||||
|
||||
```Java
|
||||
```java
|
||||
// Iterative Cartesian product computation
|
||||
private static List<Card> newDeck() {
|
||||
List<Card> result = new ArrayList<>();
|
||||
@ -189,7 +189,7 @@ private static List<Card> newDeck() {
|
||||
|
||||
下面是一个基于流的实现,它使用了中间操作 `flatMap` 方法。这个操作将一个流中的每个元素映射到一个流,然后将所有这些新流连接到一个流 (或展平它们)。注意,这个实现包含一个嵌套的 lambda 表达式`(rank -> new Card(suit, rank))`:
|
||||
|
||||
```Java
|
||||
```java
|
||||
// Stream-based Cartesian product computation
|
||||
private static List<Card> newDeck() {
|
||||
return Stream.of(Suit.values())
|
||||
|
@ -6,7 +6,7 @@
|
||||
|
||||
有时,可能会看到类似于此代码片段的流代码,该代码构建了文本文件中单词的频率表:
|
||||
|
||||
```Java
|
||||
```java
|
||||
// Uses the streams API but not the paradigm--Don't do this!
|
||||
Map<String, Long> freq = new HashMap<>();
|
||||
try (Stream<String> words = new Scanner(file).tokens()) {
|
||||
@ -18,7 +18,7 @@ try (Stream<String> words = new Scanner(file).tokens()) {
|
||||
|
||||
这段代码出了什么问题? 毕竟,它使用了流,lambdas 和方法引用,并得到正确的答案。 简而言之,它根本不是流代码; 它是伪装成流代码的迭代代码。 它没有从流 API 中获益,并且它比相应的迭代代码更长,更难读,并且更难于维护。 问题源于这样一个事实:这个代码在一个终结操作 `forEach` 中完成所有工作,使用一个改变外部状态(频率表)的 lambda。`forEach` 操作除了表示由一个流执行的计算结果外,什么都不做,这是“代码中的臭味”,就像一个改变状态的 lambda 一样。那么这段代码应该是什么样的呢?
|
||||
|
||||
```Java
|
||||
```java
|
||||
// Proper use of streams to initialize a frequency table
|
||||
Map<String, Long> freq;
|
||||
try (Stream<String> words = new Scanner(file).tokens()) {
|
||||
@ -33,7 +33,7 @@ try (Stream<String> words = new Scanner(file).tokens()) {
|
||||
|
||||
将流的元素收集到真正的集合中的收集器非常简单。有三个这样的收集器:`toList()`、`toSet()` 和 `toCollection(collectionFactory)`。它们分别返回集合、列表和程序员指定的集合类型。有了这些知识,我们就可以编写一个流管道从我们的频率表中提取出现频率前 10 个单词的列表。
|
||||
|
||||
```Java
|
||||
```java
|
||||
// Pipeline to get a top-ten list of words from a frequency table
|
||||
List<String> topTen = freq.keySet().stream()
|
||||
.sorted(comparing(freq::get).reversed())
|
||||
@ -51,7 +51,7 @@ List<String> topTen = freq.keySet().stream()
|
||||
|
||||
最简单的映射收集器是 toMap(keyMapper、valueMapper),它接受两个函数,一个将流元素映射到键,另一个映射到值。在条目 34 中的 `fromString` 实现中,我们使用这个收集器从 enum 的字符串形式映射到 enum 本身:
|
||||
|
||||
```Java
|
||||
```java
|
||||
// Using a toMap collector to make a map from string to enum
|
||||
private static final Map<String, Operation> stringToEnum =
|
||||
Stream.of(values()).collect(
|
||||
@ -64,7 +64,7 @@ private static final Map<String, Operation> stringToEnum =
|
||||
|
||||
`toMap` 的三个参数形式对于从键到与该键关联的选定元素的映射也很有用。例如,假设我们有一系列不同艺术家(artists)的唱片集(albums),我们想要一张从唱片艺术家到最畅销专辑的 map。这个收集器将完成这项工作。
|
||||
|
||||
```Java
|
||||
```java
|
||||
// Collector to generate a map from key to chosen element for key
|
||||
Map<Artist, Album> topHits = albums.collect(
|
||||
toMap(Album::artist, a->a, maxBy(comparing(Album::sales))));
|
||||
@ -74,7 +74,7 @@ Map<Artist, Album> topHits = albums.collect(
|
||||
|
||||
`toMap` 的三个参数形式的另一个用途是产生一个收集器,当发生冲突时强制执行 last-write-wins 策略。 对于许多流,结果是不确定的,但如果映射函数可能与键关联的所有值都相同,或者它们都是可接受的,则此收集器的行为可能正是您想要的:
|
||||
|
||||
```Java
|
||||
```java
|
||||
// Collector to impose last-write-wins policy
|
||||
toMap(keyMapper, valueMapper, (oldVal, newVal) ->newVal)
|
||||
```
|
||||
@ -85,7 +85,7 @@ toMap(keyMapper, valueMapper, (oldVal, newVal) ->newVal)
|
||||
|
||||
除了 toMap 方法之外,Collectors API 还提供了 `groupingBy` 方法,该方法返回收集器以生成基于分类器函数 (classifier function) 将元素分组到类别中的 map。 分类器函数接受一个元素并返回它所属的类别。 此类别来用作元素的 map 的键。 `groupingBy` 方法的最简单版本仅采用分类器并返回一个 map,其值是每个类别中所有元素的列表。 这是我们在条目 45 中的 `Anagram` 程序中使用的收集器,用于生成从按字母顺序排列的单词到单词列表的 map:
|
||||
|
||||
```Java
|
||||
```java
|
||||
Map<String, Long> freq = words
|
||||
.collect(groupingBy(String::toLowerCase, counting()));
|
||||
```
|
||||
|
@ -6,7 +6,7 @@
|
||||
|
||||
遗憾的是,这个问题没有好的解决方法。 乍一看,似乎可以将方法引用传递给 `Stream` 的 iterator 方法。 结果代码可能有点嘈杂和不透明,但并非不合理:
|
||||
|
||||
```Java
|
||||
```java
|
||||
// Won't compile, due to limitations on Java's type inference
|
||||
for (ProcessHandle ph : ProcessHandle.allProcesses()::iterator) {
|
||||
// Process the process
|
||||
@ -15,21 +15,21 @@ for (ProcessHandle ph : ProcessHandle.allProcesses()::iterator) {
|
||||
|
||||
不幸的是,如果你试图编译这段代码,会得到一个错误信息:
|
||||
|
||||
```Java
|
||||
```java
|
||||
Test.java:6: error: method reference not expected here
|
||||
for (ProcessHandle ph : ProcessHandle.allProcesses()::iterator) {
|
||||
```
|
||||
|
||||
为了使代码编译,必须将方法引用强制转换为适当参数化的 `Iterable` 类型:
|
||||
|
||||
```Java
|
||||
```java
|
||||
// Hideous workaround to iterate over a stream
|
||||
for (ProcessHandle ph : (Iterable<ProcessHandle>)ProcessHandle.allProcesses()::iterator)
|
||||
```
|
||||
|
||||
此代码有效,但在实践中使用它太嘈杂和不透明。 更好的解决方法是使用适配器方法。 JDK 没有提供这样的方法,但是使用上面的代码片段中使用的相同技术,很容易编写一个方法。 请注意,在适配器方法中不需要强制转换,因为 Java 的类型推断在此上下文中能够正常工作:
|
||||
|
||||
```Java
|
||||
```java
|
||||
// Adapter from Stream<E> to Iterable<E>
|
||||
public static <E> Iterable<E> iterableOf(Stream<E> stream) {
|
||||
return stream::iterator;
|
||||
@ -38,7 +38,7 @@ public static <E> Iterable<E> iterableOf(Stream<E> stream) {
|
||||
|
||||
使用此适配器,可以使用 for-each 语句迭代任何流:
|
||||
|
||||
```Java
|
||||
```java
|
||||
for (ProcessHandle p : iterableOf(ProcessHandle.allProcesses())) {
|
||||
// Process the process
|
||||
}
|
||||
@ -48,7 +48,7 @@ for (ProcessHandle p : iterableOf(ProcessHandle.allProcesses())) {
|
||||
|
||||
相反,如果一个程序员想要使用流管道来处理一个序列,那么一个只提供 `Iterable` 的 API 会让他感到不安。JDK 同样没有提供适配器,但是编写这个适配器非常简单:
|
||||
|
||||
```Java
|
||||
```java
|
||||
// Adapter from Iterable<E> to Stream<E>
|
||||
public static <E> Stream<E> streamOf(Iterable<E> iterable) {
|
||||
return StreamSupport.stream(iterable.spliterator(), false);
|
||||
@ -63,7 +63,7 @@ public static <E> Stream<E> streamOf(Iterable<E> iterable) {
|
||||
|
||||
诀窍是使用幂集中每个元素的索引作为位向量(bit vector),其中索引中的第 n 位指示源集合中是否存在第 n 个元素。 本质上,从 0 到 2n-1 的二进制数和 n 个元素集和的幂集之间存在自然映射。 这是代码:
|
||||
|
||||
```Java
|
||||
```java
|
||||
// Returns the power set of an input set as custom collection
|
||||
public class PowerSet {
|
||||
public static final <E> Collection<Set<E>> of(Set<E> s) {
|
||||
@ -104,7 +104,7 @@ public class PowerSet {
|
||||
|
||||
然而,实现输入列表的所有子列表的流是直截了当的,尽管它确实需要一点的洞察力(insight)。 让我们调用一个子列表,该子列表包含列表的第一个元素和列表的前缀。 例如,(a,b,c)的前缀是(a),(a,b)和(a,b,c)。 类似地,让我们调用包含后缀的最后一个元素的子列表,因此(a,b,c)的后缀是(a,b,c),(b,c)和(c)。 洞察力是列表的子列表只是前缀的后缀(或相同的后缀的前缀)和空列表。 这一观察直接展现了一个清晰,合理简洁的实现:
|
||||
|
||||
```Java
|
||||
```java
|
||||
// Returns a stream of all the sublists of its input list
|
||||
public class SubLists {
|
||||
|
||||
@ -127,7 +127,7 @@ public class SubLists {
|
||||
|
||||
请注意,`Stream.concat` 方法用于将空列表添加到返回的流中。 还有,`flatMap` 方法(条目 45)用于生成由所有前缀的所有后缀组成的单个流。 最后,通过映射 `IntStream.range` 和 `IntStream.rangeClosed` 返回的连续 int 值流来生成前缀和后缀。这个习惯用法,粗略地说,流等价于整数索引上的标准 for 循环。因此,我们的子列表实现似于明显的嵌套 for 循环:
|
||||
|
||||
```Java
|
||||
```java
|
||||
for (int start = 0; start < src.size(); start++)
|
||||
for (int end = start + 1; end <= src.size(); end++)
|
||||
System.out.println(src.subList(start, end));
|
||||
@ -135,7 +135,7 @@ for (int start = 0; start < src.size(); start++)
|
||||
|
||||
可以将这个 for 循环直接转换为流。结果比我们以前的实现更简洁,但可能可读性稍差。它类似于条目 45 中的笛卡尔积的使用流的代码:
|
||||
|
||||
```Java
|
||||
```java
|
||||
// Returns a stream of all the sublists of its input list
|
||||
public static <E> Stream<List<E>> of(List<E> list) {
|
||||
return IntStream.range(0, list.size())
|
||||
|
@ -4,7 +4,7 @@
|
||||
|
||||
考虑条目 45 中的程序:
|
||||
|
||||
```Java
|
||||
```java
|
||||
// Stream-based program to generate the first 20 Mersenne primes
|
||||
public static void main(String[] args) {
|
||||
primes().map(p -> TWO.pow(p.intValueExact()).subtract(ONE))
|
||||
@ -40,7 +40,7 @@ static Stream<BigInteger> primes() {
|
||||
|
||||
如果在并行化流管道时,这种可能性对你不利,那是因为它们确实存在。一个认识的人,他维护一个数百万行代码库,大量使用流,他发现只有少数几个地方并行流是有效的。这并不意味着应该避免并行化流。**在适当的情况下,只需向流管道添加一个 `parallel` 方法调用,就可以实现处理器内核数量的近似线性加速。** 某些领域,如机器学习和数据处理,特别适合这些加速。
|
||||
|
||||
```Java
|
||||
```java
|
||||
// 作为并行性有效的流管道的简单示例,请考虑此函数来计算π(n),素数小于或等于 n:
|
||||
// Prime-counting stream pipeline - benefits from parallelization
|
||||
static long pi(long n) {
|
||||
@ -53,7 +53,7 @@ static long pi(long n) {
|
||||
|
||||
在我的机器上,使用此功能计算π(10<sup>8</sup>)需要 31 秒。 只需添加 `parallel()` 方法调用即可将时间缩短为 9.2 秒:
|
||||
|
||||
```Java
|
||||
```java
|
||||
// Prime-counting stream pipeline - parallel version
|
||||
static long pi(long n) {
|
||||
return LongStream.rangeClosed(2, n)
|
||||
|
@ -8,7 +8,7 @@
|
||||
|
||||
对于公共方法和受保护方法,请使用 Java 文档`@throws`注解来记在在违反参数值限制时将引发的异常(条目 74)。 通常,生成的异常是`IllegalArgumentException`,`IndexOutOfBoundsException`或`NullPointerException`(条目 72)。 一旦记录了对方法参数的限制,并且记录了违反这些限制时将引发的异常,那么强制执行这些限制就很简单了。 这是一个典型的例子:
|
||||
|
||||
```Java
|
||||
```java
|
||||
/**
|
||||
* Returns a BigInteger whose value is (this mod m). This method
|
||||
* differs from the remainder method in that it always returns a
|
||||
@ -31,7 +31,7 @@ public BigInteger mod(BigInteger m) {
|
||||
|
||||
在 Java 7 中添加的`Objects.requireNonNull 方`法灵活方便,因此没有理由再手动执行空值检查。 如果愿意,可以指定自定义异常详细消息。 该方法返回其输入的值,因此可以在使用值的同时执行空检查:
|
||||
|
||||
```Java
|
||||
```java
|
||||
// Inline use of Java's null-checking facility
|
||||
this.strategy = Objects.requireNonNull(strategy, "strategy");
|
||||
```
|
||||
@ -42,7 +42,7 @@ this.strategy = Objects.requireNonNull(strategy, "strategy");
|
||||
|
||||
对于未导出的方法,作为包的作者,控制调用方法的环境,这样就可以并且应该确保只传入有效的参数值。因此,非公共方法可以使用断言检查其参数,如下所示:
|
||||
|
||||
```Java
|
||||
```java
|
||||
// Private helper function for a recursive sort
|
||||
private static void sort(long a[], int offset, int length) {
|
||||
assert a != null;
|
||||
|
@ -6,7 +6,7 @@
|
||||
|
||||
如果没有对象的帮助,另一个类是不可能修改对象的内部状态的,但是在无意的情况下提供这样的帮助却非常地容易。例如,考虑以下类,表示一个不可变的时间期间:
|
||||
|
||||
```Java
|
||||
```java
|
||||
// Broken "immutable" time period class
|
||||
public final class Period {
|
||||
private final Date start;
|
||||
@ -40,7 +40,7 @@ public final class Period {
|
||||
|
||||
乍一看,这个类似乎是不可变的,并强制执行不变式,即 `period` 实例的开始时间并不在结束时间之后。然而,利用 `Date` 类是可变的这一事实很容易违反这个不变式:
|
||||
|
||||
```Java
|
||||
```java
|
||||
// Attack the internals of a Period instance
|
||||
Date start = new Date();
|
||||
Date end = new Date();
|
||||
@ -52,7 +52,7 @@ end.setYear(78); // Modifies internals of p!
|
||||
|
||||
为了保护 `Period` 实例的内部不受这种攻击,必须将每个可变参数的防御性拷贝应用到构造方法中,并将拷贝用作 `Period` 实例的组件,以替代原始实例:
|
||||
|
||||
```Java
|
||||
```java
|
||||
// Repaired constructor - makes defensive copies of parameters
|
||||
public Period(Date start, Date end) {
|
||||
this.start = new Date(start.getTime());
|
||||
@ -70,7 +70,7 @@ public Period(Date start, Date end) {
|
||||
|
||||
虽然替换构造方法成功地抵御了先前的攻击,但是仍然可以对 `Period` 实例进行修改,因为它的访问器提供了对其可变内部结构的访问:
|
||||
|
||||
```Java
|
||||
```java
|
||||
// Second attack on the internals of a Period instance
|
||||
Date start = new Date();
|
||||
Date end = new Date();
|
||||
@ -80,7 +80,7 @@ p.end().setYear(78); // Modifies internals of p!
|
||||
|
||||
为了抵御第二次攻击,只需修改访问器以返回可变内部字属性的防御性拷贝:
|
||||
|
||||
```Java
|
||||
```java
|
||||
// Repaired accessors - make defensive copies of internal fields
|
||||
public Date start() {
|
||||
return new Date(start.getTime());
|
||||
|
@ -18,7 +18,7 @@
|
||||
|
||||
**与布尔型参数相比,优先使用两个元素枚举类型**,除非布尔型参数的含义在方法名中是明确的。枚举类型使代码更容易阅读和编写。此外,它们还可以方便地在以后添加更多选项。例如,你可能有一个 `Thermometer` 类型的静态工厂方法,这个方法的签名是以下这个枚举:
|
||||
|
||||
```Java
|
||||
```java
|
||||
public enum TemperatureScale { FAHRENHEIT, CELSIUS }
|
||||
```
|
||||
|
||||
|
@ -2,7 +2,7 @@
|
||||
|
||||
假如你某一天不走运的话,可能遇到如下代码:
|
||||
|
||||
```Java
|
||||
```java
|
||||
/* Horrible abuse of exceptions. Don't ever do this! */
|
||||
try {
|
||||
int i = 0;
|
||||
@ -14,7 +14,7 @@ try {
|
||||
|
||||
这段代码有什用,看起来根本不明显,这正是它没有真正被使用的原因(详见 67 条)。事实证明,作为一个要对数组元素进行遍历的实现方式,它的构想是十分拙劣的。当这个循环企图访问数组边界之外的第一个数组元素的时候,使用 try-catch 并且忽略 ArrayIndexOutOfBoundsException 异常的手段来达到终止无限循环的目的。假定它与数组循环的标准模式是等价的,对于它的标准模式每个 Java 程序员都可以一眼辨认出来:
|
||||
|
||||
```Java
|
||||
```java
|
||||
for ( Mountain m : range )
|
||||
m.climb();
|
||||
```
|
||||
@ -33,7 +33,7 @@ for ( Mountain m : range )
|
||||
|
||||
这条原则对于 API 设计也有启发**。设计良好的 API 不应该强迫它的客户端为了正常的控制流程而使用异常。**如果类中具有“状态相关”(state-dependent)的方法,即只有在特定的不可预知的条件下才可以被调用的方法,这个类往往也应该具有一个单独的“状态测试”(state-testing)方法,即表明是否可以调用这个状态相关的方法。比如 Iterator 接口含有状态相关的 next 方法,以及相应的状态测试方法 hasNext。这使得利用传统的 for 循环(以及 for-each 循环,在内部使用了 hasNext 方法)对集合进行迭代的标准模式成为可能。
|
||||
|
||||
```Java
|
||||
```java
|
||||
for ( Iterator<Foo> i = collection.iterator(); i.hasNext(); ){
|
||||
Foo foo = i.next();
|
||||
...
|
||||
@ -42,7 +42,7 @@ for ( Iterator<Foo> i = collection.iterator(); i.hasNext(); ){
|
||||
|
||||
如果 Iterator 缺少 hasNext 方法,客户端将被迫改用下面的做法:
|
||||
|
||||
```Java
|
||||
```java
|
||||
/* Do not use this hideous code for iteration over a collection! */
|
||||
try {
|
||||
Iterator<Foo> i = collection.iterator();
|
||||
|
@ -2,7 +2,7 @@
|
||||
|
||||
第 50 条介绍了一个不可变的日期范围类,它包含可变的私有变量 Date。该类通过在其构造器和访问方法(accessor)中保护性的拷贝 Date 对象,极力维护其约束条件和不可变性。该类代码如下所示:
|
||||
|
||||
```Java
|
||||
```java
|
||||
// Immutable class that uses defensive copying
|
||||
public final class Period {
|
||||
private final Date start;
|
||||
@ -40,7 +40,7 @@ public final class Period {
|
||||
|
||||
假设我们仅仅在 Period 类的声明加上了 implements Serializable 字样。那么这个丑陋的程序代码将会产生一个 Period 实例,他的结束时间比起始时间还早。对于高位 byte 值进行强制类型转换是 Java 缺少 byte 并且做出 byte 类型签名的不幸决定的后果:
|
||||
|
||||
```Java
|
||||
```java
|
||||
public class BogusPeriod {
|
||||
// Byte stream couldn't have come from a real Period instance!
|
||||
private static final byte[] serializedForm = {
|
||||
@ -80,7 +80,7 @@ public class BogusPeriod {
|
||||
|
||||
为了修整这个问题,可以为 Period 提供一个 readObject 方法,该方法首先调用 defaultReadObject,然后检查被反序列化之后的对象有效性。如果有效性检查失败,readObject 方法就会抛出一个 InvalidObjectException 异常,这使得反序列化过程不能成功的完成:
|
||||
|
||||
```Java
|
||||
```java
|
||||
// readObject method with validity checking - insufficient!
|
||||
private void readObject(ObjectInputStream s)
|
||||
throws IOException, ClassNotFoundException {
|
||||
@ -93,7 +93,7 @@ private void readObject(ObjectInputStream s)
|
||||
|
||||
尽管这样的修成避免了攻击者创建无效的 Period 实例,但是这里依旧隐藏着一个更为微妙的问题。通过伪造字节流,要想创建可变的 Period 实例仍是有可能的,做法事:字节流以一个有效的 Period 实例开头,然后附加上两个额外的引用,指向 Period 实例中两个私有的 Date 字段。攻击者从 ObjectInputStream 读取 Period 实例,然后读取附加在其后面的“恶意编制的对线引用”。这些对象引用使得攻击者能够访问到 Period 对象内部的私有 Date 字段所引用的对象。通过改变这些 Date 实例,攻击者可以改变 Period 实例。如下的类演示了这种攻击方式:
|
||||
|
||||
```Java
|
||||
```java
|
||||
public class MutablePeriod {
|
||||
// A period instance
|
||||
public final Period period;
|
||||
@ -139,7 +139,7 @@ public class MutablePeriod {
|
||||
|
||||
要查看正在进行的攻击,请运行以下程序:
|
||||
|
||||
```Java
|
||||
```java
|
||||
public static void main(String[] args) {
|
||||
MutablePeriod mp = new MutablePeriod();
|
||||
Period p = mp.period;
|
||||
@ -155,7 +155,7 @@ public static void main(String[] args) {
|
||||
|
||||
在我本地机器上运行这个程序产生的输出结果如下:
|
||||
|
||||
```Java
|
||||
```java
|
||||
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
|
||||
```
|
||||
@ -164,7 +164,7 @@ Wed Nov 22 00:21:29 PST 2017 - Sat Nov 22 00:21:29 PST 1969
|
||||
|
||||
问题的根源在于,Period 的 readObject 方法并没有完成足够的保护性拷贝。 **当一个对象被反序列化的时候,对于客户端不应该拥有的对象引用,如果那个字段包含了这样的对象引用,就必须做保护性拷贝,这是非常重要的。** 因此,对于每个可序列化的不可变类,如果它好汉了私有的可变字段,那么在它的 readObject 方法中,必须要对这些字段进行保护性拷贝。下面的这些 readObject 方法可以确保 Period 类的约束条件不会遭到破坏,以保持它的不可变性:
|
||||
|
||||
```Java
|
||||
```java
|
||||
// readObject method with defensive copying and validity checking
|
||||
private void readObject(ObjectInputStream s)
|
||||
throws IOException, ClassNotFoundException {
|
||||
@ -180,7 +180,7 @@ private void readObject(ObjectInputStream s)
|
||||
|
||||
注意,保护性拷贝是在有效性检查之前进行的。我们没有使用 Date 的 clone 方法来执行保护性拷贝机制。这两个细节对于保护 Period 类免受攻击是必要的(详见 50 条)。同时也注意到,对于 final 字段,保护性字段是不可能的。为了使用 readObject 方法,我们必须要将 start 和 end 字段声明成为非 final 的。很遗憾的是,这还算是相对比较好的做法。有了这新的 readObject 方法,并且取消了 start 和 end 的 final 修饰符之后,MutablePeriod 类将不再有效。此时,上面的攻击程序会产生如下输出:
|
||||
|
||||
```Java
|
||||
```java
|
||||
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
|
||||
```
|
||||
|
@ -2,7 +2,7 @@
|
||||
|
||||
第 3 条讲述了 Singletion(单例)模式,并且给出了以下这个 Singletion 示例。这个类限制了对其构造器的访问,以确保永远只创建一个实例。
|
||||
|
||||
```Java
|
||||
```java
|
||||
public class Elvis {
|
||||
public static final Elvis INSTANCE = new Elvis();
|
||||
|
||||
@ -17,7 +17,7 @@ public class Elvis {
|
||||
|
||||
如果 Elvis 类要实现 `Serializable` 接口,下面的 `readResolve` 方法就足以保证它的单例属性:
|
||||
|
||||
```Java
|
||||
```java
|
||||
// readResolve for instance control - you can do better!
|
||||
private Object readResolve() {
|
||||
// Return the one true Elvis and let the garbage collector
|
||||
@ -36,7 +36,7 @@ private Object readResolve() {
|
||||
|
||||
为了更具体的说明这一点,我们以如下这个单例模式为例:
|
||||
|
||||
```Java
|
||||
```java
|
||||
// Broken singleton - has nontransient object reference field!
|
||||
public class Elvis implements Serializable {
|
||||
public static final Elvis INSTANCE = new Elvis();
|
||||
@ -56,7 +56,7 @@ public class Elvis implements Serializable {
|
||||
|
||||
如下“盗用者”类,是根据以上描述构造的:
|
||||
|
||||
```Java
|
||||
```java
|
||||
public class ElvisStealer implements Serializable {
|
||||
static Elvis impersonator;
|
||||
private static final long serialVersionUID = 0;
|
||||
@ -74,7 +74,7 @@ public class ElvisStealer implements Serializable {
|
||||
|
||||
下面是一个不完整的程序,它反序列化一个手工制作的流,为那个有缺陷的单例产生两个截然不同的实例。这个程序省略了反序列化方法,因为它与第 88 条一样。
|
||||
|
||||
```Java
|
||||
```java
|
||||
public class ElvisImpersonator {
|
||||
// Byte stream couldn't have come from a real Elvis instance!
|
||||
private static final byte[] serializedForm = {
|
||||
@ -113,7 +113,7 @@ public class ElvisImpersonator {
|
||||
通过将 `favoriteSongs` 字段声明为 `transient`,可以修复这个问题,但是最好把 `Elvis` 做成一个单元素的枚举类型(详见第 3 条)。就如 `ElvisStealer` 攻击所示范的,用 `readResolve` 方法防止“临时”被反序列化的实例收到攻击者的访问,这种方法十分脆弱需要万分谨慎。
|
||||
|
||||
如果将一个可序列化的实例受控的类编写为枚举,Java 就可以绝对保证出了所声明的常量之外,不会有其他实例,除非攻击者恶意的使用了享受特权的方法。如 `AccessibleObject.setAccessible`。能够做到这一点的任何一位攻击者,已经拥有了足够的特权来执行任意的本地代码,后果不堪设想。将 Elvis 写成枚举的例子如下所示:
|
||||
```Java
|
||||
```java
|
||||
// Enum singleton - the preferred approach
|
||||
public enum Elvis {
|
||||
INSTANCE;
|
||||
|
@ -6,7 +6,7 @@
|
||||
|
||||
例如,以第 50 条中编写不可变的 Period 类为例,它在第 88 条中被作为可序列化的。以下是一个类的序列化代理。Period 类是如此简单,以致于它的序列化代理有着与类完全相同的字段。
|
||||
|
||||
```Java
|
||||
```java
|
||||
// Serialization proxy for Period class
|
||||
private static class SerializationProxy implements Serializable {
|
||||
private final Date start;
|
||||
@ -22,7 +22,7 @@ private static class SerializationProxy implements Serializable {
|
||||
|
||||
接下来,将下面的 writeReplace 方法添加到外围类中。通过序列化代理,这个方法可以被逐字的复制到任何类中。
|
||||
|
||||
```Java
|
||||
```java
|
||||
// writeReplace method for the serialization proxy pattern
|
||||
private Object writeReplace() {
|
||||
return new SerializationProxy(this);
|
||||
@ -33,7 +33,7 @@ private Object writeReplace() {
|
||||
|
||||
有了 writeReplace 方法之后,序列化系统永远不会产生外围类的序列化实例,但是攻击者有可能伪造企图违反该类约束条件的示例。为了防止此类攻击,只需要在外围类中添加如下 readObject 方法。
|
||||
|
||||
```Java
|
||||
```java
|
||||
// readObject method for the serialization proxy pattern
|
||||
private void readObject(ObjectInputStream stream)
|
||||
throws InvalidObjectException {
|
||||
@ -47,7 +47,7 @@ private void readObject(ObjectInputStream stream)
|
||||
|
||||
以下是上述的 Period.SerializationProxy 的 readResolve 方法:
|
||||
|
||||
```Java
|
||||
```java
|
||||
// readResolve method for Period.SerializationProxy
|
||||
private Object readResolve() {
|
||||
return new Period(start, end); // Uses public constructor
|
||||
@ -62,7 +62,7 @@ private Object readResolve() {
|
||||
|
||||
现在考虑这种情况:如果序列化一个枚举类型,它的枚举有 60 个元素,然后给这个枚举类型再增加 5 个元素,之后反序列化这个枚举集合。当它被序列化的时候,是一个 RegularEnumSet 实例,但是它一旦被反序列化,他就变成了 JunmboEnumSet 实例。实际发生的情况也正是如此,因为 EnumSet 使用序列化代理模式如果你感兴趣,可以看看如下的 EnumSet 序列化代理,它实际上就是这么简单:
|
||||
|
||||
```Java
|
||||
```java
|
||||
// EnumSet's serialization proxy
|
||||
private static class SerializationProxy<E extends Enum<E>>
|
||||
implements Serializable {
|
||||
|
Loading…
Reference in New Issue
Block a user