什么是静态工厂方法

在 Java 中,获得一个类实例最简单的方法就是使用 new 关键字,通过构造函数来实现对象的创建。
就像这样:

    Fragment fragment = new MyFragment();
    Date date = new Date();
    byte[] buf = new byte[2048];
    File dir = new File(path);

而我们也要在日常开发中“考虑”静态工厂方法,静态工厂方法也是获取这个类自身的一个实例,他的存在是为了更好的描述和处理这个类。比如:

    Calendar calendar = Calendar.getInstance();
    Boolean b=Boolean.valueOf(xxx);
  Calendar.java
    public static Calendar getInstance()
    {
      return createCalendar(TimeZone.getDefault(), Locale.getDefault(Locale.Category.FORMAT));
    }
  Boolean.java
   public static Boolean valueOf(boolean b) {
        return (b ? TRUE : FALSE);
    }

↑ 像这样的:不通过 new,而是用一个静态方法来对外提供自身实例的方法,即为我们所说的静态工厂方法(Static factory method)

知识点:new 究竟做了什么?

简单来说:当我们使用 new 来构造一个新的类实例时,其实是告诉了 JVM 我需要一个新的实例。JVM 就会自动在内存中开辟一片空间,然后调用构造函数来初始化成员变量,最终把引用返回给调用方。

静态工厂方法的优势

1. 静态工厂方法有名字

  • 构造函数的方式 需要名字和类名相同,当有多个重载,参数类型、返回值不同等多种情况下(比如Date函数重载),对于使用者来说可能阅读要查阅每个参数的意义了才能不调用错误的构造器。
  • 静态工厂方法可以使用不同的方法名字使得其构造的对象更加明晰。我们完全可以通过方法名明白构造了什么样的对象,帮助客户端能更好的准确的调用正确的实例。
  • 对于构造函数来说,只有参数有差异(类型、数量或者顺序)才能够重载
  • 静态工厂方法允许我们有相同的参数类型,只要名字不同即可。

即如果构造器的参数本身没有确切的描述正被返回的对象,那么具有适当名称的静态工厂会更容易使用,代码更容易阅读。

2. 不用每次被调用时都创建新对象

这个很容易理解了,有时候外部调用者只需要拿到一个实例,而不关心是否是新的实例;又或者我们想对外提供一个单例时 —— 如果使用工厂方法,就可以很容易的在内部控制,防止创建不必要的对象,减少开销。

在实际的场景中,单例的写法也大都是用静态工厂方法来实现的。

public class Single {
    private static Single instance;
    private Single() {}
    public static Single getInstance() {
        if (instance == null) {
            synchronized (Single.class) {
                if (instance == null) {
                    instance = new Single();
                }
            }
        }
        return instance;
    }
}

3. 可以返回原返回类型的子类

这一点符合设计模式中的基本的原则之一——『里氏替换』原则,就是说子类应该能替换父类。构造方法只能返回确切的自身类型,而静态工厂方法则能够更加灵活,可以根据需要方便地返回任何它的子类型的实例。

Class Person {
    public static Person getInstance(){
        return new Person();
        // 这里可以改为 return new Player() / Cooker()
    }
}
Class Player extends Person{
}
Class Cooker extends Person{
}

比如上面这段代码,Person 类的静态工厂方法可以返回 Person 的实例,也可以根据需要返回它的子类 Player 或者 Cooker。(当然,这只是为了演示,在实际的项目中,一个类是不应该依赖于它的子类的。但如果这里的 getInstance () 方法位于其他的类中,就更具有的实际操作意义了)

4. 在创建带泛型的实例时,能使代码变得简洁

//常规实例化方式
Map<String, List<String>> map =
    new HashMap<String, List<String>>();

public static <K, V> HashMap<K, V> newInstance() {
    return new HashMap<K, V>();
}
//使用静态工厂方法实例化,简化繁琐的声明
Map<String, List<String>> map = HashMap.newInstance();

不过自从 java7 开始,这种方式已经被优化过了 —— 对于一个已知类型的变量进行赋值时,由于泛型参数是可以被推导出,所以可以在创建实例时省略掉泛型参数。

Map<String,Date> map = new HashMap<>();

所以这个问题实际上已经不存在了。

5. 除此之外

以上是《Effective Java》中总结的几条应该使用静态工厂方法代替构造器的原因,如果你看过之后仍然犹豫不决,那么我觉得可以再给你更多一些理由 —— 我个人在项目中是大量使用静态工厂方法的,从我的实际经验来世,除了上面总结的几条之外,静态工厂方法实际上还有更多的优势。

5.1 可以有多个参数相同但名称不同的工厂方法


