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类型的长度是不可改变的。(因为底层是数组)
第二: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类型的不可变,是长度的不可变,它的值确实是可以通过反射进行改变的。