二. 创建和销毁对象
1. 考虑用静态工厂方法代替构造器
优点
静态工厂方法与构造器不同的优势在于:
- 它们有名称
- 不必在每次调用它们的时候都创建一个新对象
- 它们可以返回原返回类型的任何子类型的对象
缺点
但是, 静态工厂方法也有缺点, 主要在于:
- 类如果不含公有的或者受保护的构造器, 就不能被子类化.(鼓励使用复合, 而不是继承)
- 它们与其它的静态方法实际上没有任何区别.
(静态工厂方法的一些惯用名称: valueOf, of, getInstance, newInstance, getType 以及 newType)
1 | public static Boolean valueOf(boolean b) { |
2. 遇到多个构造器参数时要考虑用构建器
如果类的构造器或者静态工厂方法中具有多个参数, 设计这种类时, Builder模式就是种不错的选择.
Builder模式模拟了具名的可选参数, 就像Ada和Python中的一样.
1 | public static NutritionFacts { |
调用builder
1 | NutritionFacts cocaCola = new NutritionFacts.Builder(240, 8) |
3. 用私有构造器或者枚举类型强化 Singleton 属性
创建单例有多种的方法:
- 公有静态成员是个final域
1 | public class Elvis { |
有一点要提醒的是: 享有特权的客户端可以借助 AccessibleObject.setAccessible 方法, 通过反射机制调用私有构造器.
如果需要抵御这种攻击, 可以修改构造器, 让它在被要求创建第二个实例的时候抛出异常.
- 公有的成员是个静态工厂方法
1 | public class Elvis { |
工厂方法的优势之一在于, 它提供了灵活性: 在不改变其中API的前提下, 我们可以改变该类是否应该为 Singleton 的想法.
- 序列化一个Singleton
为了维护并保证 Singleton, 除了在声明中加上 “implements Serializable“, 还必须声明所有实例域都是瞬时(transient)的, 并提供一个 readResolve 方法.
1 | private Object readResolve() { |
- 单元素的枚举类型, 最好的实现方式(Java 1.5)
1 | public enum Elvis() { |
这种方法在功能上与公有域方法相近, 但是它更加简洁, 无偿地提供了序列化机制, 绝对防止多次实例化, 即使是在面对复杂的序列化或者反射攻击的时候.
单元素的枚举类型已经成为实现 Singleton 的最佳方法.
4. 通过私有构造器强化不可实例化的能力
用于只包含静态方法和静态域的类.
例如用于:
- 把基本类型的值或者数组类型上的相关方法组织起来. (比如: java.lang.Math 或者 java.util.Arrays)
- 把实现特定接口的对象上的静态方法(包括工厂方法)组织起来. (比如: java.util.Collections)
- 把 final 类上的方法组织起来, 以取代扩展该类的做法.
包含私有构造器
1 | public class UtilityClass { |
5. 避免创建不必要的对象
- 重用不可变的对象
不要这样做:
1 | String s = new String("stringette"); |
该语句每次被执行的时候都创建一个新的 String 实例. 参数 “stringette“ 本身就是一个 String 实例, 如果这种用法是在一个循环中, 或者是在一个被频繁调用的方法中, 就会创建出成千上万不必要的 String 实例.
应该这样做:
1 | String s = "stringette"; |
这个版本只用了一个 String 实例, 而不是每次执行的时候都创建一个新的实例.
- 使用静态工厂方法要优于构造器
比如: Boolean.valueOf(String) 几乎总是优先于构造器 Boolean(String)
- 重用那些已知不会被修改的可变对象
不要这样做:
1 | public class Person { |
isBabyBoomer 每次被调用的时候, 都会新建一个 Calendar, 一个 TimeZone 和两个 Date 实例, 这是不必要的.
应该这样做:
1 | public class Person() { |
- 要优先使用基本类型而不是装箱基本类型, 要当心无意识的自动装箱
不要这样做:
1 | // Slow program. Where is the object creation? |
变量 sum 被声明成 Long 而不是 long, 意味着程序构造了大约 231 个多余的 Long 实例.
- 对象池一般来说不是一个好的做法
除非池中的对象是非常重量级的, 比如像: 数据库连接池.
6. 消除过期的对象引用
你能找到其中的”内存泄漏”吗?
1 | public class Stack { |
如果一个栈先是增长, 然后再收缩, 从栈中弹出来的对象将不会被当作垃圾回收. 因为栈内部维护着这些对象的过期引用(指的是永远都不会被解除的引用).
- 清空对象引用
1 | public pop() { |
清空对象引用应该是一种例外, 而不是一种规范行为. 不要过度的去清空每一个对象的引用, 这样做既没必要, 也不是我们所期望的.
只有类是自己管理内存的, 才应该去清空对象引用.
- 缓存中的内存泄漏
解决缓存的内存泄漏有几种可能的方案:
使用 WeakHashMap. 记住只有当所要缓存项的生命周期, 是由该键的外部引用而不是由值决定时, WeakHashMap 才有用处.
一个更为常见的做法是, 缓存应该时不时地清除掉没用的项. 这项工作可以使用一个后台线程来完成, 或者也可以在给缓存添加新条目的时候顺便进行清理. LinkedHashMap 类利用它的 removeEldestEntry 方法可以很容易地实现后一种方案.
- 监听器和回调中的内存泄漏
若客户端注册了回调, 但却没有显式地取消注册, 就很可能会发生内存泄漏.
解决这个问题的方法是, 只保存它们的弱引用(weak reference). 例如, 只将它们保存成 WeakHashMap 中的键.
- 时不时的使用 Heap Profiler 工具去发现不可见的内存泄漏问题
7. 避免使用终结方法
终结方法通常是不可预测的, 也是很危险的, 一般情况下是不必要的.
注重时间的任务不应该由终结方法来完成
不能保证终结方法会被及时地执行.
不应该依赖终结方法来更新重要的持久状态
Java 语言规范不仅不保证终结方法会被及时执行, 而且根本就不保证它们会被执行.
如果异常发生在终结方法之中, 甚至连警告都不会打印出来.
使用终结方法会有非常严重的性能损失.
解决方案:
提供一个显式的终止方法, 比如像: InputStream, OutputStream 和 java.sql.Connection 上的 close 方法.
显式的终止方法通常与 try-finally 结构结合起来使用, 以确保及时终止.
1 | Foo foo = new Foo(...); |
终结方法有两个合理的用途:
当对象的所有者忘记调用前面建议的显式终止方法时, 终结方法可以充当”安全网”.
(如果你正考虑编写这样的安全网终结方法, 就要认真考虑清楚, 这种额外的保护是否值得你付出这份额外的代价)在本地对等体(native peer)中使用, 因为垃圾回收器不会知道它们.
(在本地对等体并不拥有关键资源的前提下, 终结方法正是执行这项任务最合适的工具. 如果本地对等体拥有必须被及时终止的资源, 那么该类就应该具有一个显式的终止方法)
在以上很少见的情况下, 既然使用了终结方法, 就要记住调用 super.finalize