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

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

    一般来说,最好能重用对象,而不是在每次需要的时候就创建一个相同功能的新对象。重用方式既快速,又不需要消耗额外的内存。如果对象是不可变的(immutable),它就始终可以被重用。

    下面有一个比较极端的例子- -,看看:

String s = new String("stringette");

    该语句每次被执行的时候都创建一个新的String实例,但是这些创建对象的动作全部都是多余的。传递给String构造器的参数本身就是一个String类型的实例,在功能方面和new出来的String对象基本无差。应该直接改为String s = "stringette;

    这个版本只用了一个String实例,而不是每次执行的时候都去创建一个新的,而且,它可以保证对于所有在同一台虚拟机中运行的代码,只要它们包含相同的字符串,这些字符串实例就会被重用。

    对于同时提供了静态工厂方法和构造器的不可变类,通常使用静态工厂方法而不是构造器,以避免创建不必要的对象。例如,静态工厂方法Boolean.valueOf(String)几乎总是优先于构造器Boolean(String)。构造器在每次被调用的时候都会创建一个新的对象,而静态工厂则从来不要求这样做,实际上也不会这么做。

    对于上面列举的Boolean例子,跟踪Boolean.java的代码会发现:

public Boolean(String s) {
    this(parseBoolean(s));
}

public static boolean parseBoolean(String s) {
    return ((s != null) && s.equalIgnoreCase("true"));
}

public static Boolean valueOf(String s) {
    return parseBoolean(s) ? TRUE : FALSE;
}

    每调用一次前者就会产生一个boolean类型的变量,要么是true要么是false,但是后者是将TRUEFALSE直接返回给了调用者,这两个变量在Boolean.java中早就已经创建好了:

public static final Boolean TRUE = new Boolean(true);
public static final Boolean FALSE = new Boolean(false);

    除了重用不可变的对象之外,也可以重用那些已知不会被修改的可变对象。可以看看下面这个例子,其中涉及可变的Date对象,它们的值一点计算出来之后就不再变化。这个类建立了一个模型:其中有一个人,并有一个isBabyBoomer方法,用来检验这个人是否为一个“baby boomer”(生育高峰期出生的小孩),换句话说就是检验这个人是否是1946年到1964年期间出生的。

public class Person {

    private final Date birthDate;

    public boolean isBabyBoomer() {
        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 birthDate.compareTo(boomStart) >= 0 &&
            birthDate.compareTo(boomEnd) < 0;
    }
}

    依据上面的代码使用,每次isBabyBoomer方法被调用的斯之后,都会新建一个Calendar,一个TimeZone和两个Date实例,这是不必要的。下面的版本中使用一个静态的初始化器(initializer),避免了这种效率低下的情况:

public 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(1964, 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,TimeZone,Date实例一次,而不是每次调用函数的时候都会去创建。如果isBabyBoom方法被频繁地调用,这种方法将会显著的提高性能,代码的含义也更加的清晰了。将boomStart和boomEnd从局部连良改为final静态域,这些日起显然是被作为常量对待的,从而使得代码更加容易理解。

    如果改进之后的isBabyBoomer方法永远不会被调用,那就没有必要一开始就初始化BOOM_STARTBOOM_END域。通过延迟初始化(lazily initializing),即把这些不必要的初始化工作延迟到第一次调用isBabyBoomer()的时候再进行。

    在之前的例子中,讨论的对象都是可被重用的,因为它们被初始化之后不会再改变了。其他有些情形并不总是这么明显了。考虑适配器(adapter)的情形。适配器是指这样一个对象:它把功能委托给一个后备对象(backup object),从而为后备对象提供一个可代替的接口。由于适配器除了后备对象之外,没有其他的状态信息,所以针对某个给定的特定适配器而言,它不需要创建多个适配器实例。

    例如,Map接口的keySet()方法返回该Map对象的Set试图,其中包含了该Map所有的键(Key)。粗看起来,好像每次执行keySet()方法都因该创建一个新的Set实例,但是,对于一个给定的Map对象,实际上每次调用keySet都返回的是同样一个Set实例。虽然被返回的Set实例一般是可以改变的,但是所有返回的对象引用都指向了同一个实例变量,因此当这个变量里面的数据发生变化的时候,通过所有引用访问到的信息都是一样的。对于这段的解释,还是在Map.java中的代码最有说服力:

public Set<K> keySet() {
        if (keySet == null) {
            keySet = new AbstractSet<K>() {
                public Iterator<K> iterator() {
                    return new Iterator<K>() {
                        private Iterator<Entry<K,V>> i = entrySet().iterator();

                        public boolean hasNext() {
                            return i.hasNext();
                        }

                        public K next() {
                            return i.next().getKey();
                        }

                        public void remove() {
                            i.remove();
                        }
                    };
                }

                public int size() {
                    return AbstractMap.this.size();
                }

                public boolean isEmpty() {
                    return AbstractMap.this.isEmpty();
                }

                public void clear() {
                    AbstractMap.this.clear();
                }

                public boolean contains(Object k) {
                    return AbstractMap.this.containsKey(k);
                }
            };
        }
        return keySet;
    }

    上面会检测变量keySet变量是否为空,如果为空则遍历所有的Key并加入其中,如果不为空则直接返回该引用。keySet在每个Map实例中都有一个实例对象存在。

    在Java1.5 发行版本中,有一种创建多余对象的新方法:自动装箱(autoboxing),它允许程序员将基本类型和装箱基本类型混用(类似于Integer和int)。按需求来自动装箱和自动拆箱。看下面的这个例子:

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属于装箱类型,也就是说在这个计算过程中花费了大量的时间来进行装箱和拆箱,创建了很多无用的Long类型对象。因此要小心的使用装箱类型,优先使用基本类型,避免不必要的装箱过程。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值