优点
静态工厂方法与构造器不同的优势在于:
- 它们有名称
- 不必在每次调用它们的时候都创建一个新对象
- 它们可以返回原返回类型的任何子类型的对象
缺点
但是, 静态工厂方法也有缺点, 主要在于:
- 类如果不含公有的或者受保护的构造器, 就不能被子类化.(鼓励使用复合, 而不是继承)
- 它们与其它的静态方法实际上没有任何区别.
(静态工厂方法的一些惯用名称: 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();
创建单例有多种的方法:
- 公有静态成员是个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 的最佳方法.
用于只包含静态方法和静态域的类.
例如用于:
- 把基本类型的值或者数组类型上的相关方法组织起来. (比如: 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
}
- 重用不可变的对象
不要这样做:
String s = new String("stringette");
该语句每次被执行的时候都创建一个新的 String 实例. 参数 "stringette" 本身就是一个 String 实例, 如果这种用法是在一个循环中, 或者是在一个被频繁调用的方法中, 就会创建出成千上万不必要的 String 实例.
应该这样做:
String s = "stringette";
这个版本只用了一个 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 实例.
- 对象池一般来说不是一个好的做法
除非池中的对象是非常重量级的, 比如像: 数据库连接池.
你能找到其中的"内存泄漏"吗?
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 工具去发现不可见的内存泄漏问题
终结方法通常是不可预测的, 也是很危险的, 一般情况下是不必要的.
-
注重时间的任务不应该由终结方法来完成
不能保证终结方法会被及时地执行.
-
不应该依赖终结方法来更新重要的持久状态
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
以下情况时不要覆盖:
-
类的每个实例本质上是唯一的. 例如: 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 对象替换为其它的类型
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 比较计算中没有用到的任何域)
-
不要试图从散列码计算中排除掉一个对象的关键部分来提高性能.
提供好的 toString 实现可以使类用起来更加舒适.
在实际应用中, toString 方法应该返回对象中包含的所有值得关注的信息.
无论你是否决定指定格式, 都应该在文档中明确地表明你的意图.
无论是否指定格式, 都为 toString 返回值中包含的所有信息, 提供一种编程式的访问途径, 使得对象的使用者不需要自己去解析这些字符串. 例如: PhoneNumber 类应该包含针对 areaCode, prefix 和 lineNumber 的访问方法.
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 Yum(Yum yum);
拷贝工厂
public static Yum newInstance(Yum yum);
拷贝构造器的做法, 及其静态工厂方法的变形, 都比 Cloneable/clone 的方式具有更多的优势:
- 不依赖于某一种很有风险的, 语言之外的对象创建机制;
- 不要求遵守尚未制定好文档的规范;
- 不会与 final 域的正常使用发生冲突;
- 不会抛出不必要的受检异常;
- 不需要进行类型转换.
除此之外, 基于接口的拷贝构造器和拷贝工厂(更准确的应叫 "转换构造器" 和 "转换工厂"), 允许客户选择拷贝的实现类型
public HashSet(Set set) -> TreeSet;
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
}
封装:
-
设计良好的模块会隐藏所有的实现细节.
-
模块之间只通过它们的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;
}
退化类(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
}
不可变类只是其实例不能被修改的类. 每个实例中包含的所有信息都必须在创建该实例的时候就提供, 并在对象的整个生命周期内固定不变.
不可变的类比可变类更加易于设计、实现和使用. 它们不容易出错, 且更加安全.
为了使类成为不可变, 要遵循下面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 的
-
可以减轻一些规则以提高性能(缓存, 延迟初始化...)