第2章 创建和销毁对象
1. 考虑用静态工厂方法代替构造器
- 静态工厂方法与构造器不同的第一大优势在于,他们有名称,产生的客户端代码更容易阅读。
- 静态工厂方法与构造器不同的第二大优势在于,不必在每次调用它们的时候都创建一个新的对象。
Boolean.valueOf(boolean)
- 静态工厂方法与构造器不同的第三大优势在于,它们可以返回原返回类型的任何子类型的对象。
java.util.EnumSet
没有公有构造器,只有静态工厂方法。它们返回两种实现类之一,具体则取决于底层枚举类型的大小:如果它的元素有64个或者更少,就像大多数枚举类型一样,静态工厂方法就会返回一个RegularEnumSet
实例,用单个long进行支持;如果枚举类型有65个或者更多元素,工厂就返回JumboEnumSet
实例,用long数组进行支持。 - 静态工厂方法的第四大优势在于,在创建参数化类型实例的时候,它们使代码变得更加简洁。在调用参数化类的构造器时,即使类型参数很明显,也必须指明。这通常要求你连续两次提供类型参数。
Map<String, List<String>> m = new HashMap<String, List<String>>();
,随着类型参数变得越来越长,越来越复杂,这一冗长的说明也很快变得痛苦起来。但是有了静态工厂方法,编译器可以替你找到类型参数,这称作类型推导。
public static <K, V> HashMap<K, V> newInstance() {
return new HashMap<K, V>();
}
- 静态工厂方法的主要缺点在于,类如果不含共有的或者受保护的构造器,就不能被子类化。
- 静态工厂方法的第二个缺点在于,他们与其他静态方法实际上没有任何区别。下面是静态工厂方法的一些惯用名称:
valueOf
,of
,getInstance
,newInstance
,getType
,newType
。
2. 遇到过个构造器参数时要考虑用构建器
静态工厂方法和构造器有个共同的局限性:它们都不能很好地扩展到大量的可选参数。重叠构造器模式可行,但是当有许多参数的时候,客户端代码会很难编写,并且仍然较难以阅读。JavaBeans模式,这种模式下,调用一个无参构造器来创建对象,然后调用setter方法来设置每个必要的参数,以及每个相关的可选参数。JavaBeans模式自身有很严重的缺点,因为构造过程被分到了几个调用中,在构造过程中JavaBean可能处于不一致的状态。类无法仅仅通过检验构造器参数的有效性来保证一致性。JavaBeans模式阻止了把类做成不可变的可能,这就需要程序员付出额外的努力来确保它的线程安全。
Builder模式不直接生成想要的对象,而是让客户端利用所有必要的参数调用构造器(或者静态工厂),得到一个builder对象,然后客户端在builder对象上调用类似于setter的方法,来设置每个相关的可选参数,最后,客户端调用无参的build方法来生成不可变的对象,这个builder类是它构建的类的静态成员类。builder模式模拟了具名的可选参数。
与构造器相比,builder的微略优势在于,builder可以由多个可变的参数varargs。构造器就像方法一样,只能有一个可变参数。
如果类的构造器或者静态工厂中具有多个参数,设计这种类时,Builder模式就是种不错的选择。特别是当大多数参数都是可选的时候,与使用传统的重叠构造器模式相比,使用Builder模式的客户端代码将更易于阅读和编写,构建器也比JavaBeans更加安全。
3. 用私有构造器或者枚举类型强化Singleton属性
Java1.5之前实现Singleton有两种方法,这两种方法都要把构造器保持为私有的,并导出公有的静态成员,以便允许客户端能够访问该类的唯一实例。
为了使Singleton类变成是可序列化的,仅仅在声明中加上implements Serializable
是不够的。为了维护名保证Singleton,必须声明所有实例域都是瞬时transient的,并提供一个readResolve
方法。否则,每次反序列化一个序列化的实例时,都会创建一个新的实例。
从Java1.5起,实现Singleton还有第三种方法,只需编写一个包含单个元素的枚举类型。这种方法在功能上与公有域方法相近,但是它更加简洁,无偿提供了序列化机制,绝对防止多次实例化,即使是在面对复杂的序列化或者反射攻击的时候。
Constructor.java newInstance
if ((clazz.getModifiers() & Modifier.ENUM) != 0)
throw new IllegalArgumentException("Cannot reflectively create enum objects");
Enum.java
/**
* enum classes cannot have finalize methods.
*/
protected final void finalize() { }
/**
* prevent default deserialization
*/
private void readObject(ObjectInputStream in) throws IOException,
ClassNotFoundException {
throw new InvalidObjectException("can't deserialize enum");
}
private void readObjectNoData() throws ObjectStreamException {
throw new InvalidObjectException("can't deserialize enum");
}
4.通过私有构造器强化不可实例化的能力
企图将类做成抽象类来强制该类不可被实例化,这是行不通的。该类可以被子类化,并且该子类也可以被实例化。由于只有当类不包含显式的构造器时,编译器才会生成缺省的构造器,因此我们只要让这个类包含私有构造器,它就不能被实例化了。私有构造器这种习惯做法也有副作用,它使得一个类不能被子类化。所有的构造器都必须显式或隐式地调用超类superclass构造器,在这种情形下,子类就没有可访问的超类构造器可调用了。
5. 避免创建不必要的对象
String s = new String("stringette");
语句每次执行都创建一个新的String实例,但是这些创建对象的动作全都是不必要的。传递给String构造器的参数"stringette"本身就是一个String实例,功能方面等同于构造器创建的所有对象。String s = "stringette";
这个版本只用了一个String实例,而不是每次执行的时候都创建一个新的实例。
要优先使用基本类型而不是装箱基本类型,要当心无意识的自动装箱。
通过维护自己的对象池object pool来避免创建对象并不是一种好的做法,除非池中的对象是非常重量级的。
6. 消除过期的对象引用
所谓过期引用,是指永远也不会再被解除的引用。
一般而言,只要类时自己管理内存,程序员就应该警惕内存泄漏问题。一旦元素被释放掉,则该元素中包含的任何对象引用都应该被清空。
内存泄漏的另一个常见来源是缓存。如果你正好要实现这样的缓存:只要在缓存之外存在对某个项的键的引用,该项就有意义,那么就可以用WeakHashMap
代表缓存;当缓存中的项过期之后,它们就会自动被删除。记住只有当所要的缓存项的生命周期是由该键的外部引用而不是值决定时,WeakHashMap
才有用处。
“缓存项的生命周期是否有意义”并不是很容易确定,随着时间的推移,其中的项会变得越来越没有价值。在这种情况下,缓存应该时不时地清除掉没用的项。这项清除工作可以由一个后台线程(可能是Timer
或者ScheduledThreadPoolExecutor
)来完成,或者也可以在给缓存添加新条目的时候顺便进行清理。LinkedHashMap
类利用它的removeEldestEntry
方法可以很容易地实现后一种方案。对于更加复杂的缓存,必须直接使用java.lang.ref
。
内存泄漏的第三个常见来源是监听器和其他回调。确保回调立即被当做垃圾回收的最佳方法是只保存他们的弱引用(weak reference),例如,只将它们保存成WeakHashMap
中的键。
7.避免使用终结方法
终结方法(finalizer)通常是不可预测的,也是很危险的,一般情况下是不必要的。使用终结方法会导致行为不稳定,降低性能,以及可移植问题。
在C++中,析构器是回收一个对象所占用资源的常规方法,是构造器所必须的对应物。在Java中,当一个对象变得不可到达的时候,垃圾回收器会回收与该对象相关联的存储空间。C++的析构器也可以被用来回收其他的非内存资源。而在Java中,一般用try-finally块来完成类似的工作。
终结方法的缺点在于不能保证会被及时地执行,从一个对象变得不可到达开始,到它的终结方法被执行,所花费的这段时间是任意长的。这意味着,注重时间的任务不应该由终结方法来完成。
Java语言规范不仅不保证终结方法会被及时地执行,而且根本就不保证它们会被执行。不应该依赖终结方法来更新重要的持久状态。
如果未被捕获的异常在终结过程中被抛出来,那么这种异常可以被忽略,并且该对象的终结过程也会终止。未被捕获的异常会使对象处于破坏的状态,如果另一个线程企图使用这种被破坏的对象,则可能发生任何不确定的行为。正常情况下,未被捕获的异常将会使线程终止,并打印出stack trace,但是,如果异常发生在终结方法之中,则不会如此,甚至连警告都不会打印出来。
使用终结方法有一个非常严重的性能损失。
显式终止方法的典型例子是InputStream
,OutputStream
的close
方法,java.util.Timer
上的cancel
方法。显式终止方法通常与try-finally结构集合起来使用,以确保及时终止。
终结方法有什么好处呢?它们有两个合法用途:
- 第一个用途是,当对象的所有者忘记调用前面段落中建议的显示终止方法时,终结方法可以充当安全网。显示终止方法模式的示例中所示的四个类(
FileInputStream
,FileOutputStream
,Timer
,java.sql.Connection
),都具有终结方法,当它们的终止方法未能被调用的情况下,这些终结方法充当了安全网。
FileOutputStream.java
/**
* Cleans up the connection to the file, and ensures that the
* <code>close</code> method of this file output stream is
* called when there are no more references to this stream.
*
* @exception IOException if an I/O error occurs.
* @see java.io.FileInputStream#close()
*/
protected void finalize() throws IOException {
if (fd != null) {
if (fd == FileDescriptor.out || fd == FileDescriptor.err) {
flush();
} else {
/* if fd is shared, the references in FileDescriptor
* will ensure that finalizer is only called when
* safe to do so. All references using the fd have
* become unreachable. We can call close()
*/
close();
}
}
}
- 终结方法的第二种合理用途与对象的本地对等体native peer有关。本地对等体是一个本地对象native object,普通对象通过本地方法native method委托给一个本地对象。因为本地对等体不是一个普通对象,所以垃圾回收器不会知道它,当它的Java对等体被回收的时候,它不会被回收。在本地对等体并不拥有关键资源的前提下,终结方法正是执行这项任务最合适的工具。
终结方法链并不会被自动执行。如果累有终结方法,并且子类覆盖了终结方法,子类的终结方法就必须手工调用超类的终结方法。你应该在一个try块中终结子类,并在相应的finally块中调用超类的终结方法。这样做可以保证:即使子类的终结过程中抛出异常,超类的终结方法也会得到执行。