《Effective Java》笔记01:考虑用静态工厂方法替代构造器

对于类而言,为了创建对象,最常见的方法就是提供一个公有的构造器

还有一种方法,类可以提供一个公有的静态工厂方法(static factory method)(不同于设计模式中的工厂方法),它是一个返回类实例的静态方法。下面是一个来自 Boolean 的简单示例,这个方法将 boolean 基本类型值,String 类型转换成了一个 Boolean 对象引用:

public final class Boolean implements java.io.Serializable, Comparable<Boolean> {
    public static final Boolean TRUE = new Boolean(true);
    public static final Boolean FALSE = new Boolean(false);

    public static Boolean valueOf(boolean b) {
        return (b ? TRUE : FALSE);
    }
    public static Boolean valueOf(String s) {
        return toBoolean(s) ? TRUE : FALSE;
    }
    private static boolean toBoolean(String name) {
        return ((name != null) && name.equalsIgnoreCase("true"));
    }
}

公有构造器的方式的缺点

  • 只能通过 new className() 的方式来实现
  • 每次调用必然返回一个新的对象
  • 返回类型就是该类

使用静态工厂方法的优势

静态工厂方法是有名称的

使用有适当的名称的静态工厂方法,会更加便于阅读。

如果一个类有多个构造器,而且构造器的参数没有确切的描述被返回的对象,如果 2 个构造函数拥有相同个数和类型的参数,虽说可以改变顺序,但这样会不便于阅读。

例如:一个类 Complex(复数,就是数学课上学的,有实数部和虚数部),对它的构造可能有这样 2 种需求:

  1. 分别给出实数部和虚数部来构造之;
  2. 基于极坐标来构造(提供“半径”和“角度”)

由于这两种构造方式,都是由两个 float 型参数的,对于构造函数将无能为力,而静态工厂方法可以从方法名来区别开,而且还带来了易于辨识的好处:

public class Complex {

    private final float re;
    private final float im;

    private Complex(float re, float im){
        this.re = re;
        this.im = im;
    }

    public static Complex valueOf(float re, float im){
        return new Complex(re, im);
    }

    public static Complex valueOfPolar(float r, float theta){
        return new Complex((float)(r * Math.cos(theta)), (float)(r * Math.sin(theta)));
    }
}

不必在每次调用它们的时候都创建一个新的对象

我们调用静态工厂方法返回的可能是缓存的一个对象,而不是新对象。可以进行重复利用,从而避免创建不必要的重复对象。

如果程序经常请求创建相同的对象,并且创建的代价很高,则静态工厂方法可以极大地提升性能。

前面提到的 Boolean.valueOf(boolean) 便说明了这项技术,还有单例,枚举(enum)类型。

可以返回原返回类型的的任何子类型的对象

这样在选择返回对象的类时就有了更大的灵活性。返回的对象可以不是公有的,java 的集合框架(java.util.Collections)就采用了这种实现,返回的子类都是非公有的,外部无法访问,对调用者来说并不知道是哪个子类的对象,隐藏了实现细节,减少了 API 的数量,提高了易用性。例如,EnumSet 中,创建一个空的 enum set

// 如果,枚举类型的元素个数不超过 64 个则返回 RegalarEnumSet 实例,否则返回 JumboEnumSet 实例。
public static <E extends Enum<E>> EnumSet<E> noneOf(Class<E> elementType) {
    Enum[] universe = getUniverse(elementType);
    if (universe == null)
        throw new ClassCastException(elementType + " not an enum");

    if (universe.length <= 64)
        return new RegularEnumSet<>(elementType, universe);
    else
        return new JumboEnumSet<>(elementType, universe);
}
// 这两个实现类的存在对于客户端来说是不可见的。
// 如果 RegularEnumSet 不能再给小的枚举类型提供性能优势,就可能从未来的发行版本中将它删除,不会造成不良的影响。
// 同样的了,如果证明对性能要好处,也可能在未来的发行版本中添加第三甚至第四个 Enum 实现。
// 客户端永远也不知道也不关心他们从工厂方法得到的对象的类,他们只关心他是 EnumSet 的某个子类即可。

使用这种静态工厂方法时,甚至要求客户端通过接口来引用被返回的对象,而不是通过它的实现类来引用被返回的对象,这是一种良好的习惯。

返回对象所属的类,在编写包含该方法时可以不存在

静态工厂方法返回对象所属的类,在编写包含该静态工厂方法时可以不必存在。这种灵活的静态方法构成了服务提供者框架(Service Provide Framework)的基础,例如 JDBC(Java Database Connectivity) API。服务提供者框架是这样一个系统:多个服务提供者实现一个服务,系统为服务提供者的客户端提供多个实现,并把他们从多个实现中解耦出来

服务提供者框架中有三个重要的组件:服务接口(Service Interface),这是提供者实现的提供者注册API(Provide Registration API),这是系统用来注册实现的,让客户端访问它们的;服务访问API(Service Access API),是客户端用来获取服务的实例的

服务访问 API 一般允许但是不要求客户端指定某种选择提供者的条件。如果没有这样的规定,API 就会返回默认实现的一个实例。服务访问 API 是“灵活的静态工厂”,它构成了服务提供者框架的基础。

