String字符串浅析

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 源码我们可以知道:

  1. String 类是被 final 修饰,即 String 字符串一旦被创建就无法被修改了。
  2. 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修饰的也会在编译期产生。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值