学Java的人或多或少都会得到这么一个信息:String是不可变的。那么果真如此吗?
本文前置知识:反射,Java内存模型。
一、如何改变一个String
打开String的源码,赫然可以看见,其实String对象的数据储存在它的value
数组中。
在早起版本的Java中,这是一个char[]
类型的数组,较晚版本中替换为byte[]
类型。
public final class String {
private final byte[] value;
// ……
}
那么,如果利用反射把这个数组替换掉,是不是就能改变String了呢?
接下来进行尝试。
创建一个modifyString
方法,利用反射修改字符串中的value
数组,并在main函数中测试效果(注意,在低版本Java中这里的byte[]
应修改为char[]
):
private static void modifyString(String src, String dst) throws NoSuchFieldException, IllegalAccessException {
Field valueField = String.class.getDeclaredField("value");
valueField.setAccessible(true);
byte[] newValue = dst.getBytes();
valueField.set(src, newValue);
}
public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException {
String s = "hello, world!";
modifyString(s, "you're so cool!");
System.out.println("s = " + s);
}
可以看到,输出显示s
的确改变了!
s = you're so cool!
一个大胆的想法
看到上面的结果之后,我有了一个大胆的想法。
同样是modifyString
方法,但是main
函数改成了下面这样:
public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException {
String s = "hello, world!";
modifyString(s, "you're so cool!");
System.out.println("hello, world!");
}
甚至直接这样:
public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException {
modifyString("hello, world!", "you're so cool!");
System.out.println("hello, world!");
}
猜猜看会输出什么?有兴趣的可以自己试试。
二、原理简析
1. 字符串常量池
Java中的字符串会存储在字符串常量池中。理论上字符串常量池位于方法区,实际是存储是在堆中(见Java内存模型)。
字符串常量池中储存了使用过的字符串对象。当需要使用某个字符串时,首先在字符串常量池中查找有没有相应的对象,如果找到了就直接返回,否则就创建一个新的字符串对象,然后放进字符串常量池中。
当运行到s = "hello, world!"
时,这个字符串类型的变量s
就指向了对应常量池中的"hello, world!"
对象。为了方便区分,这里称字符串常量池中的"hello, world!"
对象为helloworld
。
众所周知,Java中一切对象都是引用传递,当使用modifyString(s, "you're so cool!")
方法修改字符串时,其实修改的就是helloworld.value
。这样一来,相当于直接修改了常量池中字符串的值。
所以,当我们运行以下代码时:
public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException {
String s = "hello, world!";
String t = "hello, world!";
modifyString(s, "you're so cool!");
System.out.println(t);
}
得到的输出是you're so cool!
。本质上的原因就是,s
和t
都是指向的同一个字符串常量池中的对象helloworld
。也就是说,s
和t
本质上只是类似于一个指针,真实的对象都是常量池中的helloworld
。
当helloworld
对象的内部值value
被修改之后,表面上的感官即是s
和t
的值都发生了改变。
再看这段代码:
public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException {
modifyString("hello, world!", "you're so cool!");
System.out.println("hello, world!");
}
同样的道理,最后System.out.println("hello, world!")
输出的是you're so cool!
,谜底揭晓,也是很神奇了。
2. new String()
通过new String()
创建的字符串,情况就有所变化。
查看以下代码:
public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException {
String s = "hello, world!";
String t = new String("hello, world!");
modifyString(s, "you're so cool!");
System.out.println(t);
}
最终输出hello, world!
而不是you're so cool!
,其原因是因为变量t
指向的是在堆中创建的String
对象而非字符串常量池中的helloworld
对象。
在执行
t = new String("hello, world!)
时,会在堆内存中开辟一块空间放置这个对象,并把字符串常量池中的helloworld
的value
数组赋值给t
。下面是String类的构造方法:
public String(String original) {
this.value = original.value;
this.coder = original.coder;
this.hash = original.hash;
}
当使用modifyString(s, "you're so cool!")
修改字符串时,是把helloworld.value
给替换掉了;而t.value
没有被替换掉,仍旧是指向的"hello, world!"
所对应的字节数组。如图所示:
看到这里,不知道你有没有一个大胆的想法?
3. 又一个大胆的想法
在上面的分析中,已经知道,对于以下代码:
public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException {
String s = "hello, world!";
String t = new String("hello, world!");
modifyString(s, "you're so cool!");
System.out.println("t = " + t);
}
private static void modifyString(String src, String dst) throws NoSuchFieldException, IllegalAccessException {
Field valueField = String.class.getDeclaredField("value");
valueField.setAccessible(true);
byte[] newValue = dst.getBytes();
valueField.set(src, newValue);
}
t
并没有被改变,仍然输出t = hello, world!
。那么,如果改变modifyString
的行为,使其直接修改value
数组呢?
private static void modifyString(String src, String dst) throws NoSuchFieldException, IllegalAccessException {
Field valueField = String.class.getDeclaredField("value");
valueField.setAccessible(true);
byte[] oldValue = (byte[]) valueField.get(src);
byte[] newValue = dst.getBytes();
System.arraycopy(newValue, 0, oldValue, 0, Math.min(oldValue.length, newValue.length));
}
这时候,再运行main函数,就发现,t
的值也改变了。不过由于value
数组的长度限制,只能显示原字符串的长度:
输出
t = you're so coo
三、Android之殇
打开Android SDK中的String类,会发现里面已经没有value
数组了。这是因为Android修改了String类的实现,直接在native层面管理value
数组,而在String中加入了数组长度count
。
而一系列跟value
相关的方法,比如charAt
、compareTo
等都改成了native方法。
并且,Android禁止了所有String的构造方法,创建字符串要么使用双引号的形式,要么使用StringFactory
。
Android这么做的原因据称是为了性能,能在字节码上面优化运行效率。我觉得这同时也是为了安全,避免对字符串常量池中的值进行修改。
反正,这个花活儿在Android里是玩不了了。
网友评论