使可变性最小化。

来源:互联网 发布:炉石大数据各数据意义 编辑:程序博客网 时间:2024/05/18 01:09

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

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

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

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

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

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

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

大多数重要的不可变类都使用了函数的(functional)做法,因为这些方法返回了一个函数的结果,这些函数对操作数进行运算但并不修改他。与之相对应的更常见的是过程的(procedural)或者命令式的(imperative)做法,使用这些方式时,将一个过程作用在它的操作数上,会导致它的状态发生改变。

不可变对象比较简单。不可变对象可以只有一种状态,即被创建时的状态。如果你能够确保所有的构造器都建立了这个类的约束关系,就可以确保这些约束关系在整个生命周期内永远不再发生变化,你和使用这个类的程序员都无需再做额外的工作来维护这些约束关系。另一方面,可变的对象可以有任意复杂的状态空间。如果文档中没有对mutator方法所执行的状态转换提供精确地描述,要可靠地使用一个可变类是非常困难的,甚至是不可能的。

不可变对象本质上是线程安全的,他们不要求同步。当多个线程并发访问这样的对象时,他们不会遭到破坏。这无疑是获得线程安全最容易的办法。实际上,没有任何线程会注意到其他线程对于不可变对象的影响。所以,不可变对象可以被自由地共享。不可变类应该充分利用这种优势,鼓励客户端尽可能地重用现有的实例。要做到这一点,一个很简单的办法就是,对于频繁使用的值,为他们提供公有的静态final常量。

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

“不可变对象可以被自由的共享”导致的结果是,永远也不需要进行保护性拷贝。实际上,你根本不需做任何拷贝,因为这些拷贝始终等于原始的对象。因此,你不需要也不应该为不可变的类提供clone方法或者拷贝构造器(copy constructor)。

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

不可变对象为其他对象提供了大量的构件(building blocks),无论是可变的还是不可变的对象,如果知道一个复杂对象内部的组件对象不会改变,要维护他的不变性约束是比较容易的。这条原则的一种特例在于,不可变对象构成了大量的映射键(map key)和集合元素(set element);一旦不可变对象进入到映射(map)或者集合(set)中,尽管这破坏了映射或者集合的不变性约束,但是也不用担心他们的值会发生变化。

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

如果你执行一个多步骤的操作,并且每个步骤都会产生一个新的对象,除了最后的结果之外其他的对象最终都会被丢弃,此时性能问题就会显露出来。处理这种问题有两种办法。第一种办法,先猜测一下会经常用到哪些多步骤的操作,然后将他们作为基本类型提供。如果某个多步骤操作已经作为基本类型提供,不可变的类就可以不必在每个步骤单独创建一个对象。不可变的类在内部可以更加灵活。例如,BigInteger有一个包级私有的可变“配套类(companing class)”,它的用途是加速诸如“模指数(modular exponentiation)”这样的多步骤操作。

如果能够精确地预测出客户端将要在不可变的类上执行哪些复杂的多阶段操作,这种包级私有的可变配套类的方法就可以工作的很好。如果无法预测,最好的办法是提供一个公有的可变配套类。在Java平台类库中,这种方法的主要例子是String类,它的可变配套类时StringBuilder(和基本上已经废弃的StringBuffer)。

除了“使类成为final的”这种方法之外,还有另外一种更加灵活的方法可以做到这一点。让不可变的类变成final的另一种办法就是,让类的所有构造器都变成私有的或者包级私有的,并添加公有的静态工厂(static factory)来代替公有的构造器。

虽然这种方法并不常用,但它经常是最好的替代方法。它最灵活,因为它允许使用多个包级私有的实现类。对于处在它的包外部的客户端而言,不可变的类实际上是final的,因为可能把来自另一个包的类,缺少公有的或受保护的构造器的类进行扩展。除了允许多个实现类的灵活性之外,这种方法还使得很可能通过改善静态工厂的对象缓存能力,在后续的发行版本中改进该类的性能。

没有一个方法能够对对象的状态产生外部可见(externally visible)的改变。然而,许多不可变的类拥有一个或者多个非final的域,他们在第一次被请求执行这些计算的时候,把一些开销昂贵的类拥有一个或者多个非final的域,他们在第一次被请求执行这些计算的时候,把一些开销昂贵的计算结果缓存在这些域中。如果将来再次请求同样地计算,就直接返回这些缓存的值,从而解决了重新计算所需要的开销。这种技巧可以很好地工作,因为对象是不可变的,它的不可变性保证了这些计算如果被再次执行,就会产生同样地结果。

有关序列化功能的一条告诫有必要在这里提出来。如果你选择让自己的不可变类实现Serializable接口,并且他包含一个或者多个指向可变对象的域,就必须提供一个显式的readObject或者readResolve方法,或者使用ObjectOutputStream.writeunshared和ObjectInputStream.readUnshared方法,即使默认的序列化形式是可以接受的,也是如此。否则攻击者可能从不可变的类创建可变的实例。

总之,坚决不要为每个get方法编写一个相应的set方法。除非有很好地理由要让类成为可变的泪,否则就应该是不可变的。不可变的类有许多优点,唯一缺点是在特定的情况下存在潜在的性能问题。

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

构造器应该创建完全初始化的对象,并建立其所有的约束关系。不要在构造器或者静态工厂之外再提供公有的初始化方法,除非有令人信服的理由必须这么做。同样地,也不应该提供“重新初始化”方法(它使得对象可以被重用,就好像这个对象是由另一不同的初始状态构造出来一样)。与所增加的复杂性相比,“重新初始化”方法通常并没有带来太多的性能优势。

可以通过TimeTask类来说明这些原则。它是可变的,但是他的状态空间被有意的设计的非常小。你可以创建一个实例,对它进行调度使他执行起来,也可以随意的取消它。一旦一个定时器任务(time task)已经完成,或者已经被取消,就不可能再对它重新调度。

原创粉丝点击