原来String、StringBuffer、StringBuilder底层是这么回事

前言

字符串的使用在不仅在Java代码中随处可见,而且在面试也常被问起。所以,对于咱们朝夕相处的老友,我觉得有必要好好来重新认识它们,了解其底层实现原理。

String类

  • String表示字符串,用一对""表示
  • String声明为final,表示不可以被继承
  • String实现了Serializable接口,表示String可以被实例化
  • 实现了Comparable接口,表示可以比较大小
  • 定义了final char value[],存储字符串数据
    源码如下:
    在这里插入图片描述

大家都知道,String代表不可变的字符序列。简称:不可变性,那如何来体现它的不可变性呢?借助测试代码,我们来看看:

public class StringTest {
    @Test
    public void  test(){
        String s1="xf";  
        String s2="xf"; 
        System.out.println(s1==s2);  //true
        s1="hn";
        System.out.println(s1==s2);  //false

        String s3=new String("xf");

        System.out.println(s3==s2); //false
	}
}

通过字面量声明(区别于new的方式)两个字符串s1,s2,从内存分配来看,变量值’xf’是在方法区的字符常量池中,由于字符串常量池中是不会存储相同内容的字符串的,所以’xf’只有一份,并且s1,s2同时指向相同的地址,内存分配如图:
在这里插入图片描述
所以第一次打印为true;接着s1赋值为’hn’,由于String底层是用final修饰的char型数组,所以是不可变的,只能重新赋值,同理在字符常量池中有了新的值,内存变为如下:
在这里插入图片描述
这时s1,s2的内存地址就不相同了,所以第二次打印为false。
接着通过new+构造器的方式创建了s3,此时内存分配如下:
在这里插入图片描述
此时s3指向的是value的堆地址,和s2是不相同的,所以打印false,而其中的value指向的是’xf’常量池的地址,值为常量池中’xf’的一个副本,保证了常量池中相同字符的唯一性。
注:在Java中==这个符号是比较运算符,它可以基本数据类型和引用数据类型是否相等,如果是基本数据类型,==比较的是值是否相等,如果是引用数据类型,==比较的是两个对象的内存地址是否相等,而String不是基本数据类型,所以==比较的是内存地址;当对字符串重新赋值时,需要重新指定内存区域,不能用原有的value进行赋值,通过字面量的方式如:String str=“xf”,区别于(new String(“xf”))这时的字符串值是声明在字符串常量池中。当对字符串进行连接操作时,仍然需要重新制定内存区域进行赋值;字符串常量池中是不会存储相同内容的字符串的

String的拼接特性

public class StringTest {
    @Test
    public void  test(){
        String s1="xf";
        String s2="nihao";

        String s3="xf"+"nihao";

        String s4=s1+"nihao";

        String s5="xfnihao";

        String s6=(s1+"nihao").intern();
        System.out.println(s3==s4);  //false
        System.out.println(s3==s5);  //true
        System.out.println(s5==s6);  //true
    }
}

我们知道任何数据和字符串进行加号(+)运算,最终得到是一个拼接的新的字符串。大致内存过程
1)常量池创建“xf”对象,并赋值给s1,所以s1指向了“xf”
2)常量池创建“nihao”对象,并赋值给s2,所以s2指向了“nihao”
3)s3通过“xf”+“nihao”,由于这里是两个常量走+的拼接方法,得到的结果为“xfnihao”并保存在常量池中,所以s3最终指向“xfnihao”
4)s4通过s1+“nihao”进行+拼接,底层实际创建一个StringBuilder对象,接着将字符常量“nihao”加载后并调用append方法,最后通过调用toString输出新字符串,此时的s4是在堆上,而不是在常量池中的,StringBuilder的toString源码如下:
在这里插入图片描述
5)s5赋值为“xfnihao”,同理也指向了前面的s3(常量池中相同字符的唯一性)所以s5s3为true
6)而s6通过调用String的intern方法(本地方法),最终结果就在常量池中,即和s5一同指向了“xfnihao”,所以s5
s6为true
下面展示对应的反编译后的字节码:

