StringBuffer和StringBuilder
String类是不可变的类型,也就是尝试对字符串进行修改的时候,不会对原字符串进行修改,而是在原字符串的基础上进行修改,然后生成一个新的字符串存放在常量池中。
对String对象的任何改变都不影响到原对象,相关的任何change操作都会生成新的对象
- String类是final类,也即意味着String类不能被继承,并且它的成员方法都默认为final方法。在Java中,被final修饰的类是不允许被继承的,并且该类中的成员方法都默认为final方法。在早期的JVM实现版本中,被final修饰的方法会被转为内嵌调用以提升执行效率。而从Java SE5/6开始,就渐渐摈弃这种方式了。因此在现在的Java SE版本中,不需要考虑用final去提升方法调用效率。只有在确定不想让该方法被覆盖时,才将方法设置为final。
- 上面列举出了String类中所有的成员属性,从上面可以看出String类其实是通过char数组来保存字符串的。
String str="hello world"和String str=new String(“hello world”)的区别:
public class Main {
public static void main(String[] args) {
String str1 = "hello world";
String str2 = new String("hello world");
String str3 = "hello world";
String str4 = new String("hello world");
System.out.println(str1==str2);
System.out.println(str1==str3);
System.out.println(str2==str4);
}
}
执行结果如下所示:
false
true
false
接下来从JVM角度简单解释一下原因:
在class文件中有一部分 来存储编译期间生成的 字面常量以及符号引用,这部分叫做class文件常量池,在运行期间对应着方法区的运行时常量池。
String str1 = “hello world”;和String str3 = “hello world”; 都在编译期间生成了 字面常量和符号引用,运行期间字面常量"hello world"被存储在运行时常量池(当然只保存了一份)。通过这种方式来将String对象跟引用绑定的话,JVM执行引擎会先在运行时常量池查找是否存在相同的字面常量,如果存在,则直接将引用指向已经存在的字面常量;否则在运行时常量池开辟一个空间来存储该字面常量,并将引用指向该字面常量。
众所周知,通过new关键字来生成对象是在堆区进行的,而在堆区进行对象生成的过程是不会去检测该对象是否已经存在的。因此通过new来创建对象,创建出的一定是不同的对象,即使字符串的内容是相同的。
那么为了节省内存,提高效率,如果可以在原来字符串的基础上直接进行修改操作,这样会更加方便,StringBuffer和StringBuilder就是可以修改的字符序列。
定义与区别
首先对来看String,StringBuffer,StringBuilder的继承关系图,如下所示。
在使用 StringBuffer 类时,每次都会对 StringBuffer 对象本身进行操作,而不是生成新的对象,所以如果需要对字符串进行修改推荐使用 StringBuffer。
StringBuilder 类在 Java 5 中被提出,它和 StringBuffer 之间的最大不同在于 StringBuilder 的方法不是线程安全的(不能同步访问)。
由于 StringBuilder 相较于 StringBuffer 有速度优势,所以多数情况下建议使用 StringBuilder 类。
在初始化的时候,默认是16个字节的空间,但是可以变长,具体参考源码,这里主要对于其构造函数进行简单学习。
/**
* Constructs a string buffer with no characters in it and an
* initial capacity of 16 characters.
*/
public StringBuffer() {
super(16);
}
/**
* Constructs a string buffer with no characters in it and
* the specified initial capacity.
*
* @param capacity the initial capacity.
* @exception NegativeArraySizeException if the {@code capacity}
* argument is less than {@code 0}.
*/
public StringBuffer(int capacity) {
super(capacity);
}
“+=”优化
String中“+=”在编译时会被优化,首先查看以下代码
package varString;
public class demo02 {
public static void main(String[] args) {
// 开始时间
long start = System.currentTimeMillis();
String string = "";
// StringBuffer string = new StringBuffer();
// StringBuilder string = new StringBuilder();
for (int i = 0; i < 99999; i++) {
string += i;
// string.append(i);
}
System.out.println(string);
long end = System.currentTimeMillis();
System.out.println("用时:" + (end - start));
}
}
将编译生成的class文件进行反编译,得到以下代码:
// Decompiled by Jad v1.5.8e2. Copyright 2001 Pavel Kouznetsov.
// Jad home page: http://kpdus.tripod.com/jad.html
// Decompiler options: packimports(3) fieldsfirst ansi space
// Source File Name: demo02.java
package varString;
import java.io.PrintStream;
public class demo02
{
public demo02()
{
}
public static void main(String args[])
{
long start = System.currentTimeMillis();
String string = "";
for (int i = 0; i < 0x1869f; i++)
string = (new StringBuilder()).append(string).append(i).toString();
System.out.println(string);
long end = System.currentTimeMillis();
System.out.println((new StringBuilder()).append("用时:").append(end - start).toString());
}
}
从反编译得到的代码可知,“+=”操作被优化为了StringBuilder中的append,
string += i;
string = (new StringBuilder()).append(string).append(i).toString();
上述过程中产生了10000个对象,但是如果采用下列代码:
package varString;
public class demo02 {
public static void main(String[] args) {
// 开始时间
long start = System.currentTimeMillis();
// String string = "";
// StringBuffer string = new StringBuffer();
StringBuilder string = new StringBuilder();
for (int i = 0; i < 99999; i++) {
// string += i;
string.append(i);
}
System.out.println(string);
long end = System.currentTimeMillis();
System.out.println("用时:" + (end - start));
}
}
上述代码中只产生了一个StringBuilder对象,相比于前面的String方法,会有很大的优化空间。
常用函数
以下是 StringBuffer 类支持的主要方法:
方法 | 功能 |
---|---|
public StringBuffer append(String s) | 将指定的字符串追加到此字符序列。 |
public StringBuffer reverse() | 将此字符序列用其反转形式取代 |
public delete(int start, int end) | 移除此序列的子字符串中的字符。 |
public insert(int offset, int i) | 将 int 参数的字符串表示形式插入此序列中。 |
insert(int offset, String str) | 将 str 参数的字符串插入此序列中。 |
replace(int start, int end, String str) | 使用给定 String 中的字符替换此序列的子字符串中的字符。 |
以下列表列出了 StringBuffer 类的其他常用方法:
方法 | 功能 |
---|---|
int capacity() | 返回当前容量。 |
char charAt(int index) | 返回此序列中指定索引处的 char 值。 |
void ensureCapacity(int minimumCapacity) | 确保容量至少等于指定的最小值。 |
void getChars(int srcBegin, int srcEnd, char[] dst, int dstBegin) | 将字符从此序列复制到目标字符数组 dst 。 |
int indexOf(String str) | 返回第一次出现的指定子字符串在该字符串中的索引。 |
int indexOf(String str, int fromIndex) | 从指定的索引处开始,返回第一次出现的指定子字符串在该字符串中的索引。 |
int lastIndexOf(String str) | 返回最右边出现的指定子字符串在此字符串中的索引。 |
int lastIndexOf(String str, int fromIndex) | 返回 String 对象中子字符串最后出现的位置。 |
int length() | 返回长度(字符数)。 |
void setCharAt(int index, char ch) | 将给定索引处的字符设置为 ch 。 |
void setLength(int newLength) | 设置字符序列的长度。 |
CharSequence subSequence(int start, int end) | 返回一个新的字符序列,该字符序列是此序列的子序列。 |
String substring(int start) | 返回一个新的 String ,它包含此字符序列当前所包含的字符子序列。 |
String substring(int start, int end) | 返回一个新的 String ,它包含此序列当前所包含的字符子序列。 |
String toString() | 返回此序列中数据的字符串表示形式。 |
StringBuffer类和StringBuilder类所支持的方法基本一致
接下来其中一些简单的方法进行演示。
public class RunoobTest{
public static void main(String args[]){
StringBuilder sb = new StringBuilder(10);
sb.append("Runoob..");
System.out.println(sb);
sb.append("!");
System.out.println(sb);
sb.insert(8, "Java");
System.out.println(sb);
sb.delete(5,8);
System.out.println(sb);
}
}
效率测试
package varString;
public class demo02 {
public static void main(String[] args) {
// 开始时间
long start = System.currentTimeMillis();
// String string = "";
// StringBuffer string = new StringBuffer();
StringBuilder string = new StringBuilder();
for (int i = 0; i < 99999; i++) {
// string += i;
string.append(i);
}
System.out.println(string);
long end = System.currentTimeMillis();
System.out.println("用时:" + (end - start));
}
}
通过上述代码对String +=,StringBuffer,StringBuilder的速度进行简单测试,+=会使用47s的时间,StringBuffer会使用25ms的时间,StringBuilder会使用18ms的时间。
线程安全
在线程安全上,StringBuilder是线程不安全的,而StringBuffer是线程安全的
如果一个StringBuffer对象在字符串缓冲区被多个线程使用时,StringBuffer中很多方法可以带有synchronized关键字,所以可以保证线程是安全的,但StringBuilder的方法则没有该关键字,所以不能保证线程安全,有可能会出现一些错误的操作。所以如果要进行的操作是多线程的,那么就要使用StringBuffer,但是在单线程的情况下,还是建议使用速度比较快的StringBuilder。
(一个线程访问一个对象中的synchronized(this)同步代码块时,其他试图访问该对象的线程将被阻塞)
总结一下
- String:适用于少量的字符串操作的情况
- StringBuilder:适用于单线程下在字符缓冲区进行大量操作的情况
- StringBuffer:适用多线程下在字符缓冲区进行大量操作的情况
面试题
下面这段代码的输出结果是什么?
String a = “hello2”; String b = “hello” + 2; System.out.println((a == b));
输出结果为:true。原因很简单,“hello”+2在编译期间就已经被优化成"hello2",因此在运行期间,变量a和变量b指向的是同一个对象。
反编译代码:
public static void main(String args[])
{
String a = "hello2";
String b = "hello2";
System.out.println(a == b);
}
下面这段代码的输出结果是什么?
String a = “hello2”; String b = “hello”; String c = b + 2; System.out.println((a == c));
输出结果为:false。由于有符号引用的存在,所以 String c = b + 2;不会在编译期间被优化,不会把b+2当做字面常量来处理的,因此这种方式生成的对象事实上是保存在堆上的。因此a和c指向的并不是同一个对象。
反编译得到的代码:
public static void main(String args[])
{
String a = "hello2";
String b = "hello";
String c = (new StringBuilder()).append(b).append(2).toString();
System.out.println(a == c);
}
下面这段代码的输出结果是什么?
String a = “hello2”; final String b = “hello”; String c = b + 2; System.out.println((a == c));
输出结果为:true。对于被final修饰的变量,会在class文件常量池中保存一个副本,也就是说不会通过连接而进行访问,对final变量的访问在编译期间都会直接被替代为真实的值。那么String c = b + 2;在编译期间就会被优化成:String c = “hello” + 2;
反编译得到的代码:
public static void main(String args[])
{
String a = "hello2";
String b = "hello";
String c = "hello2";
System.out.println(a == c);
}
下面这段代码输出结果为false,这里面虽然将b用final修饰了,但是由于其赋值是通过方法调用返回的,那么它的值只能在运行期间确定,因此a和c指向的不是同一个对象。
public class Main {
public static void main(String[] args) {
String a = "hello2";
final String b = getHello();
String c = b + 2;
System.out.println((a == c));
}
public static String getHello() {
return "hello";
}
}
String str = new String(“abc”)创建了多少个对象?
反编译代码如下所示:
public static void main(String args[])
{
String str = new String("abc");
}
很显然,new只调用了一次,也就是说只创建了一个对象。
而这道题目让人混淆的地方就是这里,这段代码在运行期间确实只创建了一个对象,即在堆上创建了"abc"对象。而为什么大家都在说是2个对象呢,这里面要澄清一个概念 该段代码执行过程和类的加载过程是有区别的。在类加载的过程中,确实在运行时常量池中创建了一个"abc"对象,而在代码执行过程中确实只创建了一个String对象。
因此,这个问题如果换成 String str = new String(“abc”)涉及到几个String对象?合理的解释是2个。