Effective Java(3rd)-Item17 最小化可变性

  不可变类只是一个实例不能被修改的类。在每个实例中包含的所有信息在对象的声明周期内都是固定的,所以无法观察到任何更改。Java平台库包含了许多不可变类,包括String,装箱基本类,BigInteger和BigDecimal。这里有许多很好的理由:不可变类比可变类更容易被设计,实现和使用。它们更不容易出错和更安全。
  为了使类不可变,遵循以下五个规则:
1.不要提供修改对象状态的方法(称为mutators)。
2.确保类不能被继承。这可以阻止粗心或恶意的子类通过表现为对象的状态已更改来破坏类的不可变行为。防止子类通常化通过使类final,但是还有一个替代方案,我们将来会讨论它。
3.使所有字段final。在某种意义上,通过系统强制执行,清楚地表达了你的意图。此外,如果一个新创建的实例的引用在没有同步的情况下从一个线程传递到另一个线程,确保正确的行为是有必要的,如内存模型中所述[JLS, 17.5; Goetz06, 16]。
4.使所有字段私有。这防止客户端获取对字段引用的可变对象的访问权限并直接修改这些对象。虽然技术上允许不可变类拥有final字段包含原始值或不可变的对象的引用,但不推荐使用,因为它排除了在未来版本中改变内部表示的可能性(条目15 16).
5.确保对任何可变组件的独占访问。如果你的类有字段引用可变对象,确保类的客户端不能获得这些对象的引用。永远不要初始化此类字段为客户端提供的对象引用或从访问者中返回该字段。在构造方法,访问器和readObject (item88) 中创建防御副本 (item50)
  在先前条目的许多示例类都是不可变的。其中一个例子是条目11的PhoneNumber,它具有每个属性的访问器但是没有mutator。这是一个稍微复杂的例子:

