《Effective Java》读书笔记 —— 对象的创建和销毁

本文主题是创建和销毁对象,关注一下几个问题:

  • 何时以及如何创建对象
  • 何时以及如何避免创建对象
  • 如何去报它们能够适时销毁
  • 如何管理对象销毁之前必须进行的各种清理动作

1.考虑使用静态工厂方法代替构造器(静态工厂模式)

创建类实例的方式有两种:

  • 公有的构造器
  • 公有的静态工厂方法
静态工厂方法
  • 优势

    • 静态工厂方法与构造器不同的第一大优势在于,它们有名称,如果构造器的参数本身没有确切的描述返回的对象,那么适当名称的静态工厂会更加合适
    • 不必每次调用它们的时候都创建一个新对象,使得不可变对象可以使用预先构建好的实例,利用缓存实例进行复用,为重复的调用返回相同的对象,如果创建对象的代价很高,这个技术可以极大提升性能
    • 可以返回类型的任何子类型的对象,在选择返回对象时有更大的灵活性。
      • 可以返回非公有对象,同时又不会使对象的类变成公有的,隐藏实现类
      • 公有的静态工厂方法所返回对象的类不仅可以是非公有的,而且该类可以对着每次调用而发生变化,取决于静态工厂方法的参数值(工厂方法模式)
    • 创建参数化类型(泛型)实例时,使代码变得更加简洁
  • 缺点

    • 类如果不含有公有或者受保护构造器,就不能被子类化
    • 与其他的静态方法实际上没有任何区别
  • 静态工厂方法命名规范

    • valueOf
      • 该方法返回的实例与它的参数具有相同的值,这样的静态工厂方法实际上是类型转换方法
    • of
    • getInstance
      • 返回的实例通过方法的参数来描述,如果没有参数,则返回唯一的单例
    • newInstance
      • 确保返回的实例都与其他实例不同
    • getType
    • newType

2.遇到多个构造器参数时要考虑用构建器(Builder创建者模式)

静态工厂和构造器有个共同的局限性,不能很好地扩展到大量可选参数。

处理有大量可选参数的构造器的方式:

  • 重叠构造器
  • JavaBeans 模式
  • Builder 模式
重叠构造器

提供第一个只有必要参数的构造器,第二个构造器有一个可选参数,第三个有两个可选参数,以此类推,最后一个构造器包含所有可选参数。

public NutritionFact(int servingSize, int servings){}
public NutritionFact(int servingSize, int servings, int calories){}
public NutritionFact(int servingSize, int servings, int calories, int fat){}
public NutritionFact(int servingSize, int servings, int calories, int fat, int sodium){}
...

缺点:重叠构造器模式可行,但是当有许多参数的时候,客户端代码会很难编写和难以阅读

JavaBeans 模式

另一种替代方法,JavaBeans 模式,调用一个无参构造器来创建对象,然后调用setter方法设置每个必要参数,以及每个相关的可选参数。

NutritionFact cocoCola = new NutritionFact();
cocoCola.setServingSize(240);
cocoCola.setServings(8);
cocoCola.setCalories(100);
cocoCola.setSodium(35);
cocoCola.setCarbohydrate(27);

缺点:

  • 构造过程被分到几个调用中,构造过程 JavaBean 可能处于不一致的状态。类无法仅仅通过校验构造器参数的有效性来保证一致性
  • JavaBean 模式阻止了把类做成不可变的可能,需要确保它的线程安全
Builder 模式

不直接生成想要的对象,客户端利用多有必要的参数调用构造器(或静态工厂)得到一个builder对象,然后客户端再builder 对象上调用类似setter方法,来设置每个相关的可选参数,最后客户端调用无参的build方法来生成不可变的对象,这个builder是类的静态成员类。

public class NutritionFacts {
    private final int calories = 0;
    private final int fat = 0;
    private final int sodium = 0;
       
    // 静态内部类 Builder 对象 
    public static class Builder {
        private int calories = 0;
        private int fat = 0;
        private int sodium = 0;
    
        // setter方法返回当前builder对象,方便链式调用
        public Builder setCalories(int val) {
            calories = val;
        }
          
        public Builder setFat(int val) {
            fat = val;
        }  
        
        public Builder setSodium(int val) {
            sodium = val;
        }
        
        public NutritionFacts build() {
            return new NutritionFacts(this);
        }
    }
    
    // 传入 Builder 对象的构造方法
    public NutritionFacts(Builder builder) {
        calories = builder.calories;
        fat = builder.fat;
        sodium = builder.sodium;
    }
}

缺点:需要额外开销

总结

如果类的构造器或者静态工厂中具有多个参数,设计这种类时,Builder 模式就是不错的选择。

3.用私有构造器或者枚举类型强化Singleton属性(单例模式)

Singleton 指仅仅被实例化一次的类。

实现Singleton的方式有很多种

方式一

把构造器保持为私有的,并导出公有的静态成员,并且静态成员是个final的

public class Elvis {
    public static final Elvis INSTANCE = new Elvis();
    private Elvis() {...};
    
}

问题:无法抵御通过反射调用私有构造器的攻击。

方案:可以修改构造器,让它在要求创建第二个实例的时候抛出异常。

