Item 1 : using static factory method instead of constructor
多使用静态工厂方法而不是构造器
优势:
- 构造器容易造成参数顺序混乱,静态工厂方法名称有很好的表意性,对于类属性较多的情况有着非常明显的优势
- 静态工厂方法都不必每次调用都创建新的对象,对于创建开销很大的对象或者同一个对象反复调用来说有性能提升(实现instance-controlled)
instance-controlled classes(Flyweight Pattern):- 保证是一个单例或者不可实例化
- 保证不存在两个完全相同的实例
- 静态工厂方法返回对象灵活,可以是自己或者是子对象
- 静态工厂方法可以根据参数返回不同的对象(自身或者是子对象,或者说是不同的实现)
- 静态工厂方法返回的对象在方法所在的类写入之前都可以不用存在
不足:
- 只提供静态工厂方法,由于没有公共或者保护的构造器无法被继承,扩展性有限
- 开发人员很难注意到静态工厂方法,java文档中构造器的文档比静态工厂方法更显眼(可以忽略)
常用静态工厂方法命名:
- from 单参数的类型转换
- of 多参数聚合转换
- valueOf from和of更详细的转换
- instance or getInstance 有可能返回相同的实例
- create or newInstance 返回新实例
- getType 同getInstance,如果工厂方法定义在另一个类中
- newType 同上
- type 同上
Item 2 : considering a builder when faced with many constructor parameters
构造器参数过多时考虑使用builder模式
builder模式在Java 9中其实使用非常多,比如介绍 Java9 HTTP2新特性 里面HttpClient HttpRequest等等的实例化方式都是builder模式。
静态工厂方法和构造器在面对大量可选参数时候的扩展性非常不好:
- 普通伸缩构造模式在参数多了的情况下调用代码难于编写和阅读
想象一下当我new 一个对象需要传入超过5个参数的时候,根本不知道参数先后顺序,还要看文档,是多么痛苦的一件事情。 - get,set方法过于啰嗦,虽然解决了上面的不足,但是在构造对象的过程中对象可能已经被改变,导致线程不安全,这是JavaBeans模式的一个短板
当一个类有超过10个属性的时候使用get,set方法简直就是一场灾难。
比如你可以像下面一样实现builder模式:
public class Human {
private String name;
private Integer id;
private char sex;
private Date birthday;
private Double height;
private Double weight;
private String motto;
public static class Builder {
private String name;
private Integer id;
private char sex;
private Date birthday;
private Double height;
private Double weight;
private String motto;
public Builder name(String name) {
this.name = name;
return this;
}
public Builder id(Integer id) {
this.id = id;
return this;
}
public Builder sex(char sex) {
this.sex = sex;
return this;
}
public Builder birthday(Date birthday) {
this.birthday = birthday;
return this;
}
public Builder height(Double height) {
this.height = height;
return this;
}
public Builder weight(Double weight) {
this.weight = weight;
return this;
}
public Builder motto(String motto) {
this.motto = motto;
return this;
}
public Human build() {
return new Human(this);
}
}
private Human(Builder builder) {
name = builder.name;
id = builder.id;
sex = builder.sex;
birthday = builder.birthday;
height = builder.height;
weight = builder.weight;
motto = builder.motto;
}
}
构造一个Human:
Human.Builder builder = new Human.Builder();
builder.id(1).name("Tom").birthday(new Date()).height(1.8).weight(60.0).motto("Hello World");
builder.build();
builder模式的优缺点:
- 更加灵活,安全,对参数可以做更多的处理,调用代码可读性好
- 在性能要求极端的场景下builder创建所消耗的性能是不被容忍的
- 从传统的get,set或者构造方法迁移的时候代码量大(删除旧代码),所以最好一开始就使用builder模式
使用场景:
- 参数过多难以处理,特别是可选参数过多时
- 安全
Item 3 : making singleton property private constructor or enum type
用枚举代替单例属性或者给它提供一个私有构造方法
构造一个单例:
- 声明一个公共静态final属性,无参构造方法私有
优点是api清晰,属性是final的表明其是个单例 - 声明一个私有静态final属性,提供一个公共静态获取实例方法返回该属性,同样无参构造方法私有
优点是灵活性,返回的实例可以根据需要修改,不用修改api来决定是否是单例;
可以写一个通用的单例工厂方法,如果需要的话;
可以作为一个 Supplier 比如:Supplier supplier = SingletonEnforce::getInstance; supplier.get();
但是上面第二种方法有一定的危险,因为私有的构造方法也有可能被调用:
Constructor constructor = Class.forName("me.minei.effective.SingletonEnforce").getDeclaredConstructor();
constructor.setAccessible(true);
constructor.newInstance();
这种情况在私有构造方法里判断是否实例化过,已经实例化抛出异常即可。
声明一个单元素枚举,通常这是最好的方法来实现单例,如果要扩展一个非枚举子类的话就不用这种方式实现:
public enum SingletonEnum {
INSTANCE;
}
使用前两种方式实现的单例如果需要序列化的话不光需要实现 Serializable 接口,同时所有实例属性都需要加 transient 关键字和提供一个私有的 readResolve 方法:
private Object readResolve() {
return INSTANCE;
}
否则每次反序列化时都会有新的实例创建。
Item 4 : Enforce noninstantiability with a private constructor
给不能实例化对象强制实现一个私有构造方法
对于不需要实例化的类提供一个私有构造器(里面抛出异常等等处理)来避免被实例化,通常是对于一些接口,使接口抽象是不可取的因为可以被继承然后实例化。
这样做不好的一点就是无法被继承。
Item 5 : Prefer dependency injection to hardwiring resources
优先依赖注入底层资源
静态聚合工具类(接口)和单例不适合依赖参数实例化的场景,简单说就是不要用单例或者是静态聚合类(接口)去实现一个依赖除自身的类,
并且不要用类直接去创建这些资源,传递给构造器(builder工厂方法)去处理,提升类的灵活性、可重用性和可测试性。
这种情况下需要将参数传递给构造器来实例化(依赖注入模式)
这种方式的一个变种是传入一个 factory 给构造器,这个 factory 可以重复的返回对象实例(Factory Method pattern)
Java8中的 Supplier
传入构造器的最好是一个通配的类型以便于扩展:
Mosaic create(Supplier<? extends Tile> tileFactory) { ... }
Item 6 : Avoid creating unnecessary objects
避免创建不必要的对象
- 适当的重用对象
- 避免创建无用的和重复的对象
- 可通过静态工厂方法来避免创建不必要的对象
- 有时候在调用工厂方法的时候不希望某个final属性被初始化而是当调用涉及到的方法的时候才初始化(后面会有提到),这样做性能提升不明显
- 多使用基本数据类型,小心隐藏的自动拆装箱带来的性能损耗
比如下面的代码:private static long sum() { Long sum = 0L; for (long i = 0; i <= Integer.MAX_VALUE; i++) sum += i; return sum; }
如果上面sum改成long性能会提升巨大
Item 7 : Eliminate obsolete object references
及时处理过期对象引用
- 无意的对象引用保留可能会造成gc无法回收,内存泄露,无用对象直接置空即可(null)
- 不管任何时候,只要是一个类自己管理内存,程序员都要小心内存泄露(数组等等)
缓存可能造成内存泄漏(无用对象没有及时被回收)
- 使用WeakHashMap
- 启用一个后台进程去清理过期缓存
监听器和其他回调可能导致内存泄露
- 可以使用 weak references 将其放入 WeakHashMap
内存泄露往往不是立刻发生,而是可能潜伏在系统多年,只有发生了才会体现出来,可以使用 http://goog-perftools.sourceforge.net/doc/heap_profiler.html 来帮助分析
Item 8 : Avoid finalizers and cleaners
避免使用 finalizer 和 cleaner
- Java9中finalizers被cleaners替代,finalizer和cleaner不能保证被及时执行
- 绝对不要在finalizer和cleaner中做一些关键操作,比如关闭文件
- 不要依赖finalizer和cleaner修改一些持久状态,比如释放公共资源的锁
System.gc和System.runFinalization两个方法虽然会增加finalizer和cleaner被执行的几率但是并不能保证,
只保证System.runFinalizersOnExit和Runtime.runFinalizersOnExit肯定被执行,但是后面的两种方法已经被弃用。 - finalization过程中抛出的未捕获异常被忽略,导致对象的finalization终止,cleaner没有这个问题因为cleaner可以自己控制进程
- 使用finalizers和cleaners有严重的性能缺陷
- finalizers有一个严重的安全问题:容易被 finalizer 攻击
如果构造器或者与序列化等价的方法(比如 readObject, readResolve)抛出异常,一个恶意子类的 finalizer 可以运行在部分构建的实例上,
这个恶意子类可以引用通过一个静态属性引用父类防止父类被gc回收,一旦这个引用成功,那就很容易执行父类中并不允许存在的方法。
解决:只需提供一个final finalize 空方法即可
可以实现 AutoCloseable 来代替finalizer和cleaner,然后实现其 close 方法即可,每个实例不再使用调用该方法即可,一般使用 try-with-resources 来确保万一。
值得一提的是,这种类最好有一个字段来记录一个实例是否被 close ,如果不小在实例已经被 closed 之后再次调用,记得抛出 IllegalStateException 异常。
finalizers和cleaners两个正确的用法:
- 资源占用着忘记调用上面提到的 close 方法来释放资源,虽然也并不能保证 finalizer 和 cleaner及时执行,但总比什么都不做好,但也需要考虑性能的缺失是否值得
- 处理 native peers, 因为 native peer 不是普通的java对象,所以gc不知道它也无法回收它,而finalizers和cleaners适合来完成这个任务
总的来说不要使用 cleaners 或者不要使用 Java9之前的 cleaners;
不要使用 finalizers ,除非需要安全操作或者关闭重要资源,并且留意他们的不确定性和性能。
Item 9 : Prefer try-with-resources to try-finally
优先使用 try-with-resources 而不是 try-finally
Java中有很多的资源需要手动释放,执行 close 方法即可,比如 InputStream,BufferedReader 等等,他们都直接或者间接的实现了 AutoCloseable 接口。
使用需要释放的资源时尽可能用 try-with-resources ,可以提升代码可读性,更加简洁清楚,异常堆栈信息更有利于排查bug,比如下面的代码:
private static final int BUFFER_SIZE = 16;
static void copy(String src, String dst) throws IOException {
InputStream in = new FileInputStream(src);
try {
OutputStream out = new FileOutputStream(dst);
try {
byte[] buf = new byte[BUFFER_SIZE];
int n;
while ((n = in.read(buf)) >= 0)
out.write(buf, 0, n);
} finally {
out.close();
}
} finally {
in.close();
}
}
private static final int BUFFER_SIZE = 16;
static void copy(String src, String dst) throws IOException {
try (InputStream in = new FileInputStream(src); OutputStream out = new FileOutputStream(dst)) {
byte[] buf = new byte[BUFFER_SIZE];
int n;
while ((n = in.read(buf)) >= 0) {
out.write(buf, 0, n);
}
}
}
可以明显看到使用try-with-resources 代码简洁很多,更加清楚。