引言

如果将线程安全的安全程度由强到弱排序,那么java语言中各种操作共享的数据可以分为以下五类:不可变、绝对线程安全、相对线程安全、线程兼容和线程对立。我们在这里只讨论与String实现相关的不可变性。
不可变给java带来的是最直接、最纯粹的安全性。不可变的对象一定是线程安全的,如果一个对象是不可变的,那么并发编程中涉及到的原子性和可见性等问题,也就随之消失了,无论是对象的方法实现还是方法的调用者,都不需要再进行任何线程安全保障措施。正确地理解不可变的含义以及不可变对象的构造原则,我们就能更清楚地认识String。

不可变与final关键字

提到不可变性,很多人第一个想到的就是final关键字,确实,final是构造不可变对象不可或缺的一步,但构造不可变对象却不是仅有final就能实现的,也就是说,将一个类的所有带有状态的变量都声明为final的不一定能保证这个类的对象是不可变的。你还需要清楚地理解不可变对象和不可变的对象引用,以及其他一些问题,在java中,构造一个不可变对象是有几个原则需要遵循的。接下来,我们就一步一步的讲解怎么去构造一个不可变对象。

构造不可变对象的原则

《java并发编程实战》中关于不可变对象有这样的描述:
当满足以下条件时,对象才是不可变的:

  • 对象创建以后其状态就不能修改。
  • 所有的对象域都是final的。
  • 对象是正确创建的(在对象的创建期间,this引用没有逸出)。

    分析

    对象状态的定义

    要理解这几个原则,首先我们要明白对象的状态是什么。对象的状态是指存储在状态变量(例如实例或者静态域)中的数据,对象的状态还包括其他依赖对象的域,例如,某个HashMap的状态不仅存储在HashMap本身,还存储在许多的Map .Entry对象中。例如下面的类ObjectState, 它的状态是由i、s和map的状态共同构成的,而map中又会包含很多的Map.Entry,这些Map.Entry对象的状态也属于ObjectState对象状态的一部分。

    1. public class ObjectState {
    2. private final int i;
    3. private final String s;
    4. private final Map<String,Object> map;
    5. public ObjectState(int i, String s, Map<String, Object> map) {
    6. this.i = i;
    7. this.s = s;
    8. this.map = map;
    9. }
    10. public void modifyMap(){
    11. this.map.put("aNewMapKey",new Object());
    12. }
    13. }

    final修饰基本数据类型和引用类型

    其次,我们要知道当用final修饰简单类型和引用类型时的不同含义。

    基本数据类型

    对于基本数据类型,例如上面的i,是直接存储在ObjectState对象中的值,final关键字对它的不变性的含义就是值的不变性,所以,对于基本数据类型,我们只需要用final修饰,其他的什么都不需要做,一旦它在构造函数中被赋值,就能保证状态是永远不可变的。更深层次的思考一下,如果一个类的定义中只有基本类型的域,那么对象创建后其状态就不能修改这个原则,本身就是final关键字来实现的,或者说,对于这类对象,对象创建后其状态就不能修改与所有的对象域都是final的这两个原则可以说是重复的。
    下面的类肯定是不可变的,尽管i和j是public的,尽管我定义了公有的方法getI来返回这个值,因为这些值不能被修改,我不用怕这些值会被其他类用到:

    1. public class ObjectState {
    2. public final int i;
    3. public final long l;
    4. public ObjectState(int i,long l) {
    5. this.i = i;
    6. this.l = l;
    7. }
    8. public int getI() {
    9. return i;
    10. }
    11. }

    你可能已经想到了,java中的Integer、Long、Double等数值包装类型就是通过这种方式来实现的不变性。

    引用数据类型

    而对于引用类型值,例如这里的map,final保证的是ObjectState对象中到map的引用的是不可变的,而map的数据,我们还是可以修改的。也就是说,除了在构造方法中的第一次赋值,我们不能执行 map= new HashMap()这样的调用。但是可以像上面代码中的modifyMap()方法那样来修改map的状态,这样也就间接地修改了ObjectState的状态。
    对于这类引用类型的对象,第一条原则就是很有必要的了,我们需要通过类的行为定义来自行保证其状态不能被修改。怎么保证就要求我们知道这类对象可能会以什么样的方式发布出去。

    对象的发布与逸出

    发布一个对象的意思是,是对象能够在当前作用域之外的代码中被使用。例如,将一个指向该对象的引用保存到一个可以被其他代码访问的地方,或者在某一个非私有的方法中返回该引用,或者将引用传递给其他类的方法。
    下面的代码示例中,我把这三种情况写在了一起: ```java public class ObjectState { public final Map map1 ; private final Map map2 ; private final Map map3 ;

    public ObjectState(Map map1, Map map2, Map map3) {

    1. this.map1 = map1;
    2. this.map2 = map2;
    3. this.map3 = map3;

    }

    public Map getMap2() {

    1. return map2;

    } private void passMap3(){

    1. MapModifier.modifyAMap(map3);

    } }

public class MapModifier { public static void modifyAMap(Map map){ // } }

  1. 在这个类中,有三个被final修饰的引用类型map1map2map3map1的访问权限是public的,这意味着任何代码都可以访问并修改它,map2访问权限是private的,但是却在公有的方法getMap2中返回了引用,拿到了引用,我们就可以做修改状态的操作了,而map3同样是私有访问权限的,却在passMap3方法中传递给了MapModifiermodifyAMap这个外部方法,这个方法会修改map3的状态。<br />这三个map的情况分别对应于上面说的对象发布的三种方式,对于第三种情况,我们还需要知道一个外部方法的定义:对一个类C来说,外部方法是指行为并不完全由C来定义的方法,包括其他类的方法和类C中可以被子类重写的方法(非finalprivate方法),对于map3,我们还可以以下面的方式来发布:
  2. ```java
  3. public class ObjectState {
  4. public final Map<String,String> map1 ;
  5. private final Map<String,String> map2 ;
  6. protected final Map<String,String> map3 ;
  7. public ObjectState(Map<String, String> map1, Map<String, String> map2, Map<String, String> map3) {
  8. this.map1 = map1;
  9. this.map2 = map2;
  10. this.map3 = map3;
  11. }
  12. public Map<String, String> getMap2() {
  13. return map2;
  14. }
  15. protected void possibleModify(){
  16. }
  17. }
  1. public class ObjectStateImpl extends ObjectState {
  2. public ObjectStateImpl(Map<String, String> map1, Map<String, String> map2, Map<String, String> map3) {
  3. super(map1, map2, map3);
  4. }
  5. @Override
  6. protected void possibleModify() {
  7. super.possibleModify();
  8. this.map3.put("aNewKey","aNewValue");
  9. }
  10. }

