java中的字符串String的不可变性

1,026 阅读5分钟

java中的String的不可变性,网上已经有了大把大把的文章证明了。一般都是都是通过代码证明的,当然,我也不能免俗,我先列一段代码:

 public static void main(String[]args){
    String one=new String("abc");
    String two=new String("abc");
    System.out.println("第一步one==two:"+(one==two));
    System.out.println("第二步one.equals(two):"+one.equals(two));
    System.out.println();
    
    String three=one;
    System.out.println("第三步one==three:"+(one==three));
    System.out.println("第四步one.equals(three):"+one.equals(three));
    System.out.println();
    
    one="abcdefg";
    System.out.println("第五步one==three:"+(one==three));
    System.out.println("第六步one.equals(three):"+one.equals(three));
    System.out.println();
    
    String seven="abc";
    String eight="abc";
    System.out.println("第七步seven==eight:"+(seven==eight));
    System.out.println("第八步seven.equals(eight):"+seven.equals(eight));
}

答案为:

    第一步one==two:false
    第二步one.equals(two):true
    
    第三步one==three:true
    第四步one.equals(three):true
    
    第五步one==three:false
    第六步one.equals(three):false
    
    第七步seven==eight:true
    第八步seven.equals(eight):true

我们一步一步来看代码。

第一步:第一步的答案是为false,这当然是没有疑问的,是new出的两个不同对象,通过“==”来比较的话,比较的是两个对象之间的引用,当然是不相同的,所以为false。

第二步:第二步的答案是true。这是对的,虽然是两个不同的对象,但是两个对象引用指向的值都是一样的,而通过“equals”来进行比较的话,比较的是两个对象的引用所指向的值,所以为true。

第三步第四步:第三步和第四步的答案都为true。在看第三步之前,我们要看到之前有一个代码:

 String three=one;

这个代码,意味着将one这个字符串的引用值赋给three,也就是说,one和three指向的是同一个对象,那么第三步和第四步的值当然都为true了。

第五步第六步:到了这一步之前,先看之前的代码:

    String one=new String("abc");
    String three=one;
    one="abcdefg";
    System.out.println("第五步one==three:"+(one==three));
    System.out.println("第六步one.equals(three):"+one.equals(three));

我把之前的和第五步和第六步相关的代码提炼出来。因为刚开始one和three是指向的同一个对象,但是后面one又改变了,one="abcdefg",于是得出的第五步和第六步的值都是false。这是为什么呢?

如果one和three指向的都是同一个对象,那么对one的修改应该是完全同步到three上面才对啊?

其实,在one=“abcdefg”这段代码,因为在现在的jdk版本中,String常量池的存在于堆中,当发现在常量池里面没有“abcdefg”这个字符串,那么就会生成一个新的字符串对象。 那么,我们就能理解另外为什么第五步和第六步的值都为false了,因为one已经实际上是一个新new出来的对象,和three是完全不同的两个对象了。

这个地方,我们引入的正是java中的String的不可变性

第七步第八步:到这一步,我们先把代码提炼出来。

    String seven="abc";
    String eight="abc";
    System.out.println("第七步seven==eight:"+(seven==eight));
    System.out.println("第八步seven.equals(eight):"+seven.equals(eight));

答案都是true,我们不免产生了迷惑,这和第五步,第六步说的似乎不一样啊?这个时候我们深入的去了解一下在java中,String类型的到底是怎么生成对象的。

先说new String("abc")的方法创建的字符串,这种方法是不管什么时候,都是new一个对象出来。

而另外一种是String seven="abc";这种类型的,这种类型的方法呢,先在栈中创建一个对String类的对象引用变量str,然后查找栈中(也是我们所说的字符串常量池,jdk版本为1.6及之前,常量池是存在Pern Gen区,也就是方法区。1.7版本后常量池就存在与堆中了)有没有存放"abc",如果没有,则将"abc"存放进栈,并令str指向”abc”,如果已经有”abc” 则直接令str指向“abc”。 也就是说,这种方法,可能不会new一个对象出来,可能只是指向了同一个引用而已。

这样的话,我们便能理解第五步第六步,也能理解第七步和第八步了。seven是生成了一个新的对象,但是eight并没有生成一个新的对象,它只是在栈中发现了它需要的而且是由seven生成的“abc”,于是它便直接指向了这个对象。

当然,仅仅通过我们的代码来验证,还是不够的,通常我们需要去了解一下String类型的源码,才能更加深刻的理解java中的String的不可变这个特性。

String类型的源码如下:

从这一段代码我们可以发现,String类型的底层其实是char类型的数组,而且是由final修饰的。于是我们可以得出两个结论:

    第一:String类型的长度是不可改变的。(因为底层是数组)
    
    第二:String类型的值是不可改变的。(因为是final修饰的)

当然,实际上在java中因为反射的原因,我们可以对String类型的值进行修改,真正能坚持不变的可能是String类型的长度。

通过反射修改String类型的值的代码如下:

    static final Unsafe unsafe = getUnsafe();
    static final boolean is64bit = true;

 public static void main(String[]args) throws NoSuchFieldException, IllegalAccessException {
    String s = "Hello World";
    Double[] ascending = new Double[16];
    for(int i=0;i<ascending.length;i++){
        ascending[i] = (double) i;
    }
    System.out.println("未通过反射修改字符串的值:");
    printAddresses(s, ascending);
    //获取String类中的value属性
    Field valueField = String.class.getDeclaredField("value");
    //改变value属性的访问权限
    valueField.setAccessible(true);
    //获取s对象上的value属性的值
    char[] value = (char[]) valueField.get(s);
    //改变value所引用的数组中的第6个字符
    value[5] = '_';
    System.out.println("通过反射修改字符串的值:");
    printAddresses(s, ascending);
}

/**
 * 获取对象的引用值
 * @param label
 * @param objects
 */
public static void printAddresses(String label, Object... objects) {
    System.out.print("s="+label + "  引用值为: 0x");
    long last = 0;
    int offset = unsafe.arrayBaseOffset(objects.getClass());
    int scale = unsafe.arrayIndexScale(objects.getClass());
    switch (scale) {
        case 4:
            long factor = is64bit ? 8 : 1;
            final long i1 = (unsafe.getInt(objects, offset) & 0xFFFFFFFFL) * factor;
            System.out.print(Long.toHexString(i1));
            for (int i = 1; i < objects.length; i++) {
                final long i2 = (unsafe.getInt(objects, offset + i * 4) & 0xFFFFFFFFL) * factor;
            }
            break;
        case 8:
            throw new AssertionError("Not supported");
    }
    System.out.println();
}

private static Unsafe getUnsafe() {
    try {
        Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");
        theUnsafe.setAccessible(true);
        return (Unsafe) theUnsafe.get(null);
    } catch (Exception e) {
        throw new AssertionError(e);
    }
}

打印出的结果为:

    未通过反射修改字符串的值:
    s=Hello World  引用值为: 0x76be43a18
    通过反射修改字符串的值:
    s=Hello_World  引用值为: 0x76be43a18

我们发现,对象的值变了,但是引用没有变。

结论:

所以,实际上,String类型的不可变,是长度的不可变,它的值确实是可以通过反射进行改变的。