Effective Java 第二版

Effective Java 第二版

0825

引言

  • java语言支持四种类型:
    • 接口【interface】
    • 类【class】
    • 数组【array】
    • 基本类型【primitive】

前三种类型成为引用类型,类实例和数组是对象,而基本类型的值不是对象。

  • 类的成员组成:
    • 域【field】
    • 方法【method】
    • 成员类【member class】
    • 成员接口【member interface】

方法签名【signature】由它的名称所有参数类型组成;签名不包括它的返回类型

第二章 创建和销毁对象

第1条 考虑使用静态工厂方法替代构造器

  • 获得对象实例常用的两种方式:
    • 公开的构造器
    • 公开的静态工厂方法
1
2
3
public static Boolean valueOf(boolean b){
return b ? Boolean.TRUE : Boolean.FALSE;
}

类可以通过静态工厂方法提供它的客户端,而不是通过构造器。

  • 提供静态工厂方法而不是公有的构造器,这样做有几大优势
    1. 它们有名称
    2. 不必在每次调用它们的时候都创建一个新的对象
    3. 它们可以返回原返回类型的任何子类型的对象
    4. 在创建参数化类型实例的时候,它们使代码变得更加简洁
  • 静态工厂方法的主要缺点
    1. 类如果不含有公开的或者受保护的构造器,就不能被子类化
    2. 它们与其他的静态方法实际上没有任何区别

逐一解释:

  • 静态工厂方法与构造器不同的第一大优势在于,他们有名称
    • 如果构造器本身没有明确的描述正被返回的对象,那么具有适当名称的静态工厂会更容易使用,代码更容易阅读
    • 一个类只能够有一个带有制定签名的构造器
    • 由于静态工厂有名称,所以描述更为清楚
    • 当一个类需要多个带有相同签名的构造器时,就用静态工厂方法替代构造器,并慎重的选择名称以便突出它们之间的区别
  • 静态工厂方法与构造器不同的第二大优势在于,不必在每次调用他们的时候都创建一个新的对象
    • 这使得不可变类可以使用预先构建好的实例,或者将构建好的实例缓存起来,进行重复利用,从而避免创建不必要的重复对象。
    • 如果程序经常请求创建对象,并且创建对象的代价很高,则这项技术可以极大地提升性能
    • 静态方法能够为重复的调用返回相同的对象,这样有助于类总能严格的控制在某个时刻哪些实例应该存在,这种类被称作实例受控的类
    • 设立受控的类可以确保它是 Singleton 或者是不可实例化的
    • 它还使得不可变的类可以确保不会存在两个相等的实例
      • 即当且仅当 a==b 的时候才有 a.equals(b)true
      • 如果类保证了这一点就可以使用 == 操作符来替代 equals (Object) 方法,这样就可以提升性能
    • 枚举类型保证了这一点
  • 静态工厂方法与构造器不同的第三大优势在于,它们可以返回原返回类型的任何子类型的对象
    • 静态工厂方法返回的对象所属的类,在编写包含该静态工厂方法的类时可以不必存在,这种领过的静态工厂方法构成了服务提供者框架的基础
      • 服务提供者框架是指一个系统,多个服务提供者实现一个服务,系统为服务提供者的客户端提供多个市县,并把它们从多个实现中解耦出来
      • 服务提供者框架中有三个重要的组件
        • 服务接口:这是提供者实现的
        • 提供者注册API:这是系统用来注册实现
        • 服务访问API:客户端用来获取服务实例,是“灵活的静态工厂”
        • 服务提供者API:提供者负责创建其服务实现的实例
  • 静态工厂方法与构造器不同的第四大优势在于,在创建参数化类型实例的时候,它们使代码变得更加简洁
1
2
3
4
Map<String,List<String>> map = new HashMap<String,List<String>> ()
// 1.7 以后, 添加了 类型推导
Map<String, List<String>> map = new HashMap<>();
  • 我们使用静态工厂方法,先定义一个泛型方法
1
2
3
public static <K, V> Map<K, V> newInstance() {
return new HashMap<K, V>();
}
  • [x] 是可以使用下面这句简洁的代码代替上面这段繁琐的声明
