原帖:
http://www.ibm.com/developerworks/cn/java/j-jtp02183/
不变对象是指在实例化后其外部可见状态无法更改的对象。Java 类库中的 String 、 Integer 和 BigDecimal 类就是不变对象的示例 ― 它们表示在对象的生命期内无法更改的单个值。
不变性的长处
如果正确使用不变类,它们会极大地简化编程。因为它们只能处于一种状态,所以只要正确构造了它们,就决不会陷入不一致的状态。您不必复制或克隆不变对象,就能自由地共享和高速缓存对它们的引用;您可以高速缓存它们的字段或其方法的结果,而不用担心值会不会变成失效的或与对象的其它状态不一致。不变类通常产生最好的映射键。而且,它们本来就是线程安全的,所以不必在线程间同步对它们的访问。
自由高速缓存
因为不变对象的值没有更改的危险,所以可以自由地高速缓存对它们的引用,而且可以肯定以后的引用仍将引用同一个值。同样地,因为它们的特性无法更改,所以您可以高速缓存它们的字段和其方法的结果。
如果对象是可变的,就必须在存储对其的引用时引起注意。请考虑清单 1 中的代码,其中排列了两个由调度程序执行的任务。目的是:现在启动第一个任务,而在某一天启动第二个任务。
清单 1. 可变的 Date 对象的潜在问题
Date d = new Date();
Scheduler.scheduleTask(task1, d);
d.setTime(d.getTime() + ONE_DAY);
scheduler.scheduleTask(task2, d);
因为 Date 是可变的,所以 scheduleTask 方法必须小心地用防范措施将日期参数复制(可能通过 clone() )到它的内部数据结构中。不然, task1 和 task2 可能都在明天执行,这可不是所期望的。更糟的是,任务调度程序所用的内部数据结构会变成讹误。在编写象 scheduleTask() 这样的方法时,极其容易忘记用防范措施复制日期参数。如果忘记这样做,您就制造了一个难以捕捉的错误,这个错误不会马上显现出来,而且当它暴露时人们要花较长的时间才会捕捉到。不变的 Date 类不可能发生这类错误。
固有的线程安全
大多数的线程安全问题发生在当多个线程正在试图并发地修改一个对象的状态(写-写冲突)时,或当一个线程正试图访问一个对象的状态,而另一个线程正在修改它(读-写冲突)时。要防止这样的冲突,必须同步对共享对象的访问,以便在对象处于不一致状态时其它线程不能访问它们。正确地做到这一点会很难,需要大量文档来确保正确地扩展程序,还可能对性能产生不利后果。只要正确构造了不变对象(这意味着不让对象引用从构造函数中转义),就使它们免除了同步访问的要求,因为无法更改它们的状态,从而就不可能存在写-写冲突或读-写冲突。
不用同步就能自由地在线程间共享对不变对象的引用,可以极大地简化编写并发程序的过程,并减少程序可能存在的潜在并发错误的数量。
在恶意运行的代码面前是安全的
把对象当作参数的方法不应变更那些对象的状态,除非文档明确说明可以这样做,或者实际上这些方法具有该对象的所有权。当我们将一个对象传递给普通方法时,通常不希望对象返回时已被更改。但是,使用可变对象时,完全会是这样的。如果将 java.awt.Point 传递给诸如 Component.setLocation() 的方法,根本不会阻止 setLocation 修改我们传入的 Point 的位置,也不会阻止 setLocation 存储对该点的引用并稍后在另一个方法中更改它。(当然, Component 不这样做,因为它不鲁莽,但是并不是所有类都那么客气。)现在, Point 的状态已在我们不知道的情况下更改了,其结果具有潜在危险 ― 当点实际上在另一个位置时,我们仍认为它在原来的位置。然而,如果 Point 是不变的,那么这种恶意的代码就不能以如此令人混乱而危险的方法修改我们的程序状态了。
良好的键
不变对象产生最好的 HashMap 或 HashSet 键。有些可变对象根据其状态会更改它们的 hashCode() 值(如清单 2 中的 StringHolder 示例类)。如果使用这种可变对象作为 HashSet 键,然后对象更改了其状态,那么就会对 HashSet 实现引起混乱 ― 如果枚举集合,该对象仍将出现,但如果用 contains() 查询集合,它就可能不出现。无需多说,这会引起某些混乱的行为。说明这一情况的清单 2 中的代码将打印“false”、“1”和“moo”。
清单 2. 可变 StringHolder 类,不适合用作键
public class StringHolder {
private String string;
public StringHolder(String s) {
this.string = s;
}
public String getString() {
return string;
}
public void setString(String string) {
this.string = string;
}
public boolean equals(Object o) {
if (this == o)
return true;
else if (o == null || !(o instanceof StringHolder))
return false;
else {
final StringHolder other = (StringHolder) o;
if (string == null)
return (other.string == null);
else
return string.equals(other.string);
}
}
public int hashCode() {
return (string != null ? string.hashCode() : 0);
}
public String toString() {
return string;
}
...
StringHolder sh = new StringHolder("blert");
HashSet h = new HashSet();
h.add(sh);
sh.setString("moo");
System.out.println(h.contains(sh));
System.out.println(h.size());
System.out.println(h.iterator().next());
}
何时使用不变类
不变类最适合表示抽象数据类型(如数字、枚举类型或颜色)的值。Java 类库中的基本数字类(如 Integer 、 Long 和 Float )都是不变的,其它标准数字类型(如 BigInteger 和 BigDecimal )也是不变的。表示复数或精度任意的有理数的类将比较适合于不变性。甚至包含许多离散值的抽象类型(如向量或矩阵)也很适合实现为不变类,这取决于您的应用程序。
Flyweight 模式
不变性启用了 Flyweight 模式,该模式利用共享使得用对象有效地表示大量细颗粒度的对象变得容易。例如,您可能希望用一个对象来表示字处理文档中的每个字符或图像中的每个像素,但这一策略的幼稚实现将会对内存使用和内存管理开销产生高得惊人的花费。Flyweight 模式采用工厂方法来分配对不变的细颗粒度对象的引用,并通过仅使一个对象实例与字母“a”对应来利用共享缩减对象数。有关 Flyweight 模式的更多信息,请参阅经典书籍 Design Patterns(Gamma 等著;请参阅 参考资料)。
Java 类库中不变性的另一个不错的示例是 java.awt.Color 。在某些颜色表示法(如 RGB、HSB 或 CMYK)中,颜色通常表示为一组有序的数字值,但把一种颜色当作颜色空间中的一个特异值,而不是一组有序的独立可寻址的值更有意义,因此将 Color 作为不变类实现是有道理的。
如果要表示的对象是多个基本值的容器(如:点、向量、矩阵或 RGB 颜色),是用可变对象还是用不变对象表示?答案是……要看情况而定。要如何使用它们?它们主要用来表示多维值(如像素的颜色),还是仅仅用作其它对象的一组相关特性集合(如窗口的高度和宽度)的容器?这些特性多久更改一次?如果更改它们,那么各个组件值在应用程序中是否有其自己的含义呢?
事件是另一个适合用不变类实现的好示例。事件的生命期较短,而且常常会在创建它们的线程以外的线程中消耗,所以使它们成为不变的是利大于弊。大多数 AWT 事件类都没有作为严格的不变类来实现,而是可以有小小的修改。同样地,在使用一定形式的消息传递以在组件间通信的系统中,使消息对象成为不变的或许是明智的。
编写不变类的准则
编写不变类很容易。如果以下几点都为真,那么类就是不变的:
它的所有字段都是 final
该类声明为 final
不允许 this 引用在构造期间转义
任何包含对可变对象(如数组、集合或类似 Date 的可变类)引用的字段:
是私有的
从不被返回,也不以其它方式公开给调用程序
是对它们所引用对象的唯一引用
构造后不会更改被引用对象的状态
最后一组要求似乎挺复杂的,但其基本上意味着如果要存储对数组或其它可变对象的引用,就必须确保您的类对该可变对象拥有独占访问权(因为不然的话,其它类能够更改其状态),而且在构造后您不修改其状态。为允许不变对象存储对数组的引用,这种复杂性是必要的,因为 Java 语言没有办法强制不对 final 数组的元素进行修改。注:如果从传递给构造函数的参数中初始化数组引用或其它可变字段,您必须用防范措施将调用程序提供的参数或您无法确保具有独占访问权的其它信息复制到数组。否则,调用程序会在调用构造函数之后,修改数组的状态。清单 3 显示了编写一个存储调用程序提供的数组的不变对象的构造函数的正确方法(和错误方法)。
清单 3. 对不变对象编码的正确和错误方法
class ImmutableArrayHolder {
private final int[] theArray;
// Right way to write a constructor -- copy the array
public ImmutableArrayHolder(int[] anArray) {
this.theArray = (int[]) anArray.clone();
}
// Wrong way to write a constructor -- copy the reference
// The caller could change the array after the call to the constructor
public ImmutableArrayHolder(int[] anArray) {
this.theArray = anArray;
}
// Right way to write an accessor -- don't expose the array reference
public int getArrayLength() { return theArray.length }
public int getArray(int n) { return theArray[n]; }
// Right way to write an accessor -- use clone()
public int[] getArray() { return (int[]) theArray.clone(); }
// Wrong way to write an accessor -- expose the array reference
// A caller could get the array reference and then change the contents
public int[] getArray() { return theArray }
}
通过一些其它工作,可以编写使用一些非 final 字段的不变类(例如, String 的标准实现使用 hashCode 值的惰性计算),这样可能比严格的 final 类执行得更好。如果类表示抽象类型(如数字类型或颜色)的值,那么您还会想实现 hashCode() 和 equals() 方法,这样对象将作为 HashMap 或 HashSet 中的一个键工作良好。要保持线程安全,不允许 this 引用从构造函数中转义是很重要的。
偶尔更改的数据
有些数据项在程序生命期中一直保持常量,而有些会频繁更改。常量数据显然符合不变性,而状态复杂且频繁更改的对象通常不适合用不变类来实现。那么有时会更改,但更改又不太频繁的数据呢?有什么方法能让 有时更改的数据获得不变性的便利和线程安全的长处呢?
util.concurrent 包中的 CopyOnWriteArrayList 类是如何既利用不变性的能力,又仍允许偶尔修改的一个良好示例。它最适合于支持事件监听程序的类(如用户界面组件)使用。虽然事件监听程序的列表可以更改,但通常它更改的频繁性要比事件的生成少得多。
除了在修改列表时, CopyOnWriteArrayList 并不变更基本数组,而是创建新数组且废弃旧数组之外,它的行为与 ArrayList 类非常相似。这意味着当调用程序获得迭代器(迭代器在内部保存对基本数组的引用)时,迭代器引用的数组实际上是不变的,从而可以无需同步或冒并发修改的风险进行遍历。这消除了在遍历前克隆列表或在遍历期间对列表进行同步的需要,这两个操作都很麻烦、易于出错,而且完全使性能恶化。如果遍历比插入或除去更加频繁(这在某些情况下是常有的事), CopyOnWriteArrayList 会提供更佳的性能和更方便的访问。
结束语
使用不变对象比使用可变对象要容易得多。它们只能处于一种状态,所以始终是一致的,它们本来就是线程安全的,可以被自由地共享。使用不变对象可以彻底消除许多容易发生但难以检测的编程错误,如无法在线程间同步访问或在存储对数组或对象的引用前无法克隆该数组或对象。在编写类时,问问自己这个类是否可以作为不变类有效地实现,总是值得的。您可能会对回答常常是肯定的而感到吃惊。
分享到:
相关推荐
构建不变类的一般准则包括: 1. **构造函数**:构造函数应初始化所有字段,并确保在构造完成后,对象处于有效且不可变的状态。 2. **不可变字段**:对象的所有字段应声明为final,确保它们在构造后不能被重新赋值。...
在JavaScript开发中,特别是"其它杂项"这类非标准或特定场景的应用,这样的工具显得尤为重要。 ### 静态类型检查 在传统的JavaScript中,我们使用动态类型,这意味着变量的类型可以在运行时改变。这为编程提供了...
四、抽象类的好处 * 当更新组件版本时,可根据需要将附加方法添加到基类,但抽象方法和属性必须保持不变。 * 它可以很好的组织类的结构,举个例子,我们将人建模为抽象类,具有抽象方法 speak(),然后子类可以继承...
新式类的主要好处在于它们支持多重继承,并且能够更好地处理方法解析顺序(Method Resolution Order, MRO)。 - **多继承**:新式类在处理多继承时,采用广度优先搜索策略来查找方法。这意味着在寻找一个方法时,会...
针对这一周报的分析,分析师们认为“外延扩张红利期+内生稳定增长”的投资逻辑是不变的,长期看好港股学校类公司的发展。他们建议持续关注天立教育、中教控股、希望教育、华夏视听教育、中国科培、中国东方教育等...
SIFT特征具有尺度不变性和旋转不变性,使其在不同尺度和角度下的图像匹配中表现出色。传统的SIFT特征提取方法需要构建多尺度空间来寻找图像中的关键点,这会增加计算量和时间消耗。 ##### 2.2 单尺度SIFT 为了解决...
"中国饮料功能化趋势及发展机遇——新常态下的“变与不变”" 本资源摘要信息旨在探讨中国饮料功能化趋势及发展机遇,分析消费者需求和市场趋势,以便帮助企业和个人更好地理解中国饮料市场的发展趋势和机遇。 洞悉...
A、BorderLayout B、FlowLayout C、GridLayout D、CardLayout 6、 用于存放创建后则不变的字符串常量是( )。 A、String类 B、StringBuffer类 C、Character类、D、以上都不对 三、判别题 1、 一个...
4. 化肥的分类:肥料根据主要营养成分分为氮肥(如尿素、氨水)、磷肥(如磷矿粉)、钾肥(如硫酸钾)和复合肥(如硝酸钾、磷酸二氢铵),不同类型的化肥对植物生长有不同的益处。 5. 能源的分类:能源分为可再生...
这样做的好处是,即使内部实现发生了变化,只要公共接口保持不变,调用这些类的代码就不需要修改,这种特性被称为封装。 对象则是类的实例或运行时实体。一个类的定义可以创建多个对象,每个对象都有其自己的状态...
LBP操作具有旋转不变性和灰度不变性,使得它在许多视觉任务中表现出色。 预处理在特征提取中起着至关重要的作用,因为原始图像数据可能包含噪声、光照变化、对比度不一致等因素,这些都可能影响到LBP特征的有效性。...
继承的主要好处在于代码复用、对象分类和软件的增量开发。通过继承,我们可以将具有共同特征的对象归为一类,形成类的层次结构。同时,派生类可以扩展或修改基类的功能,以适应特定需求,这种特性对于软件的版本升级...
- **提高可维护性**:当内部实现发生变化时,只要接口保持不变,客户端代码就不需要修改。 - **增强封装性**:隐藏实现细节,只暴露必要的接口,有助于保护数据的安全性和完整性。 - **促进复用**:通过继承和多态等...
Java中的不可变类(immutable)是一种特殊的类,其对象一旦创建后,其状态就不能再发生变化。这类类在Java中有着重要的地位,...在设计自己的类时,考虑使其不可变,可以带来许多潜在的好处,包括提升代码质量和性能。
这种逼近方法的好处在于,它通常能够将复杂的非线性问题转换为相对简单的线性问题进行求解,这样可以降低问题的求解难度,尤其是对于那些非凸或难以直接求解的问题。 在具体的技术细节上,η-逼近方法涉及到向量值...
这样做的好处在于,一方面可以轻松适应不同网站的UI设计风格,另一方面也方便前端设计师进行页面调整而不必深入后端代码细节。此外,这种方式还有利于支持不同的编码格式。 3. **支持非数字分页**:在某些应用场景...
•但对引用类型的变量而言,它仅仅保存的是一个引用,final只能保证他的地址不变,但不能保证对象,所以引用 类型完全可以改变他的对象。 可执行“宏替换”的final变量 •对一个final变量来说,不管它...
2023年的面向对象设计可能涵盖了最新的技术和实践,但基本概念仍然保持不变。 1. **类**是面向对象编程的基础,它是一个模板或者蓝图,定义了对象的属性(特性)和行为(方法)。类使得我们可以创建具有相同特征和...