构造函数虽然也可以有多个,但是由于函数名已经被固定,所以就要求参数必须有差异时(类型、数量或者顺序)才能够重载了。
举例来说:

class Child{
    int age = 10;
    int weight = 30;
    public Child(int age, int weight) {
        this.age = age;
        this.weight = weight;
    }
    public Child(int age) {
        this.age = age;
    }
}

Child 类有 age 和 weight 两个属性,如代码所示,它已经有了两个构造函数:Child(int age, int weight) 和 Child(int age),这时候如果我们想再添加一个指定 wegiht 但不关心 age 的构造函数,一般是这样:

public Child( int weight) {
    this.weight = weight;
}

↑ 但要把这个构造函数添加到 Child 类中,我们都知道是行不通的,因为 java 的函数签名是忽略参数名称的,所以 Child(int age)Child(int weight) 会冲突。

这时候,静态工厂方法就可以登场了。

class Child{
    int age = 10;
    int weight = 30;
    public static Child newChild(int age, int weight) {
        Child child = new Child();
        child.weight = weight;
        child.age = age;
        return child;
    }
    public static Child newChildWithWeight(int weight) {
        Child child = new Child();
        child.weight = weight;
        return child;
    }
    public static Child newChildWithAge(int age) {
        Child child = new Child();
        child.age = age;
        return child;
    }
}

其中的 newChildWithWeightnewChildWithAge,就是两个参数类型相同的的方法,但是作用不同,如此,就能够满足上面所说的类似Child(int age)Child(int weight)同时存在的需求。
(另外,这两个函数名字也是自描述的,相对于一成不变的构造函数更能表达自身的含义,这也是上面所说的第一条优势 —— 『它们有名字』)

5.2 可以减少对外暴露的属性


软件开发中有一条很重要的经验:对外暴露的属性越多,调用者就越容易出错。所以对于类的提供者,一般来说,应该努力减少对外暴露属性,从而降低调用者出错的机会。

考虑一下有如下一个 Player 类:

// Player : Version 1
class Player {
    public static final int TYPE_RUNNER = 1;
    public static final int TYPE_SWIMMER = 2;
    public static final int TYPE_RACER = 3;
    protected int type;
    public Player(int type) {
        this.type = type;
    }
}

Player 对外提供了一个构造方法,让使用者传入一个 type 来表示类型。那么这个类期望的调用方式就是这样的:

    Player player1 = new Player(Player.TYPE_RUNNER);
    Player player2 = new Player(Player.TYPE_SWEIMMER);

但是,我们知道,提供者是无法控制调用方的行为的,实际中调用方式可能是这样的:

    Player player3 = new Player(0);
    Player player4 = new Player(-1);
    Player player5 = new Player(10086);

提供者期望的构造函数传入的值是事先定义好的几个常量之一,但如果不是,就很容易导致程序错误

—— 要避免这种错误,使用枚举来代替常量值是常见的方法之一,当然如果不想用枚举的话,使用我们今天所说的主角静态工厂方法也是一个很好的办法。

插一句:
实际上,使用枚举也有一些缺点,比如增大了调用方的成本;如果枚举类成员增加,会导致一些需要完备覆盖所有枚举的调用场景出错等。

如果把以上需求用静态工厂方法来实现,代码大致是这样的:


// Player : Version 2
class Player {
    public static final int TYPE_RUNNER = 1;
    public static final int TYPE_SWIMMER = 2;
    public static final int TYPE_RACER = 3;
    int type;

    private Player(int type) {
        this.type = type;
    }

    public static Player newRunner() {
        return new Player(TYPE_RUNNER);
    }
    public static Player newSwimmer() {
        return new Player(TYPE_SWIMMER);
    }
    public static Player newRacer() {
        return new Player(TYPE_RACER);
    }
}

注意其中的构造方法被声明为了 private,这样可以防止它被外部调用,于是调用方在使用 Player 实例的时候,基本上就必须通过 newRunner、newSwimmer、newRacer 这几个静态工厂方法来创建,调用方无须知道也无须指定 type 值 —— 这样就能把 type 的赋值的范围控制住,防止前面所说的异常值的情况。

插一句:
严谨一些的话,通过反射仍能够绕过静态工厂方法直接调用构造函数,甚至直接修改一个已创建的 Player 实例的 type 值,但本文暂时不讨论这种非常规情况。

5.3 多了一层控制,方便统一修改


我们在开发中一定遇到过很多次这样的场景:在写一个界面时,服务端的数据还没准备好,这时候我们经常就需要自己在客户端编写一个测试的数据,来进行界面的测试,像这样:

    // 创建一个测试数据
    User tester = new User();
    tester.setName("隔壁老张");
    tester.setAge(16);
    tester.setDescription("我住隔壁我姓张!");
    // use tester
    bindUI(tester);
    ……