服务提供者框架的第四个组件是可选的:服务提供者接口(Service Provide Interface),这些提供者负责创建其服务实现的实例。如果没有服务提供者接口,实现就按照类名称注册,并通过反射方式进行实例化。

对于 JDBC 来说,Connection 就是它的服务接口,DriverManager.registerDriver 是提供者注册 API,DriverManager.getConnection 是服务访问 API,Driver 就是服务提供者接口。

DriverManager.registerDriver(new com.mysql.jdbc.Driver());
// 使用服务提供者接口

Class.forName("com.mysql.jdbc.Driver");// 不使用上面的
// 由于 MySQL 在 Driver 类的实现中自己注册了一次,而我们又注册了一次,于是会导致 MySQL 驱动被注册两次
// 创建 MySQL 的 Driver 对象时,导致了程序和具体的 MySQL 驱动绑死在了一起,在切换数据库时需要改动 Java 代码,所以不使用 new
// 获取数据库连接
java.sql.Connection conn = java.sql.DriverManager
        .getConnection("jdbc:mysql:///databasename?user=root&password=root");

从 Java 6 开始,平台包含一个通用的服务提供者框架 java.util.ServiceLoader,所以你不需要,一般也不应该自己编写。 JDBC 不使用 ServiceLoader,因为前者早于后者。

简单实现

服务提供者框架(Service Provide Framework)

服务接口(Service Interface)

// 相当于 Connection 接口,由 Sun 提供
public interface Service {
}

服务提供者接口(Service Provide Interface)

// 相当于 Driver 接口,由第三方厂家实现
public interface Provider {
    Service newService();
}

无法实例化的类,用来注册和访问服务,

// 好比 DriverManager
public class Services{
    private Services(){}

    // 服务提供者 map
    private static final Map<String, Provider> providers = new ConcurrentHashMap<String, Provider>();
    // 默认的服务提供者名字
    public static final String DEFAULT_PROVIDER_NAME = "<def>";

    // 服务提供者注册 API,即注册工厂实现,相当于 DriverManager.registerDriver
    public static void registerDefaultProvider(Provider p){
        registerProvider(DEFAULT_PROVIDER_NAME, p);
    }

    // 注册 Provider
    public static void registerProvider(String name, Provider p){
        providers.put(name, p);
    }

    // 服务访问 API,向外界提供业务实现,相当于 DriverManager.getConnectio
    public static Service newInstance(){
        return newInstance(DEFAULT_PROVIDER_NAME);
    }

    public static Service newInstance(String name){
        Provider p = providers.get(name);
        if (p == null) {
            throw new IllegalArgumentException("No Provider registered with name:" + name);
        }
        return p.newService();
    }
}

使用静态工厂方法的缺点

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

对于公有的静态工厂所返回的非公有类,也同样如此。如,你想将 Collections Framework中的任何方便的实现类子类化,是不可能的。

但是这样有时候也有好处,即它鼓励程序员使用复合(compostion),而不是继承。

程序员很难找到它们

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

下面是静态工厂的一些惯用名称:

  • from,A 类型转换方法,它接受单个参数并返回此类型的相应实例,例如:Date d = Date.from(instant);
  • of,一个聚合方法,接受多个参数并返回该类型的实例,并把他们合并在一起,例如:Set faceCards = EnumSet.of(JACK, QUEEN, KING);
  • valueOf,不太严格讲,该方法返回的实例与它的参数具有相同的值。这样的静态工厂方法实际上是类型转换方法。例如:Integer.valueOf(1)
  • instancegetInstance,返回的实例是通过方法的参数来描述的。但是不能够说与参数具有同样的值。对于单例 Singleton 来说,该方法没有参数,并返回唯一的实例。
  • createnewInstance,向 getInstance 一样,但 newInstance 能够确保返回的每个实例都与所有其他实例不同。
  • getType,就像 getInstance 一样,但是在工厂方法处于不同的类的时候使用(子类)。Type 表示工厂方法所返回的对象类型。例如:FileStore fs = Files.getFileStore(path);
  • newType,就像 newInstance一样,但是在工厂方法处于不同的类的时候使用(子类)。Type 表示工厂方法所返回的对象类型。例如:BufferedReader br = Files.newBufferedReader(path);

总结

总之,静态工厂方法和公共构造方法都有它们的用途,并且了解它们的相对优点是值得的。通常,静态工厂更可取,因此避免在没有考虑静态工厂的情况下提供公共构造方法。


转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 bin07280@qq.com

文章标题:《Effective Java》笔记01:考虑用静态工厂方法替代构造器

文章字数:2.6k

本文作者:Bin

发布时间:2016-04-28, 10:28:20

最后更新:2019-08-06, 00:42:23

原始链接:http://coolview.github.io/2016/04/28/Effective-Java/%E3%80%8AEffective%20Java%E3%80%8B%E7%AC%94%E8%AE%B001%EF%BC%9A%E8%80%83%E8%99%91%E7%94%A8%E9%9D%99%E6%80%81%E5%B7%A5%E5%8E%82%E6%96%B9%E6%B3%95%E6%9B%BF%E4%BB%A3%E6%9E%84%E9%80%A0%E5%99%A8/

版权声明: "署名-非商用-相同方式共享 4.0" 转载请保留原文链接及作者。

目录