【笔记17】最小化可变性

本文介绍了Java不可变类,即实例创建后信息固定不变,如String等。阐述了使类不可变的五条规则,以复数类为例说明。指出不可变类简单、线程安全、可共享等优点,也提及创建对象代价高的缺点。还给出设计建议,如尽量用静态工厂,限制类可变性。

不可变类只是其实例不能被修改的类。每个实例中包含的所有信息都必须在创建该实例的时候就提供,并在对象的整个生命周期(lifetime)内固定不变。Java平台类库中包含许多不可变的类,其中有String、基本类型的包装类、BigInteger和BigDecimal。存在不可变的类有许多理由:不可变的类比可变的泪更加易于设计、实现和使用。他们不容易出错,且更加安全。

为了使类成为不可变,要遵循下面五条规则

1、不要提供任何会修改对象状态的方法(也称为mutator),即改变对象属性的方法。

2、保证类不会被扩展。这样可以防止粗心或者恶意的子类假装对象的状态已经改变,从而破坏该类的不可变行为。为了防止子类化,一般做法是使这个类成为final的。

3、使所有的域都是final的。通过系统的强制方式,这可以清楚地表明你的意图。而且,如果一个指向新创建实例的引用在缺乏同步机制的情况下,从一个线程被传递到另一个 线程,就必须确保正确的行为。

4、使所有的域都成为私有的。这样可以防止客户端获得访问被域引用的可变对象的权限,并防止客户端直接修改这些对象。虽然从技术上讲,允许不可变的类具有公有的final 域,只要这些域包含基本类型的值或者指向不可变对象的引用,但是不建议这样做,因为这样会使得在以后的版本中无法再改变内部的表示法。

5、确保对于任何可变组件的互斥访问。如果类具有指向可变对象的域,则必须确保该类的客户端无法获得指向这些对象的引用。并且,永远不要用客户端提供的对象引用初 始化这样的域,也不要从任何访问方法(accessor)中返回该对象引用。在构造器、访问方法和readObject中请使用保护性拷贝(defensive copy)技术。

一个复数例子:

public final class Complex {
    private final double re;
    private final double im;
    
    public Complex(double re, double im) {
        this.re = re;
        this.im = im;
    }
    
    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);
    }
    
    @Override
    public boolean equals(Object o) {
        if(o == this)
            return true;
        if(!(o instanceof Complex))
            return false;
        Complex c = (Complex) o;
        return Double.compare(re, c.re) == 0 && Double.compare(im, c.im) == 0;
    }
    
    @Override
    public int hashCode() {
        int result = 17 * hashDouble(re);
        result = 31 * result + hashDouble(im);
        return result;
    }
    
    private int hashDouble(double val) {
        long longBits = Double.doubleToLongBits(val);
        return (int)(longBits ^ (longBits >>> 32));
    }
    
    @Override
    public String toString() {
        char s = '-';
        return "(" + re + " + " + im + "i)";
    }
    
}

加法、减法、乘法和除法四种基本的算术运算返回的是一个新的Complex实例,而不是修改这个实例。这样实例的状态就无法被改变(状态在实例创建的时候就确定了,并且没有提供改变状态的方法),Complex类成为一个不可变类。

不可变类要比可变类更加易于设计,实现和使用。它们不容易出错,且更加安全。
不可变类有很多优点:
1.不可变对象比较简单。
不可变对象可以只有一种状态,即被创建时的状态。
2.不可变对象本质上是线程安全的,它们不要去同步。当多个线程并发访问这样的对象时,它们不会遭到破坏。

客户端应该尽可能地重用现有的实例。例如Complex类可能提供下面的常量:

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

进一步扩展,不可变类可以提供一些静态工厂,把频繁被请求的实例缓存起来,从而当现有实例可以符合请求时,不必创建新的实例:

private static final Complex ZERO = new Complex(0, 0);

public Complex valueOfZero() {
    return ZERO;
}

3.不可变对象可以被自由地共享。

4.不仅可以共享不可变对象,甚至也可以共享它们的内部信息。

如BigInteger类的内部使用了符号数值表示法。符号用一个int类型的值signum表示,数值则用一个int数组mag表示。它有一个negate方法产生一个新的BigInteger,其中数值是一样的,符号是相反的,它不需要拷贝数组,新建的BigInteger也指向原始实例中的同一个内部数组。

public BigInteger negate() {
        return new BigInteger(this.mag, -this.signum);//构造器传入的参数是原始实例的数组this.mag
}

5.不可变对象为其他对象提供了大量的构件

不可变类真正唯一的缺点是,对于每个不同的值都需要一个单独的对象考虑String的“+”,String s = “a”+“b”会创建三个字符串“a”、“b”、“ab”。创建这种对象的代价可能很高,特别是对于大型对象的情形。

为了确保不可变性,类绝对不允许自身被子类化。除了“使类成为final的”这种方法之外,还有另一种更加灵活的办法可以做到这一点。让不可变的类变成final的另一种办法就是,让类的所有构造器都变成私有的或者包级私有的,并添加公有的静态工厂来代替公有的构造器。如下:

public class Complex{
    private final double re;
    private final double im;

    private Complex(double im, double re) {
        this.im = im;
        this.re = re;
    }
    
    public static Complex valueOf(double re,double im){
        return new Complex(re,im);
    }
}

  总之,坚决不要为每个get方法编写一个相应的set方法。除非有很好的理由也要让类成为可变的类,否则就应该是不可变的。

  对于有些类而言,其不可变性是不切实际的。如果类不能被做成是不可变的,仍然应该是尽可能地限制它的可变性。降低对象可以存在的状态数,可以更容易地分析该对象的行为,同时降低出错的可能性。因此,除非有令人信服的理由要使域变成是非final的,否则要使每个域都是final的。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值