Effective-java 3 中文翻译系列 (Item 17 使可变性最小化)

原文链接

文章也上传到的

github

(欢迎关注,欢迎大神提点。)


ITEM 17 使可变性最小化


一个不可变的类指的是一个类的对象不能被修改。在这个对象的生命周期中它所包含的信息是不变的。Java中有很多不可变的类,比如String、原始类型、BigInteger、BigDecimal。不可变类比可变类更容易设计、实现和使用,而且更少出错也更安全。那么怎样才能使类变得不可变呢?请遵循以下5条原则:

  • 不提供修改类对象的方法
  • 类不能被继承。这样可以防止恶意或者粗心的子类改变其不可变性。一般会用final修饰类防止被子类修改,但是我们还有一另一种替代方案会在后面进行讨论。
  • 将所有属性设置成final。使用这个系统的强制语法不但清晰的表达了你的意图,而且可以防止在不同线程中访问同一个对象出现的不同行为问题(例如读写问题)。
  • 将所有属性设置成private。这样可以防止可变对象被调用者获取并修改。虽然在不可变的类中可以将一个public final 的属性设置一个初始(默认的)值或者指向一个不可变的对象,但是这是是不推荐的做法,以为这样在后续的代码中我们就没办去再次修改这个值了,因为其是final的。所以我们建议不设置初始值,除非你真的有需要这么做。
  • 确保任何可变对象都只能被自己访问。如果你的类中有任何引用可变对象的属性,确保它不能被调用者获取到。不要在accessor(访问器)方法中返回这样的属性。在构造器、accessors和readObject(Item 88)方法中进行防御性拷贝(Item 50)。

前面的很多例子中的类都是不可变的,例如Item 11中的PhoneNumber,它的每个属性都可以被访问但不能被修改。这里有一个更加完整的例子:

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 plus(Complex c) {
        return new Complex(re + c.re, im + c.im);
    }
    
    public Complex minus(Complex c) {
        return new Complex(re - c.re, im - c.im);
    }
    
    public Complex times(Complex c) {
        return new Complex(re * c.re - im * c.im,
                re * c.im + im * c.re);
    }
    
    public Complex dividedBy(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;
        // See page 47 to find out why we use compare instead of ==
        return Double.compare(c.re, re) == 0 && Double.compare(c.im, im) == 0;
    }
    
    @Override public int hashCode() {
        return 31 * Double.hashCode(re) + Double.hashCode(im);
    }
    
    @Override public String toString() {
        return "(" + re + " + " + im + "i)";
    }
}

这个类表示一个复数(有实部和虚部的数字)。除了一些标准的对象方法之外,它提供了访问实部和虚部的方法以及基础的加、减、乘、除方法。请注意:这些算数运算的返回结果中并没有修改这个对象的属性,而是生成一个新的Complex实例对象。这种模式被称为函数式方法,因为这种方法把调用函数的结果在不修改函数调用者的同时返回给调用者。注意方法名是介词(如plus)而不是动词(如add),是为了强调方法不会修改对象的值。在BigInteger和BigDecimal中没有遵循这种命名规范,所以造成了很多错误的调用。

如果你不熟悉函数式方法,可能会觉得它看着不太自然,但是它具有不可变性而且有很多优点。使对象不可变。如果你能确认类的所有构造方法都不改变其状态,并且无论何时都不会被改变,那么一个不可变对象的状态就可以一直被保持在它被创建时的状态。因为可变对象具有可变性,如果在setter方法中没有给出详细说明其变化的描述,那么这个可变的类将很难甚至不能被可靠的使用。

不可变对象与生俱来就是线程安全的,所以不需要synchronization。它们不会被多个线程同时访问所污染,所以这是实现线程安全最简单的方法。不可变对象可以自由分享。不可变的类应该鼓励调用者尽可能重用同一个类对象。简单有效的方法是提供public static final 的常量以供公共使用。例如,Complex类可以提供这些常量:

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);

这种方法还可以更进一步。一个不可变的类可以提供static 的工厂(Item 1)来缓存经常要访问的对象,以避免当一个对象存在了还会被经常的创建。现在的基础类型的包装类和BigInteger都是这么做的。这样带来的好处是:避免创建多余的相同对象,减少内存消耗和垃圾回收消耗。使用static工厂而不是public的构造方法是为了在将来想添加一个内容时提供灵活性,不用修改调用者的代码。

一个不可变对象可以被自由分享,这就意味着不需要对他们进行防御性拷贝(Item50)。因为它们的内容永远和初始时是一样的。所以不可变的类中你不需要提供clone方法或者copy构造方法(Item13)。这在Java早期的时候是不太好理解的,所以在String类中仍然有copy的构造方法,但是尽量应该不要这么使用(Item6)。

不仅可以公开不可变对象,而且可以公开它们的实现。例如:BigInteger类使用一个有符号的数,符号用一个int类型表示,值用int数组表示。negate方法返回一个符号相反绝对值相等的新的BigInteger对象。即使它是可变的也不需要使用copy数组;这个新的对象指向相同的内部数组。

不可变对象作为其他对象的组成元素很有优势,无论这个对象可不可变,因为不用担心不可变量被胡乱修改。

不可变对象免费提供失败原子性(Item76)。它们的状态不会被改变,所以不可能不一致。

不可变对象的主要缺点是:对于不同的值它都需要一个全新的对象。尤其是对于较大的对象,创建出来的代价是高昂的。例如:你有一个百万为的BigInteger对象,当你想改变它的最低位时:

