《Effective Java 中文版 第二版》

《Effective Java 中文版 第二版》第二章 第5条:

2017-12-01  本文已影响0人  不平凡的小黄宁

本章的主题是创建和销毁对象:何时以及如何创建对象,何时以及如何避免创建对象,如何确保它们能够适时地销毁,以及如何管理对象销毁之前必须进行的各种清理动作。


[toc]

一般来所,最好能重用对象而不是每次需要的时候就创建一个相同功能的新对象。重用方式既快速,又流行。如果对象是不可变的(immutable)(见第 15 条),它就始终可以被重用。

String

书中举了一个极端的反面例子:

String s = new String("stringette"); // DON'T DO THIS!

该语句每次执行的时候都创建一个新的String实例,但是这是多余的,("stringette")本身就是一个 String 实例,如果此语句在循环中,或是频繁调用的方法中,将会创建出成千上万不必要的 String 实例。

改进后的版本如下所示:

String s = "stringette";

这个版本只用了一个 String 实例,而不是每次执行的时候都创建一个新的实例,而且它可以保证,对于所有在同一台虚拟机中运行的代码,只要它们包含相同的字符串字面常量,该对象就会被重用。【这点跟 JVM 有关,上一个版本每次都会 new 一个实例,此版本将先查找字符串池,在判断是否创建对象】

原理:

静态工厂

对于同时提供了静态工厂方法(见第1条)和构造器的不可变类,通常可以使用静态工厂方法而不是构造器。例如,Boolean.valueOf(String)Boolean(String),因为构造器每次被调用的时候都会创建一个新的对象,而静态工厂方法则从来不要求,也不会这样做。【构造器需要 new 操作,静态方法在 JVM 中早已分配固定的空间】

已知不会被修改的对象

除了重用不可变的对象之外,也可以重用那些已知不会被修改的可变对象。下面是一个比较微妙、也比较常见的反面例子。

其中设计可变的 Date对象,它们的值一旦计算出来之后就不再变化。这个类建立了一个模型:其中有一个人,并有一个isBabyBoomer方法,用来检验这个人是否为一个"baby boomer(生育高峰期出身的小孩)",换句话所就是检验这个人是否出生于1946年至1964年期间。

public class Person {
    private final Date 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每次被调用的时候,都会新建一个Calendar、一个TimeZone和两个Date实例,这是不必要的开销。

静态的初始化器

此版本用一个静态的初始化器(initializer),避免了这种效率低下的情况。

class Person {
    private final Date birthDate;
    // Other fields, methods, and constructor omitted
    
    /**
    * The starting and ending dates of the baby boom.
    */
    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;
    }
}

自动装箱(autoboxing)

在Java1.5发行版中,允许程序员将基本类型和他们的装箱基本类型(包装类(Boxed Primitive Type))混用,感觉很神奇,它使得基本类型和它们的包装类之间变得模糊,其实只是语法糖,底层依然老老实实的构造装箱基本类型实例【可通过反编译查看】。如果无意识的在循环中使用了此技术,将创建非常多个中间对象,使性能继续下降【Long sumlong i】:

// Hideously slow program! Can you spot the object creation?
public static void main(String[] args) {
    Long sum = 0L;
    for (long i = 0; i < Integer.MAX_VALUE; i++) {
        sum += i;
    }
    System.out.println(sum);
}

要优先使用基本类型而不是装箱基本类型,要当心无意识的自动装箱。

总结

不要错误地认为本条目所介绍的内容暗示着“创建对象的代价非常昂贵,我们应该要尽可能地避免创建对象”。相反,由于小对象的构造器只做很少量的显式工作,所以小对象的创建和回收动作是非常廉价的,特别是现代的JVM实现上更是如此。通过创建附加的对象,提升程序的清晰性、简洁性和功能性,这通常是好事。

本条目对象的是第39条中有关“保护性拷贝(defensive copying)”的内容。本条目提及“当你应该重用现有对象的时候,请不要创建新的对象”,而第39条则说“当你应该创建新对象的时候,请不要重用现有对象”。注意,在提倡使用保护性拷贝的时候,因重用对象而付出的代价要远远大于因创建重复对象而付出的代价。必要时如果没能实施保护性拷贝,将会导致潜在的错误和安全漏洞;而不必要地创建对象则只会影响程序的风格和性能。

上一篇 下一篇

猜你喜欢

热点阅读