GitHub - Sai628/Effective-Java-Summary-In-Chinese: "Effective Java 中文版(第2版)"总结 -- (美)Joshua Bloch 著
一. 目录
二. 创建和销毁对象
1. 考虑用静态工厂方法代替构造器
优点
静态工厂方法与构造器不同的优势在于:
- 它们有名称
- 不必在每次调用它们的时候都创建一个新对象
- 它们可以返回原返回类型的任何子类型的对象
缺点
但是, 静态工厂方法也有缺点, 主要在于:
- 类如果不含公有的或者受保护的构造器, 就不能被子类化.(鼓励使用复合, 而不是继承)
- 它们与其它的静态方法实际上没有任何区别.
(静态工厂方法的一些惯用名称: valueOf, of, getInstance, newInstance, getType 以及 newType)
public static Boolean valueOf(boolean b) { return b ? Boolean.TRUE : Boolean.FALSE; }
如果类的构造器或者静态工厂方法中具有多个参数, 设计这种类时, Builder模式就是种不错的选择.
Builder模式模拟了具名的可选参数, 就像Ada和Python中的一样.
public static NutritionFacts { private final int servingSize; private final int servings; private final int calories; private final int fat; private final int sodium; private final int carbohydrate; public static class Builder { // Required paramters private final int servingSize; private final int servings; // Optional paramters - initialized to default values private int calories = 0; private int fat = 0; private int sodium = 0; private int carbohydrate = 0; public Builder(int servingSize, int servings) { this.servingSize = servingSize; this.servings = servings; } public Builder calories(int val) { calories = val; return this; } public Builder fat(int val) { fat = val; return this; } public Builder sodium(int val) { sodium = val; return this; } public Builder carbohydrate(int val) { carbohydrate = val; return this; } public NutritionFacts build() { return new NutritionFacts(this); } } private NutritionFacts(Builder builder) { servingSize = builder.servingSize; servings = builder.servings; calories = builder.calories; fat = builder.fat; sodium = builder.sodium; carbohydrate = builder.carbohydrate; } }
调用builder
NutritionFacts cocaCola = new NutritionFacts.Builder(240, 8) .calories(100).sodium(35).carbohydrate(27).build();
3. 用私有构造器或者枚举类型强化 Singleton 属性
创建单例有多种的方法:
- 公有静态成员是个final域
public class Elvis { public static final Elvis INSTANCE = new Elvis(); private Elvis() { ... } ... public void singASong() { ... } }
有一点要提醒的是: 享有特权的客户端可以借助 AccessibleObject.setAccessible 方法, 通过反射机制调用私有构造器.
如果需要抵御这种攻击, 可以修改构造器, 让它在被要求创建第二个实例的时候抛出异常.
- 公有的成员是个静态工厂方法
public class Elvis { private static final Elvis INSTANCE = new Elvis(); private Elvis() { ... } public static Elvis getInstance() { return INSTANCE; } public void singASong() { ... } }
工厂方法的优势之一在于, 它提供了灵活性: 在不改变其中API的前提下, 我们可以改变该类是否应该为 Singleton 的想法.
- 序列化一个Singleton
为了维护并保证 Singleton, 除了在声明中加上 "implements Serializable", 还必须声明所有实例域都是瞬时(transient)的, 并提供一个 readResolve 方法.
private Object readResolve() { // Return the one true Elvis and let the garbage collector take care of the Elvis impersonator return INSTANCE; }
- 单元素的枚举类型, 最好的实现方式(Java 1.5)
public enum Elvis() { INSTANCE; ... public void singASong() { ... } }
这种方法在功能上与公有域方法相近, 但是它更加简洁, 无偿地提供了序列化机制, 绝对防止多次实例化, 即使是在面对复杂的序列化或者反射攻击的时候.
单元素的枚举类型已经成为实现 Singleton 的最佳方法.
4. 通过私有构造器强化不可实例化的能力
用于只包含静态方法和静态域的类.
例如用于:
- 把基本类型的值或者数组类型上的相关方法组织起来. (比如: java.lang.Math 或者 java.util.Arrays)
- 把实现特定接口的对象上的静态方法(包括工厂方法)组织起来. (比如: java.util.Collections)
- 把 final 类上的方法组织起来, 以取代扩展该类的做法.
包含私有构造器
public class UtilityClass { // Suppress default constructor for noninstantiability private UtilityClass() { throw new AssertionError(); } ... // Remainder omitted }
5. 避免创建不必要的对象
- 重用不可变的对象
不要这样做:
String s = new String("stringette");
该语句每次被执行的时候都创建一个新的 String 实例. 参数 "stringette" 本身就是一个 String 实例, 如果这种用法是在一个循环中, 或者是在一个被频繁调用的方法中, 就会创建出成千上万不必要的 String 实例.
应该这样做:
这个版本只用了一个 String 实例, 而不是每次执行的时候都创建一个新的实例.
- 使用静态工厂方法要优于构造器
比如: Boolean.valueOf(String) 几乎总是优先于构造器 Boolean(String)
- 重用那些已知不会被修改的可变对象
不要这样做:
public class Person { private final Date birthDate; ... 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 实例, 这是不必要的.
应该这样做:
public class Person() { private final Date birthDate; ... 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; } }
- 要优先使用基本类型而不是装箱基本类型, 要当心无意识的自动装箱
不要这样做:
// Slow program. Where is 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); }
变量 sum 被声明成 Long 而不是 long, 意味着程序构造了大约 231 个多余的 Long 实例.
- 对象池一般来说不是一个好的做法
除非池中的对象是非常重量级的, 比如像: 数据库连接池.
6. 消除过期的对象引用
你能找到其中的"内存泄漏"吗?
public class Stack { private Object[] elements; private int size = 0; private static final int DEFAULT_ININTIAL_CAPACITY = 16; public Stack() { elements = new Object[DEFAULT_ININTIAL_CAPACITY]; } public void push(Object e) { ensureCapacity(); elements[size++] = e; } public Object pop() { if (size == 0) { throw new EmptyStackException(); } return elements[--size]; } private void ensureCapacity() { if (elements.length == size) { elements = Arrays.copyOf(elements, 2 * size + 1); } } }
如果一个栈先是增长, 然后再收缩, 从栈中弹出来的对象将不会被当作垃圾回收. 因为栈内部维护着这些对象的过期引用(指的是永远都不会被解除的引用).
- 清空对象引用
public pop() { if (size == 0) { throw new EmptyStackException(); } Object result = elements[--size]; elements[size] = null; // Eliminate obsolete reference. return result; }
清空对象引用应该是一种例外, 而不是一种规范行为. 不要过度的去清空每一个对象的引用, 这样做既没必要, 也不是我们所期望的.
只有类是自己管理内存的, 才应该去清空对象引用.
- 缓存中的内存泄漏
解决缓存的内存泄漏有几种可能的方案:
使用 WeakHashMap. 记住只有当所要缓存项的生命周期, 是由该键的外部引用而不是由值决定时, WeakHashMap 才有用处.
一个更为常见的做法是, 缓存应该时不时地清除掉没用的项. 这项工作可以使用一个后台线程来完成, 或者也可以在给缓存添加新条目的时候顺便进行清理. LinkedHashMap 类利用它的 removeEldestEntry 方法可以很容易地实现后一种方案.
- 监听器和回调中的内存泄漏
若客户端注册了回调, 但却没有显式地取消注册, 就很可能会发生内存泄漏.
解决这个问题的方法是, 只保存它们的弱引用(weak reference). 例如, 只将它们保存成 WeakHashMap 中的键.
- 时不时的使用 Heap Profiler 工具去发现不可见的内存泄漏问题
7. 避免使用终结方法
终结方法通常是不可预测的, 也是很危险的, 一般情况下是不必要的.
-
注重时间的任务不应该由终结方法来完成
不能保证终结方法会被及时地执行.
-
不应该依赖终结方法来更新重要的持久状态
Java 语言规范不仅不保证终结方法会被及时执行, 而且根本就不保证它们会被执行.
-
如果异常发生在终结方法之中, 甚至连警告都不会打印出来.
-
使用终结方法会有非常严重的性能损失.
解决方案:
提供一个显式的终止方法, 比如像: InputStream, OutputStream 和 java.sql.Connection 上的 close 方法.
显式的终止方法通常与 try-finally 结构结合起来使用, 以确保及时终止.
Foo foo = new Foo(...); try { // Do what must be done with foo ... } finally { foo.terminate(); // Explicit termination method }
终结方法有两个合理的用途:
-
当对象的所有者忘记调用前面建议的显式终止方法时, 终结方法可以充当"安全网".
(如果你正考虑编写这样的安全网终结方法, 就要认真考虑清楚, 这种额外的保护是否值得你付出这份额外的代价) -
在本地对等体(native peer)中使用, 因为垃圾回收器不会知道它们.
(在本地对等体并不拥有关键资源的前提下, 终结方法正是执行这项任务最合适的工具. 如果本地对等体拥有必须被及时终止的资源, 那么该类就应该具有一个显式的终止方法)
在以上很少见的情况下, 既然使用了终结方法, 就要记住调用 super.finalize
三. 对于所有对象都通用的方法
8. 覆盖 equals 时请遵守通用约定
以下情况时不要覆盖:
-
类的每个实例本质上是唯一的. 例如: Thread
-
不关心类是否提供了"逻辑相等"的测试功能. 例如: java.util.Random
-
超类已经覆盖了 equals, 从超类继承过来的行为对于子类也是合适的. 例如: Set, List, Map
-
类是私有的或者是包级私有的, 可以确定它的 equals 方法永远不会被调用.
以下情况时, 应该覆盖 equals:
如果类具有自己特有的"逻辑相等"概念(不同于对象等同的概念), 而且超类还没有覆盖 equals 以实现期望的行为.
覆盖 equals 方法时, 需实现了等价关系:
- 自反性: x.equals(x)==true
- 对称性: x.equals(y)==y.equals(x)
- 传递性: x.equals(y)==y.equals(z)==z.equals(x)
- 一致性: x.equals(y)==x.equals(y)==x.equals(y)=...
- 非空性: x.equals(null)->false
实现高质量 equals 方法的决窍:
-
使用 == 操作符, 检查"参数是否为这个对象的引用". (这是为了性能的优化)
-
使用 instanceof 操作符, 检查"参数是否为正确的类型".
-
把参数转换成正确的类型.
-
对于该类中的每个"关键(significant)"域, 检查参数中的域是否与该对象中对应的域相匹配.
-
当你编写完成了 equals 方法之后, 应该问自己三个问题: 它是否是对称的, 传递的, 一致的? (自反性和非空性通常上会自动满足)
@Override public boolean equals(Object o) { if (o == this) { return true; } if (!(o instanceof PhoneNumber)) { return false; } PhoneNumber pn = (PhoneNumber)o; return pn.lineNumber == lineNumber && pn.prefix == prefix && pn.areaCode == areaCode; }
最后的一些告诫:
-
覆盖 equals 时总要覆盖 hashCode
-
不要企图让 equals 方法过于智能(简单才是你的朋友)
-
不是将 equals 声明中的 Object 对象替换为其它的类型
9. 覆盖 equals 时总要覆盖 hashCode
hashCode 的通用约定:
-
在应用程序的执行期间, 只要对象的 equals 方法的比较操作所用到的信息没有被修改, 那么对这同一个对象调用多次, hashCode 方法都必须始终如一地返回同一个整数. 在同一个应用程序的多次执行过程中, 每次执行所返回的整数可以不一致.
-
如果两个对象根据 equals(Object) 方法比较是相等的, 那么调用这两个对象中任意一个对象的 hashCode 方法都必须产生同样的整数结果.
-
如果两个对象根据 equals(Object) 方法比较是不相等的, 那么调用这两个对象中任意一个对象的 hashCode 方法, 则不一定要产生不同的整数结果. 但是程序员应该知道, 给不相等的对象产生截然不同的整数结果, 有可能提高散列表(hash table)的性能.
覆盖 hashCode 方法的决窍:
-
把某个非零的常数值, 比如: 17, 保存在一个名为 result 的 int 类型的变量中.
-
对于对象中每个关键域 f (指 equals 方法中涉及的每个域), 完成以下步骤:
a. 为该域计算 int 类型的散列码 c:
i. boolean 类型: (f ? 1 : 0) ii. byte, char, short 或者 int 类型: (int)f iii. long 类型: (int)(f ^ (f >>> 32)) iv. float 类型: Float.floatToIntBits(f) v. double 类型: Double.doubleToLongBits(f), 然后按照步骤 2.a.iii, 为得到的 long 类型值计算散列值 vi. 对象引用: 如果该类的 equals 方法通过递归地调用 equals 的方式来比较这个域, 则同样为这个域递归地调用 hashCode. 如果这个域的值为 null, 则返回 0(或者其它某个常数, 但通常为 0) vii. array 类型: 把每一个元素当做单独的域来处理. 递归地应用上述规则, 对每个重要的元素计算一个散列码, 然后根据步骤 2.b 中的做法把这些散列值组合起来. 如果数组域中的每个元素都很重要, 可以利用 Java 1.5 中新增的 Arrays.hashCode 方法.b. 按照下面的公式, 把步骤 2.a 中计算得到的散列码 c 合并到 result 中:
result = 31 * result + c; -
返回 result.
-
问问自己 "相等的实例是否都具有相等的散列码?".
// Lazily initialized, cached hashCode private volatile int hashCode; @Override public int hashCode() { int result = hashCode; if (result == 0) { result = 17; result = 31 * result + areaCode; result = 31 * result + prefix; result = 31 * result + lineNumber; } return result; }
需要注意的是:
-
在散列码的计算过程中, 可以把冗余域排除在外. (必须排除 equals 比较计算中没有用到的任何域)
-
不要试图从散列码计算中排除掉一个对象的关键部分来提高性能.
10. 始终要覆盖 toString
提供好的 toString 实现可以使类用起来更加舒适.
在实际应用中, toString 方法应该返回对象中包含的所有值得关注的信息.
无论你是否决定指定格式, 都应该在文档中明确地表明你的意图.
无论是否指定格式, 都为 toString 返回值中包含的所有信息, 提供一种编程式的访问途径, 使得对象的使用者不需要自己去解析这些字符串. 例如: PhoneNumber 类应该包含针对 areaCode, prefix 和 lineNumber 的访问方法.
11. 谨慎地覆盖 clone
Cloneable 接口并没有包含任何方法. 如果一个类实现了 Cloneable 接口, Object 的 clone 方法就会返回该对象的逐域拷贝, 否则就会抛出 CloneNotSupportedException 异常.
如果你覆盖了非 final 类中的 clone 方法, 则应该返回一个通过调用 super.clone 而得到的对象. 对于实现了 Cloneable 的类, 我们总是期望它也提供一个功能适当的公有的 clone 方法.
如果对象中 没有包含 可变对象的域, 可以使用简单的 clone 的实现:
@Override public PhoneNumber clone() { try { // PhoneNumber.clone must cast the result of super.clone() before returning it. return (PhoneNumber)super.clone(); } catch (CloneNotSupportedException e) { throw new AssertionError(); // Can't happen } }
但如果对象中 包含了 可变对象的域, 我们就需要另一种解决方案了. 因为可变域会指向内存中相同的对象, 原始的实例与被克隆的实例将会共享这些对象.
clone 方法就是另一个构造器, 你必须确保它不会伤害到原始的对象, 并确保正确地创建被克隆对象中的约束条件.
在可变域对象上递归地调用 clone 是最容易的做法:
@Override public Stack clone() { try { Stack result = (Stack)super.clone(); // From Java 1.5, don't need casting when cloning arrays. result.elements = elements.clone(); return result; } catch (CloneNotSupportedException e) { throw new AssertionError(); } }
clone 架构与引用可变对象的 final 域的正常用法是不相兼容的, 除非在原始对象和克隆之间可以安全地共享此可变对象.
对于更复杂的对象的克隆, 有时递归地调用 clone 是不够的, 还需要一些特别的方法.
例如:
-
对于被克隆对象中的对象数组域, 很可能需要进行"深度拷贝". 对于容易导致栈溢出的调用, 可以在 deepCopy 中用迭代代替递归.
-
使用另一种方式去完成克隆操作: 先调用 super.clone, 然后把结果对象中的所有域都设置成它们的空白状态, 然后调用高层的方法来重新产生对象的状态.
如同构造器一样, clone 方法不应该在构造的过程中, 调用新对象中任何非 final 的方法.
Object 的 clone 方法被声明为可抛出 CloneNotSupportedException 异常, 但是, 覆盖版本的 clone 方法可能会忽略这个声明.
公有的 clone 方法应该省略这个声明.
如果专门为了继承而设计的类覆盖了 clone 方法, 覆盖版本的 clone 方法就应该模拟 Object.clone 的行为:
- 它应该被声明为 protected;
- 它应该被声明为抛出 CloneNotSupportedException 异常;
- 它 不应该 实现 Cloneable 接口.
这样可使得子类具有实现或不实现 Cloneable 接口的自由, 就仿佛它们直接扩展了 Object 一样.
值得注意的是:
如果你决定用线程安全的类实现 Cloneable 接口, 要记得它的 clone 方法必须得到很好的同步.
简而言之, 实现了 Cloneable 接口的类, 都应该像以下步骤这样创建一个方法:
- 用一个公有的方法覆盖 clone;
- 返回的对象类型是当前的类;
- 首先调用 super.clone 方法;
- 然后修改任何需要修正的域.
最好提供某些其它的途径来代替对象拷贝, 或者干脆不提供这样的功能.
拷贝构造器
拷贝工厂
public static Yum newInstance(Yum yum);
拷贝构造器的做法, 及其静态工厂方法的变形, 都比 Cloneable/clone 的方式具有更多的优势:
- 不依赖于某一种很有风险的, 语言之外的对象创建机制;
- 不要求遵守尚未制定好文档的规范;
- 不会与 final 域的正常使用发生冲突;
- 不会抛出不必要的受检异常;
- 不需要进行类型转换.
除此之外, 基于接口的拷贝构造器和拷贝工厂(更准确的应叫 "转换构造器" 和 "转换工厂"), 允许客户选择拷贝的实现类型
public HashSet(Set set) -> TreeSet;
12. 考虑实现 Comparable 接口
Comparable 是一个接口, 它并没有在 Object 中声明.
为实现 Comparable 接口的对象数组进行排序可以简单地使用: Arrays.sort(a);
一旦类实现了 Comparable 接口, 它就可以跟许多泛型算法以及依赖于该接口的集合实现进行协作. 你付出很小的努力就可以获得非常强大的功能.
compareTo 方法需遵循以下的约定(自反性, 对称性, 传递性)
-
if a > b then b < a; if a == b then b == a; if a < b then b > a;
-
if a > b and b > c then a > c;
-
if a == b and b == c then a == c;
-
强烈建议: (x.compareTo(y) == 0) == (x.equals(y))
比较整数型基本类型的域, 可以使用关系操作符 < 和 >. 对于浮点域, 需使用 Double.compare 或者 Float.compare. 对于数组域, 则要把这些指导原则应用到每个元素上.
如果一个类有多个关键域, 那么, 按什么样的顺序来比较这些域是非常关键的. 你必须从最关键的域开始, 逐步进行到所有的重要域.
public int compareTo(PhoneNumber pn) { // Compare area codes if (areaCode < pn.areaCode) { return -1; } if (areaCode > pn.areaCode) { return 1; } // Area codes are equal, compare prefixes if (prefix < pn.prefix) { return -1; } if (prefix > pn.prefix) { return 1; } // Area codes and prefixes are equal, compare line numbers if (lineNumber < pn.lineNumber) { return -1; } if (lineNumber > pn.lineNumber) { return 1; } return 0; // All fields are equal }
四. 类和接口
13. 使类和成员的可访问性最小化
封装:
-
设计良好的模块会隐藏所有的实现细节.
-
模块之间只通过它们的API进行通信, 一个模块不需要知道其它模块的内部工作情况.
-
解除组成系统的各模块之间的耦合关系, 使得这些模块可以独立地:
- 开发(可以并行的进行)
- 测试(即使整个系统无法通过测试, 独立的模块也可能测试成功)
- 优化与修改(不会影响到其它的模块)
- 理解(不需要被其它的模块理解)
- 重用
尽可能地使每个类或者成员不被外界访问
如果一个包级私有的顶层类(或者接口), 只是在某一个类的内部被使用, 就应该考虑使它成为唯一使用它的那个类的私有嵌套类.
为了测试而将一个公有类的私有成员变成包级私有的, 这还可以接受, 但是要将访问级别提高到超过它, 这就无法接受了.
不能为了测试, 而将类/接口或者成员变量变成包的导出API的一部分.
实例域决不能是公有的. 包含公有可变域的类并不是线程安全的.
对于静态域, 如果这些域包含基本类型的值, 或者是包含指向不可变对象的引用, 那么可以通过公有的静态 final 域来暴露这些常量. 如果 final 域包含可变对象的引用, 它便具有非 final 域的所有缺点.
长度非零的数组总是可变的. 所有, 类具有公有的静态final数组域, 或者返回这种域的访问方法, 这几乎总是错误的.
// 存在的安全漏洞! public static final Thing[] VALUES = { ... };
解决方案:
private static final Thing[] PRIVATE_VALUES = { ... }; public static final List<Thing> VALUES = Collections.unmodifiableList(Arrays.asList(PRIVATE_VALUES));
或者:
private static final Thing[] PRIVATE_VALUES = { ... }; public static final Thing[] values() { return PRIVATE_VALUES; }
14. 在公有类中使用访问方法而非公有域
退化类(Degenerate classes)不应该是公有的
class Point { public double x; public double y; }
因为这些类:
- 没有提供封装的功能;
- 如果不改变API, 就无法改变它的数据表示法;
- 无法强加任何约束条件;
- 当域被访问的时候, 无法采取任何辅助的行动.
对于可变的类来说, 应该用包含私有域和公有设值方式(setter)的类代替:
class Point { private double x; private double y; public Point(double x, double y) { this.x = x; this.y = y; } public double getX() { return x; } public double getY() { return y; } public void setX(double x) { this.x = x; } public void setY(double y) { this.y = y; } }
-
如果类可以在它所在的包的外部进行访问, 就提供访问方法.
-
如果类是包级私有的, 或者是私有的嵌套类, 直接暴露它的数据域并没有本质的错误.
-
公有类永远都不应该暴露可变的域. 虽然还是有问题, 但是让公有类暴露不可变的域其危害比较小.
// Public class with exposed immutable fields - questionable public final class Time { private static final int HOURS_PER_DAY = 24; private static final int MINUTES_PER_HOUR = 60; public final int hour; public final int minute; public Time(int hour, int minute) { if (hour < 0 || hour >= HOURS_PER_DAY) { throw new IllegalArgumentException("Hour: " + hour); } if (minute < 0 || minute >= MINUTES_PER_HOUR) { throw new IllegalArgumentException("Min: " + minute); } this.hour = hour; this.minute = minute; } ... // Remainder omitted }
15. 使可变性最小化
不可变类只是其实例不能被修改的类. 每个实例中包含的所有信息都必须在创建该实例的时候就提供, 并在对象的整个生命周期内固定不变.
不可变的类比可变类更加易于设计、实现和使用. 它们不容易出错, 且更加安全.
为了使类成为不可变, 要遵循下面5条规则:
-
不要提供任何会修改对象状态的方法
-
保证类不会被扩展
-
使所有的域都是 final 的
-
使所有的域都成为私有的
-
确保对于任何可变组件的互斥访问
public final class Complex { private final double re; private final double im; public Complex(double re, double im) { this.re = re; this.im = im; } // Accessors with no corresponding mutators public double realPart() { return re; } public double imaginaryPart() { return im; } public Complex add(Complex c) { return new Complex(re + c.re, im + c.im); } public Complex subtract(Complex c) { return new Complex(re - c.re, im - c.im); } ... @Override public boolean equals(Object o) { ... } }
这些算术运算创建并返回新的 Complex 实例. (函数的做法)
不可变对象比较简单, 它们在整个生命周期内只有一种状态.
不可变对象本质上是线程安全的, 它们不要求同步. 它们可以被自由地共享, 并且可以重用现有的实例.
public static final Complex ZERO = new Complex(0, 0); public static final Complex ONE = new Complex(1, 0); public static final Complex I = new Complex(0, 1);
不可变的类可以提供一些静态工厂, 它们把频繁被请求的实例缓存起来, 从而当现在实例可以符合请求的时候, 就不必创建新的实例.
不仅可以共享不可变对象, 甚至也可以共享它们的内部信息.
不可变�对象为其它对象提供了大量的构件(building blocks).
不可变类真正唯一的缺点是, 对于每个不同的值都需要一个单独的对象. 在某些情况下, 这会带来性能的问题.
如何禁止不可变对象子类化
- 使类成为 final 的
- 让类的所有构造器都变成私有的或者包级私有的, 并添加公有的静态工厂来代替公有的构造器
// Immutable class with static factories instead of constructors public class Complex { private final double re; private final double im; private Complex(double re, double im) { this.re = re; this.im = im; } public static Complex valueOf(double re, double im) { return new Complex(re, im); } ... // Remainder unchanged }
除了允许多个实现类的灵活性之外, 这种方法还使得有可能通过改善静态工厂的对象缓存能力, 在后续的发行版本中改进该类的性能. 并且允许创建更多的工厂方法, 其名字就可以阐明其功能.
总结
-
除非有很好的理由要让类成为可变的类, 否则就应该是不可变的
-
如果类不能被做成是不可变的, 仍然应该尽可能地限制它的可变性
-
除非有令人信服的�理由要使域变成是非 final 的, 否则要使每个域都是 final 的
-
可以减轻一些规则以提高性能(缓存, 延迟初始化...)