方式二

方式二中,公有成员不再是属性,而是一个静态方法getInstance

public class Elvis {
    pvivate static final Elvis INSTANCE = new Elvis();
    private Elvis() {...};
    
    public static Elvis getInstance() {return INSTANCE;}
    
}

问题:如果此类实现了序列化,序列化之后的结果都会创建一个新的实例。
方案:重写readResolve方法

private Object readResolve() {
    return INSTANCE;
}
方式三

编写一个包含单个元素的枚举类型。

public enum Elvis {
    INSTANCE;
}

与公有方法相近,但更加简洁,无偿的提供了序列化机制,并且防止序列到导致多次实例化,并且防止反射的攻击。

总结

单元素的枚举类型已经成为实现Singleton的最佳方法。

4.通过私有构造器强化不可实例化的能力

工具类不希望被实例化,实例对它没有任何意义。

在缺少显示构造器时,编译器会自动提供一个公有的,无参的缺省构造器。

可通过创建私有构造器,并构造器中抛出异常,来避免实例化此类。

5.避免创建不必要的对象

一般来说,最好能重用对象而不是在每次需要的时候就创建一个相同功能的新对象。如果对象是不可变的,那么它就应该始终被重用。

举例一
String s = new String("mystring")  // 每次都会创建一个新的String实例
String s = "mystring"           // 推荐,保证在同一台虚拟机中运行的代码,只要包含相同的字符串字面常量,就会被重用
举例二:不可变类

对于不可变类,优先使用静态工厂方法,每次调用可以重用,避免创建不必要的对象。

举例三

通过静态初始化器避免在每次调用方法时都会生成一些不必要的对象。

class Person {
    static {
        // 初始化整个类需要用到的不可变可重用对象
    }
    public boolean isBaby() {
        // 这里使用到一些不可变的对象,无需每次都创建,把创建操作放到静态初始化器中,这里直接使用即可
    }
}

缺点:如果方法没有被调用,那么初始化工作就没有必要,可以通过延迟初始化,即把初始化工作放到方法初次调用时。

举例四

自动装箱会创建出多余的对象。

sum 声明为 Long 类型,导致循环内部构造大量大于 Long 实例。

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

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

举例五

通过维护自己的对象池来避免创建对象并不会一种好的做法,除非池中的对象是非常重量级的,现代JVM实现具有高度优化的垃圾回收器,其性能很容易就超过轻量级对象池的性能。

6.消除过期的对象引用

  • 只要类自己管理内存,程序就应该警惕内存泄漏问题
  • 内存泄漏另一个场景来源是缓存
  • 另一个场景是监听器和其他回调,如果注册了回调,却没有显式地取消注册,那么会产生内存泄漏,确保回调立即被当做垃圾回收的最佳方法是只保持它们的弱引用。

7.避免使用终结方法

  • 介绍
    • 终结方法(finalizer)通常是不可预测的,也是很危险的,一般情况下是不必要的
    • 使用终结方法导致行为不稳定,降低性能,以及可移植性问题
    • C++的析构函数可以被用来回收其他的非内存资源,Java 中,一般用try-finally块来完成类似工作
    • 终结方法的缺点在于不能保证被及时执行,JVM会延迟执行终结方法,所以不要用来关闭已经打开的文件,程序不能依赖终结方法被执行的时间点
    • 不应该依赖终结方法来更新重要的持久状态
    • System.gc 和 System.runFinalization 增加了终结方法执行的机会,但不能保证终结方法一定会被执行
    • System.runFinalizersOnExit 可以保证终结方法被执行,当然此方法已被废弃
    • 如果被捕获的异常在终结方法中被抛出,那么这种异常会被忽略
    • 使用终结方法有非常严重的性能损失,即使什么也不做
  • 终止资源(文件或线程资源)
    • 显示提供一个终止方法,在实例不再需要时,调用此方法
    • 通常与try-finally结构结合,在finally中显式调用终止方法
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 160,026评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,655评论 1 296
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,726评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,204评论 0 213
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,558评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,731评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,944评论 2 314
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,698评论 0 203
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,438评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,633评论 2 247
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,125评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,444评论 3 255
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,137评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,103评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,888评论 0 197
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,772评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,669评论 2 271

推荐阅读更多精彩内容

  • Effective Java笔记一 创建和销毁对象 第1条 考虑用静态工厂方法代替构造器 第2条 遇到多个构造器参...
    圣骑士wind阅读 371评论 0 2
  • 目录 第二章 创建和销毁对象 1 考虑用静态工厂方法替代构造器 对于代码来说, 清晰和简洁是最重要的. 代码应该被...
    高广超阅读 1,396评论 0 12
  • 第一章:Java程序设计概述 Java和C++最大的不同在于Java采用的指针模型可以消除重写内存和损坏数据的可能...
    loneyzhou阅读 1,172评论 1 7
  • 感觉时间到了。 该去实现对自己的承诺了。 我不可能把所有东西都抓在手里的。
    JabinW阅读 212评论 0 0
  • 我有两条内裤一条红色一条蓝色穿着红色的,蓝色的休息穿着蓝色的,红色的休息没有哪个更重要
    wuya阅读 111评论 0 0