概述
通常最好重用单个对象,而不是在每次需要时都创建一个功能完全一样的新对象。重用不仅更快而且更流行。如果对象是不可变的(Immutable,Item15),那它总是能被重用的。
看下面这个极端的反例:
String s = new String("stringette"); // DON'T DO THIS!
这条语句每次执行的时候都会创建一个新的String实例,而且每个对象都是不必要的。String构造函数的参数("stringette")本身就是一个String实例,在功能上与所有通过该构造函数创建出来的对象是完全一致的。如果上述用法是在一个循环中,或者在一个经常被调用的方法中,则会创建出成千上万不必要的String实例。
改进后的版本非常简单:
String s = "stringette";
这个版本只用了一个单个String实例,而不是每次执行时都创建一个新实例。而且,它确保同一虚拟机中的其他代码重用,只要他们包含相同的字符串字面值。
对于同时提供了静态工厂方法和构造函数的不可变类,使用静态工厂方法总是能避免创建不必要的对象。【例】例如,静态工厂方法Boolean.valueOf(String)几乎总是比构造函数Boolean(String)更可取。构造函数每次被调用时都会创建一个新对象,而静态工厂方法则从来不要求这样做,实际上也不会这么做。
除了重用不可变对象,也可以重用那些已知不会被修改的可变对象。【例】下面是一个微妙的、常见的反例,其中涉及到可变的Data对象,其值一旦被计算出来就不会变更。这个类建立了一个Person模型,拥有一个isBabyBoomer方法判断该人是否baby boomer,即出生在1946到1964年之间的人。
public class Person {
private final Date birthDate;
// 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对象。下面这个版本通过静态初始化块避免了这种效率低下的问题。
class Person {
private final Date birthDate;
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方法每次调用的时候都去创建。如果isBabyBoomer被频繁调用的话这种方法能显著提高性能。在我的机器上,初始版本调用1千万次耗时32000毫秒;而改进版本只需130ms,快了250倍。除了性能提高了,代码页更加清晰,将boomStart和boomEnd从局部变量改为静态final变量,更清晰地表明它们是常量,似的代码更易读。这种优化带来的性能提升并不总是这么显著,因为Calendar对象的创建代价非常高昂。
在上例中,当Person类被初始化,而isBabyBoomer不被调用时,BOOM_START和BOOM_END变量也会被初始化,这是不必要的。可以通过在isBabyBoomer首次被调用时延迟初始化(Item71)这些变量来消除这种不必要的初始化,但是不建议这样做。正如延迟初始化中常见的情况一样,这样会使方法的实现变得复杂,而且并不能使性能大大超过已有水平(Item55)。
上例中,由于对象被初始化后不会被修改,所以他们显然是能被重用的;而有些情况则没有这么明显能看出来。【例】例如适配器(Adapter),或称为视图(View)。适配器是这样一种对象:它将功能委托给一个后备对象,给后备对象提供一个可选择的接口。由于适配器除了其后备对象外,没有其他的状态,所以对于给定的对象就没有必要创建多个适配器实例。
public abstract class AbstractMap<K,V> implements Map<K,V>
transient volatile Set<K> keySet = null; // volatile!!
public Set<K> keySet() {
if (keySet == null) {
keySet = new AbstractSet<K>() {
。。。
};
}
return keySet;
}
}
JDK1.5后有一种新的创建多余对象的情形: 自动装箱(autoboxing)。自动装箱允许程序员混用基本类型和装箱基本类型,根据需要进行自动装箱和自动拆箱。自动装箱使基本类型和装箱基本类型的区别变得模糊,但它们的区别并未被消除。它们之间有微妙的语义区别,也有显著的性能差别(Item49)。【例】考虑下面的例子,计算所有正整数的和,程序用longsuanfa,因为int不够大,不足以存储所有正整数的和:
// Hideously slow program!
public static void main(String[] args){
Long sum = 0L;
for(long i=0; i<Integer.MAX_VALUE; i++){
sum += i;
}
System.out.println(sum);
}
这个程序能计算出正确的答案,但是速度要慢得多,只因为打错了一个字符。sum变量被声明为Long而不是long,这意味着程序会创建大约2的31次方个不必要的Long实例(大约每次long类型的i和Long类型的sum相加时就会创建一个)。将Long修改为long后,在我的机器上的运行时间从43秒降为6.8秒。
教训很明显:优先使用基本类型,而不是装箱基本类型;并且当心不经意的自动装箱。
本条目并不是说创建对象代价很昂贵,于是要避免创建对象;相反,由于小对象的构造函数只做少量的显式工作,其创建与回收的代价是很小的,尤其是在现代JVM实现上。通过创建附加对象提高程序的清晰性、简洁性、功能性通常是件好事。
相反,通过维护自己的对象池来避免创建对象则是一个坏主意,除非对象池里的对象是非常重量级的。正确使用对象池的一个经典例子是数据库连接。建立数据库连接的成本是非常高的,所以重用这些对象是有意义的。同时,数据库license可能限制你只能建立一定数量的连接。但是,一般而言,维护自己的对象池会把代码弄得混乱,增加内存占用,损害性能。现代JVM实现具有高度优化的垃圾回收器,性能很容易就超过轻量级对象的对象池。
和本条目对应的是Item39,关于保护性拷贝(defensive copying)的条目。
- Item5:当应该重用一个已存在的对象时,就不要创建新对象;
- Item39:当应该创建一个新对象时,就不要重用已存在的对象。
注意,当需要保护性拷贝时却重用对象带来的代价,要远远大于创建不必要的多余对象的代价。当需要进行保护性拷贝却未做到,可能会导致潜在的bug和安全漏洞;而创建不必要的对象仅仅只影响编码风格和性能而已。
最后
以上就是害羞河马为你收集整理的【Effective Java】Ch2_创建销毁对象:Item5_避免创建不必要的对象的全部内容,希望文章能够帮你解决【Effective Java】Ch2_创建销毁对象:Item5_避免创建不必要的对象所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
发表评论 取消回复