何时以及如何创建对象,何时以及如何避免创建对象,如何确保它们能够适时地销毁,以及如何管理对象销毁之前必须进行的各种清理动作。
01 用静态工厂方法代替构造器
为方面理解这条,我们先上代码:
public class BigInteger extends Number implements Comparable<BigInteger> {
// ...
// ...
/**
* Constructs a randomly generated positive BigInteger that is probably
* prime, with the specified bitLength.
* ...
*/
public BigInteger(int bitLength, int certainty, Random rnd) {
BigInteger prime;
if (bitLength < 2)
throw new ArithmeticException("bitLength < 2");
prime = (bitLength < SMALL_PRIME_THRESHOLD
? smallPrime(bitLength, certainty, rnd)
: largePrime(bitLength, certainty, rnd));
signum = 1;
mag = prime.mag;
}
// ...
// ...
/**
* Returns a positive BigInteger that is probably prime, with the
* specified bitLength. The probability that a BigInteger returned
* by this method is composite does not exceed 2<sup>-100</sup>.
* ...
*/
public static BigInteger probablePrime(int bitLength, Random rnd) {
if (bitLength < 2)
throw new ArithmeticException("bitLength < 2");
return (bitLength < SMALL_PRIME_THRESHOLD ?
smallPrime(bitLength, DEFAULT_PRIME_CERTAINTY, rnd) :
largePrime(bitLength, DEFAULT_PRIME_CERTAINTY, rnd));
}
// ...
// ...
}
如上所示,获得一个可能为素数的 BigInteger
实例,有两种方式:直接使用构造器 new BigInteger(int bitLength, int certainty, Random rnd)
,或使用静态工厂方法 BigInteger.probablePrime(int bitLength, Random rnd)
。哪种方式更好呢?很显然,静态工厂方法,因为它能表达意图:不用看方法注释,一看名字就知道是做什么。而构造器显然没有这种自描述的能力,尤其是有很多个构造器的时候。
除此之外,使用静态工厂方法创建对象还有一个很大的优势是——灵活性:
- 每次都创建新对象?还是复用已有的对象?
- 返回的对象类型,可以是当前对象,也可以是子对象。没错,多态开始起作用了;
- 根据参数的不同,可以控制返回的对象实例,比如不同的子类;
- 返回对象所属的类,甚至在编写该静态工厂方法时可以不存在,这就是
Service Provider Framework
机制。
除了最后一条,前面几条都非常好理解,这里就不做赘述。关于 Service Provider Framework
机制,笔者特地梳理了一篇总结。
说完优点,再看看不足之处:不易被编程人员发现。
因为创建对象时,大家首先想到的都是构造器。如果把构造器的权限设置为 private
呢,子类化会受到影响。那改成 protected
岂不是最佳方案?不一定,除非增加代码注释,否则会引起误会。因为 protected
的隐喻是,让子类调用,而不是外部。这并不能代表:请不要使用构造器创建实例,而要使用我提供的静态工厂方法。有什么法子可以缓解这种境况?一个好名字或许能发挥一些余力,下面是一些行业实践:
from
:类型转换,单个参数,比如:LocalDateTime dateTime = LocalDateTime.from(temporal);
of
:聚合方法,多个参数,比如:List<String> list = List.of("a", "b", "c");
valueOf
:对from
和of
的一种替代方式,比如:Long value = Long.valueOf("100000");
instance
orgetInstance
:根据参数获取对象实例,比如:NumberFormat numberFormat = NumberFormat.getInstance();
create
ornewInstance
:与上面一条相似,但每次都会创建一个新实例,比如:Object stringArr = Array.newInstance(String.class, 2);
getType
:与getInstance
类似,Type
表示所返回的对象类型,比如:FileStore fs = Files.getFileStore(filePath);
newType
:与newInstance
类似,比如:BufferedReader br = newBufferedReader(path);
type
:getType
和newType
的简化版,比如:List<Complaint> list = Collections.list(legacyLitany);
02 遇到多个构造器参数时要考虑使用构建器
这也是一个比较常见的使用技巧。当每次的构造器参数都有可能不同时,如果将这些构造器都创建出来,让人搞不清楚这些构造器的意图是什么。为了应对这种情况,有的人就选择使用 setter
方法,比如:
Xxx xxx = new Xxx();
xxx.setX1("x1");
xxx.setX2(2);
xxx.setX3(true);
//...
但这种方式也有其不足,那就是数据一致性。本来需要 5 个参数,使用 setter
方法后,忘了某一两个,毕竟没有地方提示你:此处需要 5 个参数。针对这种场景,构建器模式就被提了出来,简单来说就是构造一个中间层进行数据拼装,然后再转成实际需要的类,比如:
public class JoinQueryTableParams {
private String tableName;
private List<String> fields;
private ConditionGroup conditions;
private DBTableRelationType joinType;
private String joinColumn;
private String refJoinColumn;
public JoinQueryTableParams(String tableName, List<String> fields, ConditionGroup conditions, DBTableRelationType joinType, String joinColumn, String refJoinColumn) {
this.tableName = tableName;
this.fields = Optional.ofNullable(fields).orElse(Collections.emptyList());
this.conditions = Optional.ofNullable(conditions).orElse(ConditionGroup.empty());
this.joinType = joinType;
this.joinColumn = joinColumn;
this.refJoinColumn = refJoinColumn;
}
public static JoinQueryTableParamsBuilder builder() {
return new JoinQueryTableParamsBuilder();
}
public static class JoinQueryTableParamsBuilder {
private String tableName;
private List<String> fields;
private ConditionGroup conditions;
private DBTableRelationType joinType;
private String joinColumn;
private String refJoinColumn;
JoinQueryTableParamsBuilder() {
}
public JoinQueryTableParamsBuilder tableName(String tableName) {
this.tableName = tableName;
return this;
}
public JoinQueryTableParamsBuilder fields(List<String> fields) {
this.fields = fields;
return this;
}
public JoinQueryTableParamsBuilder conditions(ConditionGroup conditions) {
this.conditions = conditions;
return this;
}
public JoinQueryTableParamsBuilder joinType(DBTableRelationType joinType) {
this.joinType = joinType;
return this;
}
public JoinQueryTableParamsBuilder joinColumn(String joinColumn) {
this.joinColumn = joinColumn;
return this;
}
public JoinQueryTableParamsBuilder refJoinColumn(String refJoinColumn) {
this.refJoinColumn = refJoinColumn;
return this;
}
public JoinQueryTableParams build() {
return new JoinQueryTableParams(this.tableName, this.fields, this.conditions, this.joinType, this.joinColumn, this.refJoinColumn);
}
}
}
上面代码示例中的 JoinQueryTableParamsBuilder
就是笔者提到的中间层,当我们使用 JoinQueryTableParams.builder()
时,便会创建 JoinQueryTableParamsBuilder
实例,再调用 build()
方法,则会调用 JoinQueryTableParams
类的构造器,即创建我们需要的类实例。前面提到 setter
方法无法保证数据的一致性,比如进行参数校验等,那么现在呢?从上面的创建过程不难发现,我们完全可以在 JoinQueryTableParams
类的构造器中处理,保证数据合法、一致等。
另外,Builder 模式也适用于类层次结构,也就是说在子类继承父类时,也可以使用这种方式,将父类的属性通过 builder 模式进行构建。但父子类使用 builder 模式可能没上面那么容易,但我们可以借助 lombook
类库中的 @SuperBuilder
注解。通过将其放在父子类上,然后查阅生成的代码,其内部逻辑便一目了然了。下面 QueryTableParams
和 PageQueryTableParams
中的 builder 代码便是通过这种方式获得的:
// 父类
public class QueryTableParams {
private String tableName;
private List<String> fields;
private ConditionGroup conditions;
protected QueryTableParams(QueryTableParamsBuilder<?, ?> b) {
this.tableName = b.tableName;
this.fields = b.fields;
this.conditions = b.conditions;
}
public static QueryTableParamsBuilder<?, ?> builder() {
return new QueryTableParamsBuilderImpl();
}
private static final class QueryTableParamsBuilderImpl extends QueryTableParamsBuilder<QueryTableParams, QueryTableParamsBuilderImpl> {
private QueryTableParamsBuilderImpl() {
}
protected QueryTableParamsBuilderImpl self() {
return this;
}
public QueryTableParams build() {
return new QueryTableParams(this);
}
}
public abstract static class QueryTableParamsBuilder<C extends QueryTableParams, B extends QueryTableParamsBuilder<C, B>> {
private String tableName;
private List<String> fields;
private ConditionGroup conditions;
public QueryTableParamsBuilder() {
}
protected abstract B self();
public abstract C build();
public B tableName(String tableName) {
this.tableName = tableName;
return this.self();
}
public B fields(List<String> fields) {
this.fields = fields;
return this.self();
}
public B conditions(ConditionGroup conditions) {
this.conditions = conditions;
return this.self();
}
}
}
// 子类
public class PageQueryTableParams extends QueryTableParams {
private int pageNum;
private int pageSize;
protected PageQueryTableParams(PageQueryTableParamsBuilder<?, ?> b) {
super(b);
this.pageNum = b.pageNum;
this.pageSize = b.pageSize;
}
public static PageQueryTableParamsBuilder<?, ?> builder() {
return new PageQueryTableParamsBuilderImpl();
}
private static final class PageQueryTableParamsBuilderImpl extends PageQueryTableParamsBuilder<PageQueryTableParams, PageQueryTableParamsBuilderImpl> {
private PageQueryTableParamsBuilderImpl() {
}
protected PageQueryTableParamsBuilderImpl self() {
return this;
}
public PageQueryTableParams build() {
return new PageQueryTableParams(this);
}
}
public abstract static class PageQueryTableParamsBuilder<C extends PageQueryTableParams, B extends PageQueryTableParamsBuilder<C, B>> extends QueryTableParams.QueryTableParamsBuilder<C, B> {
private int pageNum;
private int pageSize;
public PageQueryTableParamsBuilder() {
}
protected abstract B self();
public abstract C build();
public B pageNum(int pageNum) {
this.pageNum = pageNum;
return this.self();
}
public B pageSize(int pageSize) {
this.pageSize = pageSize;
return this.self();
}
}
}
书中说:子类方法声明返回父类中声明的返回类型的子类型,这被称作协变返回类型(covariant return type)。它允许客户端无须转换类型就能使用这些构建器。笔者倒觉得无需去记这些概念,通过一个使用样例把上面的实现串起来,这其中多态的使用技巧也能理解个大概。
最后提醒一下,Builder 模式比重叠构造器模式更加冗长,因此它只在有很多参数的时候才使用,比如4个或者更多个参数。
03 用私有构造器或者枚举类型强化 Singleton 属性
Singleton 模式在 Java 中的使用非常多,比如 Spring 框架之中。但如果从单元测试的角度来看,它并不是一个好模式,因为我们没法 Mock 它。没法 Mock 的意思是:无法改变该对象实例内部的状态和行为,即无法改变属性的值或是方法的逻辑。
关于单例模式,有懒汉,饿汉,双重检测,枚举等多种实现方式,笔者整理了几个,提交在这里:Github Singleton。如果消耗的性能不高,大多数情况下,我们可以选择最简单的饿汉模式,比如:
public class IdGeneratorStarve {
private AtomicLong id = new AtomicLong();
private static final IdGeneratorStarve instance = new IdGeneratorStarve();
private IdGeneratorStarve() {
}
public static IdGeneratorStarve getInstance() {
return instance;
}
public long generateId() {
return id.getAndIncrement();
}
}
但即使这样,也不是百分百安全,因为可以使用反射这个后门绕过私有构造器,比如:
public class IdGeneratorStarveTest {
@Test
void should_create_more_id_generator() throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
Constructor constructor = IdGeneratorStarve.class.getDeclaredConstructor();
constructor.setAccessible(true);
IdGeneratorStarve idGeneratorStarve = (IdGeneratorStarve) constructor.newInstance();
Assertions.assertNotEquals(IdGeneratorStarve.getInstance(), idGeneratorStarve);
}
}
如果想避免这个问题,可以增加校验,即不允许创建第二个实例,比如:
public class IdGeneratorStarve {
private AtomicLong id = new AtomicLong();
private static boolean instanceCreated = false;
private static final IdGeneratorStarve instance = new IdGeneratorStarve();
private IdGeneratorStarve() {
if (instanceCreated) {
throw new IllegalStateException("cannot create new IDGeneratorStarve instance for singleton");
}
instanceCreated = true;
}
public static IdGeneratorStarve getInstance() {
return instance;
}
public long generateId() {
return id.getAndIncrement();
}
}
但在序列化的场景下,这种写法还是有问题,因为反序列化依然会创建新对象,比如:
public class IdGeneratorStarveTest {
@Test
void should_create_serializable_id_generator() {
String dataName = "id-generator.data";
try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(dataName));
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(dataName))) {
oos.writeObject(IdGeneratorStarve.getInstance());
IdGeneratorStarve idGeneratorStarve = (IdGeneratorStarve) ois.readObject();
Assertions.assertNotEquals(idGeneratorStarve, IdGeneratorStarve.getInstance());
Assertions.assertNotEquals(idGeneratorStarve.getId(), IdGeneratorStarve.getInstance().getId());
} catch (Exception ex) {
throw new RuntimeException(ex);
} finally {
try {
Files.deleteIfExists(Path.of(dataName));
} catch (IOException e) {
log.error("delete file failed: ", e);
}
}
}
}
为此,我们需要将所有实例域都声明为 transient
,并提供一个 readResolve()
方法:
public class IdGeneratorStarve implements Serializable {
// `transient` for id
private transient AtomicLong id = new AtomicLong();
private static boolean instanceCreated = false;
private static final IdGeneratorStarve instance = new IdGeneratorStarve();
private IdGeneratorStarve() {
if (instanceCreated) {
throw new IllegalStateException("cannot create new IDGeneratorStarve instance for singleton");
}
instanceCreated = true;
}
public static IdGeneratorStarve getInstance() {
return instance;
}
public long generateId() {
return id.getAndIncrement();
}
public AtomicLong getId() {
return id;
}
// return `instance` by `readResolve()`
private Object readResolve() {
return instance;
}
}
如果想再简化一下上面的使用方式,可以将 instance
调整为 public-field
,然后移除 getInstance()
方法,即:
public static final IdGeneratorStarve instance = new IdGeneratorStarve();
由于 getInstance()
方法是一个静态方法,它相比直接使用公有的属性域做了一定的封装:在不改变 API 的前提下,我们随时改变 Singleton 的假设,从而增加了代码的灵活性。
最后,如果想避开前面提到的各种安全问题,最简单的方式是使用枚举:
public enum IdGeneratorEnum implements Serializable {
INSTANCE;
private AtomicLong id = new AtomicLong();
IdGeneratorEnum() {
}
public long generateId() {
return id.getAndIncrement();
}
public IdGeneratorEnum getInstance() {
return INSTANCE;
}
public AtomicLong getId() {
return id;
}
}
04 通过私有构造器强化不可实例化的能力
有时可能需要编写只包含静态方法和静态域的类。这些类的名声很不好,因为有些人在面向对象的语言中滥用这样的类来编写过程化的程序,但它们也确实有特别的用处。
我们经常会使用一些工具类,这些工具类里面都是静态方法和静态域,比如集合工具类:java.util.Collections
。既然是各种静态方法,所以这样的类不需要实例化。但我们往往不会在自己的工具类中增加构造器,这就会导致这些工具类都会有一个默认公有的无参构造器,从而让这些类有了实例化和子类化的可能。解决方法也很简单,增加一个私有构造器即可,比如:
public class Collections {
// Suppresses default constructor, ensuring non-instantiability.
private Collections() {
}
// ...
private static final int BINARYSEARCH_THRESHOLD = 5000;
private static final int REVERSE_THRESHOLD = 18;
private static final int SHUFFLE_THRESHOLD = 5;
private static final int FILL_THRESHOLD = 25;
private static final int ROTATE_THRESHOLD = 100;
private static final int COPY_THRESHOLD = 10;
private static final int REPLACEALL_THRESHOLD = 11;
private static final int INDEXOFSUBLIST_THRESHOLD = 35;
// ...
public static <T extends Comparable<? super T>> void sort(List<T> list) {
list.sort(null);
}
为了让代码具备更多的可读性,可以像上面的代码那样,增加一行注释:Suppresses default constructor, ensuring non-instantiability.
05 优先考虑依赖注入来引用资源
不要用 Singleton 和静态工具类来实现依赖一个或多个底层资源的类,且该资源的行为会影响到该类的行为 ;也不要直接用这个类来创建这些资源。而应该将这些资源或者工厂传给构造器(或者静态工厂,或者构建器),通过它们来创建类。这个实践就被称作依赖注入,它极大地提升了类的灵活性,可重用性和可测试性。
依赖注入,又被称为控制反转,可能很多小伙伴在使用 Spring 这类框架后才接触到:
但其实,像下面这样,把 dictionary
作为构造器参数传入给 SpellChecker
,就是依赖注入。因为 SpellChecker
所依赖的资源是通过外部传入的,而不是自产自销,这大大增加了业务逻辑的灵活性。
public class SpellChecker {
private final Lexicon dictionary;
public SpellChecker(Lexicon dictionary) {
this.dictionary = Objects.requireNonNull(dictionary);
}
public boolean isValid(String word){
// ...
}
public List<String> suggestions(String typo){
// ...
}
}
06 避免创建不必要的对象
String
类的实例不要调用构造器,直接赋值使用即可,即:String s = "abc";
;- 有静态工厂方法时,优先使用静态工厂方法,而不是构造器(原因前文已经说过了);
- 优先使用基本类型而不是基本类型的装箱类型;
- 如果需要创建的对象很昂贵,应该将其缓存下来重用。
虽然要避免创建不必要的对象,但:
小对象的构造器只做很少量的显式工作,所以小对象的创建和回收动作是非常廉价的,特别是在现代的 JVM 实现上更是如此。通过创建附加的对象,提升程序的清晰性、简洁性和功能性,这通常是件好事。
07 消除过期的对象引用
清空对象引用应该是一种例外,而不是一种规范行为。消除过期引用最好的方法是让包含该引用的变量结束其生命周期。
书中提到了三种内存泄漏的情况,更多内容,笔者将在后续额外补充一篇总结,特别是涉及到 WeakHashMap
:
- 集合中(最常见的是栈)引用了很多对象,但集合伸缩时没有手动回收这些对象;
- 缓存池,其实跟上面的集合有点像,对象放进去之后,很容易被我们忘记;
- 监听器和回调,如果你实现了一个API,客户端在这个API中注册回调,却没有显式地取消注册,那么除非你采取某些动作,否则它们就会不断地堆积起来。
08 避免使用终结方法和清除方法
Java语言规范不仅不保证终结方法或者清除方法会被及时地执行,而且根本就不保证它们会被执行。
Cleaner 规范指出:“清除方法在
System.exit
期间的行为是与实现相关的。不确保清除动作是否会被调用。”总而言之,除非是作为安全网,或者是为了终止非关键的本地资源,否则请不要使用清除方法,对于在 Java 9 之前的发行版本,则尽量不要使用终结方法。
简单来说就是忘记这两个特性,笔者也曾总结过一篇:Java 之 finalize 终结。
09 try-with-resources 优先于 try-finally
Java 7 提供的语法糖,不仅解决了繁琐的资源关闭问题,同时能将这中间多个异常打印到堆栈轨迹之中,不会出现后面异常覆盖前面异常的情况。语法糖的意思是:编译器会帮我们生成原生的代码,即那些繁杂的逻辑:
public class TryWithResourcesTest {
private void test() throws IOException, ClassNotFoundException {
String dataName = "a.txt";
try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(dataName));
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(dataName))) {
oos.writeObject(IdGeneratorEnum.INSTANCE);
IdGeneratorEnum idGeneratorEnum = (IdGeneratorEnum) ois.readObject();
System.out.println("idGeneratorEnum = " + idGeneratorEnum);
}
}
}
将上面的代码进行编译,再使用 Intellij IDEA 打开对应的 class 文件,便能看到资源的关闭以及 addSuppressed()
方法的使用。
public class TryWithResourcesTest {
public TryWithResourcesTest() {
}
private void test() throws IOException, ClassNotFoundException {
String dataName = "a.txt";
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(dataName));
try {
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(dataName));
try {
oos.writeObject(IdGeneratorEnum.INSTANCE);
IdGeneratorEnum idGeneratorEnum = (IdGeneratorEnum)ois.readObject();
System.out.println("idGeneratorEnum = " + String.valueOf(idGeneratorEnum));
} catch (Throwable var8) {
try {
ois.close();
} catch (Throwable var7) {
var8.addSuppressed(var7);
}
throw var8;
}
ois.close();
} catch (Throwable var9) {
try {
oos.close();
} catch (Throwable var6) {
var9.addSuppressed(var6);
}
throw var9;
}
oos.close();
}
}