不可变对象

如果对象的状态在创建之后不能改变。那么这个对象被认为是不可变的。对不可变对象的最大依赖被广泛认为是创建简单可靠代码的合理策略。

不可变对象在并发应用程序中特别有用。因为它们不能改变状态,所以它们不能被线程干扰损坏或在不一致状态中观察到。

程序员通常不愿意使用不可变的对象,因为他们担心创建一个新对象相对于在原地更新一个对象的成本。对象创建的影响经常被高估了,而且还可以被一些与不可变对象相关的效率所抵消。包括由于垃圾收集减少的开销,以及减少需要保护可变对象免受改变的代码。

下面的小节接受一个类,它的实例是可变的,并且派生出一个类,它具有不可变的实例。这样做给出了这种转换的一般规则,并展示了不可变对象的一些优点。

一个同步类的例子

SynchronizedRGB类定义了可以代表颜色的对象。每个对象以三个整数来代表一个主色值和一个字符串的颜色的名称。

public class SynchronizedRGB {
    private int red;
    private int green;
    private int blue;

    private String name;

    private void check(int red, int green, int blue) {
        if (red < 0 || red > 255
            || green < 0 || green > 255
            || blue < 0 || blue > 255) {
            throw new IllegalArgumentException();
        }
    }

    public SynchronizedRGB(int red, int green, int blue, String name) {
        check(red, green, blue);
        this.red = red;
        this.green = green;
        this.blue = blue;
        this.name = name;
    }

    public void set(int red,
            int green,
            int blue,
            String name) {
        check(red, green, blue);
        synchronized (this) {
            this.red = red;
            this.green = green;
            this.blue = blue;
            this.name = name;
        }
    }

    public synchronized int getRGB() {
        return ((red << 16) | (green << 8) | blue);
    }

    public synchronized String getName() {
        return name;
    }

    public synchronized void invert() {
        red = 255 - red;
        green = 255 - green;
        blue = 255 - blue;
        name = "Inverse of " + name;
    }
}

必须小心使用SynchronizedRGB,避免出现不一致的情况。假设线程执行了以下代码:

SynchronizedRGB color = new SynchronizedRGB(0, 0, 0, "Pitch Black");
    ...
    int myColorInt = color.getRGB();      //Statement 1
    String myColorName = color.getName(); //Statement 2

另一个线程调用了在上述语句1之后,语句2之前调用了color.set,那么myColorInt的值和myColorName就不匹配了。为了避免这个结果,语句1和语句2必须放在同步代码块中。

这种不一致性仅仅在可变对象中是可能的——对于SynchronizedRGB的不可变版本就根本不是一个问题。

定义不可变对象的策略

以下规则定义了创建不可变对象的简单策略。并不是记录为”不可变的”所有的类都遵循这些规则。这并不一定意味着这些类的创建者是草率的——他们可能有充分的理由相信,他们的类的实例在创建以后从未改变。然而,这样的策略需要有经验地分析,而不是针对初学者的。

  1. 不要提供”setter”方法——改变字段和字段指向的对象的方法
  2. 将所有字段都设置为final和static
  3. 不允许子类重写方法——最简单实现这一点的方式就是将类声明为final。更加有经验的方法是将构造器私有并且在工厂方法中产生实例
  • 不要提供改变可变对象的方法
  • 不要共享可变对象的引用,从不存储传递给构造函数的外部可变对象引用。如果有必要,创建副本,并将引用存储到副本中。同样,在需要时创建内部可变对象的副本,以避免在方法中返回原始对象。

将该策略应用于synchronizedrgb结果如下:

  1. 在此类中有两个setter方法,直接删除或者返回一个新的对象而不是修改已有对象
  2. 将所有字段都设置为final
  3. 类本身声明为final
  4. 只有一个域指向了对象,这个对象本身是不可变的。
final public class ImmutableRGB {

        // Values must be between 0 and 255.
        final private int red;
        final private int green;
        final private int blue;
        final private String name;

        private void check(int red,
                           int green,
                           int blue) {
            if (red < 0 || red > 255
                || green < 0 || green > 255
                || blue < 0 || blue > 255) {
                throw new IllegalArgumentException();
            }
        }

        public ImmutableRGB(int red,
                            int green,
                            int blue,
                            String name) {
            check(red, green, blue);
            this.red = red;
            this.green = green;
            this.blue = blue;
            this.name = name;
        }


        public int getRGB() {
            return ((red << 16) | (green << 8) | blue);
        }

        public String getName() {
            return name;
        }

        public ImmutableRGB invert() {
            return new ImmutableRGB(255 - red,
                           255 - green,
                           255 - blue,
                           "Inverse of " + name);
        }
}