副标题#e#
稳定工具是指在实例化后其外部可见状态无法变动的工具。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 是稳定 的,那么这种恶意的代码就不能以如此令人杂乱而危险的要领修改我们的措施状 态了。
#p#副标题#e#
精采的键
稳定工具发生最好的 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());
}
何时利用稳定类
#p#分页标题#e#
稳定类最适合暗示抽象数据范例(如数字、列举范例或颜色)的值。Java 类 库中的根基数字类(如 Integer 、 Long 和 Float )都是稳定的,其它尺度数 字范例(如 BigInteger 和 BigDecimal )也是稳定的。暗示复数或精度任意的 有理数的类将较量适合于稳定性。甚至包括很多离散值的抽象范例(如向量或矩 阵)也很适合实现为稳定类,这取决于您的应用措施。
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 }
}
#p#分页标题#e#
通过一些其它事情,可以编写利用一些非 final 字段的稳定类(譬喻, String 的尺度实现利用 hashCode 值的惰性计较),这样大概比严格的 final 类执行得更好。假如类暗示抽象范例(如数字范例或颜色)的值,那么您还会想 实现 hashCode() 和 equals() 要领,这样工具将作为 HashMap 或 HashSet 中 的一个键事情精采。要保持线程安详,不答允 this 引用从结构函数中转义是很 重要的。
偶然变动的数据
有些数据项在措施生命期中一直保持常量,而有些会频繁变动。常量数据显 然切合稳定性,而状态巨大且频繁变动的工具凡是不适适用稳定类来实现。那么 有时会变动,但变动又不太频繁的数据呢?有什么要领能让 有时变动的数据获 得稳定性的便利和线程安详的优点呢?
util.concurrent 包中的 CopyOnWriteArrayList 类是如何既操作稳定性的 本领,又仍答允偶然修改的一个精采示例。它最适合于支持事件监听措施的类( 如用户界面组件)利用。固然事件监听措施的列表可以变动,但凡是它变动的频 繁性要比事件的生成少得多。
除了在修改列表时, CopyOnWriteArrayList 并稳定动根基数组,而是建设 新数组且废弃旧数组之外,它的行为与 ArrayList 类很是相似。这意味着当调 用措施得到迭代器(迭代器在内部生存对根基数组的引用)时,迭代器引用的数 组实际上是稳定的,从而可以无需同步或冒并发修改的风险举办遍历。这消除了 在遍历前克隆列表或在遍历期间对列表举办同步的需要,这两个操纵都很贫苦、 易于堕落,并且完全使机能恶化。假如遍历比插入或撤除越发频繁(这在某些情 况下是常有的事), CopyOnWriteArrayList 会提供更佳的机能和更利便的会见 。
竣事语
利用稳定工具比利用可变工具要容易得多。它们只能处于一种状态,所以始 终是一致的,它们原来就是线程安详的,可以被自由地共享。利用稳定工具可以 彻底消除很多容易产生但难以检测的编程错误,如无法在线程间同步会见或在存 储对数组或工具的引用前无法克隆该数组或工具。在编写类时,问问本身这个类 是否可以作为稳定类有效地实现,老是值得的。您大概会对答复经常是必定的而 感想受惊。