第5条--避免创建不必要的对象

先说几句题外话,重新看这本书,真的是有很多新的收获,而且明显地感觉比第一次学习时轻松很多,所以永远不要放弃学习,即使过程再艰难再晦涩,只要坚持去做去学总会有提升,而一点一点的提升最终就会产生一个巨大的质变,再回头去看的时候就会惊讶地发现其实自己已经走了很远。对于初学者来说,仔细读JDK的源码和一些优秀框架的源码对于编程能力的提高是非常有帮助的,先去读一些常用接口和类的源码,比如java.lang、java.util、java.io包下面的一些东西,学习java访问数据库时候可以参考java.sql包,学习网络编程可以参考java.net包去看,学习并发编程可以再去研究java.util.concurrent包,学习算法和设计模式等知识时也可以拿jdk甚至junit、spring这些优秀的设计来当例子看。我想在EffectiveJava的学习记录之后继续写几个源码解读的系列,希望对初学者有所帮助。

好了,进入正题吧,这一条我想把作者的讲解顺序做一个调整,我觉得这样大家会快速理解这一条的中心思想。
避免创建不必要的对象,不是说尽量少地创建对象,也不是说重用对象一定是好的选择,而是避免创建那些不需要创建的对象。
实际上,小对象的创建和回收是非常廉价的,通过创建小对象来提升程序的逻辑性、简洁性和功能,这通常是件好事;而通过维护对象池避免重复创建对象的做法只在极少的情况下是被鼓励的,除非创建对象的开销非常大,例如创建数据库连接这样的操作。另外,在某些情况下是必须创建新对象的,这和后面“保护性拷贝”所讲的内容相对应。在需要使用保护性拷贝的时候,复用对象会造成安全隐患。
那究竟哪些对象是不必要创建的呢?
我们来看一个极端反面的例子:
String s = new String("string");
很多公司的笔试题都会举这个例子,"string"本身就是一个实例,new String又会创建一个String的实例。String的域都是final修饰的,所以String类型的实例一旦创建后就是不可变的,对String的操作而产生的实例都是新创建的实例,如果这种操作非常频繁的话尽量使用StringBuffer或者StringBuilder来代替。


再来看另外一个例子,有一个Person类,它有一个isBabyBoomer方法,用来检验这个人是否为一个"baby boomer(生育高峰期出生的小孩)",换句话说,就是检验这个人是否出生于1946至1964年间:

public class Person {
    private final Date birthDate;
    
    public Person(Date birthDate){
        this.birthDate = 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中创建的两个Date对象虽然是可变的,但是在意义上是不可变的,所以完全没必要每次执行isBabyBoomer方法都创建对象,可以将boomStart和boomEnd作为常量在类的第一次实例化时初始化。

  上面的两个例子中的对象都是明显能够被重用的,还有些情况是不那么明显的,考虑适配器的情形。适配器是指这样一个对象:它把功能委托给一个后备对象(backing object),从而为后备对象提供一个可以替代的接口。由于适配器除了后备对象之外,没有其他的状态信息,所以针对某个给定对象的特定适配器而言,它不需要创建多个适配器实例。
  上面这段话不是太容易理解,我们来对照Map的keySet方法来分析,先来看看HashMap中keySet方法的实现:
   

public Set<K> keySet() {
        Set<K> ks = keySet;
        return (ks != null ? ks : (keySet = new KeySet()));
    }

    private final class KeySet extends AbstractSet<K> {
        public Iterator<K> iterator() {
            return newKeyIterator();
        }
        public int size() {
            return size;
        }
        public boolean contains(Object o) {
            return containsKey(o);
        }
        public boolean remove(Object o) {
            return HashMap.this.removeEntryForKey(o) != null;
        }
        public void clear() {
            HashMap.this.clear();
        }
    }

 
  我们发现keySet被实例化以后每次调用keySet返回的都是同一个KeySet实例,这里KeySet类就是一个适配器,它的所有方法只是对Map接口中方法的一种适配或者说是替代方案。所以,对于同一个Map来说,没必要创建多个KeySet的实例。
 
  JDK1.5中引入了一种新的机制,自动装箱(autoboxing),它允许基本类型和装箱类型的混用,按需要自动装箱和拆箱,对于这个机制,还是慎用为好,看看这个例子:
  

Long sum = 0L;
  for(long i=0; i<Integer.MAX_VALUE; i++){
    sum +=i;
  }

 
  这段程序只因sum的类型被设置成装箱类型Long,执行效率受到了很大的影响,当然这也是一个比较极端的例子。
  最后还是重复那句话,避免创建不必要的对象,不是说尽量少地创建对象,也不是说重用对象一定是好的选择,而是避免创建那些不需要创建的对象。而这里发现哪些是不需要创建的对象是重点。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值