BigInteger moby = ...;
moby = moby.flipBit(0);

方法flipBit会创建一个新的百万位的BigInteger对象出来,不同的仅仅是最后一位。这个操作消耗的时间和空间和BigInteger的大小成正比。对比java.util.BitSet和BigInteger,BitSet是可变的,有任意长度的bit位,提供仅改变数值中一位的方法:

BitSet moby = ...;
moby.flip(0);

如果你执行很多步骤,每一步都产生一个新对象,并仅仅在最后才释放所有对象的话,这将会产生很严重的性能问题。有两种方法应对这个问题。第一是将公用的多步操作封装起来,就不用每一步都生成一个新的对象了。例如BigInteger类有一个包私有的可变“伙伴类”,用来加速类似于模幂运算的多步操作。

如果你能预测调用者可能用你的不可变类做的哪些操作,就可以将这些操作做成包私有级别的方法。如果没办法预测可能需要什么操作时,你最好提供一个public的可变伙伴类,例如String类,它的可变伙伴类是StringBuilder(它的已废弃的前身是StringBuffer)。

现在你已经知道了如何创建一个不可变的类,而且知道了不可变性的优点和缺点,让我们导论一下设计上的替代品。会想一下为了保证不可变性,一个类不能被子类化,这可以通过final来实现。但是有另一个更加灵活的的方案:不是使不可变的类被final修饰,而是使构造方法变成private或者package-private的,然后添加public static工厂方法代替public 构造方法(Item 1)。请看下面实例:

//用不可变类的静态工厂代替构造方法
public 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);
    }
    ... // Remainder unchanged
}

通常这种方法是最好的替代品,具有最灵活的特性,因为这样可以实现多包级私有特性。包外的类没办法继承这个类,因为对于其他包而言这个类就是final的,不能继承不同包的类而且也没有public或者protected的构造方法。这个方法还能在将来的版本中通过优化静态方法中的对象缓存来优化性能。

在BigInteger和BigDecimal类被创造的时候,不可变类必须保证final这个观点并没有被普遍的认知,所以它们的方法都是有可能被重写的。不幸的是,这种错误现在并不能被修改,因为它们现在都要做向后兼容。如何你依赖于BigInteger和BigDecimal的不可变性实现一个安全的类时,一定要确认它们是真实的BigInteger和BigDecimal,而不是它们的(不被我们信任的)子类。如果确认它们是子类,那么应该做防御性拷贝,因为它们可能已经重写了实现(Item50):

public static BigInteger safeInstance(BigInteger val) {
    return val.getClass() == BigInteger.class ?
        val : new BigInteger(val.toByteArray());
}

文章开始时我们说不可变类是不能修改它的对象并且他的属性都是final的。事实上,这些规则有点过于严格了,可以适当放松来提高性能。实际上,没有方法能提供外部可见的内部变化。然而,一些不可变的类有一个或多个非final的属性,用来缓存一些结果,这些结果在第一次被需要的时候会消耗一些计算成本,但是在之后被需要的时候就不需要再次计算,缓存可以直接返回不可变的对象,这保证了重复计算时返回相同的结果。

例如PhoneNumber的hashCode方法(Item11),第一次计算的时候会缓存起来,下次就可以直接被使用。同样的延迟初始的方法(Item83)String类也使用了。

特别注意当你的不可变类实现Serializable接口时,并且如果它包含一个或多个可变的对象,即使默认serialized可用,你也必须提供一个清晰的readObject或readResolve方法,或者使用ObjectOutputStream.writeUnshared 和ObjectInputStream.readUnshar

ed方法。否则攻击者可能会用你的类创建一个可变对象,这种情况在Item88中会详细说明。

在不需要的情况下,不要为每一个getter方法写一个setter方法,如果类可以做成不可变的,就尽量不要使它们可变。不可变的类有很多的优点,只有一个缺点是在某些情况下可能有性能问题。Java平台有几个类本应该是不可变但实际上却不是,如java.util.Date和java.awt.Point。只有在你必须实现令人满意的性能的时候才应该把不可变类变成可变的公共类(Item 67)。

不能变成不可变的类,那就应该尽量限制它的可变形。减少状态的数量就能使分析问题更加容易,也能减少可能的错误。应该把变量设置成final 的,除非有足够的原因需要设置成非final的。结合item15说的,你应该自然的把变量设置成private final的,除非有原因需要设置成其他的。

构造方法应该创建完全初始化的对象,以使它们的不确定性确立。除非有充足的理由,都不要创建除了构造器和静态方法之外的公共初始化方法。同样,也不要提供重新初始化方法重新把一个对象初始化成新的状态。因为这样增加了复杂度换来了仅仅一点性能的优势。

类CountDownLatch 遵循了这个规则。它是可变的但是它的状态空间被限制的很小。你创建一个对象,使用它的时候:一旦countdown 的锁计数降到0,你是不能重新使用它的。

最后需要提一下这里的Complex类不是一个工业标准的类,仅仅是为了说明不可变性存在的。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 220,458评论 6 513
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 94,030评论 3 396
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 166,879评论 0 358
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 59,278评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 68,296评论 6 397
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 52,019评论 1 308
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,633评论 3 420
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,541评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 46,068评论 1 319
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,181评论 3 340
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,318评论 1 352
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,991评论 5 347
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,670评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,183评论 0 23
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,302评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,655评论 3 375
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,327评论 2 358

推荐阅读更多精彩内容