Effective Java笔记一 建立和销毁对象

Effective Java笔记一 建立和销毁对象

  • 第1条 考虑用静态工厂方法代替构造器
  • 第2条 遇到多个构造器参数时要考虑用构建器
  • 第3条 用私有构造器或者枚举类型强化Singleton属性
  • 第4条 经过私有构造器强化不可实例化的能力
  • 第5条 避免建立没必要要的对象
  • 第6条 消除过时的对象引用
  • 第7条 避免使用终结方法

第1条 考虑用静态工厂方法代替构造器

对于类而言, 最经常使用的获取实例的方法就是提供一个公有的构造器, 还有一种方法, 就是提供一个公有的静态工厂方法(static factory method), 返回类的实例.java

(注意此处的静态工厂方法与设计模式中的工厂方法模式不一样.)程序员

提供静态工厂方法而不是公有构造, 这样作有几大优点:算法

  • 静态工厂方法有名称. 能够更确切地描述正被返回的对象.
    当一个类须要多个带有相同签名的构造器时, 能够用静态工厂方法, 而且慎重地选择名称以便突出它们之间的区别.
  • 没必要在每次调用它们的时候都建立一个新对象. 能够重复利用实例. 若是程序常常请求建立相同的对象, 而且建立对象的代价很高, 这项改动能够提高性能. (不可变类, 单例, 枚举).
  • 能够返回原类型的子类型对象. 适用于基于接口的框架, 能够隐藏实现类API, 也能够根据参数返回不一样的子类型.
    因为接口不能有静态方法, 所以按照惯例, 接口Type的静态工厂方法被放在一个名为Types的不可实例化的类中.
    (Java的java.util.Collections). 服务提供者框架(Service Provider Framework, 如JDBC)的基础, 从实现中解耦.
  • 在建立参数化类型实例的时候, 使代码更简洁.

静态工厂方法的缺点:sql

  • 类若是不含public或者protected的构造器, 就不能被子类化. 对于公有的静态工厂方法所返回的非公有类, 也一样如此.
  • 静态工厂方法与其余的静态方法没有区别. 在API文档中没有明确标识出来. 可使用一些惯用的名称来弥补这一劣势:
    • valueOf(): 类型转换方法, 返回的实例与参数具备相同的值.
    • of(): valueOf()的一种更简洁的替代.
    • getInstance(): 返回的实例经过参数来描述, 对于单例来讲, 该方法没有参数, 返回惟一的实例.
    • newInstance(): 像getInstance()同样, 但newInstance()能确保返回的每一个实例都与其余实例不一样.
    • getType(): 像getInstance()同样, Type表示返回的对象类型, 在工厂方法处于不一样的类中的时候使用.
    • newType(): 和newInstance()同样, Type表示返回类型, 在工厂方法处于不一样的类中的时候使用.

第2条 遇到多个构造器参数时要考虑用构建器

静态工厂和构造器有一个共同的局限性: 它们都不能很好地扩展到大量的可选参数.数据库

重载多个构造器方法可行, 可是当有许多参数的时候, 代码会很难写难读.设计模式

第二种替代方法是JavaBeans模式, 即一个无参数构造来建立对象, 而后调用setter方法来设置每一个参数. 这种模式也有严重的缺点, 由于构造过程被分到了几个调用中, 在构造过程当中JavaBean可能处于不一致的状态.
类没法经过检验构造器参数的有效性来保证一致性. 另外一点是这种模式阻止了把类作成不可变的可能.数组

第三种方法就是Builder模式. 不直接生成想要的对象, 而是利用必要参数调用构造器(或者静态工厂)获得一个builder对象, 而后在builder对象上调用相似setter的方法, 来设置可选参数, 最后调用无参的build()方法来生成不可变的对象.缓存

这个Builder是它构建的类的静态成员类.
Builder的setter方法返回Builder自己, 能够链式操做.安全

Builder模式的优点: 可读性加强; 能够有多个可变参数; 易于作参数检查和构造约束检查; 比JavaBeans更加安全; 灵活性: 能够利用单个builder构建多个对象, 能够自动填充某些域, 好比自增序列号.框架

Builder模式的不足: 为了建立对象必须先建立Builder, 在某些十分注重性能的状况下, 可能就成了问题; Builder模式较冗长, 所以只有参数不少时才使用.