1
Map<String,List<String>> map = HashMap.newInstance();
  • 静态工厂的第一个缺点在于,类如果不含有公开的或者受保护的构造器,就不能被子类化
    • 要想将Collections Framework 中的任何方法的实现类子类化,这是不可能的。
    • 它鼓励程序员使用复合【composition】,而不是继承
  • 静态工厂的第二个缺点在于,他们与其他的静态方法没有任何区别
    • 静态工厂的一些惯用名称如下:
名称 说明
valueOf 不严格的讲,该方法返回的实例与它的参数具有相同的值。这中方法实际上就是类型转换方法
of valueOf的一种更为简洁的替代,在EnumSet中使用并流行起来
getInstance 返回的实例是通过方法的参数来描述的,但是不能够说与参数具有同样的值
newInstance 像getInstance一样单newInstance能够确保能够返回的每个实例都与其所有其他实例不同
getType 像getInstatnce一样,但是在工厂方法处于不同的类中的时候使用。Type表示工厂方法返回的对象类型
newType 想newInstance一样,但是在工厂方法处于不同的类中的时候使用。Type表示工厂方法所返回的对象类型

静态工厂方法和公有构造器都各有用处,静态工厂通常更加合适,因此切忌第一反应就是提供共有的构造器,而不先考虑静态工厂

第2条 遇到多个构造器参数时要考虑用构建器

静态工厂和构造器都有个共同的局限性:它们都不能很好的扩展到大量的可选参数

  • [x] 使用重载构造器模式构建实例
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
/**
* @author by benny on 2016/9/17.
* @version 1.0
* @description 通过重载构造器模式
*/
public class ConstrutorBuilder {
private final int servingSize;
private final int calories;
private final int fat;
private final int sodium;
public ConstrutorBuilder(int servingSize, int calories) {
this(servingSize, calories, 0, 0);
}
public ConstrutorBuilder(int servingSize, int calories, int fat) {
this(servingSize, calories, fat, 0);
}
public ConstrutorBuilder(int servingSize, int calories, int fat, int sodium) {
this.servingSize = servingSize;
this.calories = calories;
this.fat = fat;
this.sodium = sodium;
}
public static void main(String[] args) {
// 虽然值传递了两个值 仍调用了所有参数的构造器
ConstrutorBuilder testBuilder = new ConstrutorBuilder(240, 8);
ConstrutorBuilder testBuilder1 = new ConstrutorBuilder(240, 8, 0, 35);
}
}
  • [x] 使用JavaBean模式构建实例
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
/**
* @author by benny on 2016/9/17.
* @version 1.0
* @description 使用JavaBean模式
*/
public class JavaBeanBuilder {
private int servingSize = -1;
private int calories = 0;
private int fat = 0;
private int sodium = 0;
public JavaBeanBuilder() { }
public void setServingSize(int servingSize) {
this.servingSize = servingSize;
}
public void setCalories(int calories) {
this.calories = calories;
}
public void setFat(int fat) {
this.fat = fat;
}
public void setSodium(int sodium) {
this.sodium = sodium;
}
public static void main(String[] args) {
JavaBeanBuilder builder = new JavaBeanBuilder();
builder.setServingSize(240);
builder.setCalories(35);
builder.setFat(100);
builder.setSodium(0);
}
}

使用构建器模式:

  • [x] 不使用抽象工厂构建实例
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
/**
* @author by benny on 2016/9/17.
* @version 1.0
* @description 构建器模式
*/
public class BuildBuilder {
private final int servingSize;
private final int calories;
private final int fat;
private final int sodium;
public static class Builder {
// 必选参数 必须设置
private final int servingSize;
private final int calories;
// 可选参数 可以设置也不可不设置
private int fat;
private int sodium;
public Builder(int servingSize, int calories) {
this.servingSize = servingSize;
this.calories = calories;
}
public Builder fat(int fat) {
this.fat = fat;
return this;
}
public Builder sodium(int sodium) {
this.sodium = sodium;
return this;
}
public BuildBuilder build() {
return new BuildBuilder(this);
}
}
public BuildBuilder(Builder builder) {
this.servingSize = builder.servingSize;
this.calories = builder.calories;
this.fat = builder.fat;
this.sodium = builder.sodium;
}
public static void main(String[] args) {
BuildBuilder build = new BuildBuilder
.Builder(240, 19)
.sodium(30)
.fat(100)
.build();
}
}
  • [x] 使用抽象工厂
  1. 定义抽象工厂
