浅析String不可变性
在所有编程语言领域,我想字符串应该是地球上最常用的表达手段了吧。
在java的世界里,String是作为类出现的,核心的一个域就是一个char数组,内部就是通过维护一个不可变的char数组,来向外部输出的。
这是jdk一段String类定义,首先类是final,表明类不可被继承;核心域是private final的,final表明这个引用所指向的内存地址不会改变,但这还不足说明value[]是不可变的;因为引用所指向的内存的值有可能发生变化,但是jdk是不会让这样的事情发生的。private 保证这个域对外部来说是不可见的,这还不够,对value还要进行 保护性拷贝 。
举一个简单的例子:
这是一个String的构造函数,参数是一个char数组引用,它并没有把这个数组引用直接赋值给实例对象的value成员变量,而是通过一个Arrays.copyOf的方式拷贝一个数组再给到对象的成员变量。为什么呢?假设它这里是直接一个赋值,那String的不可变性就彻底被破坏了,因为如此一来,存在一个外部引用与实例对象value引用指向相同的内存地址,通过外部引用就可以改变这个char数组对象,最终导致的结果就是String不再不可变。幸好JDK中所有的对value的操作都是保护性拷贝操作,不管是被赋值,还是赋值给其它外部引用。
说了这么多,为什么JAVA要String保持一个不可变的状态呢?原因其实很简单,因为String太太太太常用了,地球上没有能比这个更常用的对象了,设计成不可变的,是为了减少大量的同步锁的开销。但是要注意 并不是声明成final的类一定是不可变的 。
根据effective java一书中提到,类不变需遵循五条规则:
1.不提供任何机会修改对象状态的方法
2.保证类不被扩展
3.所有域都是final
4.所有域都是私有的
5.确保对于任何可变组件的互斥访问
有兴趣的同学可以参考 effective 第十五条,这里就不展开讲了。作了那么久的铺垫,接下来可以谈谈avoid getfield opcode了,按翻译来说就是防止"调用访问域的操作码",这段tip来自一段注释。
十分常用的replace方法,内部算法大概是这样一个过程:先找到第一个oldChar的下标i,拷贝小标i之前的旧数组的内容到新的数组,新数组[i]='newChar',遍历i之后的内容,若旧数组出现为oldChar则在新数组中替换为newChar,若没有出现,则拷贝旧值到新数组。
起初我很奇怪,到底为什么,一定要找到第一个出现oldChar的下标,为什么不直接遍历数组中每一个char 若为旧值,替换为新值。我从时间复杂度,空间复杂度去考虑这个算法,始终没有得到结果。我还是太年轻啊,后来才发觉其实还是为了维护一个String的设计原则:"对于拥有相同的字符字面量的情况下,String的构造还是优先返回原字符串对象"。这么做应该是为了解决堆内存吧。
那么,这一句注释到底是什么意思呢?要理解这句话,需要对JVM有一定的了解。
JVM在运行中的数据区,分为五个部分:方法区,堆区,虚拟机栈,本地方法栈,程序计数器。
首先类相关的信息肯定是放在方法区的,堆中放一些实例对象,程序计数器始终指向下一条将要执行的指令,虚拟机栈和本地方法栈分别是用来于普通方法和本地方法的。
着重说一下虚拟机栈,它是线程私有的,描述的是java方法执行的内存模型:每个方法在执行的同时创建一个栈帧,用于存放局部变量表,操作数栈,方法入口,动态链接等。
局部变量表用来存放一些基本数据类,和引用。操作数栈的话,是用来作运算用的,打个比方
int a=1; int b =2; int c =a+b;
JVM会先把a,b的值压入到操作数栈保存起来,等到程序计数器执行加法指令的时候,再把a,b从栈中pop出来。重点就在于a,b值是从哪里压入到栈中的,如果没有那么接下来要遍历的就是value数组了,value毫无疑问是堆中的数据,也就是每一次遍历会经历,数据由堆中取出,进入栈帧,再由栈帧压入到操作数栈,最后pop出来进行运算。
如果执行了上述代码,情况就大不相同了,val就是一个局部变量,它会被存放在局部变量表中,接来下的运算,就是局部变量表到操作数栈了,属于一个栈帧内的数据转移。JVM为了避免频繁进行堆栈数据转移,将值复制到本地变量一次,以避免在接下来的几行中循环的每一次迭代,从堆中多次取下的字段值。
除了我提到的这些,String源码中还有许多值得学习的算法,设计方式,代码优化,比如,还有常量池的实现。
最后我想求教一个问题啊,在JDK7点版本中,String引入了一段静态代码块,
我甚是不解啊,不知道有没有大神帮我解读一下这段代码的含义?