前言
最近因为疫情和年底的原因,有大量的时间来做一些自己的事,便在京东上买了一本 Effective Java 中文第三版,零散的读完全本后,虽然网上评价褒贬不一 (大部分是因为翻译问题),但真心觉得这是一本非常经典的书籍,书中一共 90 个条目,每个条目讨论一条规则,这些规则反映了最优秀的程序员在实践中常用的一些有益的做法。
本着 「你不输出,怎么进步」的原则,后边会把一些章节用来练习,然后把自己学习的过程和想法记录下来。本文将重点介绍用静态工厂方法代替构造器。
静态工厂方法概述
Java 中获取一个类的实例,最传统的方法是提供一个 (或几个) 公有的构造器,然后用 new 关键字去创建一个实例,但是在一些特定的用例中,我们还可以选择使用 静态工厂方法 去获取实例。在本书中,本文的作者 Joshua Bloch 给了一个清晰的建议。
考虑静态工厂方法而不是构造函数
静态工厂方法相对于构造方法的优劣势
静态工厂方法和公有构造器都各有用处,需要理解他们各自的长处,通常情况下静态工厂更加合适,因此切忌第一反应就是提供公有的构造器,而不优先考虑静态工厂。所以本书中也总结了其中的一下优缺点。
优势
可读性比较高
在 Java 中有大量的使用 静态工厂方法,其中 String 就是一个典型代表。在创建 String 对象中,我们都知道不太可能通过构造器去创建新的 String 对象,但它的写法是可以的,虽然看起来怪怪的。
String value = new String("Henry");
但是 String 也可以使用 静态工厂方法 来创建对象。这里提供了几种 valueOf() 的重载,每个对象都返回一个新的 String 对象,取决于具体传的参数,该命名非常清晰的表达了这个方法的作用。
String value1 = String.valueOf(0);
String value3 = String.valueOf(true);
String value3 = String.valueOf("Henry");
再举例几个 Java 中的简洁并且名称非常清晰的示例
Optional value1 = Optional.empty();
Optional value2 = Optional.of("Henry");
Optional value3 = Optional.ofNullable(null);
不必每次都创建一个新实例
在 Java 中,Boolean 类的 valueOf() 类就是提前预先构建好的实例,或将构建好的实例缓存起来,进行重复的利用,从而避免创建不必要的重复对象,减少 JVM 的开销。
public final class Boolean implements java.io.Serializable,
Comparable
{
/**
* 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);
}
在调用 valueOf() 时候返回的就是这两个实例的引用,可以避免每次都创建对象
public static Boolean valueOf(String s) {
return toBoolean(s) ? TRUE : FALSE;
}
可以返回原返回类型的任何子类对象
我们在选择返回对象的类时就更有灵活性。这种灵活性的一种应用是 API 可以返回对象,同时又不会使对象的类变成公有的。这种方式隐藏实现类会使 API 变得非常简洁, 可以根据需要方便地返回任何它的子类型的实例。并且还遵循了设计模式六大原则中的其中一条「里氏替换原则」
派生类(子类)对象可以在程式中代替其基类(超类)对象
public Class Animal {
public static Animal getInstance(){
// 可以返回 Dog or Cat
return new Animal();
}
}
Class Dog extends Animal{}
Class Cat extends Animal{}
所返回的对象的类可以随着每次调用而发生变化,这取决于静态工厂方法的参数值
这句比较绕嘴,总结一句话就是,返回对象的类可能随着发行版本的不同而不同。书中对 EnumSet 进行了举例,EnumSet 没有公有的构造器,只有静态工厂方法,判断条件是,如果元素大于 64 个,就返回 RegularEnumSet,并用单个 long 进行支持;如果大于 64 个元素,就返回 JumboEmunSet 实例,用一个 long 数组进行支持。如果 RegularEnumSet 不能再给小的枚举类型提供性能优势,就可能从未来的发行版本中将它删除,不会造成任何负面影响,并且客户端是不可见的。同样的也可进行扩展。
public static > EnumSet noneOf(Class 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);
}
缺点
类如果不包含公有的或者受保护的构造器,就不能被继承
例如:要想将 Collections 中的任何工具实现子类化,这是不可能的。但这也正是鼓励程序员使用复合,而不是继承 (原则是:如果类不是专门设计来用于被继承的就尽量不要使用继承而应该使用组合)。
程序员很难发现它们
静态工厂方法不像构造方法一样在 API 中是标识出来的。所以查看 API 时不太容易发现。所以尽可能采用标准的静态工厂方法命名方式去避免。例如:
from:类型转换方法,它接受单个参数并返回此类型的相应实例
- Date d = Date.from(instant);
of:聚合方法,接受多个参数并返回该类型的实例,并把他们合并在一起
- Set faceCards = EnumSet.of(JACK, QUEEN, KING);
valueOf:比 from 和 of 更繁琐的一种替代方法
- BigInteger prime = BigInteger.valueOf(Integer.MAX_VALUE);
创建静态工厂方法
自定义静态工厂方法
当然,我们可以实现自己的静态工厂方法。 但是什么时候需要这样做,而不是通过常规的构造函数创建类实例?
首先我们创建一个常规的 User 类:
public class User {
private final String name;
private final String email;
private final String country;
public User(String name, String email, String country) {
this.name = name;
this.email = email;
this.country = country;
}
}
这种情况下,使用构造函数创建和使用静态工厂方法创建没有什么区别。但现在变更一下条件,如果我们希望所有的 User 实例都获得 Counrty 字段的默认值需要怎么做?如果我们使用默认值初始化字段,那么我们也必须重构构造函数,从而让设计更加严格。此时可以采用静态工厂方法:
public static User createWithDefaultCountry(String name, String email) {
return new User(name, email, "China");
}
以下是我们如何为 User 实例分配默认值的地区字段的方法:
User user = User.createWithDefaultCountry("Henry", "[email protected]");
总结
在书中的前序中有一段话非常喜欢,这里摘录出来:
虽然本书中的规则不会百分之百地适用于任何时刻和任何场合,但是,它们确实体现了绝大多数情况下的最佳编程实践。你不应该盲目的遵循这些规则,但偶尔有了充分的理由之后,可以去打破这些规则。同大多数学科一样,学习编程艺术首先要学会基本的规则,然后才能知道什么时候去打破它。