1. 用静态工厂代替构造器
总结:
优点主要源于静态工厂的灵活性,它有名称,可以返回灵活的类型,包括子类,隐藏类,可拓展类。为了可读性,最佳实践是遵守命名约定from,of,valueOf,instance,getInstance,crate,newInstance,getType,newType
优点:
- 有名称。构造器的缺点,参数列表多,有顺序,容易错误使用
- 不必创建新对象。例如享元模式,常量池设计等。
- 可以返回隐藏类型。参考Collections的实现(Collection的伴生类,由于JDK 1.8接口不支持静态方法,所以一般会实现伴生类封装相关的静态方法),其中实现了多种不同的集合,但这些集合对外隐藏,不必提供大量的独立的类,让API变得更复杂。
- 可以返回不同类型对象。根据传入参数的不同条件,返回不同类型的对象。
- 返回的类可拓展。例如JDBC,返回的类可以拓展多种数据库实现。
缺点:
- 不可子类化。鼓励复合而不是继承。
- 难以发现。相比于构造器,静态工厂方法的命名更加多样,这可以通过约定解决,常见的命名如
from,of,valueOf,instance,getInstance,crate,newInstance,getType,newType
2. 多个参数考虑使用构造器
总结:
适合大量可选参数场景,流式API可读性好,方便的有效性检查,方便的初始值设置。但builder类编写起来比较麻烦。常规的实现如下:
// Builder Pattern
public class NutritionFacts {
private final int servingSize;
private final int servings;
private final int calories;
private final int fat;
private final int sodium;
private final int carbohydrate;
public static class Builder {
// 必须非空参数
private final int servingSize;
private final int servings;
// 支持默认值
private int calories = 0;
private int fat = 0;
private int sodium = 0;
private int carbohydrate = 0;
public Builder(int servingSize, int servings) {
// 进行参数校验
Objects.requireNonNull(servingSize);
Objects.requireNonNull(servings);
this.servingSize = servingSize;
this.servings = servings;
}
public Builder calories(int val) {
calories = val;
return this;
}
public Builder fat(int val) {
fat = val;
return this;
}
public Builder sodium(int val) {
sodium = val;
return this;
}
public Builder carbohydrate(int val) {
carbohydrate = val;
return this;
}
public NutritionFacts build() {
return new NutritionFacts(this);
}
}
private NutritionFacts(Builder builder) {
servingSize = builder.servingSize;
servings = builder.servings;
calories = builder.calories;
fat = builder.fat;
sodium = builder.sodium;
carbohydrate = builder.carbohydrate;
}
}
优点:
- 对拓展大量可选参数的支持。静态工厂和构造器的不足正在于拓展大量可选参数的支持,例如通过
重叠构造器
的方法去解决,但只会使得代码难以编写、使用、阅读。 - 保持不可变性。
JavaBean
的设计模式能支持可选参数拓展,但是它破坏了类的不可变性。这样一来,就必须考虑更多的线程安全问题。而且它还会引起不一致的状态——类无法通过校验构造器参数的有效性来确保一致性。 - 有效性检查。可以在方法中,在build的时候检查参数的有效性。
不足:
- 必须创建构造器,有一定的开销。
- 构造器代码编写麻烦,尤其对于只有少量参数的情况下。
3. 使用枚举类强化单例属性
该实现方式和公有域很相似,但它更加简洁。枚举类带来的好处是,提供了序列化机制,保证即使面对序列化攻击和反射攻击都只序列化一次。
// Enum singleton - the preferred approach
public enum Elvis {
INSTANCE;
public void leaveTheBuilding() { ... }
}
4. 通过私有构造器强化不可实例化
很多时候想一些工具类Utils,静态类我们不希望它被实例化,但Java会提供一个默认的无参构造函数(如果你没提供任何构造函数)。这种场景下的最佳实践就是通过私有化构造器防止其被实例化。
// Noninstantiable utility class
public class UtilityClass {
// Suppress default constructor for noninstantiability
private UtilityClass() {
// 防止内部错误调用,可以主动抛出异常
throw new AssertionError();
}
... // Remainder omitted
}
5. 优先考虑依赖注入的方式引用资源
这是一种非常常用的模式,以至于大家都不知道它就是依赖注入。它可以提供引用资源的灵活性
控制反转
另外值得一提的是,它通常和控制反转联系起来,依赖注入是实现控制反转的一种方式,即使是通过简单的构造器、set方法设置,但它将依赖这件事从程序本身(指该类,例如SpellChecker)交给了外部调用者。
依赖注入的不足是它会使得依赖变得复杂,会让大型项目变得混乱,但现代的框架都非常好地解决了这个问题,例如spring。依赖注入和控制反转,将原本依赖设置从程序转交容器(使用者),使代码对修改封闭,又可以简单支持拓展其他依赖。
// Dependency injection provides flexibility and testability
public class SpellChecker {
private final Lexicon dictionary;
public SpellChecker(Lexicon dictionary) {
this.dictionary = Objects.requireNonNull(dictionary);
}
}
6. 避免创建不必要的对象
不必要的创建对象
在以下两种情况下,不应该创建不必要的对象:
- 对象为不可变对象,可重用
- 对象创建成本很高
编译器通常会为我们做很多事,但是很多时候又会引起没必要的对象创建,例如:
// 1. ""本身就会创建对象,而且用引号的话,有重复字符串会直接复用常量池中的内容
String s = new String("bikini"); // DON'T DO THIS!
// 2. 正则匹配看起来没问题,但是每次调用都会新建一个pattern,应该对pattern编译,进行复用
// Performance can be greatly improved!
static boolean isRomanNumeral(String s) {
return s.matches("^(?=.)M*(C[MD]|D?C{0,3})"
+ "(X[CL]|L?X{0,3})(I[XV]|V?I{0,3})$");
}
// 3. 自动装箱和拆箱,常见的问题
// Hideously slow! Can you spot the object creation?
private static long sum() {
Long sum = 0L;
for (long i = 0; i <= Integer.MAX_VALUE; i++)
sum += i;
return sum;
}
优先使用静态工厂
静态工厂方法可以灵活地实现这种对象的重用,例如valueOf
,所以永远优先调用静态工厂方法。
什么时候使用连接池
另一方面,需要注意,对于现代编译器而言,创建对象的开销实际上非常小,所以大多数情况下,自己用线程池管理内存是没有必要的,但一种情况除外——对象创建开销极大。典型的例子如JDBC的连接池,线程池等等。
7. 消除过期的对象引用——避免内存泄漏
先来看一个栈的实现,看起来一点问题都没有。
// Can you spot the "memory leak"?
public class Stack {
private Object[] elements;
private int size = 0;
private static final int DEFAULT_INITIAL_CAPACITY = 16;
public Stack() {
elements = new Object[DEFAULT_INITIAL_CAPACITY];
}
public void push(Object e) {
ensureCapacity();
elements[size++] = e;
}
public Object pop() {
if (size == 0)
throw new EmptyStackException();
return elements[--size];
}
/**
* Ensure space for at least one more element, roughly
* doubling the capacity each time the array needs to grow.
*/
private void ensureCapacity() {
if (elements.length == size)
elements = Arrays.copyOf(elements, 2 * size + 1);
}
}
实际情况是,当栈里边元素被pop之后,这些元素永远不会被回收。所以需要在pop之后,把pop掉的元素(即size - 1)置为null。
如果你需要自己管理内存
Java的内存泄漏问题看起来非常棘手,隐蔽,但作者又指出,每次都刻意去清空对象是没有必要的。
清空对象是一种例外,不是一种规范。那么什么情况下需要去注意内存泄漏的问题呢?——答案是,当类自己管理内存的时候。
比如上述的stack自己用数组作为底层容器。
比如threadLocal中的map,类中存储数据的各种map容易出现这种问题。
比如监听、观察者模式,注册监听之后没有显示取消。
利器:弱引用
ThreadLocal就是使用弱引用,又比如JDK中就有这种容器,例如WeakHashMap。他们的核心是:引用对象的生命周期由外部引用决定,而不是值引用决定。弱引用决定了当一个对象只剩下弱引用的时候,一旦它被gc扫描到它就会被回收。
8. 避免使用终结方法
finalizer是在gc回收前会执行,由于gc的时间是不确定的,该方法的调用时机就是无法预期的。可想而知,如果用它来回收资源,那可能会造成严重的资源消耗。
终结方法的合理使用场景有两个:
- 作为回收资源的一种兜底,有总比没有好,慢一点回收总比不回收好
- 回收本地对象。由于本地对象,JVM的gc无法检测到,所以可以在终结方法回收类相关的本地对象。
此外,终结方法存在一个隐蔽的安全隐患:终结方法攻击,大致是本来创建失败的对象(例如构造器中创建失败抛出异常),它不应该存在,但恶意子类可以实现终结方法,让该对象引用记录在静态域中,使得它回收不了。解决方式,避免方法被重写的典型方法是,实现一个final的空终结方法。
9. try-with-resources优先于try-finally
来看try-finally的方式:
// try-finally is ugly when used with more than one resource!
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();
}
}
该实现的问题在于:
- 需要手动close,容易忘记
- 代码丑
- 容易吞异常,第一个out.close引发异常会被吞掉
最佳实践是try-with-resources,它要求相关资源类需要实现AutoCloseable方法,它解决了上述所有问题。
// try-with-resources on multiple resources - short and sweet
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);
}
}