Effective Java 读书笔记 - 第二章
这一章主要介绍了对象的创建和销毁。包括:
- 何时创建对象;
- 如何创建对象;
- 何时应该避免创建不必要的对象;
- 如何避免创建必须要的对象;
- 如何确保对象适时的销毁;
- 如何管理对象销毁时的清理操作。
Item 1:使用静态工厂方法替代构造函数
除了使用构造函数获取对象以外,还有另一种方式可以用于获取对象,那就是__静态工厂方法__。比如Boolean.valueOf(Boolean b)
方法。
public static Boolean valueOf(String s) {
return parseBoolean(s) ? TRUE : FALSE;
}
public static boolean parseBoolean(String s) {
return ((s != null) && s.equalsIgnoreCase("true"));
}
相较于构造方法,静态工厂方法有如下优势:
- 静态工厂方法拥有明确的方法名。
如果静态工厂方法名恰到好处,API 的使用者可以很容易的使用这个方法并且写出来的代码更易读,比如
BigInteger(int, int, Randong)
,这个构造函数返回一个 BigInteger 对象,它的值可能是一个素数,达到这个目的,但是更合适的方式是调用BigInteger.probablePrime(int , Random)
这个静态工厂方法。通过方法名可以明确的向调用者屏蔽多参构造方法,从而使得接口类更易使用。 - 静态工厂方法不会可以复用已经创建的对象。 单例模式正式通过这种方式返回单一对象的。
- 静态工厂方法可以返回一个类型的任意子类型。
如果希望通过隐藏具体的实现细节从而提供简洁的 API,通过静态工厂方法可以很方便的实现,基于接口的框架技术正是这种方法的典型应用:接口为静态工厂方法提供了返回类型
Type
,但是接口不能有静态方法,所以接口的静态方法定义在名为Types
的类中(该类无法实例化),例如java.util.Collections
。 通过这种方式也可以根据参数确定具体使用的实现类,而隐藏具体的实现,例如java.util.EnumSet
类的noneOf()
方法,它会根据每次传入的枚举类型的值来确定使用RegularEnumSet
还是JumboEnumSet
实现类,而这种实现类对使用者是不可见的。 静态工厂方法返回的对象类型在静态工厂类编写的时候,甚至可以不必存在,而是后续再提供,这就构成了_服务提供框架_的基础。例如 JDBC 中,Connection
类是静态工厂方法DriverManager.getConnection()
需要返回的接口类,但是具体的Connection
子类需要实现 JDBC 规范的数据库厂商来提供,而 JDBC 服务的提供,需要实现服务提供接口Driver
,并将这个接口通过DriverManager.registerDriver()
进行注册。 - 静态工厂方法可以减少代码冗余。
如果正常的创建一个指定类型参数的 HashMap,使用构造方法的代码如下:
Map<String, List<String>> map = new HashMap<String, List<String>>();
如果需要构造多个这样的对象,显然代码冗长,所以可以定义一个静态工厂方法:
public static <K, V> HashMap<K, V> newInstance() { return new HashMap<K, V>(); }
再构造一个
HashMap
对象就只需要调用newInstance
方法了。
Item 2:使用构建器替代多个参数的构造方法
静态工厂方法和构造方法有一个共同的缺陷,即一个类在有很多可选参数的情况下,很难扩展,而构造函数针对这种情况,只能定义多个重载方法来接受不同的可选参数,然而如果可选参数的类型大部分都一致,就会造成误用的情况。另外针对这种情况,也可以使用 Java Bean 的方式解决,但是 Java Bean 代码冗长,而且容易遗漏部分的参数设置,此外 Java Bean 无法构造不可变对象。
针对这种可选参数过多的需求,构建器(Builder)是一个相对合适的选择,如类 NutritionFacts
的源码所示。
//构建器
public class NutritionFacts {
// Required
private final int servingSize;
private final int servings;
// Optional
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) {
this.servingSize = servingSize;
this.servings = servings;
}
public Builder calories(int val) {
this.calories = val;
return this;
}
public Builder fat(int val) {
this.fat = val;
return this;
}
public Builder carbohydrate(int val) {
this.carbohydrate = val;
return this;
}
public Builder sodium(int val) {
this.sodium = val;
return this;
}
public NutritionFacts build() {
return new NutritionFacts(this);
}
}
private NutritionFacts(Builder builder) {
this.servingSize = builder.servingSize;
this.servings = builder.servings;
this.calories = builder.calories;
this.fat = builder.fat;
this.sodium = builder.sodium;
this.carbohydrate = builder.carbohydrate;
}
}
Item 3:使用私有构造方法或枚举类型强化单例属性
在 Java 1.5 之前,有两种方法时限单例,第一种时提供一个 public 的静态的成员变量,第二种方法时提供一个静态工厂方法,同时有一个私有的无参构造方法。
public class Elvis {
public static final Elvis FIELD_INSTANCE = new Elvis(); // Method 1
private static final Elvis FACTORY_INSTANCE = new Elvis(); // Method 2
public Elvis getInstance() {
return FACTORY_INSTANCE;
}
// Both need a private constructor
private Elvis() {
}
public void m(){
}
}
需要注意的是,在反序列化的场景,需要将每个成员变量声明为 transient
,并且提供一个 readResolve()
方法,否则,每次反序列化都会创建一个对象。
在 JDK 1.5 发布之后,可以使用枚举来实现单例,这与第一种方法类似,但是它并没有反序列化的问题。
public enum Elvis {
INSTANCE;
public void m(){
}
}
Item 4:使用私有构造方法禁止类被实例化
如果需要一个只包含静态方法和静态成员变量的工具类,那么这个类不应该被实例化。为了达到这个目的,可以给这个类添加一个私有的无参构造方法。
public class UtilityClass {
private UtilityClass() {
throw new AssertionError();
}
public static void m() {
// Do something
}
}
Item 5:避免创建必须要的对象
使用静态工厂方法而不是构造函数返回对象,可以有效避免创建不必要的对象。例如 Boolean.valueOf(String)
每次返回的都是一个常量。除了使用不可变对象时可以避免重复创建,使用可变对象时,也可以采取措施避免重复创建对象。例如 Person
类的 isBabyboomer
方法。
public class Person {
private final Date birthDate;
public Person(Date birthDate) {
this.birthDate = birthDate;
}
public boolean isBabyboomer() {
Calendar gmtCal = Calendar.getInstance(TimeZone.getTimeZone("GMT"));
gmtCal.set(1946, Calendar.JANUARY, 1, 0, 0, 0);
Date boomStart = gmtCal.getTime();
gmtCal.set(1965, Calendar.JANUARY, 1, 0, 0, 0);
Date boomEnd = gmtCal.getTime();
return birthDate.compareTo(boomStart) >= 0 && birthDate.compareTo(boomEnd) < 0;
}
}
在 isBabyboomer
方法中创建了 boomStart
和 boomEnd
这两个临时对象,但是这两个必须要每次调用该方法的时候都创建吗?尝试对这段代码进行改写如下。
public class Person {
private static final Date BOOM_START;
private static final Date BOOM_END;
static {
Calendar gmtCal = Calendar.getInstance(TimeZone.getTimeZone("GMT"));
gmtCal.set(1946, Calendar.JANUARY, 1, 0, 0, 0);
BOOM_START = gmtCal.getTime();
gmtCal.set(1965, Calendar.JANUARY, 1, 0, 0, 0);
BOOM_END = gmtCal.getTime();
}
private final Date birthDate;
public Person(Date birthDate) {
this.birthDate = birthDate;
}
public boolean isBabyboomer() {
return birthDate.compareTo(BOOM_START) >= 0 && birthDate.compareTo(BOOM_END) < 0;
}
}
这段代码避免在方法isBabyboomer
中创建了临时对象,因为在前一个版本的代码中,创建的两个临时对象在使用前后并没有改变过,所以可以将这两个对象事先创建并初始化,在需要的时候直接使用。
在 JDK 1.5 发布的对基本类型的自动装箱拆箱功能在使用不注意的情况下可能会创建不必须要的对象,比如这一段代码。
public static void sum() {
Long sum = 0L;
for(int i = 0; i <= Integer.MAX_VALUE; i++) {
sum += i;
}
System.out.println(sum);
}
由于已经将变量sum
的类型声明为Long
类型了,它时基本类型的包装类型,所以每次在计算sum += i
时都会创建一个新的对象,而这个新对象却并不是必须的,因为可以将sum
声明为long
类型,进而避免自动装箱拆箱机制。
Item 6:消除过期的对象引用
Java 会自动的为对象分配和释放内存,但是这并不代表 Java 程序员完全不需要关心内存。如果使用数组elements
来实现一个栈,使用变量size
来标识栈的大小,出栈的代码如下。
public Object pop() {
if(size == 0) {
throw new EmptyStackException();
}
return elements[--size];
}
这段代码会造成内存泄漏。加入一个栈的大小为 s1,多次调用出栈的方法后,栈的大小为 s2,处于 s2 到 s1 之间的引用对象其实时已经废弃的对象了,但是它并不能被垃圾收集器回收掉,因为这些对象还在被栈的 s2 到 s1 之间的元素引用。正确的做法应该是这样的。
public Object pop() {
if(size == 0) {
throw new EmptyStackException();
}
Object obj = elements[--size];
elements[size] = null;
return obj;
}
引起内存泄漏的另外两个主要的来源是:
- 缓存
- 监听器和回调
针对这些情况,可以采取多种方法解决,如使用WeakHashMap
,使用java.lang.ref
中的类。
Item 7:避免使用finalize
方法
在绝大多数情况下调用这个方法并不能解决问题。
- JVM 并没有保证在合适的时机调用这个方法,所以不可能在这个方法中调用对时间要求较高的方法。
- Java 语言规范并不保证这个方法一定被执行,所以不可能以来这个方法来释放资源。
- 该方法可能会带来严重的性能损失。
如果需要释放资源,请提供一个明确的方法用于调用,并结合try catch finally
一起使用。
finalize
方法主要有两种用途:
- 可以在该方法中回收资源操作,以防调用者没能显示调用资源的回收方法。
- 在该方法中回收本地资源(native resource),因为 Java 虚拟机不会自动回收本地对象资源。
当一个子类重写了父类的finalize
方法时,需要显示的调用父类的该方法,因为虚拟机不会自动调用父类的该方法,为了防止调用者犯错在子类中漏掉对父类方法的调用,需要为每个类定义一个附加对象。
public class Foo {
private final Object obj = new Object() {
@Override
protected void finalize() throws Throwable {
// Finalize outter Foo Object resource
}
};
}
这样,虚拟机在回收对象时,就会调用每个对象的finalize
方法了。