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
打印出的结果为:
未通过反射修改字符串的值: s=Hello World 引用值为: 0x76be43a18 通过反射修改字符串的值: s=Hello_World 引用值为: 0x76be43a18复制代码
我们发现,对象的值变了,但是引用没有变。
结论:
所以,实际上,String类型的不可变,是长度的不可变,它的值确实是可以通过反射进行改变的。