今天在项目中遇到了一个问题,然后我头铁的认为一直是bug,结果居然是String引起的,我一直没有往String这个点上去思考,直到debug之后… …
(菜是原罪呀😣)


不知道你对String了解多少呢?

一般来说经常用过String的人都会说String是不可变的,你觉得呢?

这个不可变到底该怎么理解?是值?地址?还是其他不可变呢?

= "a";
str = "b";
System.out.println(str);

看上面这一段代码,你认为它的答案是什么呢?
如果你认为是aaa,那就不好意思,错了。

答案:b


如果让很多人回答,会出现三种不同的意见

(第一种和第三种答案虽相同,但是思想不同)

第一种:b(这一种虽然答案是对的,但还不如答错的人,因为我估计说这种答案的人没看过String源码

第二种:a(这一种答案虽然是错的,但应该了解过String的源码,知道String是不可变的

第三种:b(此b非彼b,这种是最正确的,至于为什么,下面会说)

为什么会有字符串常量池

因为字符串就是对象,我们都知道分配对象需要消耗高昂的时间和空间;另一方面,我们经常会使用字符串;所以为了减少内存开销和提高性能,JVM处理字符串时必须要进行优化。

然后字符串常量池便出现了,当JVM运行时,系统会单独分配一块空间,这段空间也就是字符串常量池。

当我们创建字符串时,JVM会先检查字符串常量池是否会有该字符串,如果有,那么就直接返回该字符串的引地址;如果没有,则创建该字符串放到字符串常量池中并返回地址。

为什么不可变?

观看String底层代码可以看出由于数组使用了final关键字进行修饰,导致该String类的不可变性。

public final class String
implements java.io.Serializable, Comparable<String>, CharSequence {

private final char value[];

那么由于String的不可变性,那么常量池中肯定不会出现相同的字符串。

不可变的是什么?

理解了字符串常量池这个应该就很好分析了。

String str = “a”;字符串常量池没有相应的字符串,所以会在字符串常量池中创建该字符串,然后将地址返回给str

str = “b”;这一步会发生什么呢?由于String的不可变性,那么a是不可能改成b的;所以此时是str这个地址发生了变化。首先会创建b这个字符串,然后将地址返回给str,所以此时str指向了b;a还是a,还是那片空间,没有任何变化。

简单了说,变的只是引用,和字符串本身的那片空间没有任何半毛钱关系

从String中的方法探讨不可变性

字符串的截取,返回一个新的对象

public String substring(int beginIndex) {
if (beginIndex < 0) {
throw new StringIndexOutOfBoundsException(beginIndex);
}
int subLen = value.length - beginIndex;
if (subLen < 0) {
throw new StringIndexOutOfBoundsException(subLen);
}
//不过开始索引不为0,返回了一个新的String对象
return (beginIndex == 0) ? this : new String(value, beginIndex, subLen);
}

字符串的截取,返回一个新的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);
}

字符串的拼接(拼接之后copy到一个新的String对象)

public String concat(String str) {
int otherLen = str.length();
if (otherLen == 0) {
return this;
}
int len = value.length;
//copy
char buf[] = Arrays.copyOf(value, len + otherLen);
str.getChars(buf, len);
return new String(buf, true);
}

这样的方法还有很多很多,我们可以发现其中的特点,如果返回值类型是String,那么都是返回了一个新的对象。另外如果返回值是其他类型,那么在该方法中使用该字符串,依旧是先进行复制到一个新的数组中;然后再对其操作。



为什么要将String设置为不可变性

public final class String
implements java.io.Serializable, Comparable<String>, CharSequence {

private final char value[];

从第一个类的修饰符final可以得出,该类是不可继承的,也是为了防止其它人继承该类之后,对其安全性造成破坏。

一般来说操作字符串有三个重要的类,分别是
String、StringBuffer、StringBuilder
区别就不再多说

举个StringBuffer的例子

= new StringBuffer("aaa");
StringBuffer stringBuffer2 = new StringBuffer("bbb");
List<StringBuffer> list = new ArrayList<>();
list.add(stringBuffer);
list.add(stringBuffer2);
StringBuffer stringBuffer3 = stringBuffer;
stringBuffer3.append("bbb");
System.out.println(list);

[aaabbb, bbb]

通过上面这个例子可以看出,我创建一个list容器;然后向其中加入两个StringBuffer对象,通过第三个StringBuffer引用改变了容器中的内容,这是不是破坏了安全性。

再看下面这个例子

String str = new String("aaa");
String str2 = new String("bbb");
List<String> list = new ArrayList<>();
list.add(str);
list.add(str2);
String str3 = str;
str3 += "bbb";
System.out.println(list);

[aaa, bbb]

这个想必都明白,由于String不可变,所以对list容器中的内容无法修改
这一步 str3 += “bbb” 表示创建了一个新的String对象。