《Effective Java 第三版》笔记之一 创建静态工厂方法而不是使用构造器

标签:#effectivejava##java# 时间:2018/09/14 23:06:36 作者:小木

《Effective Java》是一本非常优秀的关于Java编程思想的书籍,第二版出版于2009年,已经过时了。第三版也在2018年推出了,本系列博客讲述第三版中的各项内容。

一般情况下,Java类提供实例化的方法是提供一个可以被公共访问的构造方法。但是每个编程的人员都应当了解另一种提供实例化的方式,即静态工厂方法,也就是提供一个静态方法,可以返回类的一个实例。例如,下面是一个简单的提供Boolean类实例化方法:

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

注意,静态工厂方法(a static factory method)与设计模式中的工厂方法(Factory Method)模式不是同一个概念。这里描述的静态工厂方法与设计模式一点关系都没有。

一个类除了提供公共的构造方法外,还可以使用静态工厂方法提供其实例。而使用静态工厂方法提供实例既有优点也有缺点。

优点一:与构造方法不同的是静态工厂方法有自己的方法名。如果一个构造方法的参数本身并不能描述其返回的对象(意思就是说通过参数类型无法表达出这个构造方法想要干什么——笔者注),那么使用静态工厂方法,取一个好的名字就是一种好的选择了,它会使代码更加容易阅读。例如构造方法 BigInteger(int, int, Random) 返回一个很大的近似素数(大素数在加密和解密算法中有很重要的作用,求两个很大的素数乘积很简单,而找出一个数是被哪两个大的素数相乘则是一个非常困难的事情。但是判断一个很大的值是不是素数并不是容易的事情,这个方法就是返回一个数,它在多大概率上是一个素数,这里可以称之为概素数——笔者注),这并不容易让人理解,改成BigInteger.probablePrime则相对来说易于理解许多(这个方法在Java4中已经添加了)。


这里我们用另一个例子描述可能会清晰一点。假设有一个产生随机数的类,它可以帮助我们产生介于某个最小值和某个最大值之间的随机数(使用next()方法产生):

public class RandomIntGenerator {
    private final int min;
    private final int max;

    public int next() {...}
}

在初始化的时候我们可以指定最小值或者最大值之一,也可以同时指定两者,那么这里需要构造三个构造方法:

//同时指定最大值和最小值
public RandomIntGenerator(int min, int max) {
    this.min = min;
    this.max = max;
}

//仅指定最大值
public RandomIntGenerator(int max) {
    this.min = Integer.MIN_VALUE;
    this.max = max;
}

//仅指定最小值
public RandomIntGenerator(int min) {
    this.min = min;
    this.max = Integer.MAX_VALUE;
}

显然,上述三个构造方法的后两个不能同时存在,因为他们的参数一样。那么这里使用静态工厂方法就可以实现上述效果,我们可以构造一个构造方法,必须同时指定最大值和最小值,然后构造三个静态工厂方法即可:


public class RandomIntGenerator {
    private final int min;
    private final int max;

    private RandomIntGenerator(int min, int max) {
        this.min = min;
        this.max = max;
    }

    public static RandomIntGenerator between(int max, int min) {
        return new RandomIntGenerator(min, max);
    }

    public static RandomIntGenerator biggerThan(int min) {
        return new RandomIntGenerator(min, Integer.MAX_VALUE);
    }

    public static RandomIntGenerator smallerThan(int max) {
        return new RandomIntGenerator(Integer.MIN_VALUE, max);
    }

    public int next() {...}
}

一个类只能有一个同名且特征相同的构造方法。现在人们都知道可以使用两个参数不同的构造方法来破除这种限制。但这并不是一个好主意。使用这个API的用户永远无法记住那个构造方法适用于哪种情况。在没有文档的情况下,用户永远无法知道它的作用。

由于静态工厂方法没有这种名字的限制,因此也不会有这个问题。如果一个类需要多个特征相同的构造方法的话,那么使用静态工厂方法代替是一个很好的选择。

优点二:与构造方法不同的时,静态工厂方法在调用的时候不会每次都创建一个新的对象。因此,这个特性可以用预创建的实例来构造不可变的类(在本书的item17中将对不可变类进行描述),或者当类创建的时候对其进行缓存,并在需要的时候重复使用,而不需要创建重复的类。Boolean.valueOf(boolean)方法就是这种情况:它从来不创造对象。这个特性与享元模式(Flyweight Pattern)类似。如果某个对象要被经常访问,那么这个特性可以极大地提高性能,特别是创建对象耗费资源的时候。

静态工厂方法的这种返回相同对象的特性允许类严格控制哪些对象在什么时候可以存在,这被称为实例受控(instance-controlled)。实例受控类的存在有几个原因。首先,控制实例可以保证某各类是单例模式(singleton)或者是不可实例化的(noninstantiable)。同时,它还允许不可变类保证不存在两种有相同值的类:即当且仅当 a==b的时候,a.equals(b)。这是享元模式的基础。枚举类型也提供了这种保证。