要写一连串的测试代码,如果需要测试的界面有多个,那么这一连串的代码可能还会被复制多次到项目的多个位置。

这种写法的缺点呢,首先是代码臃肿、混乱;其次是万一上线的时候漏掉了某一处,忘记修改,那就可以说是灾难了……

但是如果你像我一样,习惯了用静态工厂方法代替构造器的话,则会很自然地这么写,先在 User 中定义一个 newTestInstance 方法:

static class User{
    String name ;
    int age ;
    String description;
    public static User newTestInstance() {
        User tester = new User();
        tester.setName("隔壁老张");
        tester.setAge(16);
        tester.setDescription("我住隔壁我姓张!");
        return tester;
    }
}

然后调用的地方就可以这样写了:

    // 创建一个测试数据
    User tester = User.newTestInstance();
    // use tester
    bindUI(tester);

是不是瞬间就觉得优雅了很多?!

而且不只是代码简洁优雅,由于所有测试实例的创建都是在这一个地方,所以在需要正式数据的时候,也只需把这个方法随意删除或者修改一下,所有调用者都会编译不通过,彻底杜绝了由于疏忽导致线上还有测试代码的情况。

静态工厂方法的劣势

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

  • 类如果不含公有的或受保护的构造器,就不能被实例化。
    如果我们在类中将构造函数设为private,只提供静态工厂方法来构建对象,那么我们将不能通过继承扩展该类。
//错误示例
public class Person {
    private final SEX sex;
    private final String name;
    private final int age;

    private Person(String name, int age, SEX sex){
        this.sex = sex;
        this.name = name;
        this.age = age;
    }

    public static Person getManInstance(String name, int age){
        return new Person(name, age, SEX.man);
    }

    public static Person getWomanInstance(String name, int age){
        return new Person(name, age, SEX.woman);
    }
}

class Student extends Person {
    
}
  • 实际在编译器的静态检查中会报错,原因是父类缺少公有的构造方法,而子类无法调用父类的私有构造器,导致子类无法生成构造器。
    但是这也会鼓励我们使用复合而不是继承来扩展类。

2. 程序员很难发现它们

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

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

  1. from——类型转换方法,它只有单个参数,返回该类型的一个相对应的实例,例如:
    Date d = Date.from(instant);
  2. of——聚合方法,带有多个参数,返回该类型的一个实例,把它们合并起来,例如:
    Set<Rank> faceCards = EnumSet.of(JACK, QUEEN, KING);
  3. valueOf——比from和of更烦琐的一种替代方法,例如:
    BigTnteger prime = BigInteger.valueOf(Integer.MAX_VALUE);
  4. instance或者getInstance——返回的实例是通过方法的(如有)参数来描述的,但是不能说与参数具有同样的值,例如:
    StackWalker luke = StackWalker.getInstance(options);
  5. create或者newInstance——像instance或者getInstance一样,但create或者newInstance能够确保每次调用都返回一个新的实例,例如:
    Object newArray = Array.newInstance(classObject, arrayLen);
  6. getType——像getInstance一样,但是在工厂方法处于不同的类中的时候使用。Type表示工厂方法所返回的对象类型,例如:
    FileStore fs = Files.getFileStore(path);
  7. newType——像newInstance一样,但是在工厂方法处于不同的类中的时候使用。Type表示工厂方法所返回的对象类型,例如:
    BufferedReader br = Files.newBufferedReader(path);
  8. type——getType和newType的简版,例如:
    List<Complaint> litany = Collections.list(legacyLitany);

总结

总体来说,我觉得『考虑使用静态工厂方法代替构造器』这点,除了有名字、可以用子类等这些语法层面上的优势之外,更多的是在工程学上的意义,我觉得它实质上的最主要作用是:能够增大类的提供者对自己所提供的类的控制力

作为一个开发者,当我们作为调用方,使用别人提供的类时,如果要使用 new 关键字来为其创建一个类实例,如果对类不是特别熟悉,那么一定是要特别慎重的—— new 实在是太好用了,以致于它经常被滥用,随时随地的 new 是有很大风险的,除了可能导致性能、内存方面的问题外,也经常会使得代码结构变得混乱。

而当我们在作为类的提供方时,无法控制调用者的具体行为,但是我们可以尝试使用一些方法来增大自己对类的控制力,减少调用方犯错误的机会,这也是对代码更负责的具体体现。

参考:https://www.jianshu.com/p/ceb...

孙华栋
27 声望1 粉丝