Effective Java 读书笔记(一):创建和销毁对象
考虑用静态工厂方法代替构造器
静态工厂方法相比构造器,有何优势?
- 它们有名称:如果构造器参数本身不能描述正被返回的对象,那么具有适当名称的静态工厂方法会更容易使用,产生的客户端代码也更易于阅读。
- 不必在每次调用产生新的对象:起到缓存的作用,方便复用。比如 Bookean 的 valueOf 方法,类似于 Flyweight 模式。
public static Boolean valueOf(boolean b) {
return (b ? TRUE : FALSE);
}
- 可以返回原返回类型的任何子类型对象,提供更大的灵活性。这项技术适合基于接口的框架,用户知道,被返回的对象由相关接口精确指定,所以不需要阅读具体类的文档。
那么有什么缺点吗?
- 静态工厂方法代替构造器,构造器变为 private 的话,该类就不能子类化了。
- 静态工厂方法与其他静态方法没啥区别,如果文档没有明确标识,就不容易被发现。这一点可以在方法命名方面遵循一些惯例来解决:
- valueOf / of:该方法返回的实例与参数有相同的值,实际上做的是类型转化的事儿。
- getInstance:比较一般化的命名,如无特殊需要就用它了。
- newInstance:跟 getInstance 类似,但是 newInstance 确保每次返回的都是新的实例。
- getType / newType:静态工厂方法位于不同类的时候使用。
构造器参数较多时考虑用 Builder 构建
Builder 模式有何优点?
- 可以模拟具名的可选参数,代码更易读。
- 可以做更复杂的事情,比如参数校验。
Builder 模式有何缺点呢?需要写更多代码,在十分注重性能的场合,可能不是一个好的选择。
用私有构造器或枚举类型强化 Singleton 属性
私有构造器实现单例代码如下:
public class Singleton {
private static final Singleton INSTANCE = new Singleton();
private Singleton() { }
public static Singleton getInstance() {
return INSTANCE;
}
}
这种方法有一个缺点:如果加上 implements Serializable 提供 Java 序列化功能,那么在反序列化会生成新的实例,要解决这个问题的话,可以在其中加一个 readResolve 方法:
public class Singleton implements Serializable{
private static final Singleton INSTANCE = new Singleton();
private Singleton() { }
public static Singleton getInstance() {
return INSTANCE;
}
private Object readResolve()
throws java.io.ObjectStreamException {
return INSTANCE;
}
}
注:readResolve 是用于反序列化的,还有一个用于序列化的方法 writeReplace。序列化的时候,
首先 JVM 会先调用 writeReplace 方法,在这个阶段,我们可以进行张冠李戴,将需要进行序列化的对象换成我们指定的对象。
另一个更简单的提供单例模式的方法是使用枚举,枚举天然提供序列化机制,而且绝对能防止多次实例化。
public enum SingletonEnum {
INSTANCE;
public void justAFunction() {}
}
在私有构造器里抛异常来强化不可实例化的能力
这通常用于只想提供静态方法的工具类中:
public class Tool {
private Tool() {
throw new AssertionError();
}
}
避免创建不必要的对象
- 要优先使用基本类型,而非装箱基本类型,要当心无意识的自动装箱。
- 如果对象是不可变的,它就可以始终被重用,比如 String:
// 错误做法:浪费性能
String a = new String("hello");
// 正确做法
String a = "hello";
- 优先使用静态工厂方法而非构造器,比如 Boolean.valueOf。
消除过期的引用
可以加速内存释放,减少内存泄漏,比如 Stack 对象中的 pop 方法,移除一个对象时,除了修改栈顶指针,还会把数组对应位置置 null。
public class Stack<E> extends Vector<E> {
public synchronized E pop() {
E obj;
int len = size();
obj = peek();
removeElementAt(len - 1);
return obj;
}
public synchronized void removeElementAt(int index) {
// 省略一些代码
elementCount--;
elementData[elementCount] = null; /* to let gc do its work */
}
}
清空引用应该是一种例外,而非规范。消除过期引用的最好方法是让包含该引用的变量结束其生命周期。如果你在最紧凑的作用域范围内定义每一个变量,这种情形就会自然而然的发生。那么何时应该清空引用呢?一般而言,只要类是自己管理内存,程序员就应该警惕内存泄露问题。
避免使用终结方法 finalize
- Java 语言规范并不保证终结方法会被及时执行,甚至不保证他们会被执行。
- 性能很差。
参考文献
- Effective Java 第二版
- 关于 Java 对象序列化您不知道的 5 件事