String不可变的含义

String不可变的含义是:将一个已有字符串”123”重新赋值成”456”,不是在原内存地址上修改数据,而是重新指向一个新对象,新地址。

也就是说:不可变的含义是内部数据不可变,而不是说引用不可变。

String为什么不可变

String的内部数据是一个char数组,是对字符串数组的封装,并且是被final修饰的,创建后不可改变。

1
2
3
4
5
6
7
8
9
10
11
12
package java.lang;

public final class String
implements java.io.Serializable, Comparable<String>, CharSequence {
/** The value is used for character storage. */
private final char value[];

/** Cache the hash code for the string */
private int hash; // Default to 0

// 其他代码
}

String不可变的好处

对于String来说,好处是:

  1. 便于实现字符串池(String pool)

在Java中,由于会大量的使用String常量,如果每一次声明一个String都创建一个String对象,那将会造成极大的空间资源的浪费。Java提出了String pool的概念,在堆中开辟一块存储空间String pool,当初始化一个String变量时,如果该字符串已经存在了,就不会去创建一个新的字符串变量,而是会返回已经存在了的字符串的引用。

如果字符串是可变的,某一个字符串变量改变了其值,那么其指向的变量的值也会改变,String pool将不能够实现!

例如下边这个代码,就只会在字符串池创建一个字符串“Hello World!”:

1
2
String a = "Hello World!";
String b = "Hello World!";
  1. 使多线程安全

看下面这个场景,一个函数appendStr()在不可变的String参数后面加上一段“bbb”后返回。appendSb()负责在可变的StringBuilder后面加”bbb”。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class test {
// 不可变的String
public static String appendStr(String s) {
s += "bbb";
return s;
}

// 可变的StringBuilder
public static StringBuilder appendSb(StringBuilder sb) {
return sb.append("bbb");
}

public static void main(String[] args) {
String s = new String("aaa");
String ns = test.appendStr(s);
System.out.println("String aaa>>>" + s.toString());
// StringBuilder做参数
StringBuilder sb = new StringBuilder("aaa");
StringBuilder nsb = test.appendSb(sb);
System.out.println("StringBuilder aaa >>>" + sb.toString());
}
}

如果程序员不小心像上面例子里,直接在传进来的参数上加上“bbb”。因为Java对象参数传的是引用,所有可变的StringBuffer参数就被改变了。可以看到变量sb在Test.appendSb(sb)操作之,就变成了”aaabbb”。

有的时候这可能不是程序员的本意。所以String不可变的安全性就体现在这里。

在并发场景下,多个线程同时读一个资源,是安全的,不会引发竞争,但对资源进行写操作时是不安全的,不可变对象不能被写,所以保证了多线程的安全。

  1. 避免安全问题

在网络连接和数据库连接中字符串常常作为参数,例如,网络连接地址URL,文件路径path,反射机制所需要的String参数。其不可变性可以保证连接的安全性。如果字符串是可变的,黑客就有可能改变字符串指向对象的值,那么会引起很严重的安全问题。

因为String是不可变的,所以它的值是不可改变的。但由于String不可变,也就没有任何方式能修改字符串的值,每一次修改都将产生新的字符串,如果使用char[]来保存密码,仍然能够将其中所有的元素设置为空和清零,也不会被放入字符串缓存池中,用字符串数组来保存密码会更好。

  1. 加快字符串处理速度

由于String是不可变的,保证了hashCode的唯一性,于是在创建对象时其hashCode就可以放心的缓存了,不需要重新计算。这也就是一般将String作为Map的Key的原因,处理速度要快过其它的键对象。所以HashMap中的键往往都使用String。

在String类的定义中有如下代码:

1
private int hash; // 用来缓存HashCode

String的字符数据可变

String类使用char value[]来存字符数据,它的类型为:private final char value[];

看上去它是不可更改的,因为是final类型。注意:final只是表示不能指向其他地址,它里边的内容是可以更改的。

结论:String是可以更改的,使用反射,value.setAccessible(true),然后修改它即可。

1
2
3
4
5
6
7
8
9
10
11
12
import java.lang.reflect.Field;

public class Demo {
public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException {
String s = "1234";
System.out.println("改变前:s=" + s);
Field f = s.getClass().getDeclaredField("value");
f.setAccessible(true);
f.set(s, new char[]{'a', 'b', 'c'});
System.out.println("改变后:s=" + s);
}
}