image.png

  这个类表示复数(包含实部和虚部的数字)。除了标准的Object方法,它还提供了实部和虚部的访问器和四个基本的算术运算:加,减,乘,除。注意到算术运算创建和返回了一个新的复数实例而不是修改了这个实例。这个模式叫做函数方法因为方法返回将函数应用于其操作数的结构,而不是修改它。将其与程序或命令式方法进行对比,其中方法将过程应用于其操作数,从而导致其状态发生变化。注意到方法名是介词(比如加号)而不是动词(比如添加)。这强调了方法不改变对象的值的事实。BigInteger和BigDecimal类没有遵守这个命名约定,导致了许多使用错误。
  如果你不熟悉它,功能方法可能会看起来不自然,但是它的不变性带来了许多优点。不可变类很简单。一个不可变类可以处于一个状态,即它被创建的状态。如果你确保所有构造方法建立了类不变量,它就能保证这些不变量将始终保持为真,无需你或使用该类的程序员进一步努力。另一方面,可变对象可以具有任意复杂的状态空间。如果文档没有提供mutator方法执行的状态转换的精确描述,就很难或不可能可靠地使用可变类。
  不可变类本质上就是线程同步的;它们不需要同步。它们不会在多线程访问时被损坏。这是最简单的方式来实现线程安全。由于没有线程可以观察到另一个线程对不可变对象的任何影响,因此可以自由共享不可变对象。因此不可变对象应该鼓励客户端尽可能重用现有实例。一种简单的方式是为常用值提供public static final 常量。例如,Complext类可能提供这些常量:

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

  这个方法可以更进一步。一个不可变类可以提供静态工厂(item1)来缓存频繁请求的实例,避免存在现有的实例时创建新实例。所有装箱原始类和BigInteger都是这么做的。使用这样的静态工厂导致客户端分享实例而不是创建新实例,减少内容占用和垃圾回收成本。设计新类时选择静态工厂来代替公有构造方法,可以灵活地在将来提供缓存,无需修改客户端。
  不可变对象可以被自由共享的事实结果是,你永远不必制作它们的防御性副本(item50) 。事实上,你永远不用制作任何副本,因为副本永远等于原件。因此,你不需要也不应该在不可变类提供克隆方法或拷贝构造方法 (item13)。这在Java平台的早期阶段并不是很清楚,所以String类确实有拷贝构造方法,但是它应该很少被使用(如果有使用的话)(item6)
  你不仅可以共享不可变类,也能分享其内部。例如,BigInteger类在内部使用符号幅度表示。符号由int表示,大小由int数组表示。negate方法产生类似幅度和相反符号的new BigInteger。它不需要拷贝数组,即使它是可变的;新创建的BigInteger指向与原始相同内部数组。
  不可变对象为其他对象构建了很好的构建块,无论是可变的还是不可变的。如果你知道它的组件对象不会在它的下面发生变化,那么维护复杂对象的不变量要容易得多。这个原则的一个特例是不可变对象产生了很好的map键和set元素:一旦它们在map或set中,你不需要担心它们的值的变化,因为这样将破坏map或set的不变性。
  不可变对象免费地提供了失败原子性 (item76) 。它们的状态永远不会改变,所以没有可能出现暂时的不一致。
  不可变类的主要缺点是它们需要为每个不同的值分别使用一个对象。创建这些对象可能成本很高,特别是它们在很大的情况下。例如,假设你有一百万比特的BigInteger并且你想要更改它的低比特:

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

  flipBit方法创建了一个新的BigInteger实例,也是一百万位长,与原始的实例只差一比特。该操作需要BigInteger的大小成比例的时间和空间。与java.util.BitSet相比。与BigInteger一样,BigSet代表了一个任意长比特的序列,但是不像BigInteger,BigSet是可变的。BigSet类提供了一个方法,允许你在常量时间内改变百万位实例单比特的状态:

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

  如果在每一个步骤中执行多步生成新对象的操作,最终会丢弃除了最终结果以外的所有对象。有两个方法可以解决这个问题。第一个是猜测哪些多步操作是需要公用的并将它们作为基础。如果一个多步操作被提供作为基础,不可变类就不必在每一步创建单独的对象。在内部,不可变类可以任意智能。例如,BigInteger有一个package-private的可变“伴侣类”,它用于加速多步操作,比如模幂运算。如上文所述,使用可变伴随类比使用BigInteger要困难得多。幸运的是,你不需要使用它:BigInteger的实现者为你做了辛苦的工作。
  如果你可以准确预测客户端希望在不可变类上执行哪些复杂操作,package-private可变伴随类方法就可以正常工作。如果不可以,你最好选择提供一个公有可变伴随类。在Java平台库中这种方法的主要例子是String类,其可变伴随类是StringBuilder(以及其过时的前任,StringBuffer)。
  既然你知道如何创建一个不可变类并且你了解不可变类的优缺点,让我们讨论一些设计方案。回想一下,为了保证不可变性,一个类必须不允许自己被子类化。这可以通过让类变为final达到效果,但是有另一种更灵活地替代方案。你可以让它所有的构造方法私有或包私有,而不是让不可变类变为final,丙炔添加公有静态工厂代替公有构造方法 (item1)。为了具体表述这个意思,如下是Complex采用这个方法的例子:

image.png

  这个方法通常就是最好的选择。它是最灵活的因为它允许使用多个package-private实现类。对于其包之外的客户端,不可变类实际上是final的,因为不可能扩展来自另一个包并且缺少public或protected构造方法的类。除了允许多实现类的灵活性,这个方法还可以通过改进静态工厂的对象的缓存功能还调整后续版本中类的性能。
  当BigInteger和BigDecimal被编写时,不可变类必须是有效的final,并没有被广泛理解,所以它们的所有方法都有可能被覆写。不幸的是,保留向后兼容性的同时,这无法得到纠正。如果你编写一个类,其安全性决定于不可信的客户端的BigInteger或BigDecimal参数的不可变性,你必须检查该参数是否为“真正”的BigInteger或BigDecimal,而不是不可信的子类的实例。如果是后者,你必须假设它可能是可变的情况下防御性复制它(item50)

