考虑用静态方法代替构造器

最近开始补点基础知识,买了本Effective Java,希望能慢慢啃完这本书。下面是书中的一些知识及我个人的一些看法。

对于类而言,为了让客户端获取它自身的一个实例,最常用的方法就是提供一个自身公有的构造器,还有一种方法,类可以提供一个公有的静态工厂方法,它只是一个返回类的实例的静态方法。下面是一个Boolean(基本类型boolean的包装类)的简单示例,这个方法将boolean基本类型值转换成了一个Boolean的对象引用:

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

看它的源码,发现里面定义了一个不可变的常量,也是实例化了一个对象。

1
2
3
4
5
6
7
8
9
10
11
/**
* The {@code Boolean} object corresponding to the primitive
* value {@code true}.
*/
public static final Boolean TRUE = new Boolean(true);

/**
* The {@code Boolean} object corresponding to the primitive
* value {@code false}.
*/
public static final Boolean FALSE = new Boolean(false);

那么通过静态工厂方法相比于通过构造器有什么好处呢?

一、静态工厂方法与构造器不同的第一大优势在于,他们有名称。

如果构造器的参数本身没有确切地描述被返回的对象,那么具有适当名称的静态工厂方法会更易于阅读。例如构造器 new BigInteger(3, 10, new Random()) 返回的可能为质数(又称素数,质数定义为在大于1的自然数中,除了1和它本身以外不再有其他因数)。如果用名为 BigInteger.probablePrime(3, new Random()) 的静态方法来表示显然更清楚。

一个类只能有一个带有指定签名的构造器。我们通常知道如何避免开这一个限制:可以在构造器的参数类型,参数长度,或者是交换位置来重载。但这不是一个好主意,用户永远也记不住该用哪个构造器,如果没有对应的参考文档,往往会调用错误的构造器。

由于静态工厂方法有名称,更加灵活。当一个类需要多个带有相同签名的构造器时,就可以用它来代替构造器。

二、静态工厂方法不必每次调用它们的时候都创建一个新对象。

这使得不可变类可以使用预先构建好的实例,或者将构建好的实例缓存起来,进行重复利用,从而避免创建不必要的重复对象。比如上面提到的 Boolean valueOf(boolean b) 这个方法,它从来不会重新创建一个对象。这种方法类似于Flyweight模式(享元模式)。如果程序经常请求创建相同的对象,并且创建对象的代价很高,则这项技术可以极大地提升性能。

三、静态工厂方法可以返回原返回类型的任何子类型的对象。

这样我们在选择返回对象的类时就有了更大的灵活性。这种灵活性的应用是,API可以返回对象,同时又不会使对象的类变成公有的。以这种方法隐藏实现类会使API变得非常简洁。这项技术适用于基于接口的框架,因为在这种框架中,接口为静态工厂方法提供了自然返回类型。接口不能有静态方法,因此接口Type的静态工厂方法被放在一个名为Types的不可实例化的类中。

下面举个例子说明下

首先声明一个接口:

1
2
3
public interface Ball {
void intro();
}

接着创建 BasketBall 类和 FootBall 类。

1
2
3
4
5
6
7
8
9
10
11
12
13
public class BasketBall implements Ball {
@Override
public void intro() {
System.out.println("Play basketball");
}
}

public class FootBall implements Ball {
@Override
public void intro() {
System.out.println("Play football");
}
}

再创建一个工厂类,利用反射机制实现。

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
public class BallFactory {
/**
* 第一种方法
* @param type
* @return
*/
public static Ball getBean(String type) {
//声明一个引用,入栈
Ball ball = null;
try {
//找到目标类并重新开辟一个空间存入堆中,并向上转型
ball = (Ball) Class.forName("com.learn.other.ball." + type).newInstance();
} catch (InstantiationException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
return ball;//返回内存地址
}

/**
* 第二种方法,这种方法能保证类在编译期间就能发现到底存不存在
* @param clazz
* @return
*/
public static Ball getBean(Class<?> clazz) {
Ball ball = null;
try {
ball = (Ball) clazz.newInstance();
} catch (InstantiationException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
return ball;
}

public static void main(String[] args) {
//将内存地址赋值给ball
Ball ball1 = getBean("BasketBall");
Ball ball2 = getBean("FootBall");
Ball ball3 = getBean(BasketBall.class);
Ball ball4 = getBean(FootBall.class);
ball1.intro();
ball2.intro();
ball3.intro();
ball4.intro();
}

结果如下:

1
2
3
4
Play basketball
Play football
Play basketball
Play football

BallFactory就利用了基类,返回了该类型的子类型。上述的类实体其实对客户端是不可见的。

静态工厂方法的缺点

一、类如果不含有共有的或者受保护的构造器,就不能被子类化。

对于公有的静态工厂所返回的非公有类,也同样如此。例如,要想将 Collections Framework 中的任何方便的实现类子类化,这是不可能的。但是同样也许会因祸得福,因为它鼓励我们使用复合(组合),而不是继承。

例如下面的父类,它只能通过静态工厂方法来完成实例化对象,因为它有一个私有的构造方法,不允许被外部实例化,同时该类也不能被继承。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class Parent {

private static Parent parent;
private Parent(){}

public static Parent getInstance() {
if (parent == null) {
synchronized (Parent.class) {
if (parent == null) {
parent = new Parent();
}
}
}
return parent;
}

@Override
public String toString() {
return "I'm Parent";
}
}

当我们试图去继承该类的时候,编译器就会提示报错:

但是我们可以通过复合(组合)的方式来完成实例化:

1
2
3
4
5
6
7
8
9
public class Child {

public static Parent parent;

public static void main(String[] args) {
parent = Parent.getInstance();
System.out.println(parent);
}
}
二、它们与其他的静态方法实际上没有任何区别。

它们没有像构造器那样在API文档中明确标识出来,因此对于提供了静态工厂方法而不是构造器的类来说,要想查明如何实例化一个类,这是非常困难的。你通过在类或者接口注释中关注静态工厂,并遵守标准的命名习惯,可以弥补这一劣势。

总之,我挺认同的书中的说法,平时项目中也比较喜欢用静态方法,不需要重新实例化对象,一是没必要浪费资源,二是看起来更简洁有力(其实就是懒)。至于书中所说的缺点,我感觉跟优点比起来就是小巫见大巫了。如果有静态工厂方法实际经验的,认为其有不合理之处,十分欢迎您提出宝贵的意见。