Effective-Java-Learning-Chapter2

Effective Java Chapter2 : Creating and Destroying Objects


使用静态工厂方法代替构造器

在传统方法中,我们使用类中的构造方法来新建一个对象。但是除此之外,我们还能用别的方法来新建对象,就是使用静态工厂。每个类都能提供public静态工厂方法来返回一个对象,就像下面的一个Boolean类的例子一样。

1
2
3
public static Boolean valueOf(boolean b) {
return b ? Boolean.TRUE : Boolean.FALSE;
}

使用静态工厂方法既有优点,又有缺点

优点

1. 静态工厂方法可以命名

一个类的构造方法只能使用类名,这样会让用户感到疑惑,不清楚他们构造的对象具体细节是什么。例如,我使用一个会返回一个很大可能是素数的构造函数BigInteger(int,int,Random),很明显,不看描述文档很难猜出这个构造函数使用这些参数生成的对象是什么。但是如果我使用静态工厂方法BigInteger.probablePrime就没有这样的问题了

2. 静态工厂方法不一定每次都要新建一个对象

在传统构造方法中,只要用户使用这个构造方法,就一定会新建一个对象。这会生成很多不必要的重复的对象,很多时候用户不一定要一个新的对象。而静态工厂方法可以解决这一问题。例如上面说的Boolean类,无论多少用户使用这个方法去获得一个Boolean对象,实际上都只有2个对象生成。

3. 静态工厂方法可以返回它的返回类型的子类型的对象

这一优点可以给了我们很大的灵活性,得益于此,我们可以实现一个API,这个API不用使它的类是public就可以返回对象,API因此可以隐藏它的功能得实现,可以封装得更好。一个封装号的API,用户不用关心如何实现一个类来获取对象,只需调用接口即可。

在Java8之前的版本,由于接口不能实现静态方法,往往要建一个不可实现的类来完成对象的返回。但到了Java8之后,接口也能使用静态方法了,不必再建一个不可实现的类。

4. 返回的对象的类可以根据参数的不同而变化

我们可以声明对象的任何子类型,不比再被构造方法束缚。例如,在OpenJDK中的EnumSet类里,如果底层枚举类型不多于64个,就会返回一个RegularEnumSet实例,如果多于64个,就会返回JumboEnumSet。用户并不知道也不关心他获得的实例到底是哪个,只用知道是EnumSet类的子类型。

这种方法还有一个好处是,当我们不想用RegularEnumSet实例的时候,我们只需要改这个静态工厂方法。相反,如果用户使用new RegularEnumSet()这个构造方法来获得RegularEnumSet实例的话,我们就要把所有使用了这一实例的地方都改了。

5. 在编写包含该方法的类时,返回的对象的类不需要存在

我没看懂这句话是什么鬼意思。

后面解释的例子是一个service provider框架,大致意思是里面有3种接口,一个服务接口,表示实现;一个提供者注册API,用于注册实现,还有一个服务访问API,用于为客户端提供服务的实例。服务访问API允许用户指定选择实现的标准。在缺少这样的标准的情况下,API返回一个默认实现的实例,或者允许用户通过所有可用的实现进行遍历。服务访问API是灵活的静态工厂,它构成了服务提供者框架的基础。

缺点

(主要)只提供一个静态工厂方法使得没有public或protected构造方法的类无法被子类化。

这里我也不是很理解,他说在Collections框架中,你不可能将任何方便实现类子类化。这可能是因祸得福,因为它鼓励程序员使用组合而不是继承,并且是不可变类型。

静态工厂方法很难被程序员找到

如果你不仔细阅读这个类或这个接口的说明,你真不知道它的静态工厂方法起了什么名字。相反,你只要知道这个类的类名,你就知道这个类的构造方法。在API文档中,它也不像构造方法那么突出。只能希望大家多去关注API文档中的静态工厂方法和遵守统一的命名约定。例如下面的一些静态工厂方法常用名称。

  • from——类型转换方法,它接受单个参数并返回此类型的相应实例,例如:Date d = Date.from(instant);
  • of——一个聚合方法,接受多个参数并返回该类型的实例,并把他们合并在一起,例如:Set<Rank> faceCards = EnumSet.of(JACK, QUEEN, KING);
  • valueOf——from和to更为详细的替代 方式,例如:BigInteger prime = BigInteger.valueOf(Integer.MAX_VALUE);
  • instance或getinstance——返回一个由其参数(如果有的话)描述的实例,但不能说它具有相同的值,例如:StackWalker luke = StackWalker.getInstance(options);
  • create 或 newInstance——与instance 或 getInstance类似,另外,该方法保证每个调用返回一个新的实例,例如:Object newArray = Array.newInstance(classObject, arrayLen);
  • getType——与getInstance类似,但是如果在工厂方法在不同的类中,则使用它。Type是工厂方法返回的对象类型,例如:FileStore fs = Files.getFileStore(path);
  • newType——与newInstance类似,但是如果在工厂方法在不同的类中,则使用它。Type是工厂方法返回的对象类型,例如:BufferedReader br = Files.newBufferedReader(path);
  • type—— getType 和 newType简洁的替代方式,例如:List<Complaint> litany = Collections.list(legacyLitany);

总结

静态工厂方法和public构造方法各有长短,要了解它们各自的长处再做出选择。通常情况下静态工厂方法更好,因此不要习惯性使用public构造方法。

当构造器有很多参数时使用builder来代替

前景

设置多种构造方法

当我们试图使用构造方法新建一个类的时候,如果发现这个构造器有很多参数要设置,这些参数之间既有必要的也有不必要的。那么我们就要设置多种重载的构造方法,当这些参数的类型相同的时候,我们难以知道我们设置的是什么参数,而这些参数又应该按哪种顺序排列,这会使得我们的函数可读性和可用性都很差。就像下面的例子:

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
65
66
67
68
69
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;

​ }

}

当你使用这个构造函数新建一个实例的时候

1
2
3
NutritionFacts cocaCola =

new NutritionFacts(240, 8, 100, 0, 35, 27);

只看这一步根本不知道这些参数对应的是什么东西。

先构造再使用setter设置参数

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
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; }

}

这样看起来整齐简洁多了,那么当实例化这个类的时候呢?

1
2
3
4
5
6
7
8
9
10
11
NutritionFacts cocaCola = new NutritionFacts();

cocaCola.setServingSize(240);

cocaCola.setServings(8);

cocaCola.setCalories(100);

cocaCola.setSodium(35);

cocaCola.setCarbohydrate(27);

到了用的时候就不太方便了,而且如果引入多线程呢?这个实例可能在setter还没设置完参数的时候就被使用,那么被使用的时候参数就不是正确的,虽然我们可以使用freeze来冻结这个实例不被使用,但是这种方法并不好,实例在构造过程中参数一直在改变本就是一种危险的操作。

使用builder来优化

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
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
// Builder Pattern

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;

}

}

使用builder来代替构造器和setter之后代码量多了很多,但这是因为参数还不够多,但参数多时我们要设置更多的构造方法,而且现在使用时更加方便。如下:

1
2
3
NutritionFacts cocaCola = new NutritionFacts.Builder(240, 8)

​ .calories(100).sodium(35).carbohydrate(27).build();

出来一些必要参数要在Builder()里设置外,可选参数都能使用参数名()来设置,那么如果是在多线程环境中被其他线程使用呢?也无需担心,在build()之前并没有被实例化,只是在Builder中保存一系列的参数,在所有参数设置完后再build()这个实例才被创建。

总结

使用builder来构造实例虽然会使得这个类的代码变得复杂一点点,但是当这个类被builder来构造时可读性更佳,也不会有多线程问题。