《Effective Java》笔记15:使可变性最小化

  1. 不可变规则
  2. 例子

不可变类是其实例不能被修改的类(不只是类前加上了 final 就可以了)。每个实例中包含的所有信息都必须在创建该实例时候就提供,并在对象的整个生命周期内固定不变。

Java 平台类库上有很多不可变的类,其中有 String、基本类型的包装类、BigInteger 和 BigDecimal。

存在不可变内的许多理由:不可变类比可变类更加易于设计、实现和使用。它们不容易出错,且更加安全。

不可变规则

为使类成为不可变,要遵循以下5条规则:

  1. 不要提供任何会修改对象状态(属性)的方法。
  2. 保证类不会被扩展。防止子类化。一般做法是使这个类成为 final 的,另外作法就是让类所有构造器都变成私有的或者是包级私有的。
  3. 使用有的域都是 final 的(一般是针对非静态变量)。通过这种加上 final 修饰的强制方式,这可以清楚地表明你的意图:确保该域在使用前得到正确的初始化。而且,如果一个指向新创建的实例的引用在缺乏同步机制(一般不可变对象的访问是不需要同步的,因为状态一旦确定,就不会再更改)的情况下,从一个线程切换另一个线程就必需确保实例的正确状态,正如果内存模型中所述那样[JLS 17.5]。
  4. 使用所有的域都成为私有的。这样可以防止客户端获得访问被域引用的可变对象的权限,并防止客户端直接修改这些对象。虽然从技术上讲,允许不可变的类具有公有的 final 域,只要这些域包含基本类型的值或都指向不可变对象的引用,但是不建议这样做,因为这样会使得在以后的版本中无法以再改变内部的表示法。
  5. 确保对于任何可变域的互斥访问。如果类具有指向可变对象域,则必须确保该类的客户端无法获得指向这些对象的引用。并且,永远不要用客户端提供的对象引用来初始化这样的域,也不要从任何访问方法中返回该对象的引用(即进出都不行)。在构造器、访问方法、readObject 方法(见76条)中请使用保护性拷贝技术(见第39条)。

例子

下面是一个不可变复数(具有实部和虚部)类的例子:

//复数
public final class Complex {

    private final double re;//实部
    private final double im;//虚部

    // 私有的,让它无法扩展
    private Complex(double re, double im) {
        this.re = re;
        this.im = im;
    }
    //静态工厂方法
    public static Complex valueOf(double re, double im) {
        return new Complex(re, im);
    }

    public static Complex valueOfPolar(double r, double theta) {
        return new Complex(r * Math.cos(theta), r * Math.sin(theta));
    }

    public static final Complex ZERO = new Complex(0, 0);
    public static final Complex ONE = new Complex(1, 0);
    public static final Complex I = new Complex(0, 1);

    // 可以直接返回基本类型的值
    public double realPart() {
        return re;
    }
    public double imaginaryPart() {
        return im;
    }

    //每次加减乘除都返回一个新的对象
    public Complex add(Complex c) {
        return new Complex(re + c.re, im + c.im);
    }
    public Complex subtract(Complex c) {
        return new Complex(re - c.re, im - c.im);
    }

    public Complex multiply(Complex c) {
        return new Complex(re * c.re - im * c.im, re * c.im + im * c.re);
    }

    public Complex divide(Complex c) {
        double tmp = c.re * c.re + c.im * c.im;
        return new Complex((re * c.re + im * c.im) / tmp, (im * c.re - re * c.im) / tmp);
    }

    public boolean equals(Object o) {
        if (o == this)
            return true;
        if (!(o instanceof Complex))
            return false;
        Complex c = (Complex) o;
        // 浮点数的比较要使用Double.compare,而不能直接使用==比较
        return Double.compare(re, c.re) == 0 && Double.compare(im, c.im) == 0;
    }

    public int hashCode() {
        int result = 17 + hashDouble(re);
        result = 31 * result + hashDouble(im);
        return result;
    }

    private int hashDouble(double val) {
        long longBits = Double.doubleToLongBits(re);
        return (int) (longBits ^ (longBits >>> 32));
    }

    public String toString() {
        return "(" + re + " + " + im + "i)";
    }
}

