String是如何保证不变的?反射为什么可以改变String的值?
1. String字符串的源码分析
String 字符串到底能不能改变已经是老生常谈的问题了,但是在面试环节中,依然能够难住不少人。
下面我们根据 JDK1.8 版本下的String源码进行分析,一步一步的了解String字符串的不可变性。
String底层是使用final修饰的字符数组 value[] 来存储字符,而数组是引用类型,引用类型的值是内存中的地址,地址在初始化之后不可变,所以String的值不可变。
2. 通过反射改变字符串的值
虽然final修饰的数组地址不可改变,但是地址指向的值(堆内存中)是可以改变的,String没有对外提供相应的方法来更改值,但是可以通过反射实现。
import java.lang.reflect.Field;
public class stringDemo {
public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException {
String s = "abc";
Class clz = s.getClass();
//需要使用getDeclaredField(), getField()只能获取公共成员字段
Field field = clz.getDeclaredField("value");
field.setAccessible(true);
char[] ch =(char[])field.get(s);
ch[1] = '8';
System.out.println(s);
}
}
打印结果:
(在实际开发中,很少需要通过反射来修改String的值。这里只是提供一种思路,在某些情况下可以帮助我们解决一些实际问题。)
上面我们通过案例知道了,字符串的字符数组可以通过反射进行修改,导致字符串的“内容”发生了变化。但即使是内容发生了改变,它的hash值也是不会改变的:
import java.lang.reflect.Field;
public class stringDemo {
public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException {
String s = "abc";
System.out.println("str=" + s + "," + s.hashCode());
Class clz = s.getClass();
//需要使用getDeclaredField(), getField()只能获取公共成员字段
Field field = clz.getDeclaredField("value");
field.setAccessible(true);
char[] ch =(char[])field.get(s);
ch[1] = '8';
System.out.println("str=" + s + "," + s.hashCode());
}
}
运行结果:
从上述结果可以知道,String 字符串对象的 value 数组的元素是可以被修改的,但是hash值没有发生改变,也就是说对象没有变。
但是字符串两次打印出来的结果不一样,计算的hash值为什么是一样的呢?
/** Cache the hash code for the string */
private int hash; // Default to 0
/**
* Returns a hash code for this string. The hash code for a
* {@code String} object is computed as
* <blockquote><pre>
* s[0]*31^(n-1) + s[1]*31^(n-2) + ... + s[n-1]
* </pre></blockquote>
* using {@code int} arithmetic, where {@code s[i]} is the
* <i>i</i>th character of the string, {@code n} is the length of
* the string, and {@code ^} indicates exponentiation.
* (The hash value of the empty string is zero.)
*
* @return a hash code value for this object.
*/
public int hashCode() {
int h = hash;
if (h == 0 && value.length > 0) {
char val[] = value;
for (int i = 0; i < value.length; i++) {
h = 31 * h + val[i];
}
hash = h;
}
return h;
}
发现在第一次调用 hashCode 函数之后,字符串对象内通过 hash 这个属性缓存了 hashCode的计算结果(只要缓存过了就不会再重新计算),因此第二次打印hash值和第一次相同。
3. 如何理解String字符串的不可变性呢?
首先将 String 类声明为 fianl 保证不可继承。
然后,所有修改的方法都返回新的字符串对象,保证修改时不会改变原始对象的引用。
。。。。。。
接下来我们来分析下面这段代码,
import java.lang.reflect.Field;
public class stringDemo {
public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException {
String s = "abc";
System.out.println("str=" + s + "," + s.hashCode());
Class clz = s.getClass();
//需要使用getDeclaredField(), getField()只能获取公共成员字段
Field field = clz.getDeclaredField("value");
field.setAccessible(true);
char[] ch =(char[])field.get(s);
ch[1] = '8';
System.out.println("str=" + s + "," + s.hashCode());
System.out.println("abc");
}
}
运行结果:
是不是很神奇,明明打印的是System.out.println(“abc”);但是得到的结果是:a8c。其实道理也很简单,是因为字符串字面量都指向字符串池中的同一个字符串对象(本质是池化的思想,通过复用来减少资源占用来提高性能),通过官方的解释可以明确这一点:
官方地址:https://docs.oracle.com/javase/specs/jls/se8/html/jls-3.html#jls-3.10.5
A string literal is a reference to an instance of class String (§4.3.1, §4.3.3).
字符串字面量是指向字符串实例的一个引用。
Moreover, a string literal always refers to the same instance of class String. This is because string literals - or, more generally, strings that are the values of constant expressions (§15.28) - are “interned” so as to share unique instances, using the method String.intern.
字符串字面量都指向同一个字符串实例。
因为字面量字符串都是常量表达式的值,都通过String.intern共享唯一实例。
以上可以分析得到:对象池中存在,则直接指向对象池中的字符串对象,否则创建字符串对象放到对象池中并指向该对象。
因此可以看出,字符串的不可变性是指引用的不可变。
虽然 String 中的 value 字符数组声明为 final,但是这个 final 仅仅是让 value的引用不可变,而不是为了让字符数组的字符不可替换。
由于开始的 abc 和最后的 abc 属于字面量,指向同一个字符串池中的同一个对象,因此对象的属性修改,两个地方打印都会受到影响。