@XingdingCAO
2017-12-13T14:00:55.000000Z
字数 4076
阅读 1793
Java
constructor
通常情况下,对一个物体的重复利用是优于反复创建等效新物体的。重用利用不仅更快而且拥有更好的格式。
——在Item 1中讨论过的静态工厂方法有一个特点——实例化可控,这样就可以返回相同实例的引用。而构造方法则必须在每次调用时生成新的实例。
——例如,Boolean
类中的静态工厂方法valueOf(String)
和构造方法Boolean(String)
。前者返回了类中的静态常量。而后者必然构造了新的实例。
public static boolean parseBoolean(String s) {
return ((s != null) && s.equalsIgnoreCase("true"));
}
public static final Boolean TRUE = new Boolean(true);
public static final Boolean FALSE = new Boolean(false);
public static Boolean valueOf(String s) {
return parseBoolean(s) ? TRUE : FALSE;
}
public Boolean(boolean value) {
this.value = value;
}
public Boolean(String s) {
this(parseBoolean(s));
}
如果一个类是不变的,那么它往往就是可重用的。下面,以对String
类的重用为例来说明一下:
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence {
/** The value is used for character storage. */
private final char value[];
/** Cache the hash code for the string */
private int hash; // Default to 0
public String(String original) {
this.value = original.value;
this.hash = original.hash;
}
...
}
//Disapproved
for (int i = 0; i < 10; i++) {
String str = new String("content");
...
}
从String
的源码可以看出,每次循环都会在程序运行时新建一个对相同字符数组的引用 和 相同的哈希码cache。这些循环中的初始化操作,无疑影响着性能的表现。
String str = "content";
for (int i = 0; i < 10; i++) {
...
}
改进后,不仅减少了重复初始化的性能损耗,而且String
类的实例可以被在相同JVM上同时进行的其他程序共享,更加高效。
关于String
类的重用,和普通类相比实际上较为复杂,想要进一步了解可参考阅读这篇文章。
就像不变类的重用一样,如果你已经事先知道了某个可变类的实例在运行时并不会变化,那么这个实例就可以被重用。例如,Date
类(可变类)的实例在其值被确定后就不再改变,那么就非常适合拿来重用。仔细观察下面的例子,你会发现虽然并未出现循环,但是某个频繁使用的方法中的内部变量也亟需重用,来减少那些容易被忽略的性能损耗。
public class Person{
private final Date birthDay;
//Other fields, methods and constructors omitted
//DON'T DO THIS
public boolean isBabyBoomer(){
//Umeccessary alloction 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;
}
}
如下,改进后的Person
类,只初始化了Calendar
TimeZone
Date
类的实例一次,而不是在方法被调用时反复初始化。这样改进后,性能的提升是非常明显的(一千万次执行的耗时从32000ms下降到了130ms);而且将类的内部变量独立成为类的静态内部常量使逻辑更为清晰,可读性更强。
public 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;
}
}
但是,上例的改进措施并不一定会获得巨大的性能提升,上例中的Calendar
类的初始化十分耗时才导致了性能的巨大提升。
另外值得注意的一点就是,在Person
类被使用时,BOOM_START
BOOM_END
字段的初始化为类的初始化带了额外的性能开销。为了减少额外的开销,可以将字段的加载延迟到实际使用时再进行,即:lazily initialize
。实际的操作可以是将字段封闭在静态内部类中,然后再通过实现完全的延迟初始化。但是,为了实现完全的延迟初始化,不仅带来了复杂的实现结构,而且又会损耗性能,可能会丢掉已经获得的巨大性能提升,得不偿失。
上面的例子很明显地需要提取重复的变量,从而实现重用,而还有一些情况并没有这么明显,需要编程人员保持敏锐的嗅觉。例如,适配器Adaptor
设计模式(也被称为视图view
设计模式)中我们就容易忽略其重用性。Adaptor
是一个负责后台对象的、为后台对象提供可选接口的对象。一个Adaptor
对象,除了它所代表的后台对象的状态,它本身是没有状态的。所以,创建多个Adaptor
对象是没有必要的。
再比如,Map
接口的keySet
方法返回了一个包含了映射中所有键值的Set
对象。看起来每次调用该方法,都会得到不同的Set
对象。但实际上,对于一个给定的Map
对象,多次调用该方法得到的是同一个Set
实例。尽管Set
实例是可变的,但是所有返回值却是一致的:一个返回的对象改变了,所有的返回对象都同时改变了,因为这个对象对应的是同一个Map
对象。返回多个keySet视图对象(由Map
产生,随它变化)是无害的,但也是没有必要的。
在Java 1.5中有了新的创建不必要对象的方式,叫做自动装(拆)箱(自动将原始类型装箱为包装类亦或反之)。自动装(拆)箱模糊了原始类型与包装类的界限,但并没有消除这个界限:还有一些细微的语义上的区别和不细微的性能上的差异。请看下面的程序,它将所有int
类型的正数加了起来。为了不产生最高位的进位,程序使用了Long
来保存结果。
// 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);
}
程序给出了正确的答案,但是它远比它本来应该的速度慢。原因就在一个字母的错误。变量sum
被声明为Long
类型而非long
类型,这就意味着这个程序创建了个Long
的实例。原程序的运行时间达到了43秒,而将sum
更改为long
类型后缩短到了6.8秒。这就告诉我们偏爱原始类型而少用包装类,并且小心潜在的拆装箱操作。
本篇文章告诉你的不是:创建实例是“昂贵”(耗费资源)的、应该避免的。相反,一些构造方法做了很少的事情,这些小的对象的创建与回收是“廉价”的,尤其在现代的JVM上。创建额外的对象去加强程序的明确性、简洁性或者通常是不错的选择。
维持你自己的对象池的对象构建是应该避免的,除非对象池中的对象都是重量级 (构建时非常耗费资源或时间)的。经典的案例就是数据库的连接(例如:Tomcat中DBCP2
),构建对象的成本足够高以至于重用是合理的。此外,数据库还可能限制了连接数的上限。大体上来讲,维持你自己的对象池会使你的代码杂乱不堪,增加了内存印记,还影响着性能。现代JVM有着高度优化的垃圾回收机制,在与轻量级的对象池的比拼上可以轻松胜出。