String,StringBuffer, StringBuilder 笔记

本文深入解析Java中String、StringBuffer和StringBuilder的特性与区别,包括它们的内部实现、效率对比及线程安全性分析。通过代码示例,阐述了在不同场景下选择合适字符串类的重要性。
废话不说,直接上代码
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)中的不会被优化

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值