public class cn.xf.nihao.string.StringTest
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #13.#37        // java/lang/Object."<init>":()V
   #2 = String             #38            // xf
   #3 = String             #39            // nihao
   #4 = String             #40            // xfnihao
   #5 = Class              #41            // java/lang/StringBuilder
   #6 = Methodref          #5.#37         // java/lang/StringBuilder."<init>":()V
   #7 = Methodref          #5.#42         // java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/Str
ingBuilder;
   #8 = Methodref          #5.#43         // java/lang/StringBuilder.toString:()Ljava/lang/String;
   #9 = Methodref          #44.#45        // java/lang/String.intern:()Ljava/lang/String;
  #10 = Fieldref           #46.#47        // java/lang/System.out:Ljava/io/PrintStream;
  #11 = Methodref          #48.#49        // java/io/PrintStream.println:(Z)V
  #12 = Class              #50            // cn/xf/nihao/string/StringTest
  #13 = Class              #51            // java/lang/Object
  #14 = Utf8               <init>
  #15 = Utf8               ()V
  #16 = Utf8               Code
  #17 = Utf8               LineNumberTable
  #18 = Utf8               LocalVariableTable
  #19 = Utf8               this
  #20 = Utf8               Lcn/xf/nihao/string/StringTest;
  #21 = Utf8               test
  #22 = Utf8               s1
  #23 = Utf8               Ljava/lang/String;
  #24 = Utf8               s2
  #25 = Utf8               s3
  #26 = Utf8               s4
  #27 = Utf8               s5
  #28 = Utf8               s6
  #29 = Utf8               StackMapTable
  #30 = Class              #50            // cn/xf/nihao/string/StringTest
  #31 = Class              #52            // java/lang/String
  #32 = Class              #53            // java/io/PrintStream
  #33 = Utf8               RuntimeVisibleAnnotations
  #34 = Utf8               Lorg/junit/jupiter/api/Test;
  #35 = Utf8               SourceFile
  #36 = Utf8               StringTest.java
  #37 = NameAndType        #14:#15        // "<init>":()V
  #38 = Utf8               xf
  #39 = Utf8               nihao
  #40 = Utf8               xfnihao
  #41 = Utf8               java/lang/StringBuilder
  #42 = NameAndType        #54:#55        // append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
  #43 = NameAndType        #56:#57        // toString:()Ljava/lang/String;
  #44 = Class              #52            // java/lang/String
  #45 = NameAndType        #58:#57        // intern:()Ljava/lang/String;
  #46 = Class              #59            // java/lang/System
  #47 = NameAndType        #60:#61        // out:Ljava/io/PrintStream;
  #48 = Class              #53            // java/io/PrintStream
  #49 = NameAndType        #62:#63        // println:(Z)V
  #50 = Utf8               cn/xf/nihao/string/StringTest
  #51 = Utf8               java/lang/Object
  #52 = Utf8               java/lang/String
  #53 = Utf8               java/io/PrintStream
  #54 = Utf8               append
  #55 = Utf8               (Ljava/lang/String;)Ljava/lang/StringBuilder;
  #56 = Utf8               toString
  #57 = Utf8               ()Ljava/lang/String;
  #58 = Utf8               intern
  #59 = Utf8               java/lang/System
  #60 = Utf8               out
  #61 = Utf8               Ljava/io/PrintStream;
  #62 = Utf8               println
  #63 = Utf8               (Z)V
{
  public cn.xf.nihao.string.StringTest();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 27: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   Lcn/xf/nihao/string/StringTest;

  public void test();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=3, locals=7, args_size=1
         0: ldc           #2                  // String xf
         2: astore_1
         3: ldc           #3                  // String nihao
         5: astore_2
         6: ldc           #4                  // String xfnihao
         8: astore_3
         9: new           #5                  // class java/lang/StringBuilder
        12: dup
        13: invokespecial #6                  // Method java/lang/StringBuilder."<init>":()V
        16: aload_1
        17: invokevirtual #7                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Lja
va/lang/StringBuilder;
        20: ldc           #3                  // String nihao
        22: invokevirtual #7                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Lja
va/lang/StringBuilder;
        25: invokevirtual #8                  // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
        28: astore        4
        30: ldc           #4                  // String xfnihao
        32: astore        5
        34: new           #5                  // class java/lang/StringBuilder
        37: dup
        38: invokespecial #6                  // Method java/lang/StringBuilder."<init>":()V
        41: aload_1
        42: invokevirtual #7                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Lja
va/lang/StringBuilder;
        45: ldc           #3                  // String nihao
        47: invokevirtual #7                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Lja
va/lang/StringBuilder;
        50: invokevirtual #8                  // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
        53: invokevirtual #9                  // Method java/lang/String.intern:()Ljava/lang/String;
        56: astore        6
        58: getstatic     #10                 // Field java/lang/System.out:Ljava/io/PrintStream;
        61: aload_3
        62: aload         4
        64: if_acmpne     71
        67: iconst_1
        68: goto          72
        71: iconst_0
        72: invokevirtual #11                 // Method java/io/PrintStream.println:(Z)V
        75: getstatic     #10                 // Field java/lang/System.out:Ljava/io/PrintStream;
        78: aload_3
        79: aload         5
        81: if_acmpne     88
        84: iconst_1
        85: goto          89
        88: iconst_0
        89: invokevirtual #11                 // Method java/io/PrintStream.println:(Z)V
        92: getstatic     #10                 // Field java/lang/System.out:Ljava/io/PrintStream;
        95: aload         5
        97: aload         6
        99: if_acmpne     106
       102: iconst_1
       103: goto          107
       106: iconst_0
       107: invokevirtual #11                 // Method java/io/PrintStream.println:(Z)V
       110: return
      LineNumberTable:
        line 31: 0
        line 32: 3
        line 34: 6
        line 36: 9
        line 38: 30
        line 40: 34
        line 41: 58
        line 42: 75
        line 43: 92
        line 112: 110
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0     111     0  this   Lcn/xf/nihao/string/StringTest;
            3     108     1    s1   Ljava/lang/String;
            6     105     2    s2   Ljava/lang/String;
            9     102     3    s3   Ljava/lang/String;
           30      81     4    s4   Ljava/lang/String;
           34      77     5    s5   Ljava/lang/String;
           58      53     6    s6   Ljava/lang/String;
      StackMapTable: number_of_entries = 6
        frame_type = 255 /* full_frame */
          offset_delta = 71
          locals = [ class cn/xf/nihao/string/StringTest, class java/lang/String, class java/lang/String, clas
s java/lang/String, class java/lang/String, class java/lang/String, class java/lang/String ]
          stack = [ class java/io/PrintStream ]
        frame_type = 255 /* full_frame */
          offset_delta = 0
          locals = [ class cn/xf/nihao/string/StringTest, class java/lang/String, class java/lang/String, clas
s java/lang/String, class java/lang/String, class java/lang/String, class java/lang/String ]
          stack = [ class java/io/PrintStream, int ]
        frame_type = 79 /* same_locals_1_stack_item */
          stack = [ class java/io/PrintStream ]
        frame_type = 255 /* full_frame */
          offset_delta = 0
          locals = [ class cn/xf/nihao/string/StringTest, class java/lang/String, class java/lang/String, clas
s java/lang/String, class java/lang/String, class java/lang/String, class java/lang/String ]
          stack = [ class java/io/PrintStream, int ]
        frame_type = 80 /* same_locals_1_stack_item */
          stack = [ class java/io/PrintStream ]
        frame_type = 255 /* full_frame */
          offset_delta = 0
          locals = [ class cn/xf/nihao/string/StringTest, class java/lang/String, class java/lang/String, clas
s java/lang/String, class java/lang/String, class java/lang/String, class java/lang/String ]
          stack = [ class java/io/PrintStream, int ]
    RuntimeVisibleAnnotations:
      0: #34()
}

