JAVA 可变对象,不可变对象

来源:互联网 发布:广东省电信工程知乎 编辑:程序博客网 时间:2024/05/20 05:24

看<Effective Java>时,有多个条目是关于或涉及到Immutable object的。作者非常推崇使用immutalbe object,而非与之对应的imuttable object。这里总结一下自己的理解。

先举个例子,例如我们想实现一个字符串类string,在初始化的时候,我们用new string("hello")给它赋一个初值"hello"。后来在使用过程中,我们发现其值需要做改变为“world”。那该如何做呢?一种方式是直接在当前对象中修过内部成员,提供一个string.setValue函数;另外一种方式是创建一个新的字符串对象new string("world"),而保证之前的字符串对象不变。这里前者就是mutable object,因为在其初始化之后可变;后者是immutable object,因为一旦初始化后,其内部状态就不会发生任何变化,如果需要变化,就必须从头创建一个新的对象。而这相比较,各有什么优劣呢?

  • immutable object理解和实现上简单。顾名思义,Immutable object表示不可变的对象。或者说,对象一旦创建起来,就只能被访问,不能被修改。程序中经常使用的数据类都可以作为为immutable object。例如整数,字符串,颜色,时间,实体类等。Immuable类不能也不需要提供setter方法,只提供getter类方法或者其他不修改内部状态的方法。而mutable object,必须提供setter方法以修改内部状态。当内部变量比较多而且相互关联时,设计好这些setter并非一件易事。
  • Immutable object可以确保线程安全。多线程访问同一个对象,可能会因为一个正在读另一个正在写,或者两个都在写而导致并发问题。而对于immutable object,因为只有在一个线程创建时可以初始化其内部变量,所以后续多线程并发使用时,都只是不可变的操作,不会改变内部状态。所以可以不加任何锁的随意访问;而对于mutable object,因为有写操作,所以如果访问,必须小心的加锁。
  • Immutable object不需要保护性拷贝(deffensive copy)。对象可以在函数之间传来传去。如果一个对象可以在多处被修改,那使用者很可能无法知晓对象目前的状态。所以在参数传递的过程中,鼓励采用拷贝一份再传出去的办法来保证对象只在一个地方被修改。对于immutable object,因为它在初始化之后不可修改,所以根本不用关心这个被不知道的类不小心或者恶意修改的问题;而对于mutable object,必须采用保护性拷贝以保证修改可控。例如mutable string用来表示中文名“张三”,它调用了一个打印拼音名字的函数,但该函数不小心直接调用了name.setValue("zhangsan"),那调用者现在看到的字符串的内容也是“zhangsan"了,这并不是我们希望的。
  • Immuable object不需要拷贝构造函数和clone。拷贝构造函数和clone的目的就是创建一个一样的类。而之所以要创建一样的类,是因为需要对两个类分别作修改。所以对于immutable object,因为不能修改,所以也就不需要拷贝构造函数和clone。mutable object则很可能依赖于这类拷贝机制jinxing保护性拷贝等工作。
  • Immuable object更容易class invariant(类不变量)。只需要在构造的时候指定关心类不变量,后续不可修改,所以就不用关心了;单独与mutable object,则在内部成员比较多的情况下,一个setter只设置了部分成员,而导致成员之间的正确关系被打破,从而使对象处于一个不稳定的状态。例如假设mutable string有一个content表示内容,一个length表示长度。如果setValue只修改了content而忘记修改length,则会导致奇怪问题。
  • Immuable object只需要计算一次hashCode。因为immuable object的内部成员不发生变化,所以可以只计算一次,后续直接使用就可以了。而mutable object,必须每次计算。
  • Immutable object可能会有性能问题。例如一个对象有多个内部变量。我们只想修改一个。此时如果使用mutable object,就只修改一个就行了;但如果immutable,则必须从头初始化所有的变量。还有一种情况是对于多阶段的操作,例如多个字符串相加,如果使用immutable object,每两个相加都需要创建新的对象;但如果使用mutable object,则只需要不断的设置新的value就行了。