image.png

  本条目开头的不可变类的规则列表表明,没有方法可以修改该对象,所有字段都必须是final的。事实上,这些规则比必要的要强了一些,可以放宽要求来提高性能。事实上,没有方法可以在对象的状态中产生外部可见的变化。然而,一些不可变类有一个或更多非final字段,这些字段缓存了在第一次需要时高开销计算的结果。如果相同值被再次请求,返回被缓存的值,节省重新计算的成本。这个小技巧恰恰就是因为对象是不可变的,这保证了计算在重复时将产生相同结果。
  例如,PhoneNumber的hashCode方法 (item11) 在第一次调用时计算了哈希值并在第二次调用时对其缓存。这个技术叫延迟初始化(item83) ,String也使用了这个技术。
  有关可序列化的警告应该知晓。如果你选择将你的不可变类实现Serializable接口,并且它包含了一个或多个字段引用可变对象的字段,你必须提供一个显式的readObject或readResolve方法,或使用ObjectOutputStream.writeUnshared 和ObjectInputStream.readUnshared 方法,即使默认的序列化是可接受的。否则攻击者可以创建你的类的可变对象实例。这个主题细节将在item88中探讨。
  总而言之,抵制为每个getter编写setter的冲动。类应该是不可变的除非有非常好的原因使它们可变。不可变类提供了许多有点,它们唯一的缺点就是在一些情况下可能会出现潜在的性能问题。你可以始终使小值对象(如PhoneNumber和Comple)不可变。(Java平台库有一些类比如 java.util.Date 和java.awt.Point,应该是不可变的,但他们不是)你应该认真考虑使更大的值对象不可变,比如String和BigInteger。你应该为你的不可变类提供一个公有可变伴侣类,只有你确认它达到令人满意的性能时 (item67)
  有一些类不可变是不切实际的。如果一个类不能称为不可变的,尽可能限制它的可变性。减少对象可以存在的状态数使得更容易推理对象并减少出错的可能。因此,除非有令人信服的理由使其为非final,否则将每个字段都编写为final。结合这个条目和条目15的建议,你的自然倾向应该是声明每个字段私有并final,除非有好的理由
  构造方法应该创建完整初始化的对象并建立所有不变量。不要提供与构造方法或静态工厂分开的公有初始化方法,除非有信服的理由。类似地,不要提供“重新初始化”方法,该方法使对象可以被重用,就好像是用不同的初始状态构造的一样。这些方法通常以增加复杂性的代价来提供很少的性能益处。
  CountDownLatch类举例说明了这些原则。它是可变的,但是其状态空间有意保持很小。你创建一个实例,使用它一次,他就完成了:当倒计时锁存器的计数达到0,你就不能重复使用他。
  在本条目的Complex类中,应该在最后加上一条注释。这个例子只是用来说明不变性。它不是一个工业强度的复数实现。它使用标准公式来乘除,没有被正确舍入,并提供了复杂的NaN和无穷大for complex NaNs and infinities [Kahan91, Smith62, Thomas94].
本文写于2019.3.23,历时11天

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

推荐阅读更多精彩内容

  • [{"reportDate": "2018-01-23 23:28:49","fluctuateCause": n...
    加勒比海带_4bbc阅读 766评论 1 2
  • 目录: Android:Android 0.*Android 1.*Android 2.*Android 3.*A...
    敲代码的令狐葱阅读 3,806评论 0 2
  • 你知道吗,在人的心里有一个重要的账户,它就是心理账户。它的存在对我们的行为决策会产生意想不到的影响。 心理账户定义...
    清研巧语阅读 241评论 0 0
  • 惊醒于轰轰雷声,天色阴暗 于床不想起床,瞪着眼睛,亦不说话,看了看手机,时间还早却也无心睡眠。有的时候特别想睡觉,...
    介眉阅读 117评论 0 1
  • 今天转发了三个公文,学了不少知识,也犯了不少错,静下心来反思反思。 为什么公文要发pdf格式的?其实很简单,因为公...
    酒泉教研室王乾祥阅读 674评论 1 0