《Effective Java 中文版 第二版》第二章 第5条:避免创建不必要的对象

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


[toc]

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

String

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

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

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

改进后的版本如下所示:

String s = "stringette";

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

原理:

  • 在JAVA虚拟机(JVM)中存在着一个字符串池,其中保存着很多String对象,并且可以被共享使用,因此它提高了效率。由于String类是final的,它的值一经创建就不可改变,因此我们不用担心String对象共享而带来程序的混乱。字符串池由String类维护,我们可以调用intern()方法来访问字符串池。
  • 而改进版本的这行代码被执行的时候,JAVA虚拟机首先在字符串池中查找是否已经存在了值为"abc"的这么一个对象,它的判断依据是String类equals(Object obj)方法的返回值。如果有,则不再创建新的对象,直接返回已存在对象的引用;如果没有,则先创建这个对象,然后把它加入到字符串池中,再将它的引用返回。

静态工厂

对于同时提供了静态工厂方法(见第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;
    }
}
  • 改进后的 Person 类只在初始化的时候创建 Calendar、TimeZone 和 Date 实例一次,而不是在每次调用 isBabyBoomer 的时候创建这些实例。
  • 在作者的机器上,没调用一千万此,原版本需要 32 000ms ,而改进版本只需要 130 ms ,大约快了250倍。性能显著提高,代码含义也更加清晰。
  • 可以通过延迟初始化(lazily initializing(见第71条)),将BOOM_START BOOM_END初始化延迟到第一次调用isBabyBoomer的时候,但不建议,这样会是方法的实现更加浮渣,无法将性能显著提高到超过已经达到的水平(见第55条)。

自动装箱(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实现上更是如此。通过创建附加的对象,提升程序的清晰性、简洁性和功能性,这通常是好事。

  • 除非是非常重量级的对象,如数据库链接池等,否则应该避免维护自己的对象池(object pool)来避免创建对象。现代的JVM实现具有高度优化的垃圾回收器(CG),其性能很容易就会超过轻量级对象池的性能。

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

  • 类似于一个副本的概念:假设在图书馆借了一本书,如果你直接在上面做笔记,但这本书是共享的,将影响他人借阅。如果你是花钱复印一本,在复印书上做笔记就不同了。
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,242评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,769评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,484评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,133评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,007评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,080评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,496评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,190评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,464评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,549评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,330评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,205评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,567评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,889评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,160评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,475评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,650评论 2 335

推荐阅读更多精彩内容

  • 1. Java基础部分 基础部分的顺序:基本语法,类相关的语法,内部类的语法,继承相关的语法,异常的语法,线程的语...
    子非鱼_t_阅读 31,493评论 18 399
  • 转自:http://blog.csdn.net/jackfrued/article/details/4492194...
    王帅199207阅读 8,469评论 3 93
  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,494评论 18 139
  • 你有过一个人,一天不说话吗? 今天早上出去买早饭,有个四五岁的小弟弟对我喊:姐姐好~我也非常喜悦的回复,你也...
    Miss月色阅读 311评论 0 0
  • 回去,还是不回,这个问题一直困扰着我,很久了。 留下,可以找无数个理由,离开,或许只需要一个理由。但是这理由,须得...
    芣苡zx阅读 149评论 0 2