ObjectStateImpl类实现了ObjectState类并重写了possibleModify方法,在该方法中修改了map3的状态。

this引用逸出

this引用逸出发生在类的构造方法中,一个常见的this引用逸出是内部类导致的逸出,这个已经在这篇文章中进行了详细的分析,这里不再赘述。

String不可变的实现

既然我们知道了怎样去构建一个不可变对象,那我们就来分析一下String类的不可变是怎么实现的。

final关键字的使用

  1. public final class String
  2. implements java.io.Serializable, Comparable<String>, CharSequence {
  3. /** The value is used for character storage. */
  4. private final char value[];
  5. /** Cache the hash code for the string */
  6. private int hash; // Default to 0
  7. /** use serialVersionUID from JDK 1.0.2 for interoperability */
  8. private static final long serialVersionUID = -6849794470754667710L;
  9. /**
  10. * Class String is special cased within the Serialization Stream Protocol.
  11. *
  12. * A String instance is written into an ObjectOutputStream according to
  13. * <a href="{@docRoot}/../platform/serialization/spec/output.html">
  14. * Object Serialization Specification, Section 6.2, "Stream Elements"</a>
  15. */
  16. private static final ObjectStreamField[] serialPersistentFields =
  17. new ObjectStreamField[0];
  18. public static final Comparator<String> CASE_INSENSITIVE_ORDER
  19. = new CaseInsensitiveComparator();
  20. private static class CaseInsensitiveComparator
  21. implements Comparator<String>, java.io.Serializable {
  22. // use serialVersionUID from JDK 1.2.2 for interoperability
  23. private static final long serialVersionUID = 8575799808933029326L;

value是用来保存字符序列的字节数组,hash是string的hashCode,我们看到hash和CaseInsensitiveComparator是没有用final修饰的,CaseInsensitiveComparator我们这里就能解释一下,因为CaseInsensitiveComparator虽然是一个对象,但是这个对象可以认为是永远不变的,因为它只有一个基本数据类型的serialVersionUID状态而且是被final修饰的,除此之外没有其他状态,所以CaseInsensitiveComparator对象是不可变的。而为什么hash没有用final修饰呢?我们来看hash的计算源码:

  1. /**
  2. * Returns a hash code for this string. The hash code for a
  3. * {@code String} object is computed as
  4. * <blockquote><pre>
  5. * s[0]*31^(n-1) + s[1]*31^(n-2) + ... + s[n-1]
  6. * </pre></blockquote>
  7. * using {@code int} arithmetic, where {@code s[i]} is the
  8. * <i>i</i>th character of the string, {@code n} is the length of
  9. * the string, and {@code ^} indicates exponentiation.
  10. * (The hash value of the empty string is zero.)
  11. *
  12. * @return a hash code value for this object.
  13. */
  14. public int hashCode() {
  15. int h = hash;
  16. if (h == 0 && value.length > 0) {
  17. char val[] = value;
  18. for (int i = 0; i < value.length; i++) {
  19. h = 31 * h + val[i];
  20. }
  21. hash = h;
  22. }
  23. return h;
  24. }

hash这个成员变量的值在没有被计算时,是默认的0,在调用hashCode才会真正计算hash值。而这个值的计算公式我们不难理解,对于一个长度为n的字符串s,hash=s[0]31^(n-1) + s[1]31^(n-2) + … + s[n-1],很明显,这个值每次计算都是一个固定值。

限制对状态的修改

从前面的讲解中,我们知道如何发布一个对象的状态,在String的中,为了限制对内部状态的修改,没有对内部对象进行任何形式的发布。
(1)所有的成员变量都是private的
这样就限制了发布对象状态的第一条:将一个指向该对象的引用保存到一个可以被其他代码访问的地方
(2)所有对字符串序列进行修改的方法都返回新的字符串对象而不对原状态进行修改
这类方法包括concat()、subString()、replace()等,我们看一下concat的源码:

  1. public String concat(String str) {
  2. int otherLen = str.length();
  3. if (otherLen == 0) {
  4. return this;
  5. }
  6. int len = value.length;
  7. char buf[] = Arrays.copyOf(value, len + otherLen);
  8. str.getChars(buf, len);
  9. return new String(buf, true);
  10. }

除非要串联的字符串为空字符串返回当前对象,否则总是创建一个新的对象并返回。这样,外部代码不能通过方法调用拿到当前对象的引用进而拿到values进行修改操作。限制的是发布对象状态的第二条:在某一个非私有的方法中返回该引用。
(3)String类定义为final,不能继承。
不能继承,意味着我们不能通过子类来定义不由String控制的方法进而修改其状态。
(4)在String类中,虽然调用了其他类的方法并且传递了内部对象values的引用,例如Arrays.copyOf方法,但是这些方法都没有对该对象进行操作。
(5)构造方法中没有发生this引用逸出。
通过使用final关键字和限制对状态的修改,String满足了构造不可变对象的这几个原则,保证了对象的不可变性。

小结

不可变作为java中线程安全的一种实现方案,相对于其他方案,例如同步,是比较简单的,java提供了final关键字来帮助我们构造不可变对象。但是不可变对象的构造不仅需要final关键字,它有一系列需要遵守的原则,当我们理解了这几个原则背后的含义,就能完全理解java中不可变性。
String作为java中不可变对象的代表,分析它的不可变实现对于理解这个概念十分有帮助。