Java 提供了三个类, 用于处理字符串, 分别是 String,StringBuffer 和 StringBuilder. 其中 StringBuilder 是 jdk1.5 才引入的.
这三个类有什么区别呢? 他们的使用场景分别是什么呢?
本文的代码是在 jdk12 上运行的, jdk12 和 jdk5,jdk8 有很大的区别, 特别是 String,StringBuffer 和 StringBuilder 的实现.
jdk5 和 jdk8 中 String 类的 value 类型是 char[], 到了 jdk12,value 类型变为 byte[].
jdk5,JDK6 中的常量池是放在永久代的, 永久代和 Java 堆是两个完全分开的区域.
到了 jdk7 及以后的版本,
我们先来看看这三个类的源码.
String 类部分源码:
- public final class String
- implements java.io.Serializable, Comparable<String>, CharSequence,
- Constable, ConstantDesc {
- @Stable
- private final byte[] value;
- public String(String original) {
- this.value = original.value;
- this.coder = original.coder;
- this.hash = original.hash;
- }
- public native String intern();
String 类由 final 修饰符修饰, 所以 String 类是不可变的, 对象一旦创建, 不能改变.
String 类中有个 value 的字节数组成员 变量, 这个变量用于存储字符串的内容, 也是用 final 修饰, 一旦初始化, 不可改变.
java 提供了两种主要方式创建字符串:
- // 方式 1
- String str = "123";
- // 方式 2
- String str = new String("123");
java 虚拟机规范中定义字符串都是存储在字符串常量池中, 不管是用方式 1 还是方式 2 创建字符串, 都会从去字符串常量池中查找, 如果已经存在, 直接返回, 否则创建后返回.
java 编译器在编译 java 类时, 遇到 "abc","hello" 这样的字符串常量, 会将这些常量放入类的常量区, 类在加载时, 会将字符串常量加入到字符串常量池中.
含有表达式的字符串常量, 不会在编译时放入常量区, 例如, String str = "abc" + a
常量池的最大作用是共享使用, 提高程序执行效率.
看看下面几个案例.
案例 1:
- String str1 = "123";
- String str2 = "123";
- System.out.println(str1 == str2);
上面代码运行的结果为 true.
运行第 1 行代码时, 现在常量池中创建字符串 123 对象, 然后赋值给 str1 变量.
运行第 2 行代码时, 发现常量池已经存在 123 对象, 则直接将 123 对象的地址返回给变量 str2.
str1 和 str2 变量指向的地址一样, 他们是同一个对象, 因此运行的结果为 true.
从图中可以看出, str1 使用 "" 引号 (也是平时所说的字面量) 创建字符串, 在编译期的时候就对常量池进行判断是否存在该字符串, 如果存在则不创建直接返回对象的引用; 如果不存在, 则先在常量池中创建该字符串实例再返回实例的引用给 str1.
案例 2:
- String str1 = new String("123");
- String str2 = new String("123");
- String str3 = new String(str2);
- System.out.println((str1==str2));
- System.out.println((str1==str3));
- System.out.println((str3==str2));
上面代码运行的结果是
false false false
从上图可以看出, 执行第 1 行代码时, 创建了两个对象, 一个存放在字符串常量池中, 一个存在与堆中, 还有一个对象引用 str1 存放在栈中.
执行第 2 行代码时, 字符串常量池中已经存在 "123" 对象, 所以只在堆中创建了一个字符串对象, 并且这个对象的地址指向常量池中 "123" 对象的地址, 同时在栈中创建一个对象引用 str2, 引用地址指向堆中创建的对象.
执行第 3 行代码时, 在堆中创建一个字符串对象, 这个对象的内存地址指向变量 str2 所执向的内存地址.
通过 new 方式创建的字符串对象, 都会在堆中开辟一个新内存空间, 用于存储常量池中的字符串对象.
对于对象而言,== 操作是用于比较两个独享的内存地址是否一致, 所以上面的代码执行的结果都是 false.
案例 3:
- // 这行代码编译后的效果等同于 String str1 = "abcd";
- String str1 = "ab" + "cd";
- String str2 = "abcd";
- System.out.println((str1 == str2));
上面代码执行的结果: true.
使用包含常量的字符串连接创建的也是常量, 编译期就能确定了, 类加载的时候直接进入字符串常量池, 当然同样需要判断字符串常量池中是否已经存在该字符串.
案例 4:
- String str2 = "ab"; //1 个对象
- String str3 = "cd"; //1 个对象
- String str4 = str2 + str3 + "1";
- String str5 = "abcd1";
- System.out.println((str4==str5));
上面代码执行的结果: false.
当使用 "+" 连接字符串中含有变量时, 由于变量的值是在运行时才能确定.
如果使用的 jdk8 以前版本的虚拟机, 在拼接字符串时, 会在 jvm 堆中生成 StringBuilder 对象, 调用 append 方法拼接字符串, 最后调用 StringBuilder 的 toString 方法在 jvm 堆中生成最终的字符串对象.
通过查看字节码就可以知道 jdk8 之前版本的 "+" 拼接字符串时通过 StringBuilder 实现的. 通过查看字节码就可以知道, 如下图所示:
而如果使用的是 jdk9 以后版本的虚拟机, 则是调用虚拟机自带的 InvokeDynamic 拼接字符串, 并且保存在堆中. 字节码如下所示:
str4 的对象在字符串常量池中, str5 的对象在堆中, 所以他们的不是同一个对象, 所以返回的结果是 false.
案例 5:
String s5 = new String("2") + new String("3");
和案例 4 一样, 因为 new String("2")创建字符串, 也是在运行时才能确定对象内存地址, 和案例 4 一样.
案例 6:
- final String str1 = "b";
- String str2 = "a" + str1;
- String str3 = "ab";
- System.out.println((str2 == str3));
上面代码执行的结果为 true.
str1 是常量变量, 在编译期就确定, 直接放入到字符串常量池中, 上面的代码效果等同于:
- String str2 = "a" + "b";
- String str3 = "ab";
- System.out.println((str2 == str3));
调用 String 类的 intern()方法, 会将堆中的字符串实例放入到字符串常量池中.
案例 7:
- String str2 = "ab";
- String str3 = "cd";
- String str4 = str2 + str3 + "1";
- str4.intern();
- String str5 = "abcd1";
- System.out.println((str4==str5));
上面代码执行的结果: true. 调用了 str4.intern()方法后, 将 str4 放入到字符串常量池中, 和 str5 是同一个实例.
StringBuffer 部分源码:
- public final class StringBuffer
- extends AbstractStringBuilder
- implements java.io.Serializable, Comparable<StringBuffer>, CharSequence
- {
StringBuilder 部分源码:
- public final class StringBuilder
- extends AbstractStringBuilder
- implements java.io.Serializable, Comparable<StringBuilder>, CharSequence
- {
可见 StringBuffer 和 StringBuilder 都继承了 AbstractStringBuilder 类.
AbstractStringBuilder 类源码:
- abstract class AbstractStringBuilder implements Appendable, CharSequence {
- /**
- * The value is used for character storage.
- */
- byte[] value;
AbstractStringBuilder 也有一个字节数组的成员变量 value, 这个变量用于存储字符串的值, 这个变量不是用 final 修饰, 所以是可以改变的, 这个是和 String 的最大区别.
在调用 append 方法的时候, 会动态增加字节数组变量 value 的大小.
StringBuffer 和 StringBuilder 功能是一样的, 都是为了提高 java 中字符串连接的效率, 因为直接使用 + 进行字符串连接的话, jvm 会创建多个 String 对象, 因此造成一定的开销. AbstractStringBuilder 中采用一个 byte 数组来保存需要 append 的字符串, byte 数组有一个初始大小, 当 append 的字符串长度超过当前 char 数组容量时, 则对 byte 数组进行动态扩展, 也即重新申请一段更大的内存空间, 然后将当前 bute 数组拷贝到新的位置, 因为重新分配内存并拷贝的开销比较大, 所以每次重新申请内存空间都是采用申请大于当前需要的内存空间的方式, 这里是 2 倍.
StringBuffer 和 StringBuilder 最大的区别是 StringBuffer 是线程安全, 而 StringBuilder 是非线程安全的, 从它们两个类的源码就可以知道, StringBuffer 类的方法前面都是 synchronized 修饰符.
String 一旦赋值或实例化后就不可更改, 如果赋予新值将会重新开辟内存地址进行存储.
而 StringBuffer 和 StringBuilder 类使用 append 和 insert 等方法改变字符串值时只是在原有对象存储的内存地址上进行连续操作, 减少了资源的开销.
总结:
1, 频繁使用 "+" 操作拼接字符时, 换成 StringBuffer 和 StringBuilder 类的 append 方法实现.
2, 多线程环境下进行大量的拼接字符串操作使用 StringBuffer,StringBuffer 是线程安全的;
3, 单线程环境下进行大量的拼接字符串操作使用 StringBuilder,StringBuilder 是线程不安全的.
来源: https://www.cnblogs.com/airnew/p/11628017.html