1
2
3
4
5
6
7
8
/**
* @author by benny on 2016/9/17.
* @version 1.0
* @description 抽象工厂
*/
public interface BuilderFactory<T> {
T build();
}
  1. 使用构建器模式构建实例
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
/**
* @author by benny on 2016/9/17.
* @version 1.0
* @description 构建器模式
*/
public class BuildBuilder {
private final int servingSize;
private final int calories;
private final int fat;
private final int sodium;
public static class Builder implements BuilderFactory<BuildBuilder> {
// 必选参数 必须设置
private final int servingSize;
private final int calories;
// 可选参数 可以设置也不可不设置
private int fat;
private int sodium;
public Builder(int servingSize, int calories) {
this.servingSize = servingSize;
this.calories = calories;
}
public Builder fat(int fat) {
this.fat = fat;
return this;
}
public Builder sodium(int sodium) {
this.sodium = sodium;
return this;
}
@Override
public BuildBuilder build() {
return new BuildBuilder(this);
}
}
public BuildBuilder(Builder builder) {
this.servingSize = builder.servingSize;
this.calories = builder.calories;
this.fat = builder.fat;
this.sodium = builder.sodium;
}
public static void main(String[] args) {
BuildBuilder build = new BuildBuilder.Builder(240, 19)
.sodium(30)
.fat(100)
.build();
}
}

总结:

  • 重载构造器模式可行,但是当有许多参数的时候,客户端代码会很难编写,并且仍然较难以阅读。
  • 构造器中参数如果很多的话,一长串类型相同的参数会导致一些微妙的错误,如果客户端不小心颠倒了其中两个参数的顺序,编译器也不会报错。

  • JavaBean 模式弥补了重载构造器模式的不足,这种模式创建实例很容易,代码也很易读
  • 在这种模式下,调用一个无参的构造器来创建对象,然后调用setter()方法来这是每个必要的参数,以及每个相关的可选参数。
  • 遗憾的是,JavaBean模式自身有着很严重的缺点,因构造过程被分到几个调用中,在调用过程中,JavaBean可能处于不一致的状态,类无法仅通过构造器参数的有效性来保证一致性。

  • Builder模式,技能保证凑在构造器模式那样的安全性,也能保证像JavaBeans模式那么好的可读性。
  • Builder模式,不直接生成想要的对象,而是利用所有必要参数的构造器(或者静态工厂),得到一个builder对象,然后让客户端在builder对象上调用类似于setter的方法,来设置每个相关的可选参数。最后调用无参的build()方法来生成不可变的对象,这个builder是它构建的类的静态成员类
  • Builder模式也有自身的不足,为了常见对象,必须先创建它的构建器。
  • Builder模式还比重载构造器模式更加冗长,因为它只有在很多参数的时候才使用(比如四个或4个以上的参数),但是记住,将来你可能需要添加参数。
  • 如果一开始就使用构造器或者静态工厂,等到类需要多个参数时才添加构建器,就会无法控制,那些过时的构造器或者静态工厂十分不协调,因此,通常最好一开始就使用构建器模式

简而言之:
如果类的构造器或者静态工厂中具有多个参数,设计这种类的时候,Builder模式就是不错的选择,特别是当大多数参数都是可选的时候,与使用传统的重载构造器模式相比,使用Builder模式的客户端代码将更易于阅读和编写,构建器也比JavaBeans更加安全。

第3条 用私有构造器或者枚举类型强化 SIngleton 属性

  • Singleton 指仅仅被实例化一次的类
  • Singleton 通常被用来代表大写本质上唯一的系统组件,比如文件系统,因为无法给 Singleton 替换模拟实现,除非它实现一个充当起类型的接口

实现 Singleton 有两种方法,这两种方法都要把构造器保持为私有的,并导出公有的静态变量,一边允许客户端能够访问该类的唯一实例

  • [x] 第一种方法,共有静态变量是个final域
1
2
3
4
5
6
7
8
9
10
11
12
13
public class Singleton{
// 私有静态final域
public static final Singleton INSTANCE = new Singleton();
/*
* 私有构造器
*/
private Singleton(){ }
public void doSomething(){
...
}
}

