[Java基础]String 为什么是不可变的?
为什么String要设计成不可变的
-
线程安全:不可变对象天生就是线程安全的:因为不可变对象不能被改变,所以他们可以自由地在多个线程之间共享。不需要任何同步处理。
-
hashmap需要:
- 加快字符串处理速度由于String是不可变的,保证了hashcode的唯一性,于是在创建对象时其hashcode就可以放心的缓存了,不需要重新计算。这也就是Map喜欢将String作为Key的原因,处理速度要快过其它的键对象。所以HashMap中的键往往都使用String。String重写了hashcode方法,所以如果string是可变的,那就要频繁重新计算hashcode
-
字符串池:在Java中,由于会大量的使用String常量,如果每一次声明一个String都创建一个String对象,那将会造成极大的空间资源的浪费。Java提出了String pool的概念,在堆中开辟一块存储空间String pool,当初始化一个String变量时,如果该字符串已经存在了,就不会去创建一个新的字符串变量,而是会返回已经存在了的字符串的引用。假设现在我们有两个字符串指向了常量池中的同一个字符串常量,如果字符串是可以更改的,那么其中一个对字符串进行了更改,另一个字符串也会变化,这就违背了常量池的概念。
-
避免安全问题:在网络连接和数据库连接中字符串常常作为参数,例如,网络连接地址URL,文件路径path,反射机制所需要的String参数。其不可变性可以保证连接的安全性。如果字符串是可变的,黑客就有可能改变字符串指向对象的值,那么会引起很严重的安全问题。
String 如何实现不可变
关于这个问题,网上有人说,是因为String类被写成final或者String中的成员变量value数组被写成final,但其实并不是,下面做一个实验
public final class MyString {
public final char[] value = {'z'};
}
首先我们定义了一个类Mystring,并且类和成员变量都被设置成final
public class Main {
public static void main(String[] args) {
MyString myString = new MyString();
System.out.println(myString.value);
myString.value[0] = 'a';
System.out.println(myString.value);
myString.value[0] = 'b';
System.out.println(myString.value);
}
}
执行结果
z
a
b
这说明仅仅把类和成员变量设置成final无法实现不可变,为了实现不可变我们还需要把这个成员变量设置成private不可见。
继续我们的实验
public final class MyString {
private final char[] value = {'z'};
}
这时再去执行上面的main函数,有如下结果,
E:\MIT6.830\Test_7\src\Main.java:12:36
java: value 在 MyString 中是 private 访问控制
这时候我们无法直接修改value的值,但是这就可以了吗?当然还没有
为了保证string的不可变我们还要继续做到以下几点
- 首先设置内部成员变量的访问修饰符为private,这样就无法在类的外部直接访问到这个成员变量
- 其次我们必须保证String类不提供成员方法去修改value,String的成员方法不是直接修改value而是通过新建一个String,并且把旧的string中的值复制到新的string对象中去。
- 在value上加final保证这个数组的引用不会被修改而指向另一个数组
- 在String上加final保证String没有子类,因为子类可能提供方法去修改value,子类可以赋值给父类引用进而破坏String的不可变特性
保存字符串的数组被 final 修饰且为私有的,并且String 类没有提供/暴露修改这个字符串的方法。
String 类被 final 修饰导致其不能被继承,进而避免了子类破坏 String 不可变。
让我们来看看string提供的一些修改方法的源码
public String substring(int beginIndex, int endIndex) {
if (beginIndex < 0) {
throw new StringIndexOutOfBoundsException(beginIndex);
}
if (endIndex > value.length) {
throw new StringIndexOutOfBoundsException(endIndex);
}
int subLen = endIndex - beginIndex;
if (subLen < 0) {
throw new StringIndexOutOfBoundsException(subLen);
}
return ((beginIndex == 0) && (endIndex == value.length)) ? this
: new String(value, beginIndex, subLen);
}
public String concat(String str) {
int otherLen = str.length();
if (otherLen == 0) {
return this;
}
int len = value.length;
char buf[] = Arrays.copyOf(value, len + otherLen);
str.getChars(buf, len);
return new String(buf, true);
}
public String replace(char oldChar, char newChar) {
if (oldChar != newChar) {
int len = value.length;
int i = -1;
char[] val = value; /* avoid getfield opcode */
while (++i < len) {
if (val[i] == oldChar) {
break;
}
}
if (i < len) {
char buf[] = new char[len];
for (int j = 0; j < i; j++) {
buf[j] = val[j];
}
while (i < len) {
char c = val[i];
buf[i] = (c == oldChar) ? newChar : c;
i++;
}
return new String(buf, true);
}
}
return this;
}
可以看到这些方法都不是直接操value数组,而是新生成了一个string,把新的内容赋给这个新的string。
String真就不可改变吗
答案是否定的。
我们可以利用反射机制改变value
public static void main(String[] args) {
String str = "aaa";
System.out.println("str = " + str + "\t" + "hashcode=" + str.hashCode());
try {
Field valueField = String.class.getDeclaredField("value");
//暴力反射!
valueField.setAccessible(true);
byte[] valueCharArr = (byte[]) valueField.get(str);
//改变!
valueCharArr[0] = 'b';
System.out.println("str = " + str + "\t" + "hashcode=" + str.hashCode());
} catch (NoSuchFieldException | SecurityException
| IllegalArgumentException | IllegalAccessException e) {
e.printStackTrace();
}
}
结果:我们改变了value!
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· DeepSeek 开源周回顾「GitHub 热点速览」
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· AI与.NET技术实操系列(二):开始使用ML.NET
· 单线程的Redis速度为什么快?