常用的创建对象方式及存在问题

本文最后更新于:2022年6月29日 上午

使用构造方法

成员变量很多的时候,构造方法就没方便了.
举例:NutritionFacts是食品包装外面显示的营养成分标签,这里面有的营养成分是必须的:每一份的含量、每一罐的含量,其他的可选

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
public class NutritionFacts {
private final int servingSize; // (mL) required
private final int servings; // (per container) required
private final int calories; // optional
private final int fat; // (g) optional
private final int sodium; // (mg) optional
private final int carbohydrate; // (g) 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;
}
}



从上面的代码可见,为了尽量满足用户需要,NutritionFacts提供了多个构造方法给用户使用,以后要是加字段就麻烦了

缓解上述问题的一种方法是使用JavaBeans模式,用无参构造方法,然后按照调用setXXX设置每个所需字段,示例如下所示

1
2
3
4
5
6
7
NutritionFacts cocaCola = new NutritionFacts();
cocaCola.setServingSize(240);
cocaCola.setServings(8);
cocaCola.setCalories(100);
cocaCola.setSodium(35);
cocaCola.setCarbohydrate(27);

存在问题

  1. 首先,直观的看,这种做法违背了不可变对象的定义,创建出对象后,又用setXXX方法改变了成员变量
  2. 《Effective Java》的原话是在构造过程中JavaBean可能处于不一致的中的状态

使用静态工厂方法

相比静态工厂方法,构造方法存在以下五个典型问题

  1. 随着入参的不同,构造方法可以有多个,如下所示,然而都是同名的,这会给用户造成困惑,此刻用静态工厂方法,可以自由设置方法名(例如createWithName或者createWithAge),让用户更方便的选择合适的方法
  2. 使用构造方法意味着创建对象,而有时候我们只想使用,并不在乎对象本身是否是新建的
  3. 以动物类Animal.class为例,Animal类的构造方法创建的对象Animal的实例,而静态工厂方法的返回值声明虽然是Animal,但实际返回的实例可以是Animal的子类,例如Dog
  4. 静态工厂方法内部可以有灵活的逻辑来决定返回那种子类的实例
  5. 静态工厂方法还有一个优势:方法返回对象所属的类,在编写此静态方法时可以不存在,这句话有点晦涩,可以回想一下JDBC的获取connection的API,在编写此API的时候,并不需要知道MySQL的driver实现

存在问题

  1. 当您开发一个类时,如果决定对外提供静态工厂方法,那么将构造方法设为私有,就可以让用户只能选择静态工厂方法了,代码如下所示,然而,这样的Student类就无法被继承
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    public class Student {
    private String name;

    private int age;

    public void setName(String name) {
    this.name = name;
    }

    private Student() {

    }

    public static Student newInstance(String name) {
    Student student = new Student();
    student.setName(name);

    return student;
    }
    }

  2. 一个类的代码中,可能已有一些静态方法,再加入静态工厂方法,一堆静态方法混杂在一起,用户从中找出静态工厂方法怕是不容易

使用builder

builder pattern,《Effective Java》中文版译作建造者模式,用builder对象来创建真正的对象实例,前面提到的构造方法和静态工厂的不足,在builder pattern这里都得到了改善

来看代码吧,以刚才的NutritionFacts为例,使用builder pattern后的代码如下,新增一个静态成员类Builder,可以设置Builder的每个成员变量,最后调用其build方法的时候,才真正创建NutritionFacts对象

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

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 carbohydrate = 0;
private int sodium = 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 carbohydrate(int val)
{ carbohydrate = val; return this; }
public Builder sodium(int val)
{ sodium = 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;
}
}

以一个使用者的视角来看如何创建NutritionFacts对象,如下所示,流畅的写法,那些字段被设置以及具体的值都一目了然,最终build方法才会创建NutritionFacts对象,而且这是个不可变对象

1
2
3
4
5
6
7

NutritionFacts cocaCola = new NutritionFacts.Builder(240, 8)
.calories(100)
.sodium(35)
.carbohydrate(27)
.build();

存在问题

  1. 即便能解决构造方法和静态工厂自身的一些问题,builder pattern也不是万能的,缺点很明显:创建对象之前,先要创建builder对象,这在一些性能要求高、资源限制苛刻的场景中就不适合了
  2. 另外builder pattern适合的场景是成员变量多的时候,而这个所谓的多究竟如何理解呢?这可能是个小马过河的问题吧:见惯了几十个成员变量的类,再去看十几个成员变量的类,可能会有种很清爽的感觉,呃,扯远了,其实《Effective Java》的说法是四个或者更多个参数,就适合用builder apttern了

reference


常用的创建对象方式及存在问题
https://baymax55.github.io/2022/09/13/java/常用的创建对象方式及存在问题/
作者
baymax55
发布于
2022年9月13日
许可协议