`
tianlovv
  • 浏览: 14866 次
  • 性别: Icon_minigender_1
  • 来自: 南京
社区版块
存档分类
最新评论

不变类的好处

    博客分类:
  • java
阅读更多
原帖: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 会提供更佳的性能和更方便的访问。

结束语
使用不变对象比使用可变对象要容易得多。它们只能处于一种状态,所以始终是一致的,它们本来就是线程安全的,可以被自由地共享。使用不变对象可以彻底消除许多容易发生但难以检测的编程错误,如无法在线程间同步访问或在存储对数组或对象的引用前无法克隆该数组或对象。在编写类时,问问自己这个类是否可以作为不变类有效地实现,总是值得的。您可能会对回答常常是肯定的而感到吃惊。
分享到:
评论

相关推荐

    Java测试题4答案

    A、BorderLayout B、FlowLayout C、GridLayout D、CardLayout 6、 用于存放创建后则不变的字符串常量是( )。 A、String类 B、StringBuffer类 C、Character类、D、以上都不对 三、判别题 1、 一个...

    【05-面向对象(下)】

    •但对引用类型的变量而言,它仅仅保存的是一个引用,final只能保证他的地址不变,但不能保证对象,所以引用 类型完全可以改变他的对象。 可执行“宏替换”的final变量 •对一个final变量来说,不管它...

    1.什么是设计模式? 2.设计模式是指在软件开发中,经过验证的,用于解决在特定环境下、重复出现的、特定问题的解决方案 3.说出

    8.由于外部调用和内部实现被接口隔离开了,那么只要接口不变,内部实现的变化就不会影响到外部应用,从而使得系统更灵活,具有更好的扩展性和可维护性 9.什么是OOP?OOP有什么特性?使用OOP用什么好处? 10.oop 是...

    javaSE代码实例

    第5章 数组——以不变应万变的哲学 59 5.1 数组的声明及创建 59 5.1.1 声明数组引用 59 5.1.2 创建数组对象 60 5.2 Java中数组的实现机制 61 5.3 数组的初始化 63 5.3.1 默认初始化 63 5.3.2 利用...

    javaScript函数式编程

    全书共9章,分别介绍了JavaScript函数式编程、一等函数与Applicative编程、变量的作用域和闭包、高阶函数、由函数构建函数、递归、纯度和不变性以及更改政策、基于流的编程、类编程。除此之外,附录中还介绍了更多...

    laravel-factories-reloaded:package此软件包可让您为Laravel项目创建工厂类

    无论您使用哪个版本,此处的大多数说明都将保持不变。 尽管如此,我仍将整个自述文件重写为关于v1和v2之间差异的详细信息。 同时,玩得开心:-) 好处 使用Laravel工厂已经喜欢的功能( create , make , times , ...

    implements:使用装饰器的Pythonic接口

    您的MRO保持不变,并且在导入期间(而不是类实例化时)尽早评估接口。安装工具在PyPI上可用,可以与一起安装: pip install implements注意Python 3.6+是必需的,因为它依赖于inspect模块的新功能。好处。 从多个类...

    语言程序设计课后习题答案

    2-4 使用关键字const而不是#define语句的好处有哪些? 解: const定义的常量是有类型的,所以在使用它们时编译器可以查错;而且,这些变量在调试时仍然是可见的。 2-5 请写出C++语句声明一个常量PI,值为3.1416;再...

    二十三种设计模式【PDF版】

    类,再定义类的接口和继承层次,建立对象之间的基本关系。你的设计应该对手头的问题有针对性,同时对将来的问题和需求 也要有足够的通用性。 你也希望避免重复设计或尽可能少做重复设计。有经验的面向对象设计者会...

    CMS之数据库设计.docx

    设置这种"骨架"的好处:虽然扩展表、字段会有变化,但是"骨架"结构是不变的。这样一是可以让结构清晰,抓住中心、重点;二是当需求变化的时候,对结构的影响降到最低;三是,如果对于这种"骨架"习惯、掌握了之后,...

    利用陷波器减小放大器频响中的尖峰,提高增益平坦度

    利用这种新奇而又简单的技术可减小尖峰,提高增益平坦度,还可减小过冲,所有这些好处都是在保持原有1GHz带宽(-3dB)不变的条件下获得的。这个简单的解决方案虽然增加了3个新元件,但在平坦的频率响应、低过冲以及...

    matlab代码输入如何换行符-spire-matrix:使用BLAS的尖顶矩阵线性代数的概念验证

    它们具有伴随的类型类,例如MatrixMultiplication。 数据类型 公制空间 有限矩阵 ✓ 方矩阵 UpperTriangualMatrix 带状矩阵 设计目标 参照透明性和不变性。 这样做的想法是,这将帮助我们编写更安全的代码,并使代码...

    java版2048源码下载-jvm:实战虚拟机

    反码就是符号位不变,其他位取反。 补码的好处 0既不是正数也不是负数,反码不好表示,补码则相同。 补码将加减法的做法完全统一,无需区分正数和负数 浮点数的表示 IEEE754规范,一个浮点数由符号位,指数位和尾数...

    SPC培训资料.pptx

    好处: 1、为过程提供早期报警系统,及时监控过程是否失控,做到事前发现,降低不良率、返工率 2、使生产线对过程进行持续改进的控制 3、区分普通原因、特殊原因作为采取局部措施或系统采取措施的指南 SPC培训资料...

    C/C++笔试题(附答案,华为面试题系列)

    1)在函数体,一个被声明为静态的变量在这一函数被调用过程中维持其值不变。 2) 在模块内(但在函数体外),一个被声明为静态的变量可以被模块内所用函数访问,但不能被模块外其它函数访问。它是一个本地的全局变量...

    GSP5.exe

    ③先选运动圆的圆心,再选A点,选择“编辑”菜单的“操作类按钮”项的“移动”命令,并选择“慢速”,然后确定。这时《几何画板》窗口出现“移动”按钮,可以用“标签”工具把文字改为“外切”; ④同样方法可以作出...

    Delphi最新三层源码

    也就是不变的。以后该业务要变,就十分方便,只需要在中间层的定位器,配置一下就可以了,如果采用XML或文件配置,不需要修改任何程序,客户的业务已经发生改变。当然会采用名字调用等技术了。相关代码如下: ...

    如何做好时间规划PPT

    生活或者工作,无论做什么都应当有较高的效率,这在无形中就可以延长时间,这就是注重效率的好处。 事不宜迟,速度制胜 《孙子兵法》上说:“激水之疾,至于漂石者,势也”。速度能决定石头浮沉。面对“快鱼吃慢鱼”...

    oracle学习文档 笔记 全面 深刻 详细 通俗易懂 doc word格式 清晰 连接字符串

    ORACLE用户是学习ORACLE数据库中的基础知识,下面就介绍下类系统常用的默认ORACLE用户: 1. sys用户:超级用户,完全是个SYSDBA(管理数据库的人)。拥有dba,sysdba,sysoper等角色或权限。是oracle权限最高的用户,...

Global site tag (gtag.js) - Google Analytics