废话不说,直接上代码
package java_05;
/**
* StringBuffer,StringBuilder讲解
*/
public class StringBufferTest {
/**
* @1
* String :不可变的字符序列
* StringBuffer :可变的字符序列 线程安全,效率低,可变字符序列
* StringBuilder :可变的字符序列 线程不安全,效率高
*
* @2
* StringBuilder和StringBuffer的内部实现跟String类一样,都是通过一个char数组存储字符串的,
* 不同的是String类里面的char数组是final修饰的,是不可变的,
* 而StringBuilder和StringBuffer的char数组是可变的。
*
* @3
* 对比String,StringBuffer StringBuilder效率问题
* StringBuilder》StringBuffer》String 其中String要慢得多
*/
public static void main(String[] args) {
StringBufferTest stringBufferTest = new StringBufferTest();
stringBufferTest.test02();
}
public void test01(){
/**
* 如果添加的数据底层数组(默认数组长度为16)装不下,那就需要扩容底层数组。
* 默认情况下,扩容为原来容量的两倍+2,同时将原数组的元素都复制到新的数组中。
*/
StringBuffer sb1 = new StringBuffer();
System.out.println(sb1.length());
sb1.append("jkhg计划v");
System.out.println(sb1);
}
public void test02(){
System.out.println(System.currentTimeMillis());//时间戳
}
}
关于三者的常用方法看API
在说一下为什么String的效率最慢:
先看一段代码
public class Main {
public static void main(String[] args) {
String string = "";
for(int i=0;i<10000;i++){
string += "hello";
}
}
}
个人理解: 这段代码中, String是被final修饰的,即常说的String是不可变的。因此执行string += "hello"时,首先会在常量池中找到String指向的常量,在做相加操作。之后会将相加的结果赋给一个新的String类型。所以每做一次相加便会创建一个String对象。所以String执行效率慢,且浪费内存资源。当然在真正的执行过程中,jvm会对上面的代码进行优化,优化成如下过程
StringBuilder str = new StringBuilder(string);
str.append("hello");
str.toString();
其结果也是会创建10000个StringBuilder对象
反观StringBuilder与StringBuffer
public class Main {
public static void main(String[] args) {
StringBuilder stringBuilder = new StringBuilder();
for(int i=0;i<10000;i++){
stringBuilder.append("hello");
}
}
}
执行过程中只进行一次new操作,在原有对象基础上进行append()操作。因此效率高且节约资源。StringBuffer道理一样,只不过StringBuffer是线程安全的。
接下来说一下为什么Stringbuilder是线程不安全的
首先通过一段代码去看一下多线程操作StringBuilder对象会出现什么问题
public class StringBuilderDemo {
public static void main(String[] args) throws InterruptedException {
StringBuilder stringBuilder = new StringBuilder();
for (int i = 0; i < 10; i++){
new Thread(new Runnable() {
@Override
public void run() {
for (int j = 0; j < 1000; j++){
stringBuilder.append("a");
}
}
}).start();
}
Thread.sleep(100);
System.out.println(stringBuilder.length());
}
}
我们能看到这段代码创建了10个线程,每个线程循环1000次往StringBuilder对象里面append字符。正常情况下代码应该输出10000,但是实际运行会输出什么呢?

我们看到输出了“9326”,小于预期的10000,并且还抛出了一个ArrayIndexOutOfBoundsException异常(异常不是必现)。
1、为什么输出值跟预期值不一样
我们先看一下StringBuilder的两个成员变量(这两个成员变量实际上是定义在AbstractStringBuilder里面的,StringBuilder和StringBuffer都继承了AbstractStringBuilder)
//存储字符串的具体内容
char[] value;
//已经使用的字符数组的数量
int count;
再看StringBuilder的append()方法:
@Override
public StringBuilder append(String str) {
super.append(str);
return this;
}
StringBuilder的append()方法调用的父类AbstractStringBuilder的append()方法
public AbstractStringBuilder append(String str) {
if (str == null)
return appendNull();
int len = str.length();
ensureCapacityInternal(count + len);
str.getChars(0, len, value, count);
count += len;
return this;
}
我们先不管代码的第五行和第六行干了什么,直接看第七行,count += len不是一个原子操作。假设这个时候count值为10,len值为1,两个线程同时执行到了第七行,拿到的count值都是10,执行完加法运算后将结果赋值给count,所以两个线程执行完后count值为11,而不是12。这就是为什么测试代码输出的值要比10000小的原因。
2、为什么会抛出ArrayIndexOutOfBoundsException异常。
我们看回AbstractStringBuilder的append()方法源码的第五行,ensureCapacityInternal()方法是检查StringBuilder对象的原char数组的容量能不能盛下新的字符串,如果盛不下就调用expandCapacity()方法对char数组进行扩容。
private void ensureCapacityInternal(int minimumCapacity) {
// overflow-conscious code
if (minimumCapacity - value.length > 0)
expandCapacity(minimumCapacity);
}
扩容的逻辑就是new一个新的char数组,新的char数组的容量是原来char数组的两倍再加2,再通过System.arryCopy()函数将原数组的内容复制到新数组,最后将指针指向新的char数组。
void expandCapacity(int minimumCapacity) {
//计算新的容量
int newCapacity = value.length * 2 + 2;
//中间省略了一些检查逻辑
...
value = Arrays.copyOf(value, newCapacity);
}
Arrys.copyOf()方法
public static char[] copyOf(char[] original, int newLength) {
char[] copy = new char[newLength];
//拷贝数组
System.arraycopy(original, 0, copy, 0,
Math.min(original.length, newLength));
return copy;
}
AbstractStringBuilder的append()方法源码的第六行,是将String对象里面char数组里面的内容拷贝到StringBuilder对象的char数组里面,代码如下:
str.getChars(0, len, value, count);
getChars()方法
public void getChars(int srcBegin, int srcEnd, char dst[], int dstBegin) {
//中间省略了一些检查
...
System.arraycopy(value, srcBegin, dst, dstBegin, srcEnd - srcBegin);
}