StringBuffer

StringBuffer:与String一样,都是jdk1.0就提出了的,但是StringBuffer是可变的字符序列,底层通过继承AbstractStringBuilder的 char[] value进行字符数据存储,通过synchronized方法保证线程安全,缺点是效率低。

  • 部分源码
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述

通过看了部分源码后,主要来介绍StringBuffer底层的扩容机制

  • 源码测试
/**
 * StringBuffer
 */
public class StringBufferTest {
    public static void main(String[] args) {

        StringBuffer sb=new StringBuffer();

        System.out.println(sb.length());  //0
        System.out.println(sb.capacity()); //16

        sb.append("xf");
        System.out.println(sb.length()); //2
        System.out.println(sb.capacity()); //16


        StringBuffer sb2=new StringBuffer("hn");
        System.out.println(sb2.length()); //2
        System.out.println(sb2.capacity()); //18
        sb2.append(3.1415926);

        System.out.println(sb2);  //hn3.1415926
    }
}

为什么sb.capacity()为16,sb.length()是0呢?
当创建一个StringBuffer无参对象时,底层会默认分配char[]value长度为16。
在这里插入图片描述
调用父类方法进行数组初始化
在这里插入图片描述
继续来看下面方法
在这里插入图片描述
实际返回的是count,而count是当前对象表现出来的字符序列长度,接着调用sb.append(“xf”)后,此时的length就为2了。由于此时append后还没有超过char数组容量,所以不必进行扩容,容量还是16
sb2.length()为2我能理解,为什么sb2.capacity()变成18了呢?
此时sb2是带参数的创建对象,继续根据源码来看:
在这里插入图片描述
也就是当带参数创建StringBuffer对象时,会将底层char[]value数组的容量初始化为传入的字符长度+16,所以sb2.sb2.capacity()为18;那不断的append,万一超过了初始容量,怎么办?不用担心,StringBuffer自带扩容机制
在这里插入图片描述
当超过初始容量后,StringBuffer会自动扩容,具体方法是在原有的容量增加2倍再加2(value.length * 2 + 2),再把原来数组中的值copy到新数组中。

