前言
字符串的使用在不仅在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”,所以s5s6为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。