String字符串浅析
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 类是被 final 修饰,即 String 字符串一旦被创建就无法被修改了。- String 类的底层就是 char[] 数组。
注意:String不可变原因不是因为value字符数组被 final 修饰!!!
即,被 final 修饰的是 value 这个变量,关被 value 指向的数组空间什么事
我们无法修改 value 这个变量指向的地址,但是可以修改指向地址空间里面的值!
public final class String implements java.io.Serializable, Comparable<String>, CharSequence {
private final char value[];
//...
}
真正不可变原因:
- String 类中没有暴露任何可供修改类中 内部成员字段 的方法。
- String 整个类是被 final 修饰的,所以杜绝子类去更改 String 类的东西(不能被继承)
- 对于存储字符串的字符数组 value 使用 private 和 final 修饰,防止 value 指向的地址发生改变
那么、我们平常是怎么能够对字符串进行修改的呢?
实际上,String 类型的变量被创建后,其 值 是存储在 String常量池中的,然后 变量 存储的是指向这个 值的地址,那么当我们修改字符串变量的值时,实际上是修改了变量指向的地址,具体实现是:
String str = a;
在 栈中存放着一个String类型的变量 str ,str 会指向堆中的String常量池中的一块地址,这块地址存放着字符串 a。
当我们要将str的值修改为 b 时,即
str = b;
此时、因为String类型变量的值被创建了就无法被修改,所以 JVM虚拟机 会先在常量池中寻找是否有值 b,如果有,则是将栈中的字符串变量 str 由指向 a 的地址,转变为指向 b 的地址,以达到所谓重新赋值为b的效果,如果在常量池中没有找到 b 则会在常量池中新开辟一块空间来存放 b 值,同时将 str 从值向a的地址转换为指向 b 的地址,以此达到赋值的目的。
一般,如果出现了下面这种情况:
String str="";
for(int i=0;i<10000;i++){
str +="hello";
}
执行过程将是,提取 hello并与str字符串合并后再在常量池中开辟新的空间存储合并后的字面常量,所以循环下来,会开辟10000个空间来存储,这样做极大的浪费了内存空间,所以就出现了:StringBuffer以及StringBuilder
StringBuilder又称之为可变字符序列,可以看做是String的字符缓冲区,相当于一个容器,这个容器可以存储多个字符串,并且能够对其中的字符串进行各种操作。
再来看看下面代码,
StringBuilder str = new StringBuilder();
for(int i=0;i<10000;i++){
str.append("hello"); // 将hello添加到字符串末尾。
}
执行过程是,只new了一个对象,开辟了一次空间,每次新增hello时,都是直接在原有的基础上添加hello,这样极大的节省了空间。
那已经有了StringBuilder为什么还要StringBuffer呢?四个字,线程安全
从两者源码分析:
StringBuffer:
public synchronized StringBuffer append(StringBuffer sb) {
toStringCache = null;
super.append(sb);
return this;
}
StringBuilder:
@Override
public StringBuilder append(String str) {
super.append(str);
return this;
}
可以看到差别,StringBuffer方法上添加了锁关键字,synchronized,这个关键字可以在多线程访问是起到线程安全的作用,也就是说StringBuffer是线程安全的,这也是两者的区别。
那为什么 StringBuilder会有线程安全问题呢?
我们首先利用多线程去操作 StringBuilder类型的变量看看为什么会不安全:
public class MyTest {
public static void main(String[] args) {
StringBuilder str = new StringBuilder();
for (int i = 0; i < 10; i++) { // 开启10个线程
new Thread(new Runnable() {
@Override
public void run() {
for (int i1 = 0; i1 < 100; i1++) {
str.append("a"); // 每个线程执行100添加字符串a的方法。
}
}
}).start();
}
System.out.println(str.length());
}
}
结果:在不开启sleep线程睡眠的情况下字符串长度显示只有 286,什么意思?
按照理论上应该长度为1000,那是哪里出了问题呢
我们可以看看 StringBuilder类的源码,
public final class StringBuilder
extends AbstractStringBuilder
implements java.io.Serializable, CharSequence
{
。。。
}
可以看到,StringBuilder类继承自AbstractStringBuilder类,然后AbstractStringBuilder类中也有一个append()添加方法:
public AbstractStringBuilder append(StringBuffer sb) {
if (sb == null)
return appendNull();
int len = sb.length();
ensureCapacityInternal(count + len);
sb.getChars(0, len, value, count);
count += len;
return this;
}
可以看到,与返回字符串长度有关的变量分别是,len 与 count,那到底是具体哪一个呢,
我们又知道,在方法中定义的变量,其生存周期一定是与方法一样,方法被调用结束,方法中定义的变量也就结束了。所以不存在其他线程能作用到此变量的情况,也就是说最有可能出错在 count 这个变量上,因为它属于成员变量。被所有线程共享,那么极有可能被两个线程同时作用导致出现线程安全的问题,解决方法也很简单,在方法上加,锁关键字 synchronized ,这也就是为什么要 StringBuffer这个类的原因,能够保证线程安全。
附:下面两个有什么区别
String str = "a";
String str = new String("a");
在这之前,要搞清楚一个概念,在class文件中有一部分 来存储编译期间生成的 字面常量以及符号引用,这部分叫做class文件常量池,在运行期间对应着方法区的运行时常量池。
当然并非Class文件常量池中的内容才能进入运行时常量池,在运行期间也可将新的常量放入运行时常量池中,比如String的intern方法。
符号引用:即,我们写类之前用import 调用的其他API java.util.Date。
言归正传,所以str = “a” 属于字面常量,在编译期间就已经生成了字面常量"a"和String类的符号引用,然后"a"被保存在运行时常量池中,之后,str变量在JVM的帮助下去寻找到了"a"并执行其地址
但是 str1 = new String(“a”) 是先在堆中new出对象,然后在运行构造方法时会先在常量池中检查是否有这个值 “a” 如果有则将值 “a” 从常量池中复制一份到new出来的空间里进行存放,如果没有则是在常量池中先创建然后再复制。
所以、实际上所有的类实例化后,即在堆中new出一片空间后,是直接存放类中定义的变量的值的。而不是所谓的地址。
所以、str 存储的是字符串在常量池中的地址、而 str1 存储的是在堆中的地址,这也就是为什么两者用 = = 比较时为 false 的原因。
这里又衍生出了两个创建字符串的时机,一个是作为字面常量,在编译期间就产生了,一个是作为类进行加载然后再产生。
只要是在两个不同时机产生的字符串,即使值相同,但是用= =号判断时就是为false
注:被final修饰的也会在编译期产生。