StringBuilder

StringBuilder在jdk1.5提出的,用法和StringBuffer基本一致,主要是解决StringBuffer的效率问题,去掉了同步机制,因此StringBuilder是线程不安全的,不过效率优于StringBuffer。具体的用法就不再测试了。

String,StringBuffer,StringBuilder效率测试

   @Test
    public void test3(){

        Long startTime=0L;
        Long  endTime=0L;
        String text="";
        StringBuffer sb=new StringBuffer();
        StringBuilder sd=new StringBuilder();

        System.out.println("-----String开始-----");
        startTime=System.currentTimeMillis();
        for (int i = 0; i <10000 ; i++) {

            text+=i;
        }
        endTime=System.currentTimeMillis();
        System.out.println("-----String耗时:"+(endTime-startTime)+"ms");

        System.out.println("-----StringBuffer开始-----");
        startTime=System.currentTimeMillis();
        for (int i = 0; i <10000 ; i++) {
            sb.append(i);
        }
        endTime=System.currentTimeMillis();
        System.out.println("-----StringBuffer耗时:"+(endTime-startTime)+"ms");


        System.out.println("-----StringBuilder开始-----");
        startTime=System.currentTimeMillis();
        for (int i = 0; i <10000 ; i++) {

            sd.append(i);
        }
        endTime=System.currentTimeMillis();
        System.out.println("-----StringBuilder耗时:"+(endTime-startTime)+"ms");
    }

输出截图:
在这里插入图片描述
通过测试,在完成相同的字符串拼接操作,我们可以看到效率从高到低是StringBuilder>StringBuffer>String

总结

String是不可变的字符序列,通过final char[]value存储数据,StringBuilder和StringBuffer都是可变的字符序列,通过char[]value存储数据,两者在用法上基本一致,主要区别是StringBuffer是线程安全的,效率低;StringBuilder是线程不安全的,效率高。两者都用相同的扩容机制,在实际开发场景中,若是多次对字符串进行拼接操作,可以选择StringBuffer或StringBuilder,若不存在线程安全问题,推荐使用StringBuilder,反之使用StringBuffer。

  • 1
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值