第3条 用私有构造器或者枚举类型强化Singleton属性

Singleton(单例)指仅仅被实例化一次的类. 一般用来表明那些本质上惟一的系统组件.

使类成为Singleton会使得它的客户端代码测试变得困难, 由于没法给它替换模拟实现, 除非它实现了一个充当其类型的接口.

单例的实现: 私有构造方法, 类中保留一个字段实例(static, final), 用public直接公开字段或者用一个public static的getInstance()方法返回该字段.

为了使单例实现序列化(Serializable), 仅仅在声明中加上implements Serializable是不够的, 为了维护并保证单例, 必须声明全部实例域都是transient的, 并提供一个readResolve()方法, 返回单例的实例. 不然每次反序列化一个实例时, 都会建立一个新的实例.

从Java 1.5起, 可使用枚举来实现单例: 只须要编写一个包含单个元素的枚举类型.
这种方法无偿地提供了序列化机制, 绝对防止屡次实例化.

第4条 经过私有构造器强化不可实例化的能力

只包含静态方法和静态域的类名声不太好, 由于有些人会滥用它们来编写过程化的程序. 尽管如此, 它们确实也有特有的用处, 好比:
java.lang.Math, java.util.Arrays把基本类型的值或数组类型上的相关方法组织起来; java.util.Collections把实现特定接口的对象上的静态方法组织起来; 还能够利用这种类把final类上的方法组织起来, 以取代扩展该类的作法.

这种工具类(utility class)不但愿被实例化, 然而在缺乏显式构造器的状况下, 系统会提供默认构造器, 可能会形成这些类被无心识地实例化.

经过作成抽象类来强制该类不可被实例化, 这是行不通的, 由于可能会形成"这个类是用来被继承的"的误解, 而继承它的子类又能够被实例化.

因此只要让这个类包含一个私有的构造器, 它就不能被实例化了. 进一步地, 能够在这个私有构造器中抛出异常.

这种作法还会致使这个类不能被子类化, 由于子类构造器必须显式或隐式地调用super构造器. 在这种状况下, 子类就没有可访问的超类构造器可调用了.

第5条 避免建立没必要要的对象

通常来讲, 最好能重用对象而不是每次须要的时候建立一个相同功能的新对象. 若是对象是不可变的(immutable), 它就始终能够被重用.

好比应该用:

String s = "stringette";

而不是:

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

包含相同字符串的字面常量对象是会被重用的.

对于同时提供了静态工厂方法和构造方法的不可变类, 一般可使用静态工厂方法而不是构造器, 以免建立没必要要的对象.
好比Boolean.valueOf().

除了重用不可变对象之外, 也能够重用那些已知不会被修改的可变对象. 好比把一个方法中须要用到的不变的数据保存成常量对象(static final), 只在初始化的时候建立一次(用static块), 这样就不用每次调用方法都重复建立.

若是该方法永远不会调用, 那也不须要初始化相关的字段, 能够经过延迟初始化(lazily initializing)把这些对象的初始化放到方法第一次被调用的时候. (可是不建议这样作, 没有性能的显著提升, 而且会使方法看起来复杂.)

前面的例子中, 所讨论的对象显然是可以被重用的, 由于它们被初始化以后不会再改变. 其余有些情形则并不老是这么明显了. (适配器(adapter)模式, Map的接口keySet()方法返回一样的Set实例).

Java 1.5中加入了自动装箱(autoboxing), 会建立对象. 因此程序中优先使用基本类型而不是装箱基本类型, 要小心无心识的自动装箱.

小对象的构造器只作不多量的显式工做, 建立和回收都是很廉价的, 因此经过建立附加的对象提高程序的清晰简洁性也是好事.

经过维护本身的对象池(object pool)来避免建立对象并非一种好的作法(代码, 内存), 除非池中的对象是很是重量级的. 正确使用的典型: 数据库链接池.

第6条 消除过时的对象引用

一个内存泄露的例子: 一个用数组实现的Stack, 依靠size标记来管理栈的深度, 可是这样从栈中弹出来的过时对象并无被释放.

称内存泄露为"无心识的对象保持(unintentional object retention)"更为恰当.