私有构造器仅被调用一次,用来实例化共有的静态 final 域Singleton.INSTANCE

  • [x] 第二种方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class Singleton{
// 私有静态final域
private static final Singleton INSTANCE = new Singleton();
/*
* 私有构造器
*/
private Singleton(){ }
/*
* 公有静态工厂方法
*/
public Singleton getInstance(){
return INSTANCE;
}
public void doSomething(){ ... }
}
  • 对于Singleton.getInstance()的所有调用,都会返回一个对象引用
  • 静态工厂方法的优势在于,提供了灵活性
    • 在不改变其API的前提下,我们可以改变该类是否为Singleton的想法

为了利用这其中一种方法实现的 Singleton 类是可序列化的(Serializable)仅在声明中加上implements Serializable 是不够的。
为了维护并保证 Singleton,必须声明所有实例域都是瞬时的(transient),并提供一个readResole()方法

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
public class Singleton{
// 私有静态final域
private static final Singleton INSTANCE = new Singleton();
/*
* 私有构造器
*/
private Singleton(){ }
/*
* 公有静态工厂方法
*/
public Singleton getInstance(){
return INSTANCE;
}
public void doSomething(){
//...
}
/*
* 反序列化确保只有一个实例
* 必须提供该方法,以便重新指定反序列化得到的对象.
*/
private Object readResolve() {
return INSTANCE;
}
}
  • [x] 从Java 1.5起,实现Singleton还有第三种方法,只需要编写一个包含单个元素的枚举类型
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public enum Singleton {
INSTANCE;
public void doSomething() {
System.out.println("hello wolrd");
}
public static void main(String[] args) {
Singleton singleton = Singleton.INSTANCE;
singleton.doSomething();
}
}

这种方法在功能上与公有域方法相近,但是它更加简洁,无偿地提供了序列化机制,绝对防止多次实例化,即使是在面对复杂的序列化或者反射攻击(单例模式的反射攻击详情请参考:关于单例的那点事)的时候,虽然这种方法还没有被广泛采用,但是 单元素的枚举类型已经成为Singleton的最佳方法

第4条: 通过私有构造器强化不可实例化的能力

  • 有时候你可能需要编写只包含静态方法和静态域的类
  • 这样的工具类(utility class)不希望被实例化,实例化对它没有任何意义,然而在缺少显示构造器的情况下,编译器会自动提供一个公有的,无参的缺省构造器(default constructor)

企图通过将类做成抽象类来强制该类不可被实例化,这是行不通的。

  • 虽然抽象类不能够被实例化,但是该抽象类可以被子类化,并且该子类可以被实例化
  • 并且声明为抽象类,会误导用户以为这种类是专门为了继承而设计的
  • 设置一个类不能被继承的两种方式
    • 使用 final 修饰
    • 构造器private 修饰(构造器私有)
  • [x] 由于只有当类中不包含显示的构造器时,编译器才会生成缺省的构造器,因此只要将这这个类包含有私有构造器,它就不能被实例化了
1
2
3
4
public class ParentClass {
private ParentClass() {
}
}
1
2
3
4
5
6
7
public class SonClass extends ParentClass {
private SonClass() {
super(); ★★★★★
// 此处无法编译通过 报错 there is no default constructor avialiable in ParentClass
// 因为父类显式的将构造器私有 所有无法找到默认的构造器
}
}
  • 这种类也有一副作用,它使得一个类不能被子类化。所有的构造器都必须显式的活隐式地调用超类(superclass)构造器,在这种情况下,子类就没有可访问的超类构造器可调用了

这种情况下是可以的:能够编译通过

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class TestParentConstructer {
public class Parent {
private Parent() {
System.out.println("我是父类");
}
}
public class Son extends Parent {
public Son() {
super();
System.out.println("我是子类");
}
}
public static void main(String[] args) {
Son s = new TestParentConstructer().new Son();
AbstractClass a = new AbstractClass() {
};
}
}
Contents
  1. 1. Effective Java 第二版
  2. 2. 引言
  3. 3. 第二章 创建和销毁对象
    1. 3.1. 第1条 考虑使用静态工厂方法替代构造器
    2. 3.2. 第2条 遇到多个构造器参数时要考虑用构建器
    3. 3.3. 第3条 用私有构造器或者枚举类型强化 SIngleton 属性
    4. 3.4. 第4条: 通过私有构造器强化不可实例化的能力