本章的主题是创建和销毁对象:何时以及如何创建对象,何时以及如何避免创建对象,如何确保它们能够适时地销毁,以及如何管理对象销毁之前必须进行的各种清理动作。
[toc]
一般来所,最好能重用对象而不是每次需要的时候就创建一个相同功能的新对象。重用方式既快速,又流行。如果对象是不可变的(immutable)(见第 15 条),它就始终可以被重用。
String
书中举了一个极端的反面例子:
String s = new String("stringette"); // DON'T DO THIS!
该语句每次执行的时候都创建一个新的String
实例,但是这是多余的,("stringette")
本身就是一个 String 实例,如果此语句在循环中,或是频繁调用的方法中,将会创建出成千上万不必要的 String 实例。
改进后的版本如下所示:
String s = "stringette";
这个版本只用了一个 String 实例,而不是每次执行的时候都创建一个新的实例,而且它可以保证,对于所有在同一台虚拟机中运行的代码,只要它们包含相同的字符串字面常量,该对象就会被重用。【这点跟 JVM 有关,上一个版本每次都会 new 一个实例,此版本将先查找字符串池,在判断是否创建对象】
原理:
- 在JAVA虚拟机(JVM)中存在着一个字符串池,其中保存着很多String对象,并且可以被共享使用,因此它提高了效率。由于String类是final的,它的值一经创建就不可改变,因此我们不用担心String对象共享而带来程序的混乱。字符串池由String类维护,我们可以调用intern()方法来访问字符串池。
- 而改进版本的这行代码被执行的时候,JAVA虚拟机首先在字符串池中查找是否已经存在了值为"abc"的这么一个对象,它的判断依据是String类equals(Object obj)方法的返回值。如果有,则不再创建新的对象,直接返回已存在对象的引用;如果没有,则先创建这个对象,然后把它加入到字符串池中,再将它的引用返回。
静态工厂
对于同时提供了静态工厂方法(见第1条)和构造器的不可变类,通常可以使用静态工厂方法而不是构造器。例如,Boolean.valueOf(String)
和 Boolean(String)
,因为构造器每次被调用的时候都会创建一个新的对象,而静态工厂方法则从来不要求,也不会这样做。【构造器需要 new 操作,静态方法在 JVM 中早已分配固定的空间】
已知不会被修改的对象
除了重用不可变的对象之外,也可以重用那些已知不会被修改的可变对象。下面是一个比较微妙、也比较常见的反面例子。
其中设计可变的 Date对象,它们的值一旦计算出来之后就不再变化。这个类建立了一个模型:其中有一个人,并有一个isBabyBoomer方法,用来检验这个人是否为一个"baby boomer(生育高峰期出身的小孩)",换句话所就是检验这个人是否出生于1946年至1964年期间。
public class Person {
private final Date birthDate;
// Other fields, methods, and constructor omitted
// DON'T DO THIS!
public boolean isBabyBoomer() {
// Unnecessary allocation of expensive object
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
每次被调用的时候,都会新建一个Calendar、一个TimeZone和两个Date实例,这是不必要的开销。
静态的初始化器
此版本用一个静态的初始化器(initializer),避免了这种效率低下的情况。
class Person {
private final Date birthDate;
// Other fields, methods, and constructor omitted
/**
* The starting and ending dates of the baby boom.
*/
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();
}
public boolean isBabyBoomer() {
return birthDate.compareTo(BOOM_START) >= 0 &&
birthDate.compareTo(BOOM_END) < 0;
}
}
- 改进后的
Person
类只在初始化的时候创建 Calendar、TimeZone 和 Date 实例一次,而不是在每次调用 isBabyBoomer 的时候创建这些实例。 - 在作者的机器上,没调用一千万此,原版本需要 32 000ms ,而改进版本只需要 130 ms ,大约快了250倍。性能显著提高,代码含义也更加清晰。
- 可以通过延迟初始化(lazily initializing(见第71条)),将
BOOM_START
BOOM_END
初始化延迟到第一次调用isBabyBoomer
的时候,但不建议,这样会是方法的实现更加浮渣,无法将性能显著提高到超过已经达到的水平(见第55条)。
自动装箱(autoboxing)
在Java1.5发行版中,允许程序员将基本类型和他们的装箱基本类型(包装类(Boxed Primitive Type))混用,感觉很神奇,它使得基本类型和它们的包装类之间变得模糊,其实只是语法糖,底层依然老老实实的构造装箱基本类型实例【可通过反编译查看】。如果无意识的在循环中使用了此技术,将创建非常多个中间对象,使性能继续下降【Long sum
,long i
】:
// Hideously slow program! Can you spot the object creation?
public static void main(String[] args) {
Long sum = 0L;
for (long i = 0; i < Integer.MAX_VALUE; i++) {
sum += i;
}
System.out.println(sum);
}
要优先使用基本类型而不是装箱基本类型,要当心无意识的自动装箱。
总结
不要错误地认为本条目所介绍的内容暗示着“创建对象的代价非常昂贵,我们应该要尽可能地避免创建对象”。相反,由于小对象的构造器只做很少量的显式工作,所以小对象的创建和回收动作是非常廉价的,特别是现代的JVM实现上更是如此。通过创建附加的对象,提升程序的清晰性、简洁性和功能性,这通常是好事。
- 除非是非常重量级的对象,如数据库链接池等,否则应该避免维护自己的对象池(object pool)来避免创建对象。现代的JVM实现具有高度优化的垃圾回收器(CG),其性能很容易就会超过轻量级对象池的性能。
本条目对象的是第39条中有关“保护性拷贝(defensive copying)”的内容。本条目提及“当你应该重用现有对象的时候,请不要创建新的对象”,而第39条则说“当你应该创建新对象的时候,请不要重用现有对象”。注意,在提倡使用保护性拷贝的时候,因重用对象而付出的代价要远远大于因创建重复对象而付出的代价。必要时如果没能实施保护性拷贝,将会导致潜在的错误和安全漏洞;而不必要地创建对象则只会影响程序的风格和性能。
- 类似于一个副本的概念:假设在图书馆借了一本书,如果你直接在上面做笔记,但这本书是共享的,将影响他人借阅。如果你是花钱复印一本,在复印书上做笔记就不同了。