`
frank-liu
  • 浏览: 1667562 次
  • 性别: Icon_minigender_1
  • 来自: 北京
社区版块
存档分类
最新评论

immutable解读

    博客分类:
  • java
 
阅读更多

前言

    我们很多人在接触到immutable这个概念的时候,应该是在学习到String这个部分。书上会反复提到,String是immutable的,所以对于它的使用要特别注意了等等。除了String的实现是immutable的,还有很多其他java类库里的class也实现了同样的特性。那么, immutable是基于一个什么样的设计思路呢?为什么要折腾出这么一个玩意来?它有什么好处呢?这里,我们结合一些具体的immutable类实例来讨论。

immutable现象

    既然我们前面接触到immutable这个概念就是从String这里来的,我们就从这里开始讨论。假设我们有以下的代码:

String name = "abcdefg";
String newName = name.replace('a', 'h');

    我们这里的String对象name调用了replace方法。并将结果赋值给另外一个newName对象。从我们一贯的理解方式来看,既然name.replace()方法是需要修改String内容的,是不是表示这个对象被改变了呢?如果我们分别打印name, newName的值会发现结果如下:

abcdefg
hbcdefg

    很显然,虽然调用了这么一个修改内容的方法,但是name对象本身其实还是没有被改变。从前面的结果里我们也可以推测到,这种修改了内容的方法实际上是返回了一个新的对象,这样使得原来的对象没有受到任何的影响。

    前面这个方法的过程,对应到内存中对象的关系则如下图所示:

     在开始的时候,只有name对象,其内容为"abcdefg"

 

    而调用replace()方法之后,则变成如下:

    这里的name对象通过调用replace方法之后创建了一个新的对象,只是这个对象里的内容变成了"hbcdefg"。这样,我们这里新建的这个对象并没有影响到原来的对象,原来的name里内容没有变化。从这里的讨论,我们可以理解到,immutable本质上就是要求一个对象状态不能被改变。

    在前面我们举的这个示例里,用的是String对象。那么它本身是怎么保证做到immutable这么一个特性的呢?

String里immutable的实现

    我们可以看看String这个类的详细定义实现:

public final class String
    implements java.io.Serializable, Comparable<String>, CharSequence {
    /** The value is used for character storage. */
    private final char value[];

    /** Cache the hash code for the string */
    private int hash; // Default to 0

    /** use serialVersionUID from JDK 1.0.2 for interoperability */
    private static final long serialVersionUID = -6849794470754667710L;
//... omitted
}

    这里我们可以看到,String类型是被定义成final的。这样它本身将不能被继承。我们也就不可能通过继承它来破坏这个immutable的特性。另外,还有两个比较有意思的成员属性分别是value和hash。value是一个char类型的数组,它的定义增加了final的修饰。对于final修饰的引用类型,我们都知道,它将在被构造函数初始化之后就不能再被修改为指向其他的引用了。hash则是一个普通的int类型。

    我们来挑几个具体的方法实现看看其中的特点:

public char[] toCharArray() {
        // Cannot use Arrays.copyOf because of class initialization order issues
        char result[] = new char[value.length];
        System.arraycopy(value, 0, result, 0, value.length);
        return result;
    }

     这是toCharArray方法的实现。这里需要将String内容转换成一个字符数组。虽然String内部是用char[]来表示的,这里却是首先创建了一个同样长度的char[],然后再将里面value的值拷贝到新的数组里头。然后再将这个数组返回。试想一下,如果我们不通过复制里面的内容而是直接将value返回给用户会怎么样呢?

    value虽然是final的,但是这只是保证value这个引用不能指向别的对象了,但是不能保证它目前所指向的对象本身不会发生变化。一旦我们使用value的客户端拥有了这个引用,我们可以通过value[x]的方式来访问甚至修改里面的内容。这样就破坏了原来对象要求的immutable特性。可见,这里做的这么一通复杂的拷贝操作就是出于这方面的考虑。

    我们再看看其他的几个方法:

public String replace(char oldChar, char newChar) {
        if (oldChar != newChar) {
            int len = value.length;
            int i = -1;
            char[] val = value; /* avoid getfield opcode */
            // 找到需要替换的字符索引位置
            while (++i < len) {
                if (val[i] == oldChar) {
                    break;
                }
            }
            if (i < len) {
                char buf[] = new char[len];
                for (int j = 0; j < i; j++) {
                    buf[j] = val[j];
                }
                while (i < len) {
                    char c = val[i];
                    buf[i] = (c == oldChar) ? newChar : c;
                    i++;
                }
                return new String(buf, true);
            }
        }
        return this;
    }

    这里replace的方法也和前面类似,首先找到需要替换字符的索引。然后再新建一个和value数组同样长度的数组,并在遍历value数组的时候比较,将修改后的数组构造成一个新的String对象返回。这里既然是新建立的一个对象,而且方法里也没有修改原来value的内容,显然,原来的对象是没有任何变化的。

    String的具体实现里代码很多,对于大部分看似需要修改对象状态的方法,都是通过创建原有对象的拷贝,再针对拷贝进行修改,并返回新对象作为结束。有兴趣的朋友可以看看里面其他部分的代码。

设计immutable类型的思想

    前面看了一下immutable的一些实现示例,那么,如果我们要实现一个immutable的类型,需要注意哪些地方呢?在Oracle的官方文档里列举了一些注意事项,这里简单的引用和讨论一下。

1. 不要提供"setter"方法,这里的setter方法指的是可以修改对象成员或者成员里包含的引用的方法。

    这看来其实是想当然的,既然我们需要对象不能被改变,如果我们还定义一个方法可以修改对象里的成员,这不就直接打破了原来的设定吗?

 

2. 将所有成员都定义成final和private的。

    这里其实结合不同的设定场景还是有一些差别的。比如说如果我们将类本身定义设定成final的,则它本身不能被继承了,在这样的情况下我们就可以不一定限制这些成员为private的。

 

3. 不要允许子类来覆写方法。 因为如果我们允许子类来覆写某些方法的话就不一定能保证这些子类里覆写的方法不改变对象的状态。

 

4. 如果对象实例的成员里包含指向可改变对象的引用,则不能让这些对象被改变。

    我们可以不提供方法来修改这些可改变对象。同时,我们也不能将这些可改变对象的引用给暴露出来。以前面String类的实现为例,它里面有可改变对象char[] value。因为这个引用里的值是可以被改变的,但是我们只要保证没有任何方法会返回一个直接指向value的引用,也没有方法可以修改它,这样还是可以保证它不会被修改的。

    看了前面这一大堆的讨论,其实对于immutable类型定义的要点,也就可以总结成这么几句话。一个是不要把可以修改的属性或者方法暴露出来,所有的值设定都尽量在构造函数里搞定。一个是所有提供状态变换的方法实际上都是对原有对象的拷贝修改。

immutable类型的作用

    前面既然我们讨论了immutable类型的定义和实现要点,那么定义一个这样的类型有什么好处呢?我们可以看到,每次当我们定义一个要修改状态的方法,实际上是定义了一个新的对象,这个新的对象的属性设定成被修改后的样子。这样,如果一个对象比较大的话,实际上我们相当于每次要把原来的对象拷贝一遍出来。这样看起来即费空间又费时间。当然,这种拷贝的笨办法还是有一个好处的。

    一个主要的好处是在多线程的并发执行环境下。作为一个immutable类型的对象,因为它本身是不会被修改的。所以在原来一些为了防止对象被多线程访问出现错误的情况在这里就不用担心了。反正对象都不会变,我们连线程间同步的事都不用担心,直接上就ok。在一些并行操作的集合里,immutable类型的特性也起到很好的作用。因为每次如果有线程要访问集合的时候,可能有一部分被修改了。而如果原来有在上面做其他操作的线程在操作,这里如果对于修改的变化只是额外创建一套新的拷贝,则不会有线程间的访问冲突了。这种思路的一个具体实现就是CopyOnWriteArrayList。在很多并行集合操作里都有用到它。有兴趣的可以去看看,这里就不再详述了。

 

总结

    immutable和mutable对象不一样,它本身是不会改变的。每次我们调用的一些修改对象状态的方法实际上只是额外创造出来的对象或者部分成员的拷贝,原有的对象并没有改变。这就好像是一个对象总是创造出它的替身来,反正每次改变的或者修改的都是替身,它本身不会变。这样就不怕别人用流氓手段来改变它的真身了。果然很好很强大。

参考材料

openjdk

http://stackoverflow.com/questions/5124012/examples-of-immutable-classes

http://docs.oracle.com/javase/tutorial/essential/concurrency/imstrat.html

  • 大小: 5.2 KB
  • 大小: 13.1 KB
分享到:
评论
2 楼 frank-liu 2013-09-25  
我觉得应该差不多,他们的思想是一样的。
1 楼 mikelij 2013-09-25  
以前没有注意过这个。.net也是这样实现的吗?

相关推荐

Global site tag (gtag.js) - Google Analytics