优点三:与构造方法不同的是,静态工厂方法可以返回任意返回类的子类。这保证了你可以灵活的选择返回对象。

这种灵活性的一个应用是某个API可以返回一个对象,但是不需要这个对象的类是公共的。是用这种方式隐藏类的实现能可以导致生成非常简洁的API。这种技术导致了基于接口的框架,其中接口为静态工厂方法提供了自然的返回类型。

在Java8之前,接口是不能有静态方法的。按照惯例,要构造一个接口的静态方法可以采用如下方式,假设接口的名字为Type,可以将该接口放到一个不可实例化的类Types中。例如,Java的集合框架有45种接口实现方式,提供了如不可修改的集合,同步集合等。几乎所有的这些实现都是通过引入一个静态工厂方法实现的,他们都在一个不可实例化的类中(java.util.Collections),它们返回的所有的对象都不是公开的类。

由于没有引入外部45个单独的类,集合框架的API变小了很多。这不仅是它们大部分的API都被减掉了,而且也是从概念上也少了很多:概念的数量和难度对使用者来说都大大降低了。由于使用者知道API的返回值有精确的接口指定,因此不需要阅读文档也就可以实现这些类。进一步的,使用这样的静态工厂方法需要客户端通过接口指定返回类型,而不是通过实现类指定,是很好的方式。

在Java8中,接口不允许静态类的要求已经没有了,因此不需要使用上述复杂的方式。很多公共的静态成员要放在类里面而不是接口中。然而需要注意的是,将静态方法背后的大量的实现细节放到单独的私有包下的类依然是必要的。这是因为Java8要求接口下所有静态成员都是必须是公共的。Java 9允许私有的静态方法,但是静态字段和静态成员类依然要求是公共的。


这里依然举个例子,刚才生成随机数的方法我们做一点改动,我们不仅想生成随机整数,还想生成其他类型的随机数,我们通过next()方法获取指定类型的下一个随机数。因此,可以定义一个接口:

public interface RandomGenerator<T> {
    T next();
}

接下来我们定义两个实现类来实现上述结果:

class RandomIntGenerator implements RandomGenerator<Integer> {
    private final int min;
    private final int max;

    RandomIntGenerator(int min, int max) {
        this.min = min;
        this.max = max;
    }

    public Integer next() {...}
}
class RandomStringGenerator implements RandomGenerator<String> {
    private final String prefix;

    RandomStringGenerator(String prefix) {
        this.prefix = prefix;
    }

    public String next() {...}
}

注意,这两个类我们都定义为protected(包内可见,默认)类型,因此他们的构造方法也是这样的。也就是说在这个包外的类无法使用这两个类。那么,我们就可以使用静态工厂方法实现了:

public final class RandomGenerators {
    // Suppresses default constructor, ensuring non-instantiability.
    private RandomGenerators() {}

    public static final RandomGenerator<Integer> getIntGenerator() {
        return new RandomIntGenerator(Integer.MIN_VALUE, Integer.MAX_VALUE);
    }

    public static final RandomGenerator<String> getStringGenerator() {
        return new RandomStringGenerator("");
    }
}

RandomGenerators是一个不可实例化的工具类,只包含静态工厂方法,进而在这个包下面可以有效的实例化这些类。注意,由于返回值是RandomGenerator接口,而对于所有的客户端来说都是一样的,因此如果实例化的是RandomIntGenerator类,那么next()方法返回的就是整型随机数,否则返回的是其他类型。假设下个月我们写了一个更高效率的整数随机数生成的类。我们只要使用这个新类实现RandomGenerator<Integer>接口,即可改变静态工厂方法的返回值,那么所有的客户端都能使用这个新的实现类了。这种方式在JDK和第三方库中很常见,例如Google中的Maps,Lists,Sets,Maps或者是Collections(java.util.Collections)都是这样的。


优点四:返回对象的类型可以根据参数的不同而不同。
由于任何一个返回类的子类型都可以使用,因此这非常灵活。

例如类EnumSet没有公共构造方法,只有静态工厂方法。在OpenJDK的视线中,他们返回的实例取决于枚举类型的大小:如果该枚举类型少于64个,那么静态工厂方法返回的是RegularEnumSet实例,基于long来实现。如果该枚举类型超过64,那么返回JumboEnumSet实例,由long数组支持。

现有的这两种实现类对用户来说都是不可见的。如果RegularEnumSet类在小规模数量上不再具有优势,在后期可以去掉也没有影响。类似的,如果我们未来加上新的实现类,那么客户端也不需要担心加了什么内容。他们只需要知道是EnumSet的某个子类就行了。

优点五:如果某个类包含了一个判断是否写入的方法,那么返回的对象不一定需要存在。这种灵活的方式形成了服务提供框架(Service provider frameworks)的基础,如Java Database Connectivity API(JDBC)。服务提供框架提供了一种实现系统,这种实现对客户端来说使可用,从而使客户端与实现分离。

参考:https://jlordiales.wordpress.com/2012/12/26/static-factory-methods-vs-traditional-constructors/