本栏是博主根据如题教材进行Java进阶时所记的笔记,包括对原著的概括、理解,教材代码的报错和运行情况。十分建议看过原著遇到费解地方再来参考或与博主讨论。致敬作者Joshua Bloch跟以杨春花为首的译者团队,以及为我提供可参考博文的博主们。
避免创建不必要的对象
很容易理解的一条,无论是环保还是编程,重用都可以节省开销,降低成本,提高效率。所以能够重用已创建的实例时,就不用再重新new一个出来。但实际开发中,我们总是一不小心又犯了这种错误,比如
String s1 = "bikini"; //recommend
String s2 = new String("bikini"); //blame
使用上面的构造方法的话,就总是会新建一个对象,而不管字符串池中有没有一样的,大大提高了内存开销,降低了效率。这得怎么避免?
1.不可变的对象应该被重用
还是用上面String创建的例子。字符串类型String维护了自己的字符串池(String Pool),通过双引号间的常量,先查找字符串池中有没有对应已创建好的字符串,没有才会新建实例,并保存到字符串池中。这样在创建频繁,重复率高的条件下,内存开销会大大减小。
2.已知不会被修改的可变对象也应该被重用
比如这个例子,检测是否在生育高峰期出生时,起止时间日期一旦被计算出来就不会被更改,这时候考虑重用
/**
* 用于说明第二条
*
* @see #badIsBabyBoomer() 这是差评的判断方法,每次调用都会创建一个Calendar和两个Date,
* 而返回的却是一样的两个日期。
*
* 这时候最好把生育高峰的起止时间提出来作为常量保存,并在类的static块中对其进行初始化。
* 之后使用这种方式{@link #isBabyBoomer()}进行判断,在多次调用的情况下性能显著提升。
*
* @author LightDance
*/
class Person {
private final Date birthday;
Person(Date birthday) {
this.birthday = birthday;
}
//生育高峰在1946到1964年之间
/**
* 差评的日期比较,创建了不必要的实例
*
* @return 比较结果
*/
public boolean badIsBabyBoomer(){
Calendar gmtCal = Calendar.getInstance(TimeZone.getTimeZone("GMT"));
gmtCal.set(1946 , Calendar.JANUARY , 1 , 0 , 0 , 0);
Date boomStart = gmtCal.getTime();
gmtCal.set(1964 , Calendar.JANUARY , 1 , 0 , 0 , 0);
Date boomEnd = gmtCal.getTime();
return birthday.compareTo(boomStart) >= 0 && birthday.compareTo(boomEnd) <= 0;
}
//下面是改进后的方式
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(1964 , Calendar.JANUARY , 1 , 0 , 0 , 0);
BOOM_END = gmtCal.getTime();
}
public boolean isBabyBoomer(){
return birthday.compareTo(BOOM_START) >= 0 && birthday.compareTo(BOOM_END) <= 0;
}
}
3.并不是那么显然的情况
比如适配器adapter,它只是为后备对象提供一个可以替代的接口,由于除了后备对象外,adapter没有其他状态信息,因此对于给定对象的特定适配器,不需要创建多个适配器实例。
?:适配器:对象 是1:1还是1:n?——1:n。因为相同的数据集对应了相同的后备对象,但它们之间的适配规则是不变的,所以只需要一个适配器,就可以对所有同类的对象进行适配。
4.警惕无意识的自动装箱&拆箱带来的性能损失
比如下面的例子中addAll()方法和badAddAll()方法,两者运行时打印出结果所用时间差距非常明显,这是badAddall()中误把基本类型long写成基本装箱类型Long导致。
private static void badAddAll(){
Long sum = 0L;
for (int i = 0; i < Integer.MAX_VALUE; i++) {
sum += i;
}
System.out.println(sum);
}
private static void addAll(){
long sum = 0L;
for (int i = 0; i < Integer.MAX_VALUE; i++) {
sum += i;
}
System.out.println(sum);
}
额外说明
1.但是这并不是想表示“创建对象代价昂贵,我们应该避免创建对象”,相反,由于小型对象构造器只做很少量显式工作,因而它们的创建和回收特别廉价。通过创建附加的对象,提升程序的清晰性、简洁性和功能性,通常是件好事;
2.有时候通过维护自己的对象池来避免创建对象反而可能并不是一种很好的做法,除非池中的对象是非常重量级的。举一个使用得当的例子:数据库连接池。建立数据库连接的代价是非常昂贵的,而且建立数据库连接的数量往往被数据库所限制,因此重用这些对象非常有意义。但是一般而言,维护自己的对象池往往会把代码搞乱,增加内存占用,损害性能。现在高度优化的JVM GC性能很容易就会超过轻量级对象池。
3..与本条对应的是“保护性拷贝”————“当你应该重新创建对象时,请不要重用现有的对象”。需要使用保护性拷贝的时候,因重用对象付出的代价应远远高于创建重复对象。比如,如果不实行保护性拷贝会导致潜在的错误或安全漏洞,而不必要的创建对象只会影响程序的风格和性能时。
全代码git地址:点我点我