【Effective Java】Ch2_创建销毁对象:Item5_避免创建不必要的对象

        通常最好重用单个对象,而不是在每次需要时都创建一个功能完全一样的新对象。重用不仅更快而且更流行。如果对象是不可变的(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)。适配器是这样一种对象:它将功能委托给一个后备对象,给后备对象提供一个可选择的接口。由于适配器除了其后备对象外,没有其他的状态,所以对于给定的对象就没有必要创建多个适配器实例。

        【例】Map接口的keySet()方法返回Map对象的一个Set视图,包含该Map的所有key。看起来好像每次调用keySet()都需要创建一个新的Set实例。而实际上,虽然返回的Set通常是可变的,但返回的对象在功能上是等同的: 如果其中一个返回对象改变,其他对象也会改变,因为他们的底层都是同一个Map实例。虽然创建多个KeySet视图对象并没有害处,但也没有必要。

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和安全漏洞;而创建不必要的对象仅仅只影响编码风格和性能而已。




  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值