假设现在有两个线程同时执行了StringBuilder的append()方法,两个线程都执行完了第五行的ensureCapacityInternal()方法,此刻count=5。

这个时候线程1的cpu时间片用完了,线程2继续执行。线程2执行完整个append()方法后count变成6了

线程1继续执行第六行的str.getChars()方法的时候拿到的count值就是6了,执行char数组拷贝的时候因为数组索引越界就会抛出ArrayIndexOutOfBoundsException异常。
至此,StringBuilder为什么不安全已经分析完了。如果我们将测试代码的StringBuilder对象换成StringBuffer对象会输出什么呢?
String常见的面试题
1:下面这段代码的输出结果是什么?
String a = “hello2”; String b = “hello” + 2; System.out.println((a == b));
答案:输出true。原因:“hello” + 2在编译期间被优化成"hello2",因此在运行期间变量a和b指向同一个地址。
2.下面这段代码的输出结果是什么?
String a = “hello2”; String b = “hello”; String c = b + 2; System.out.println((a == c));
答案:false,在有符号的引用中,String c = b + 2;在编译期间不会被优化。因此这种方式生成的对象实际上是保存在堆内存中。所以两个变量指向的是不同的地址。输出为false
面试题1和2也说明,在两个String变量相加的时候,若可以被编译器优化,则不走StringBuilder的append方法。否则的话,底层代码会先走StringBuilder的apend方法进行字符串相加,然后在执行toString方法将相加后的变量赋给新的String对象
3.下面这段代码的输出结果是什么?
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;
4.下面这段代码输出结果为:
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";
}
}
答案:输出结果为false。这里面虽然将b用final修饰了,但是由于其赋值是通过方法调用返回的,那么它的值只能在运行期间确定,即编译期间还未运行方法来获取变量b的值。因此a和c指向的不是同一个对象。
5.下面这段代码的输出结果是什么?
public class Main {
public static void main(String[] args) {
String a = "hello";
String b = new String("hello");
String c = new String("hello");
String d = b.intern();
System.out.println(a==b);
System.out.println(b==c);
System.out.println(b==d);
System.out.println(a==d);
}
}
答案:false,false,false,true。这里面涉及到的是String.intern方法的使用。在String类中,intern方法是一个本地方法,在JAVA SE6之前,intern方法会在运行时常量池中查找是否存在内容相同的字符串,如果存在则返回指向该字符串的引用,如果不存在,则会将该字符串入池,并返回一个指向该字符串的引用。因此,a和d指向的是同一个对象。
6.String str = new String(“abc”)创建了多少个对象?
这个问题在很多书籍上都有说到比如《Java程序员面试宝典》,包括很多国内大公司笔试面试题都会遇到,大部分网上流传的以及一些面试书籍上都说是2个对象,这种说法是片面的。
如果有不懂得地方可以参考这篇帖子:
http://rednaxelafx.iteye.com/blog/774673/
首先必须弄清楚创建对象的含义,创建是什么时候创建的?这段代码在运行期间会创建2个对象么?毫无疑问不可能,用javap -c反编译即可得到JVM执行的字节码内容:
很显然,new只调用了一次,也就是说只创建了一个对象。
而这道题目让人混淆的地方就是这里,这段代码在运行期间确实只创建了一个对象,即在堆上创建了"abc"对象。而为什么大家都在说是2个对象呢,这里面要澄清一个概念 该段代码执行过程和类的加载过程是有区别的。在类加载的过程中,确实在运行时常量池中创建了一个"abc"对象,而在代码执行过程中确实只创建了一个String对象。
因此,这个问题如果换成 String str = new String(“abc”)涉及到几个String对象?合理的解释是2个。
个人觉得在面试的时候如果遇到这个问题,可以向面试官询问清楚”是这段代码执行过程中创建了多少个对象还是涉及到多少个对象“再根据具体的来进行回答。
7.下面这段代码1)和2)的区别是什么?
public class Main {
public static void main(String[] args) {
String str1 = "I";
//str1 += "love"+"java"; 1)
str1 = str1+"love"+"java"; //2)
}
}
1)的效率比2)的效率要高,1)中的"love"+“java"在编译期间会被优化成"lovejava”,而2)中的不会被优化
本文深入解析Java中String、StringBuffer和StringBuilder的特性与区别,包括它们的内部实现、效率对比及线程安全性分析。通过代码示例,阐述了在不同场景下选择合适字符串类的重要性。

被折叠的 条评论
为什么被折叠?