考虑到这些因素,在实现过程中,如果有的选择,而且不会产生性能问题,我们可以尽量使用immutable object。如果真有性能问题,那经常采用的策略是提供immutable object,同时再提供一个辅助的mutable object。在需要高性能计算时,使用mutable object进行计算,然后把结果在转换成immutable object。一个典型的例子就是String和StringBuilder。

 那如何实现一个immutable object?要做到下面几点

  • 不提供setter
  • 设置类为final,使其不被子类化。
  • 设置所有成员变量(域)为final和private,防止直接修改。
  • 如果某些成员变量是immuable的,那这些变量一定不能被外界访问到。如果要访问,要使用保护性拷贝来复制一份传给外界。

通过这些方式,可以保证对象只有在构造时被初始化一次,之后就变成只读的了。那如何初始化呢?

最简单的方式就是使用构造函数。一个替代方案是使用静态工厂方法,就是在类中提供一个public的静态方法,通过它来调用私有的构造函数来构造。静态工厂方法相对于构造函数有多个好处,跟immutable object相关的一点是,使用静态工厂方法可以在创建对象的时候重用之前创建好的对象。例如我想创建一个immuable object Age类表示年龄。可以使用构造函数new Age(10)。此时我又想再创建一个new String(10)。如果还是这么调用,那么是两个对象。既然Age是immutable的,那两个对象其实是浪费空间了。所以一种可选的办法是创建一个static的Age.create函数。在其中维护一个hash,保存数字到Age对象的映射。然后发现这个数字已经有对应的Age对象,就直接返回之前曾经创建好的对象。

但无论是构造函数还是静态工厂方法,都有一个问题。如果类内部有多个成员变量,那么在构造函数或静态工厂方法中需要多个参数。参数太多,方法就很容易用错。另一个问题时有时候这些参数不方便同时都准备好,需要分阶段准备。所以还必须把已经计算好的参数先保存起来供所有参数都准备好后一起使用。此时一种很好的模式是Builder。我们可以为每个复杂的immuable object对象都提供一个builder类。builder类中有跟需要创建的对象相同的成员。builder类有两类函数,一类是多个setter,每个setter设置一个builder类的成员,然后返回builder;然后是create,调用需要被创建的immuable object的构造函数。需要创建的immuable object需要提供一个以builder为参数的构造函数,把builder类的成员复制到需要创建的对象的成员中以创建对象。builder模式其实是把使用对象和创建对象进行了解耦。builder负责创建,对象本身提供访问操作供外界使用。在builder中,维护了正在准备中的多个参数,其中还可以对参数进行各种有效性判断。当然,这种解耦只有在构造足够复杂或者需要批量产生对象时才产生好的结果,否则只会增加复杂性。

那如果一个对象必须是mutable有什么需要注意的吗?

  • 在函数传递参数时,必须是用保护性拷贝。就是说,无论是使用外界传入的参数,还是向外界返回结果,如果参数和结果是mutable的,必须拷贝一份再使用或者传出。这样子可以保证我这个对象的内部成员是不会因为外界的不经意的或者恶意的操作而发生变化。面向对象的核心之一就是数据的封装性。一个对象封装了内部状态,并公开了一些方法以操作或获取这些状态。对状态的改变只能通过该对象自己的方法,而不能是其他对象的操作的副产品。所以对于mutable object,保护性拷贝是必须的。现实中很多非常难以调试的bug,都是因为一个对象的状态被遥远的另外一个对象所改变,这种蝴蝶效应类的bug特别难调试。
  • 在多线程环境中,如果一定要共享mutalbe object,那一般都加锁来确保同步。所以最好让mutable object限制在一个线程中使用,让其计算出的immuable object来共享,尽量减少两个线程同时读写同一个变量的可能性。如果一定要共享mutable object,那比较好的方式是在该对象中进行加锁等同步操作,而不是让外界调用者来自己同步,这样的对象也是线程安全的。
原创粉丝点击