不可变对象只有一种状态,即被创建时的状态。不可变对象本质上是线程安全的,它们不要求同步,可以被自由的共享。不可变类应该充分利用这种优势,鼓励客户端尽可能地重用现有的实例,要做到这一点,一个很简便的办法就是,对于频繁用到的值,为它们提供公有的静态 final 常量,例如上面的常量:

public static final Complex ZERO = new Complex(0, 0);
public static final Complex ONE = new Complex(1, 0);
public static final Complex I = new Complex(0, 1);

这种方法可以被进一步的扩展,不可变以的类可以提供一些静态工厂,它们把频繁请求主的实例缓存起来,在请求合适的对象时候,就不必要创建新的实例。所有的基本类型的包装类和 BigInteger 都有这样的静态工厂。使得实例可以共享,从而降低内存与垃圾回收成本。在设计类时,选择用静态工厂代替公有的构造器可以让你以后有缓存的灵活性,而不必影响客户端。

“不可变对象可以被自由地共享”,我们永远也不需要,也不应该为不可变对的类提供 clone 方法或者拷贝构造器。这一点在 Java 平台早期的版本中做得并不好,所以 String 类仍然具有拷贝构造器,但是应该尽量少用它。

不仅可以共享不可变对象,甚至也可以共享它们的内部信息。如 BigInteger 的 negate 方法产生一个新的 BigInteger,其中数组是一样的,符号则是相反的,它并不需要拷贝数组;新建的 BigInteger 也指向原始实例中的同一个内部数组。

不可变类真正唯一的缺点是,对于每个不同的值都需要一个单独的对象,创建这种对象的代价可能很高,特别是对于大型对象的情形。

如果你选择让自己的不可变实现 Serializable 接口,并具它包含一个或者多个指向可变对象的域,就必须提供一个显示的 readObject 或者 readResolve 方法,或者使用 ObjectOutputStream.writeUnshared 和 ObjectInputStream.readUnshared 方法,否则攻击者或能从不可变的类创建可变的实例。这个话题详细请参见 76 条。

除非有很好的理由要让类成为可变的类,否则就应该是不可变的。不可变的类优点有很多,唯一缺点是在特定的情况下存在潜在的性能问题。你应该总是使用一些小的值对象(如 Complex),成为不可变的。但你也应该认真考虑把一些较大的值对象做成不可变的(String、BigInteger),只有当你确认有性能问题时,才应该为不可变的类提供一个公有的可变配套类(如 String 的配套类 StringBuffer、StringBuilder;还有 BigInteger 的配套类为 BitSet)。

对于有些类而言,基不可变性是不切实际的。如果为不能被做成是不可变的,仍然应该尽可能地限制它的可变性。除非有使人信服的理由要使域变成是非 final 的,否则要使每个域都是 final 的。

构造器应该创建完全初始化的对象,并建立起所有的约束关系。不要在构造器或者静态工厂之外再提供公有的初始化方法,除非有使人信服的理由。同样也不应该提供“重新初始化”方法(比如 TimerTask 类,它是可变的,一旦完成或被取消,就不可能再对它重新调度),与所增加的复杂性相比,通常并没有带来太多的性能。

总之,新的内存模型中,以前不可变对象与 final 域的可变问题得到了修复,可以放心的使用了(为了确保 String 的安全初始化,1.5 中 String 的 value[]、offset、count 三个字段前都已加上了 final 修饰符,这样新的 Java 内存模型会确认 final 字段的初始化安全)。


转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 bin07280@qq.com

文章标题:《Effective Java》笔记15:使可变性最小化

文章字数:2.1k

本文作者:Bin

发布时间:2018-08-25, 16:06:22

最后更新:2019-08-06, 00:43:06

原始链接:http://coolview.github.io/2018/08/25/Effective-Java/%E3%80%8AEffective%20Java%E3%80%8B%E7%AC%94%E8%AE%B015%EF%BC%9A%E4%BD%BF%E5%8F%AF%E5%8F%98%E6%80%A7%E6%9C%80%E5%B0%8F%E5%8C%96/

版权声明: "署名-非商用-相同方式共享 4.0" 转载请保留原文链接及作者。

目录