修复方法: 一旦对象引用已通过期, 只需清空这些引用便可.

清空对象引用应该是一种例外, 而不是一种规范行为. 消除过时引用最好的方法是让包含该引用的变量结束其生命周期. 若是你是在最紧凑的做用域范围内定义变量, 这种情形就会天然发生.

通常而言, 只要类是本身管理内存, 程序员就应该警戒内存泄露问题. 一旦元素被释放掉, 则该元素中包含的任何对象引用都应该被清空.

内存泄露的另外一个常见来源是缓存. 这个问题有这几种可能的解决方案:

  • 1.缓存项的生命周期由该键的外部引用决定 -> WeakHashMap;
  • 2.缓存项的生命周期是否有意义并非很容易肯定 -> 随着时间的推移或者新增项的时候删除没用的项.

内存泄露的第三个常见来源是监听器和其余回调.
若是你实现了一个API, 客户端注册了回调却没有注销, 就会积聚对象.
API端能够只保存对象的弱引用来确保回调对象生命周期结束后会被垃圾回收.

第7条 避免使用终结方法

终结方法(finalizer)一般是不可预测的, 也是很危险的, 通常状况下是没必要要的.
使用终结方法会致使行为不稳定, 下降性能, 以及可移植性问题.

不要把finalizer当成是C++中的析构器(destructors)的对应物.
在Java中, 当一个对象变得不可到达的时候, 垃圾回收器会回收与该对象相关联的存储空间.

C++的析构器也能够用来回收其余的非内存资源, 而在Java中, 通常用try-finally块来完成相似的工做.

终结方法的缺点在于不能保证会被及时地执行. 从一个对象变得不可到达开始, 到它的终结方法被执行, 所花费的时间是任意长的. JVM会延迟执行终结方法.

及时地执行终结方法正是垃圾回收算法的一个主要功能. 这种算法在不一样的JVM上不一样.

Java语言规范不只不保证终结方法会被及时地执行, 并且根本就不保证它们会被执行. 因此不该该依赖于终结方法来更新重要的持久状态.

不要被System.gc()System.runFinalization()这两个方法所迷惑, 它们确实增长了终结方法被执行的机会, 可是它们并不保证终结方法必定会被执行.

若是未捕获的异常在终结过程当中被抛出来, 那么这种异常能够被忽略, 并且该对象的终结过程也会终止.

使用终结方法有一个严重的性能损失.

若是类的对象中封装的资源(例如文件或线程)确实须要终止, 应该怎么作才能不用编写终结方法呢? 只需提供一个显式的终止方法. 并要求该类的客户端在每一个实例再也不有用的时候调用这个方法. 注意, 该实例必须记录下本身是否已经被终止了, 若是被终止以后再被调用, 要抛出异常.
例子: InputStream, OutputStreamjava.sql.Connection上的close()方法; java.util.Timercancel()方法.
Image.flush()会释放实例相关资源, 但该实例仍处于可用的状态, 若是有必要会从新分配资源.

显式的终止方法一般与try-finally块结合使用, 以确保及时终止.

终结方法的好处, 它有两种合法用途:

  • 当显式终止方法被忘记调用时, 终结方法能够充当安全网(safety net). 可是若是终结方法发现资源还未被终止, 应该记录日志警告, 这表示客户端代码中的bug.
  • 对象的本地对等体(native peer), 垃圾回收器不会知道它, 当它的Java对等体被回收的时候, 它不会被回收. 若是本地对等体拥有必须被及时终止的资源, 那么该类就应该有一个显式的终止方法, 如前, 能够是本地方法或者它也能够调用本地方法; 若是本地对等体并不拥有关键资源, 终结方法是执行这项任务最合适的工具.

注意, 终结方法链(finalizer chaining)并不会自动执行. 子类覆盖终结方法时, 必须手动调用超类的终结方法. try中终结子类, finally中终结超类.

为了不忘记调用超类的终结方法, 还有一种写法, 是在子类中写一个匿名的类, 该匿名类的单个实例被称为终结方法守卫者(finalizer guardian), 当守卫者被终结的时候, 它执行外围实例的终结行为. 这样外围类并无覆盖超类的终结方法, 保证了超类的终结方法必定会被执行.

相关文章
相关标签/搜索