java string常量池的那些'坑'

    今天在一技术讨论群里,有一兄弟问了一个问题,大致如下:

String s0="Hello World";
String s1=new String("Hello World");
Field field = String.class.getDeclaredField("value");  
field.setAccessible(true);  
char[] value = (char[])field.get(s0);  
value[0]='a';
//打印出s1和s0的值是分别是什么?

    了解java常量池的朋友看了这个代码,第一印象一般都是s0指向的是常量池中的字符串,而s1指向的是堆上的一个值等于(equals)s0的一个全新的字符串,这个是由new语义保证的。

    但如果继续住下分析,打印出来的结果是什么样的,是一样呢?还是不一样呢?其实这里就牵扯到比较多的东西,比如说final语义,java对string这个类的优化及string这个类在不同版本、不同jvm中的实现问题。我这里只是简单的说一下我自己对这块的认识及个人的一点点想法,也有可能说的有些以偏概全,不尽准确,请朋友也结合自己的认识和实际情况做出自己的判断。

    先回到上面的问题,根据我对string类的认识,我觉得输出结果应当是一样,至少在JDK7之前是这样的。我隐约记得Java对string的优化方法中有一点就是共享内部的char[],为了共享这个char[],在string内部还维护着一个叫offset的属性。对于这个案例而言,自然会共享char[].所以输出的结果应当是一样的。在这一点上,还可以通过String类的源码来印证,如下:

    public String(String original) {
int size = original.count;
char[] originalValue = original.value;
char[] v;
  if (originalValue.length > size) {
     // The array representing the String is bigger than the new
     // String itself.  Perhaps this constructor is being called
     // in order to trim the baggage, so make a copy of the array.
            int off = original.offset;
            v = Arrays.copyOfRange(originalValue, off, off+size);
 } else {
     // The array representing the String is the same
     // size as the String, so no point in making a copy.
    v = originalValue;
 }
this.offset = 0;
this.count = size;
this.value = v;
    }

    从上面的构造方法中的12-13行的注释可以印证上述案例中的两个字符串对象内部共享了同一个char[],那修改了这个char[]的话,肯定二者是同步变化的。如果将s1的构造改成如下方式,这个问题在当前的版本中应当就不会出现了:

String s1=new String("Hello WorldXXXX".substring(0,11));

因为在上面的代码里,s0与s1内部将不在共享同一个数组了。

    说到这儿,想起以一个关于string共享char[]引发的另一个坑:对一个很大的字符串进行substr后,如果substr的这个引用一直存在可能导致,则导致大字符串不能被回收,最终可能会OOM。

    这个案例里最关键的一点是用了反射修改了字符串对象内部的char[],改变了String对象内部的状态,才导致了这么个奇怪的问题。有些朋友可能觉得既然string类是不可变的,那为啥还可以修改呢?这个问题我的答案是你要是想采用反射去修改一个对象的内部状态,这个谁也没办法约束你,因为首先破坏了JAVA面向对象的封装特性,侵入性的使用私有API修改对象的私有属性;final的语议是标识一个变量的引用不可变,并不是说final对象的内部状态不可变,就拿string类来说,其内部的hash变量值只是在显式的第一次调用hashcode()时才会去计算并将赋值给hash变量。

    最后,在分享一个我厂内网上的一个很有意思的小问题,如下:

String s0="aaaaa";
System.out.println(s0);

    在上面的代码的第一与二行之间插入代码使其输入值变成"bbbb",这个问题你能想到几种办法呢?