foreach变异非变异

    技术2024-03-29  12

    不变对象是实例化后其外部可见状态无法更改的对象。 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方法必须谨慎地将date参数(也许通过clone() )防御性地复制到其内部数据结构中。 否则, task1和task2可能都在明天执行,这不是期望的。 更糟糕的是,任务计划程序使用的内部数据结构可能会损坏。 在编写诸如scheduleTask()类的方法时,很容易忘记防御性地复制date参数。 如果您忘记了,则将创建一个细微的错误,该错误将不会出现一段时间,并且需要很长时间才能对其进行跟踪。 不可变的Date类将使这种错误无法实现。

    固有螺纹安全性

    当多个线程试图同时修改一个对象的状态(写冲突)或一个线程试图访问一个对象的状态而另一个线程在修改它的状态(读写冲突)时,则会出现大多数线程安全问题。冲突时,必须同步对共享库的访问,以便其他线程在处于不一致状态时无法访问它们。 这可能很难正确完成,需要大量文档来确保正确扩展程序,并且还可能带来负面的性能后果。 只要正确地构造了不可变的对象(这意味着不让对象引用脱离构造函数),它们就可以不受同步访问的要求,因为它们的状态无法更改,因此不会发生读写冲突。 。

    在不同步的情况下跨线程共享对不可变对象的引用的自由可以极大地简化编写并发程序的过程,并减少程序可能存在的潜在并发错误的数量。

    在行为不规范的情况下安全

    将对象作为参数的方法不应更改那些对象的状态,除非明确记录了这样做的目的或有效地假定了该对象的所有权。 当我们将对象传递给普通方法时,我们通常不希望对象返回更改。 但是,对于易变的物体,这仅仅是一种信仰行为。 如果我们将java.awt.Point传递给诸如Component.setLocation()类的方法,则不会阻止setLocation修改传入的Point的位置或存储对该点的引用,并稍后在另一种方法中对其进行更改。 (当然, Component不会这样做,因为这很粗鲁,但并非所有类都这么礼貌。)现在,我们的Point状态在我们不知情的情况下发生了变化,并可能带来危险的结果-我们仍然认为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模式采用一种工厂方法来分配对不可变细粒度对象的引用,并使用共享通过仅使对象的单个实例对应于字母“ a”来减少对象计数。 有关Flyweight模式的更多信息,请参见经典书籍Design Patterns (Gamma等人;请参阅参考资料 )。

    Java类库中不变性的另一个很好的例子是java.awt.Color 。 虽然颜色通常以某种颜色表示形式(例如RGB,HSB或CMYK)表示为数字的有序集合,但将颜色视为颜色空间中的区别值而不是有序集合更有意义。可单独寻址的值,因此将Color实现为不可变的类是有意义的。

    我们是否应该使用可变或不可变的对象来表示作为多个原始值(例如点,向量,矩阵或RGB颜色)的容器的对象? 答案是。 。 。 这取决于。 它们将如何使用? 它们是主要用于表示多维值(例如像素的颜色),还是仅用作其他对象相关属性的集合(例如窗口的高度和宽度)的容器? 这些属性多久更改一次? 如果更改了这些值,那么各个组件值是否在应用程序中具有自己的意义?

    事件是使用不可变类实现的候选人的另一个很好的例子。 事件是短暂的,并且通常在与创建事件不同的线程中使用,因此使事件成为不可变的优点多于弊。 大多数AWT事件类并未实现为严格不变的,但可以进行少量修改。 类似地,在使用某种形式的消息传递在组件之间进行通信的系统中,使消息对象不可变可能是明智的。

    编写不可变类的准则

    编写不可变的类很容易。 如果满足以下所有条件,则一个类将是不可变的:

    其所有字段均为最终字段 该课程被宣布为最终课程 在建造过程中不允许引用this参考 包含对可变对象(例如数组,集合或可变类,如Date引用的任何字段: 是私人的 从不退回或以其他方式暴露给来电者 是对其所引用对象的唯一引用 构造后请勿更改参考对象的状态

    最后一组需求听起来很复杂,但这基本上意味着,如果您要存储对数组或其他可变对象的引用,则必须确保您的类具有对该可变对象的独占访问权限(因为否则其他人可以更改其可变对象)状态),并且您在构造后不修改其状态。 这种复杂性对于允许不可变对象存储对数组的引用是必需的,因为Java语言无法强制最终数组的元素不被修改。 请注意,如果要从传递给构造函数的参数初始化数组引用或其他可变字段,则必须防御性地复制调用方提供的参数,否则不能确保对数组具有独占访问权限。 否则,调用方可以在调用构造函数后修改数组的状态。 清单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 } }

    通过一些额外的工作,可以编写使用某些非最终字段的不可变类(例如, String的标准实现使用hashCode值的延迟计算),这可能比严格的最终类执行得更好。 如果您的类表示抽象类型的值,例如数字类型或颜色,则您也将要实现hashCode()和equals()方法,以便您的对象可以作为HashMap或HashSet 。 为了保持线程安全,请务必不要this引用从构造函数中逸出,这一点很重要。

    很少更改数据

    有些数据项在程序的生存期内保持不变,而另一些则经常更改。 常量数据是不可变性的明显候选者,状态复杂且频繁变化的对象通常不适合用于不可变类的实现。 有时但不经常更改的数据又如何呢? 有什么方法可以获取有时发生变化的数据不变性的便利性和线程安全性?

    来自util.concurrent包的CopyOnWriteArrayList类是一个很好的示例,说明了如何利用不变性的力量,同时仍然允许偶尔进行修改。 它是支持事件侦听器的类(例如用户界面组件)使用的理想选择。 尽管事件侦听器列表可以更改,但更改的频率通常少于生成事件的频率。

    CopyOnWriteArrayList行为与ArrayList类非常相似,不同之处在于,在修改列表时,不是更改基础数组,而是创建一个新数组,并丢弃旧数组,而不是对基础数组进行更改。 这意味着,当调用者获得一个迭代器时,该迭代器内部保存对底层数组的引用,该迭代器引用的数组实际上是不可变的,因此可以遍历而不会发生同步或存在并发修改的风险。 这消除了遍历之前克隆列表或遍历期间在列表上同步的需求,这两种方法都是不方便的,容易出错的并且确实会降低性能。 如果遍历比插入或删除要频繁得多(在某些情况下通常如此),则CopyOnWriteArrayList可提供更好的性能和更方便的访问。

    摘要

    不可变的对象比可变的对象容易得多。 它们只能处于一种状态,因此始终保持一致,它们本质上是线程安全的,并且可以自由共享。 通过使用不可变对象,可以消除许多易于犯和难以检测的编程错误,例如无法跨线程同步访问或在存储对数组或对象的引用之前无法克隆数组或对象。 在编写一个类时,总是值得问自己,该类是否可以有效地实现为一个不变的类。 您可能会惊讶地回答“是”。


    翻译自: https://www.ibm.com/developerworks/java/library/j-jtp02183/index.html

    Processed: 0.025, SQL: 8