第 1 条:用静态工厂方法代替构造器
静态工厂方法的优点一:它们有名称
- 如果构造器的参数本身没有确切地描述正被返回的对象,那么具有适当名称的静态工厂会更容易使用,产生的客户端代码也更易于阅读
- 重载的类的构造器实际上并不是一个好主意,面对这样的 API,用户永远也记不住该用哪个构造器,结构常常会调用错误的构造器。并且在读到使用了这些构造器的代码时,如果没有参考类的文档,往往不知所云
- 当一个类需要多个带有相同签名的构造器时,就用静态工厂方法代替构造器,并且仔细选择名称以便突出静态工厂方法之间的区别
静态工厂方法的优点二:不必在每次调用它们的时候都创建一个新对象
- 这使得不可变类(详见第 17 条)可以使用预先构建好的实例,或者将构建好的实例缓存起来,进行重复利用,从而避免创建不必要的对象。类似享元(Flyweight)模式,如果程序经常请求创建相同的对象,并且创建对象的代价很高,那么该技术可以极大提升性能
- 静态工厂方法能够为重复的调用返回相同的对象,这样有助于类总能严格控制在某个时刻哪些实例应该存在,这种类被称作实例受控的类(instance-controlled)。实例受控使得类可以确保它是一个 Singleton (详见第 3 条)或者是不可实例化的(详见第 4 条)
- 它还使得不可变的值类可以确保不会存在两个相等的实例,即当且仅当
a == b
时,a.equals(b)
才为true
。这是享元模式的基础,枚举类型(详见第 34 条)保证了这一点
静态工厂方法的优点三:它们可以返回原返回类型的任何子类型的对象
- 这种灵活性的一种应用是,API 可以返回对象,同时又不会使对象的类变成公有的,以这种方式隐藏实现类会使 API 变得非常简洁。这项技术适用于基于接口的框架(interface-based framework)(详见第 20 条),因为在这种框架中,接口为静态工厂方法提供了自然返回类型
- 在 Java 8 之前,接口不能有静态方法,因此按照惯例,接口 Type 的静态工厂方法被放在一个名为 Types 的不可实例化的伴生类(详见第 4 条)中
- 使用这种静态工厂方法时,甚至要求客户端通过接口来引用被返回的对象,而不是通过它的实现类来引用被返回的对象,这是一种良好的习惯(详见第 64 条)
- Java 8 要求接口的所有静态成员都必须是公有的;Java 9 允许接口有私有的静态方法,但是静态域和静态成员类仍然需要是公有的
静态工厂方法的优点四:所返回的对象的类可以随着每次调用而发生变化,这取决于静态工厂方法的参数值
- 只要是已声明的返回类型的子类型,都是允许的。返回对象的类也可能随着发行版本的不同而不同。比如,
EnumSet
(详见第 36 条)没有共有的构造器,只有静态工厂方法 - 举例,
EnumSet
没有公有的构造器,只有静态工厂方法。在 OpenJDK 实现中,它们返回两种子类之一的一个实例,具体则取决于底层枚举类型的大小
- 只要是已声明的返回类型的子类型,都是允许的。返回对象的类也可能随着发行版本的不同而不同。比如,
静态工厂方法的优点五:方法返回的对象所属的类,在编写包含静态工厂方法的类时可以不存在
- 这种灵活的静态工厂方法构成了服务提供者框架(Service Provider Framework)的基础。服务提供者框架是指这样一个系统:多个服务提供者实现一个服务,系统为服务提供者的客户端提供多个实现,并把它们从多个实现中解耦出来
- 服务提供者框架中有三个重要的组件:服务接口(Service Interface),这是提供者实现的;提供者注册 API(Provider Registration API),这是提供者用来注册实现的;服务访问 API(Service Access API),这是客户端用来获取服务的实例
- 服务提供者框架的第四个组件服务提供者接口(Service Provider Interface)是可选的,它表示产生服务接口之实例的工厂对象。如果没有服务提供者接口,实现就通过反射方式进行实例化(详见第 65 条)
- 服务提供者框架模式有着无数种变体。例如,服务访问 API 可以返回比提供者需要的更丰富的服务接口,这就是桥接(Bridge)模式;依赖注入框架(详见第 5 条)可以被看作是一个强大的服务提供者;从 Java 6 版本开始,Java 平台就提供了一个通用的服务提供者框架
java.util.ServiceLoader
(详见第 59 条)
静态工厂方法的缺点一:类如果不含有公有的或者受保护的构造器,就不能被子类化
- 例如,要想将
Collection Framework
中的任何便利的实现类子类化,这是不可能的 - 这样也因祸得福,因为它鼓励我们使用复合(composition)而不是继承(详见第 18 条),这正是不可变类型所需要的(详见第 17 条)
- 例如,要想将
静态工厂方法的缺点二:程序员很难发现它们
在 API 文档中,它们没有像构造器那样在 API 文档中明确标识出来
同时,通过在类或者接口注释中关注静态工厂,并遵守标准的命名习惯,也可以弥补这一劣势
静态工厂方法惯用名称 含义 举例 from
类型转换方法,它只有单个参数,返回该类型的一个相对应的实例 Date d = Data.from(instant);
of
聚合方法,带有多个参数,返回该类型的一个实例,把它们合并起来 Set<Rank> faceCards = EnumSet.of(JACK, QUEEN, KING);
valueOf
比 from
和of
更繁琐的一种替代方法BigInteger prime = BigInteger.valueOf(Integer.MAX_VALUE);
instance
或者getInstance
返回的实例是通过方法的(如有)参数来描述的,但是不能说与参数具有同样的值 StackWalker luke = StackWalker.getInstance(options);
create
或者newInstance
像 instance
或者getInstance
一样,但create
或者newInstance
能够确保每次调用都返回一个新的实例Object newArray = Array.newInstance(classObject, arrayLen);
getType
像 getInstance
一样,但是在工厂方法处于不同的类中的时候使用。Type
表示工厂方法所返回的对象类型FileStore fs = Files.getFileStore(path);
newType
像 newInstance
一样,但是在工厂方法处于不同的类中的时候使用。Type
表示工厂方法所返回的对象类型BufferedReader br = Files.newBufferedReader(path);
type
getType
和newType
的简版List<Complaint> litany = Collections.list(legacyLitany);
第 2 条:遇到多个构造器参数时要考虑使用构建器
重叠构造器(telescoping constructor)
含义
- 提供的第一个构造器只有必要的参数,第二个构造器有一个可选参数
- 第三个构造器有两个可选参数,以此类推,最后一个构造器包含所有可选的参数
Demo
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40package effectivejava.chapter2.item2.telescopingconstructor;
// Telescoping constructor pattern - does not scale well! (Pages 10-11)
public class NutritionFacts {
private final int servingSize; // (mL) required
private final int servings; // (per container) required
private final int calories; // (per serving) optional
private final int fat; // (g/serving) optional
private final int sodium; // (mg/serving) optional
private final int carbohydrate; // (g/serving) optional
public NutritionFacts(int servingSize, int servings) {
this(servingSize, servings, 0);
}
public NutritionFacts(int servingSize, int servings, int calories) {
this(servingSize, servings, calories, 0);
}
public NutritionFacts(int servingSize, int servings, int calories, int fat) {
this(servingSize, servings, calories, fat, 0);
}
public NutritionFacts(int servingSize, int servings, int calories, int fat, int sodium) {
this(servingSize, servings, calories, fat, sodium, 0);
}
public NutritionFacts(int servingSize, int servings, int calories, int fat, int sodium, int carbohydrate) {
this.servingSize = servingSize;
this.servings = servings;
this.calories = calories;
this.fat = fat;
this.sodium = sodium;
this.carbohydrate = carbohydrate;
}
public static void main(String[] args) {
NutritionFacts cocaCola = new NutritionFacts(240, 8, 100, 0, 35, 27);
}
}特点
- 重叠构造器模式可行,但当有许多参数的时候,客户端代码会很难编写,并且仍然较难以阅读
- 比如,如果客户端不小心颠倒了其中两个相同类型参数的顺序,不会编译报错但往往会有运行时报错(详见第 51 条)
JavaBeans 模式
含义
- 先调用一个无参构造器来创建对象
- 然后再调用
setter
方法来设置每个必要的参数,以及每个相关的可选参数
Demo
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48package effectivejava.chapter2.item2.javabeans;
// JavaBeans Pattern - allows inconsistency, mandates mutability (pages 11-12)
public class NutritionFacts {
// Parameters initialized to default values (if any)
private int servingSize = -1; // Required; no default value
private int servings = -1; // Required; no default value
private int calories = 0;
private int fat = 0;
private int sodium = 0;
private int carbohydrate = 0;
public NutritionFacts() { }
// Setters
public void setServingSize(int val) {
servingSize = val;
}
public void setServings(int val) {
servings = val;
}
public void setCalories(int val) {
calories = val;
}
public void setFat(int val) {
fat = val;
}
public void setSodium(int val) {
sodium = val;
}
public void setCarbohydrate(int val) {
carbohydrate = val;
}
public static void main(String[] args) {
NutritionFacts cocaCola = new NutritionFacts();
cocaCola.setServingSize(240);
cocaCola.setServings(8);
cocaCola.setCalories(100);
cocaCola.setSodium(35);
cocaCola.setCarbohydrate(27);
}
}缺点
- 因为构造过程被分到了几个调用中,在构造过程中 JavaBean 可能处于不一致的状态,调试起来很困难
- JavaBeans 模式使得把类做成不可变的可能性不复存在(详见第 17 条),这就需要付出额外的努力来确保它的线程安全
建造者(Builder)模式
含义
- 它不直接生成想要的对象,而是让客户端利用所有必要的参数调用构造器(或者静态工厂),得到一个
builder
对象 - 然后客户端在
builder
对象上调用类似于setter
的方法,来设置每个相关的可选参数 - 最后,客户端调用无参的
build
方法来生成通常是不可变的对象。这个builder
通常是它构建的类的静态成员类(详见第 24 条)
- 它不直接生成想要的对象,而是让客户端利用所有必要的参数调用构造器(或者静态工厂),得到一个
Demo
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65package effectivejava.chapter2.item2.builder;
// Builder Pattern (Page 13)
public class NutritionFacts {
private final int servingSize;
private final int servings;
private final int calories;
private final int fat;
private final int sodium;
private final int carbohydrate;
public static class Builder {
// Required parameters
private final int servingSize;
private final int servings;
// Optional parameters - initialized to default values
private int calories = 0;
private int fat = 0;
private int sodium = 0;
private int carbohydrate = 0;
public Builder(int servingSize, int servings) {
this.servingSize = servingSize;
this.servings = servings;
}
public Builder calories(int val) {
calories = val;
return this;
}
public Builder fat(int val) {
fat = val;
return this;
}
public Builder sodium(int val) {
sodium = val;
return this;
}
public Builder carbohydrate(int val) {
carbohydrate = val;
return this;
}
public NutritionFacts build() {
return new NutritionFacts(this);
}
}
private NutritionFacts(Builder builder) {
servingSize = builder.servingSize;
servings = builder.servings;
calories = builder.calories;
fat = builder.fat;
sodium = builder.sodium;
carbohydrate = builder.carbohydrate;
}
public static void main(String[] args) {
NutritionFacts cocaCola = new NutritionFacts.Builder(240, 8).calories(100).sodium(35).carbohydrate(27).build();
}
}优点
- 这是一个流式 API,这样的代码容易编写,更为重要的是容易阅读
- Builder 模式模拟了具名的可选参数,就像 Python 和 Scala 编程语言中的一样
- 如果想尽快检测到无效的参数,可以在 builder 的构造器和方法中检查参数的有效性
- 查看不可变量,包括 build 方法调用的构造器中的多个参数
- 为了确保这些不变量免受攻击,从 builder 复制完参数之后,要检查对象域(详见第 50 条)
- 如果检查失败,就抛出
IllegalArgumentException
异常(详见第 72 条),其中的详细信息会说明哪些参数是无效的(详见第 75 条)
适用于类层次结构的 Builder 模式
含义
- 使用平行层次结构的
builder
时,各自嵌套在相应的类中 - 当类具有层次结构时,抽象类有抽象类的
builder
,具体类有具体类的builder
- 使用平行层次结构的
Demo
抽象类
Pizza
:位于类层次根部,表示各式各样的披萨1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29package effectivejava.chapter2.item2.hierarchicalbuilcer;
import java.util.*;
// Builder pattern for class hierarchies (Page 14)
// Note that the underlying "simulated self-type" idiom allows for arbitrary fluid hierarchies, not just builders
public abstract class Pizza {
public enum Topping { HAM, MUSHROOM, ONION, PEPPER, SAUSAGE }
final Set<Topping> toppings;
abstract static class Builder<T extends Builder<T>> {
EnumSet<Topping> toppings = EnumSet.noneOf(Topping.class);
public T addTopping(Topping topping) {
toppings.add(Objects.requireNonNull(topping));
return self();
}
abstract Pizza build();
// Subclasses must override this method to return "this"
protected abstract T self();
}
Pizza(Builder<?> builder) {
topping = builder.topping.clone(); // See Item 50
}
}Pizza.Builder
的类型是泛型(generic type),带有一个递归类型参数(recursive type parameter)(详见第 30 条)- 它和抽象的
self
方法一样,允许在子类中适当地进行方法链接,不需要类型转换。这个针对 Java 缺乏self
类型的解决方案,被称作模拟的self
类型(simulated self-type)
子类
NyPizza
:表示经典纽约风味的披萨,需要一个尺寸1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37package effectivejava.chapter2.item2.hierarchicalbuilder;
import java.util.Objects;
// Subclass with hierarchical builder (Page 15)
public class NyPizza extends Pizza {
public enum Size { SMALL, MEDIUM, LARGE }
private final Size size;
public static class Builder extends Pizza.Builder<Builder> {
private fianl Size size;
public Builder(Size size) {
this.size = Objects.requireNonNull(size);
}
public NyPizza build() {
return new NyPizza(this);
}
protected Builder self() {
return this;
}
}
private NyPizza(Builder builder) {
super(builder);
size = builder.size;
}
public String toString() {
return "New York Pizza with " + toppings;
}
}子类
Calzone
:表示馅料内置的半月型披萨,需要指定酱汁应该内置还是外置1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35package effectivejave.chapter2.item2.hierarchicalbuilder;
// Subclass with hierarchical builder (Page 15)
public class Calzone extends Pizza {
private final boolean sauceInside;
public static class Builder extends Pizza.Builder<Builder> {
private boolean sauceInside = false; // Default
public Builder sauceInside() {
sauceInside = true;
return this;
}
public Calzone build() {
return new Calzone(this);
}
protected Builder self() {
return this;
}
}
private Calzone(Builder builder) {
super(builder);
sauceInside = builder.sauceInside;
}
public String toString() {
return String.format("Calzone with %s and sauce on the %s", toppings, sauceInside ? "inside" : "outside");
}
}测试类
PizzaTest
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15package effectivejava.chapter2.item2.hierarchicalbuilder;
import static effectivejava.chapter2.item2.hierarchicalbuilder.Pizza.Topping.*;
import static effectivejava.chapter2.item2.hierarchicalbuilder.NyPizza.Size.*;
// Using the hierarchical builder (Page 16)
public class PizzaTest {
public static void main(String[] args) {
NyPizza pizza = new NyPizza.Builder(SMALL).addTopping(SAUSAGE).addTopping(ONION).build();
Calzone calzoce = new Calzone.Builder().addTopping(HAM).sauceInside().build();
System.out.println(pizza);
System.out.println(calzone);
}
}
特点
- 子类方法声明返回父类中声明的返回类型的子类型,这被称作协变返回类型(covariant return type),它允许客户端无需类型转换就能使用这些构造器
- 与构造器相比,builder 的略微优势在于,它可以有多个可变(varargs)参数。因为 builder 是利用单独的方法来设置每一个参数
- 此外,构造器还可以将多此调用某一个方法传入的参数集中到一个域中,例如上面代码中调用了两次
addTopping
方法 - Builder 模式十分灵活,可以利用单个 builder 构建多个对象。builder 的参数可以在调用 build 方法创建对象期间进行调整,也可以随着不同的对象而改变。builder 可以自动填充某些域,例如每次创建对象时自动增加序列号
- Builder 模式的确也有它自身的不足。为了创建对象,必须先创建它的构建器。虽然创建构建器的开销在实践中可能不那么明显,但在某些十分注重性能的情况下,可能就成问题了
- Builder 模式还比重叠构造器更加冗长,因此它只有在有很多参数的时候才使用,比如 4 个或者更多个参数。但是考虑到扩展性,通常最好一开始就使用构建器
- 如果类的构造器或者静态工厂中具有多个参数,设计这种类时,Builder 模式就是一种不错的选择,特别是当大多数参数都是可选或者类型相同的时候。
- 与使用重叠构造器相比,使用 Builder 模式的客户端代码将更易于阅读和编写,构建器也比 JavaBeans 更加安全
第 3 条:用私有构造器或者枚举类型强化 Singleton 属性
概念
- Singleton 是指仅仅被实例化一次的类。Singleton 通常被认为用来代表一个无状态的对象,如函数(详见第 24 条),或者那些本质上唯一的系统组件
- 使类成为 Singleton 会使它的客户端测试变得十分困难,因为不可能给 Singleton 替换模拟实现,除非实现一个充当其类型的接口
方法一:公有静态成员是个
final
域Demo
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18package effectivejava.chapter2.item3.field;
// Singleton with public final field (Page 15)
public class Elvis {
public static final Elvis INSTANCE = new Elvis();
private Elvis() { }
public void leaveTheBuilding() {
System.out.println("Whoa baby, I'm outta here!");
}
// This code would normally appear outside the class!
public static void main(String[] args) {
Elvis elvis = Elvis.INSTANCE;
elvis.leaveTheBuilding();
}
}- 由于缺少公有的或者受保护的构造器,所以保证了
Elvis
的全局唯一性:一旦Elvis
类被实例化,将只会存在一个Elvis
实例,不多也不少 - 享有特权的客户端可以借助
AccessibleObject.setAccessible
方法,通过反射机制(详见第 65 条)调用私有构造器。如果需要抵御这种攻击,可以修改构造器,让它在被要求创建第二个实例的时候抛出异常
- 由于缺少公有的或者受保护的构造器,所以保证了
优点
- API 很清楚地表明了这个类是一个 Singleton:公有的静态域是
final
的,所以该域总是包含相同的对象引用 - 更简单,优先考虑
- API 很清楚地表明了这个类是一个 Singleton:公有的静态域是
方法二:公有的成员是个静态工厂方法
Demo
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22package effectivejava.chapter2.item3.staticfactory;
// Singleton with static factory (Page 17)
public class Elvis {
private static final Elvis INSTANCE = new Elvis();
private Elvis() { }
public static Elvis getInstance() {
return INSTANCE;
}
public void leaveTheBuilding() {
System.out.println("Whoa baby, I'm outta here!");
}
// This code would normally appear outside the class!
public static void main(String[] args) {
Elvis elvis = Elvis.getInstance();
elvis.leaveTheBuilding();
}
}优点
- 它提供了灵活性:在不改变其 API 的前提下,我们可以改变该类是否应该为 Singleton 的想法。工厂方法返回该类的唯一实例,但是,它很容易被修改,比如改成为每个调用该方法的线程返回一个唯一的实例
- 如果应用程序需要,可以编写一个 泛型 Singleton 工厂(generic singleton factory)(详见第 30 条)
- 可以通过方法引用(method reference)作为提供者,比如
Elvis::instance
就是一个Supplier<Elvis>
注意
将方法一和方法二实现的 Singleton 类变成是可序列化的(Serializable)(详见第 12 章),仅仅在声明中加上
implements Serializable
是不够的为了维护并保证 Singleton,必须声明所有实例域都是
transient
的,并提供一个readResolve
方法(详见第 89 条)。否则,每次反序列化一个序列化的实例时,都会创建一个新的实例1
2
3
4
5
6// readResolve method to preserve singleton property
private Object readResolve() {
// Return the one true Elvis and let the garbage collector
// take care of the Elvis impersonator
return INSTANCE;
}
方法三:声明一个包含单个元素的枚举类型
Demo
1
2
3
4
5
6// Enum singleton - the preferred approach
public enum Elvis {
INSTANCE;
public void leaveTheBuilding() { ... }
}特点
- 枚举单例方式在功能上与公有域相似,但更加简洁,无偿地提供了序列化机制,绝对防止多次实例化,即使是在面对复杂的序列化或者反射攻击的时候
- 虽然这种方法还没有广泛采用,但是单元素的枚举类型经常成为实现 Singleton 的最佳方法
- 如果 Singleton 必须扩展一个超类,而不是扩展
Enum
的时候,则不宜使用这个方法(虽然可以声明枚举去实现接口)
第 4 条:通过私有构造器强化不可实例化的能力
概要
- 通常,工具类(utility class)不希望被实例化,因为实例化对它没有任何意义
- 在缺少显示构造器的情况下,编译器会自动提供一个共有的、无参的缺省构造器(default constructor)
- 企图通过将类做成抽象类来强制该类不可被实例化时行不通的
- 由于只有当类不包含显式的构造器时,编译器才会生成缺省的构造器,因此只要让这个类包含一个私有构造器,它就不能被实例化
Demo
1
2
3
4
5
6
7
8
9
10
11package effectivejava.chapter2.item4;
// Noninstantiable utility class
public class UtilityClass {
// Suppress default constructor for noninstantiability
private UtilityClass() {
throw new AssertionError();
}
// Remainder omitted
}AssertionError
不是必需的,但是它可以避免不小心在类的内部调用构造器- 这种习惯用法也有副作用,它使得一个类不能被子类化
第 5 条:优先考虑依赖注入来引用资源
背景
有许多类会依赖一个或多个底层的资源。例如,拼写检查器一般需要依赖多个词典
静态工具类和 Singleton 类不适合于需要引用底层资源的类
1
2
3
4
5
6
7// Inappropriate use of static utility - inflexible & untestable!
public class SpellChecker {
private static final Lexicon dictionary = ...;
private SpellChecker() {} // Noninstantiable
public static boolean isValid(String word) { ... }
public static List<String> suggestions(String typo) { ... }
}1
2
3
4
5
6
7
8// Inappropriate use of singleton - inflexible & untestable!
public class SpellChecker {
private final Lexicon dictionary = ...;
private SpellChecker(...) {}
public static INSTANCE = new SpellChecker(...);
public boolean isValid(String word) { ... }
public List<String> suggestions(String typo) { ... }
}
依赖注入
场景:能够支持类的多个实例(在本例中是指
SpellChecker
),每一个实例都能使用客户端指定的资源(在本例中是指词典)最简单的模式:当创建一个新的实例时,就将该资源传到构造器中。这是依赖注入(dependency injection)的一种形式:词典(dictionary)是拼写检查器的一个依赖(dependency),在创建拼写检查器时就将词典注入(injected)其中
1
2
3
4
5
6
7
8
9
10
11// Dependency injection provides flexibility and testability
public class SpellChecker {
private final Lexicon dictionary;
public SpellChecker(Lexicon dictionary) {
this.dictionary = Objects.requireNonNull(dictionary);
}
public boolean isValid(String word) { ... }
public List<String> suggestions(String typo) { ... }
}优点
- 虽然这个拼写检查器的范例中只有一个资源(词典),但是依赖注入却适用于任意数量的资源,以及任意的依赖形式
- 依赖注入的对象资源具有不可变性(详见第 17 条),因此多个客户端可以共享依赖对象(假设客户端们想要的是同一个底层资源)
- 依赖注入也同样适用于构造器、静态工厂(详见第 1 条)和构建器(详见第 2 条)
变体
依赖注入模式的另一种有用的变体是:将资源工厂(factory)传给构造器。工厂是可以被重复调用来创建类型实例的一个对象。这类工厂具体表现为工厂方法(Factory Method)模式
在 Java 8 中增加的接口
Supplier<T>
,最适合用于表示工厂。带有Supplier<T>
的方法,通常应该限制输入工厂的类型参数使用有限通配符类型(bounded wildcard type)(详见第 31 条),以便客户端能够传入一个工厂,来创建指定类型的任意子类型1
2// 这是一个生产马赛克的方法,它利用客户端提供的工厂来生产每一片马赛克
Mosaic create(Supplier<? extends Tile> tileFactory) { ... }
总结
- 不要用 Singleton 或静态工具类来实现依赖一个或多个底层资源的类,且该资源的行为会影响到该类的行为。也不要直接用这个类来创建这些资源
- 应该将这些资源或者工厂传给构造器(或者静态工厂,或者构建器),通过它们来创建类。这个实践被称作依赖注入,它极大地提升了类的灵活性、可重用性和可测试性
- 虽然依赖注入极大地提升了灵活性和可测试性,但它会导致大型项目凌乱不堪,因为它通常包含上千个依赖。一般通过依赖注入框架(dependency injection framework)可以解决这个问题,比如 Dagger[Dagger]、Guice[Guice] 或者 Spring[Spring]
第 6 条:避免创建不必要的对象
一般来说,最好能重用单个对象,而不是在每次需要的时候就创建一个功能相同的新对象。重用方式既快速,又流行。如果对象是不可变的(immutable)(详见第 17 条),它就始终可以被重用
极端的反面例子:
String s = new String("bikini");
- 该语句每次被执行的时候都创建一个新的
String
实例,但是这些创建对象的动作全都是不必要的 - 传递给
String
构造器的参数"bikini"
本身就是一个String
实例,功能方面等同于构造器创建的所有对象 - 如果这种用法是在一个循环中,或者是在一个被频繁调用的方法中,就会创建出成千上万不必要的
String
实例
- 该语句每次被执行的时候都创建一个新的
改进后的版本:
String s = "bikini";
- 这个版本只用了一个
String
实例,而不是每次执行的时候都创建一个新的实例 - 同时可以保证,对于所有在同一台虚拟机中运行的代码,只要它们包含相同的字符串字面常量,该对象就会被重用
- 这个版本只用了一个
对于同时提供了静态工厂方法(static factory method)(详见第 1 条)和构造器的不可变类,通常优先使用静态工厂方法而不是构造器,以避免创建不必要的对象
- 静态工厂方法
Boolean.valueOf(String)
几乎总是优先于构造器Boolean(String)
,注意构造器Boolean(String)
在 Java 9 中已经被废弃了 - 构造器在每次被调用的时候都会创建一个新的对象,而静态工厂方法则从来不要求在这样做,实际上也不会这么做
- 除了重用不可变的对象外,也可以重用那些已知不会被修改的可变对象
- 静态工厂方法
有些对象创建的成本比其他对象高很多,如果重复地需要这类“昂贵的对象”,最好是缓存下来重用
判断一个字符串是否为一个有效的罗马数字
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37package effectivejava.chapter2.item6;
import java.util.regex.Pattern;
// Reusing expensive object for improved performance
public class RomanNumerals {
// Performance can be greatly improved!
static boolean isRomanNumeralSlow(String s) {
return s.matches("^(?=.)M*(C[MD]|D?C{0,3})(X[CL]|L?X{0,3})(I[XV]|V?I{0,3})$");
}
// Reusing expensive object for improved performance
private static final Pattern ROMAN = Pattern.compile("^(?=.)M*(C[MD]|D?C{0,3})(X[CL]|L?X{0,3})(I[XV]|V?I{0,3})$");
static boolean isRomanNumeralFast(String s) {
return ROMAN.matcher(s).matches();
}
public static void main(String[] args) {
int numSets = Integer.parseInt(args[0]);
int numReps = Integer.parseInt(args[1]);
boolean b = false;
for(int i = 0; i < numSets; i++) {
long start = System.nanoTime();
for(int j = 0; j < numReps; j++) {
b ^= isRomanNumeralSlow("MCMLXXVI"); // Change Slow to Fast to see performance difference
}
long end = System.nanotime();
System.out.println((end - start) / (1_000. * numReps)) + "μs.");
}
// Prevents VM from optimizing away everything.
if(!b) {
System.out.println();
}
}
}SLOW 方法实现的问题在于它依赖
String.matches
方法。虽然 String.matches 方法最易于查看一个字符串是否与正则表达式相匹配,但并不适合在注重性能的情形中重复使用- 问题在于,它在内部为正则表达式创建了一个
Pattern
实例,却只用了一次,之后就可以进行垃圾回收了 - 创建
Pattern
实例的成本很高,因为需要将正则表达式编译成一个有限状态机(finite state machine)
- 问题在于,它在内部为正则表达式创建了一个
为了提升性能,应该显示地将正则表达式编译成一个
Pattern
实例(不可变),让它成为类初始化的一部分,并将它缓存起来,每当调用isRomanNumeral
方法的时候就重用同一个实例- 改进后的
isRomanNumeral
方法如果被频繁调用,会显示出明显的性能优势 - 除了提高性能之外,代码也更清晰了,将不可见的
Pattern
实例做成final
静态域时,可以给它起个名字,这样会比正则表达式本身更有可读性
- 改进后的
如果包含改进后的
isRomanNumeral
方法的类被初始化了,但是该方法没有被调用,那就没必要初始化ROMAN
域- 通过在
isRomanNumeral
方法第一次被调用的时候延迟初始化(lazily initializing)(详见第 83 条)这个域,有可能消除这个不必要的初始化工作,但是不建议这样做 - 正如延迟初始化中常见的情况一样,这样做会使方法的实现更加复杂,从而无法将性能显著提高到超过已经达到的水平(详见第 67 条)
- 通过在
如果一个对象是不变的,那么它显然能够被安全地重用,但其他有些情形则并不总是这么明显。考虑适配器(adapter)的情形,有时也叫作视图(view)
- 适配器是指这样一个对象:它把功能委托给一个后备对象(backing object),从而为后备对象提供一个可以替代的接口
- 由于适配器除了后备对象之外,没有其他的状态信息,所以针对某个给定对象的特定适配器而言,它不需要创建多个适配器实例
- 例如,
Map
接口的keySet
方法返回该Map
对象的Set
视图,其中包含该Map
中所有的键(key)。对于一个给定的Map
对象,实际上每次调用keySet
都返回同样的Set
实例
自动装箱(autoboxing)