什么是不可变对象?众所周知,在Java中,String类是不可变的。那么究竟什么是不可变对象呢?可以这样想:如果一个对象在创建后不能改变它的状态,那么这个对象就是不可变的。不能改变状态是指对象中的成员变量不能改变,包括基本数据类型的值不能改变,引用类型的变量不能指向其他对象,以及所指向的对象的状态引用类型无法更改。区分对象和对象引用对于Java初学者来说,总是会有这样的疑问,String是一个不可变对象。看下面的代码:Strings="ABCabc";System.out.println("s="+s);s="123456";System.out.println("s="+s);打印结果为:s=ABCabcs=123456先创建一个String对象s,然后让s的值为“ABCabc”,再让s的值为“123456”。从打印结果可以看出,s的值确实发生了变化。那么怎么说String对象是不可变的呢?其实这里有一个误解:s只是对String对象的引用,而不是对象本身。对象是内存中的一块内存区域,成员变量越多,这块内存区域占用的空间就越大。一个引用只是一个4字节的数据,它存储了它所指向的对象的地址,通过它可以访问对象。也就是说,s只是一个引用,它指向一个具体的对象,当s="123456";这段代码执行后,创建了一个新的对象“123456”,引用s又指向了这个心。原来的对象“ABCabc”仍然存在于内存中,没有改变。内存结构如下图所示:Java和C++的一个区别是Java中不可能直接操作对象本身。所有的对象都是由一个引用指向的,必须通过这个引用来访问对象本身,包括获取成员变量的值,改变对象的成员变量,调用对象的方法等。在C++中,存在三种东西:引用、对象和指针,它们都可以访问对象。其实Java中的引用和C++中的指针在概念上是差不多的。它们是存储对象在内存中的地址值,但是在Java中,引用失去了一些灵活性。例如,Java中的引用不能像C++中的指针那样进行加法和减法。为什么String对象是不可变的?要理解String的不可变性,首先要看String类中的成员变量。在JDK1.6中String的成员变量如下:thecountisthenumberofcharactersintheString.*/privatefinalintcount;/**Cachethehashcodeforthestring*/privateinthash;//Defaultto0在JDK1.7中,String类做了一些改动,主要是改变substring方法执行时的行为,与本文的主题。JDK1.7中String类的主要成员变量只有两个:publicfinalclassStringimplementsjava.io.Serializable,Comparable,CharSequence{/**Thevalueisusedforcharactersstorage.*/privatefinalcharvalue[];/**Cachethehashcodeforthestring*/privateinthash;//Defaultto0从上面的代码可以看出,Java中的String类其实就是对字符数组的封装。在JDK6中,value是String封装的数组,offset是String在这个value数组中的起始位置,count是String占用的字符数。在JDK7中,只有一个值变量,即值中的所有字符都属于String对象。这一变化不影响本文的讨论。另外还有一个hash成员变量,它是String对象的hash值的缓存,这个成员变量与本文的讨论无关。在Java中,数组也是对象(可以参考我之前的文章Java中数组的特点)。所以value只是一个引用,它指向一个真正的数组对象。其实执行Strings="ABCabc";代码后,真正的内存布局应该是这样的:value、offset、count这三个变量都是private的,没有提供setValue、setOffset、setCount等public方法给修改这些值,这样就不能在String类之外修改String。也就是说,一旦初始化就不能修改,这三个成员在String类之外是无法访问的。另外,value、offset、count这三个变量都是final的,也就是说在String类内部,这三个值一旦被初始化,就不能再改变了。所以可以认为String对象是不可变的。那么在String中,显然有一些方法,调用它们可以得到变化后的值。这些方法包括substring、replace、replaceAll、toLowerCase等,例如下面的代码:Stringa="ABCabc";System.out.println("a="+a);a=a.replace('A','a');System.out.println("a="+a);打印出来的结果是:a=ABCabca=aBCabc,那么a的值貌似变了,其实也是一样的误会。同样,a只是一个引用,而不是真正的字符串对象。当调用a.replace('A','a')时,方法内部会创建一个新的String对象,并将这个heart对象重新赋值给引用的a。String中replace方法的源码可以说明问题:读者可以自行查看其他方法,都是在方法内部重新创建一个新的String对象,并返回这个新对象。原始对象不会改变。这就是为什么像replace、substring和toLowerCase这样的方法都有返回值。这就是为什么这样的调用不会改变对象的值:Stringss="123456";System.out.println("ss="+ss);ss.replace('1','0');System.出去。println("ss="+ss);打印结果:ss=123456ss=123456String对象真的不可变吗?由上可知,String的成员变量是privatefinal的,即初始化后不可更改。那么在这些成员中,value比较特殊,因为它是一个引用变量,而不是一个真正的对象。value是被final修饰的,也就是说final不能再指向其他数组对象了,那我能不能改变value指向的数组呢?比如把数组中某个位置的字符改成下划线“_”。至少在我们写的普通代码中是做不到的,因为我们根本无法访问到这个值引用,更不用说通过这个引用来修改数组了。那么私有成员可以通过什么方式访问呢?没错,有了反射,就可以在String对象中反射value属性,然后通过改变获取的value引用来改变数组的结构。以下是示例代码:publicstaticvoidtestReflection()throwsException{//创建字符串“HelloWorld”并将其赋值给引用sStrings="HelloWorld";System.out.println("s="+s);//HelloWorld//获取String类中的value字段FieldvalueFieldOfString=String.class.getDeclaredField("value");//更改value属性的访问权限valueFieldOfString.setAccessible(true);//获取value属性的值onthesobjectchar[]value=(char[])valueFieldOfString.get(s);//改变value引用的数组中的第5个字符value[5]='_';System.out.println("s="+s);//Hello_World}打印结果为:s=HelloWorlds=Hello_World在这个过程中,s一直指向同一个String对象,但是在反射前后,String对象发生了变化,也就是说,所谓的“不可变”对象。但一般我们不会那样做。反射的这个实例也可以说明一个问题:如果一个对象的状态和其他对象的状态结合by它可以改变,那么这个对象可能不是一个不可变的对象。例如,一个Car对象组合了一个Wheel对象。虽然Wheel对象被声明为privatefinal,但是Wheel对象的内部状态是可以改变的,所以不能保证Car对象是不可变的。