05-避免创建不必要的对象

2018-10-03  本文已影响12人  GeekGray

阅读原文

05-避免创建不必要的对象

  01 - 一般来说,最好能重用对象而不是在每次需要的时候就创建一个相同功能的新对象。重用方式既快捷,又流行。如果对象是不可变的,它始终可以被重用。

作为一个极端的反面例子,考虑下面的语句

String s = new String("stringette");//Don't do this!

  02 - 该语句每次执行的时候都创建一个新的String实例,但是这些创建的对象的动作全都是不必要的。传递给String构造器的参数(“stringette”)本身就是一个String的实例,功能方面等同于构造器创建的所有对象。如果这种用法是在一个循环中,或者是在一个被频繁调用的方法中,就会创建出成千上万不必要的String实例

改进后的版本如下所示:

String s = "stringette";

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

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

  05 - 除了重用不可变的对象外,也可以重用那些已知不会被修改的可变对象。下面是一个比较微妙,也比较常见的反面例子,其中涉及可变的Date对象,它们的值一旦计算除了之后就不再变化。这个类建立了一个模型;其中有一个人,并有一个isBabyBoomer方法,用来检验这个人是否为一个“baby boomer(生育高峰期出生的小孩)”就是检验这个人使否出生于1946年至1964年期间。

/**
 * @author: Hashub小晖
 * @WeChat: NGSHMVP
 * @Date: 2018/10/2 20:53
 * @function:Avoid creatingu nnecessary objects
 */
public class Person
{
    private final Date birhtDate = new Date();
    
    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 birhtDate.compareTo(boomStart) >= 0 && birhtDate.compareTo(boomEnd) < 0;
    }

}

isBabyBoomer每次被调用的时候,都会创建一个Calender、一个TimeZone和两个Date实例,这是不必要的。下面的版本用一个静态的初始化器(initializer),避免了这种效率低下的情况:

/**
 * @author: Hashub小晖
 * @WeChat: NGSHMVP
 * @Date: 2018/10/2 21:03
 * @function:
 */
public class Person2
{
    private final Date birhtDate = new Date();

    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 isBabyBoommer()
    {
        return birhtDate.compareTo(BOOM_START) >= 0 && birhtDate.compareTo(BOOM_END) <= 0;
    }

}

  06 - 改进后的类只在初始化的时候创建了Calendar、TimeZone和Date实例一次,而不是在每次调用isBabyBoommer的时候创建这些实例。如果isBabyBoomme方法被频繁调用,这种方法将会显著提高性能。除了提高性能之外,代码的含义也更加清晰了。把boomStart和boomEnd从局部变量改为final静态域,这些日期显然是被作为常量对待的,从而使得代码更易于理解。但是,这种优化带来的效果并不总是那么明显,因为Calendar实例的创建代价特别昂贵。

  07 - 如果改进后的Person类被初始化了,他的isBabyBoomme方法却永远不会被调用,那就没有必要初始化BOOM_START和BOOM_END域。通过延迟初始化(lazily initializing),即把对这些域的初始化延迟到isBabyBoommer方法第一次调用的时候进行,则有可能消除这些不必要的初始化工作,但是不建议这样做。正如延迟初始化中常见的情况一样,这样做会使方法的实现更加复杂,从而无法将性能显著提高到超过已经达到的水平

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

  09 - 例如,Map接口的keySet方法返回该Map对象的Set视图,其中包含该Map中所有的键(key)。粗看起来,好像每次调用keySet都应该创建一个新的Set实例,但是,对于一个给定的Map对象,实际上每次调用keySet都返回同样的Set实例。虽然被返回的Set实例一般是可变的,但是所有返回的对象在功能上是等同的:当其中一个返回对象发生变化的时候,所有其它的返回对象也要发生变化,因为它们是由同一个Map实例支撑的。虽然创建keySet视图对象的多个实例并无害处,却也是没有必要的。

  010 - 在Java1.5发行版本中,有一种创建多余对象的新方法,称作自动装箱(autoboxing),它允许程序员将基本类型和装箱基本类型(Boxed Primitive Type)混用,按需要自动装箱和拆箱。自动装箱使得基本类型和装箱类型之间的差别变得模糊起来,但是并没有完全消除。它们在语义上还是有着微秒的差别,在性能上也有着比较明显的差别。考虑下面的程序,它计算所以int正值的总和。为此,程序必须使用long算法,因为int不够大,无法容纳所有的int正值的总和:

/**
 * @author: Hashub小晖
 * @WeChat: NGSHMVP
 * @Date: 2018/10/3 20:29
 * @function:
 */
public class Test
{
    public static void main(String[] args)
    {
        Long sum=0L;//time consuming 12019ms
        long sum1=0L;//time consuming 86ms
        long  startTime=System.currentTimeMillis();
        long  endTime;
        for (int i = 0; i <Integer.MAX_VALUE ; i++)
        {
            sum1+=1;
            //sum+=1;
        }
        endTime=System.currentTimeMillis();
        System.out.println(sum+"Time:"+(endTime-startTime));
    }
}

  011 - 这段程序算出的答案是正确的,但是比实际情况要更慢一些,因为打错了一个字符。变量sum被声明成Long而不是long,意味着程序构造了大约2^31个多余的Long实例(大约每次往Long sum中增加long时构造一个实例)。将sum的声明从Long改成long,效果明显提升。结论很明显:要优先使用基本类型而不是装箱基本类型,要当心无意识的自动装箱。

  012 - 由于小对象的构造器只做很少量的显式工作,所以小对象的创建和回收动作是非常廉价的,特别是在现代JVM实现上更是如此。通过创建附加的对象,提升程序的清晰性、简洁性和功能性,这通常是件好事。

  013 - 反之,通过维护自己的对象池(object pool)来避免创建对象并不是一种好的做法除非池中的对象时非常重量级的正确使用对象池的典型对象示例是数据库连接池。建立数据库的连接代价是非常昂贵的,因此重用这些对象非常有意义。而且数据库许可可能限制你只能使用一定数量的连接。但是,一般而言,维护自己的对象池必定会把代码弄得很乱,同时增加内存占用(footprint),并且还会损害性能。现代的JVM实现具有高度化的垃圾回收机器,其性能很容易就会超过轻量级对象池的性能。

  014 - 在提倡使用保护性拷贝的时候,因为重用对象而付出的代价要远远大于创建重复对象而付出的代价。必要时如果没能实施保护性拷贝,将会导致前潜在的错误和安全漏洞,而不必要地创建对象则只会影响程序的风格和性能。

上一篇下一篇

猜你喜欢

热点阅读