前言
我们很多人在接触到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对象不一样,它本身是不会改变的。每次我们调用的一些修改对象状态的方法实际上只是额外创造出来的对象或者部分成员的拷贝,原有的对象并没有改变。这就好像是一个对象总是创造出它的替身来,反正每次改变的或者修改的都是替身,它本身不会变。这样就不怕别人用流氓手段来改变它的真身了。果然很好很强大。
参考材料
http://stackoverflow.com/questions/5124012/examples-of-immutable-classes
http://docs.oracle.com/javase/tutorial/essential/concurrency/imstrat.html
相关推荐
根据提供的文件信息,本文将对《22_immutable_laws_of_marketing.pdf》中的关键营销定律进行详细解读。虽然原始内容部分无法完全识别,但从已有的信息中可以提炼出22条重要的营销定律,并尝试对其内容进行概括和解释...
### Java源码解读之String类详解 #### 一、引言 在Java开发过程中,`String`类无疑是最常用的数据类型之一。它不仅在日常编码中频繁出现,也是面试中的热门话题。本文将深入探讨`String`类的核心实现机制及其重要...
Druid还借助了Guava这一集合工具库,它提供了大量实用的类和接口,比如Immutable集合、Supplier等,用于简化代码编写和增强代码的健壮性。同样地,对于并行处理,Druid也使用了Java 8的Lambda表达式来实现简洁的并发...
General Electrical Hardware Requirements and Guidelines 1 ...The word "will" is used to state an immutable law of physics. he word "should" is used to denote a preference or desired conformance.
本篇文章将详细解读《软件开发编码规范.doc》中的核心要点,帮助开发者理解和应用这些规则。 首先,代码质量是软件开发的核心要素之一。规范指出,开发人员应删除无用的资源,避免代码冗余,提升代码效率。同时,...
- 对于不可变对象(immutable objects)的错误使用,比如String类的错误使用方式。在Java中,String对象是不可变的,一旦创建就无法更改。文档中展示了错误地对String对象进行操作的实例,这是由于对Java中引用类型...
4. Java的String类型不是基本数据类型,它是一个不可变(immutable)的类,每次对String对象的修改都会生成一个新的String对象,而不是修改原有的对象。这就解释了为什么在执行字符串连接操作时,原始String对象不会...
本书通过浅显易懂的文字与实例来介绍Java线程相关的设计模式概念,并且通过实际的Java程序范例和 UML图示来一一解说,书中在代码的重要部分加上标注使读者更加容易解读,再配合众多的说明图解,无论对于初学者还是...
- **增删改操作**:Reducer中处理数据的增删改,通常配合Immutable.js保持数据不可变性。 - **嵌套数据的增删改**:对于复杂的数据结构,需要正确处理嵌套数据的修改。 - **Effect**:用于处理副作用,如异步操作...
### 标题和描述解读:“String容量大小区分” 该标题及描述强调了在Java编程中,对String类型数据容量的理解和掌握。字符串在程序设计中极为常见,尤其是在处理文本文件时更是不可或缺。Java中String对象具有不可...
本文将从给定的文件标题、描述、标签以及部分内容中提炼出几个重要的知识点进行详细解读,帮助大家更好地理解Java的核心概念。 #### 字符串(String)的理解与运用 字符串是Java中最常用的数据类型之一,它由一...
2. ** immutable数据结构**:Clojure的数据结构如vector、list、map等都是不可变的,这确保了共享状态的安全性。 3. **闭包和高阶函数**:支持闭包和函数作为一等公民,可以作为参数传递和作为返回值。 4. **动态...
以下是从提供的文件内容中总结出来的知识点,按照文件描述进行了详细解读: 1. Java中没有多重继承,应使用接口来解决这一问题。多重继承是指一个类可以同时继承多个类的特性,在Java中这种特性是不被支持的。但是...
标签“编译器/解释器 C#”进一步强调了ILSpy在C#编程生态中的角色,尽管它不是真正的编译器或解释器,但作为反编译器,它提供了对编译后代码的解读能力。 压缩包内的文件名列表揭示了ILSpy运行所需的组件: 1. ...
本文将详细解读一系列重要的Linux命令及其应用场景,旨在帮助初学者和进阶用户更好地掌握这些实用技巧。 #### 目录管理 1. **cd (Change Directory)** - 用途:更改当前工作目录。 - 示例: ```bash cd /path/...
"Effective_java_new"可能指的是该书的一个更新版或解读版。在深入探讨这个主题之前,我们先来了解一下Java编程中的几个核心概念和最佳实践。 1. **接口优先于类(Interfaces over Classes)**:Java中的接口提供了...