String、StringBuffer、StringBuilder的比较与原理解析

String

String不可修改

String对象是不可修改的,因为底层维护的是private final char value[]

  1. 首先final修饰了char[],说明char[]的引用地址不可修改(实际是hash值),也就是说一旦初始化完成内部也不能修改它的引用地址,当然具体的元素也不会修改
  2. 由private修饰,且没有提供对应的set方法,外部不能修改char[]里面的每一个元素

所以当对String进行replace、substring、split、toLowerCase
、concat等修改操作时,都是返回新的String对象,与原来无关,如下

replace修改String误区

String a = "ABCabc";
System.out.println("a = "+ a);
a = a.replace('A', 'a');
System.out.println("a = "+ a);

结果

a = ABCabc
a = aBCabc

误以为能修改其实是底层返回了一个新的string对象

public String replace(char oldChar, char newChar) {
        if (oldChar != newChar) {
            int len = value.length;
            int i = -1;
            char[] val = value; /* avoid getfield opcode */

            while (++i < len) {
                if (val[i] == oldChar) {
                    break;
                }
            }
            if (i < len) {
                char buf[] = new char[len];
                for (int j = 0; j < i; j++) {
                    buf[j] = val[j];
                }
                while (i < len) {
                    char c = val[i];
                    buf[i] = (c == oldChar) ? newChar : c;
                    i++;
                }
                // 返回了新的string堆区对象
                return new String(buf, true);
            }
        }
        return this;
    }

String 内存结构解析

下面举几个例子:
1.

//String的两种创建方式
String s1 = new String("hello"); 
String s2 = "hello"; 
System.out.println(s1==s2); // false  比较地址,地址不相同
System.out.println(s1.equals(s2)); // true  因为string重写equals方法,只比较内容
  • 如图 第一种会先在堆中创建空间,里面维护了char[] value属性,因为数组也是引用类型,会实际指向常量池的"hello"。如果常量池没有“hello”,重新创建,如果有,直接通过value char数组引用指向。s1的引用 实际指向的是堆中的空间地址0x22

  • 如图 第二种先从常量池查找是否有“hello” 数据空间。如果有,直接指向;如果没有则重新创建,然后指向。最终s2的引用指向的是常量池空间的内存地址

在这里插入图片描述
2.

String a = new String(“abc”);
String b =new String(“abc”);
System.out.println(a.equals(b)); // true [内容]
System.out.println(a==b); //false  //跟上图分析一样, a和b new了两个String对象,地址当然不同,但是两个堆的string对象同样会去常量池查找是否有"abc"存在,有则直接使用,没有则创建后使用

在这里插入图片描述

String a = “abc”;
String b =new String(“abc”);
System.out.println(a.equals(b)); //true
System.out.println(a==b); //false
System.out.println(a==b.intern()); //true
System.out.println(b==b.intern()); //false

当调用 intern 方法时,如果string常量池已经包含一个等于此 String 对象的字符串(用 equals(Object) 方法确定),则返回池中的字符串。否则,将此 String 对象添加到池中,并返回此 String 对象的引用
即b.intern() 方法最终返回的是常量池的地址.

编译器确定时优化的情况

String a = “hello”+“abc”;//问:创建了几个对象?

分析
当Java能直接使用字符串直接量(包括在编译时就计算出来的字符串值时,如String e = “张” + “三”;),JVM就会使用常量池来管理这些字符串。

只会产生1个对象.
编译器会做优化
String a = “hello”+“abc”; 这种情况编译器会直接在常量池创建 “helloabc”而不必创建 “hello”,“abc”;
=>优化
String a = “helloabc”; 因此只创建了一个对象

  final String s2 = "111";        //pool
   String s7 = "sss" + s2;         //pool
    String s3 = "sss111";           //pool  
    System.out.println(s3 == s7);   //true

分析
对于final String s2 = “111”。s2是一个用final修饰的变量,在编译期已知,在运行s2+"sss"时直接用常量“111”来代替s2。所以s2+"sss"等效于“111”+ “sss”。在编译期就可以确定的字符串对象“111sss”存放在常量池中。

编译器不确定时(运行时才能确定变量的值)Stringbuilder帮助拼接优化

String a = “hello”;
String b =“abc”;
String c=a+b;  //创建了几个对象? 可以自行debug force step into源码查看

因为编译时不确定,JVM会对String对象重载“+”“+=”进行一些优化

在底层其实是编译器擅自调用了StringBuilder类进行+的操作,主要原因是StringBuilder的append()更加高效

因为Stringbuilder是由 char[] value构成,是非final的,字符串拼接直接在该数组上可以完成,默认构造器会创建16字符大小char[],所以直接够用,不用扩容;就算不够,也可以扩容解决,因为char[] value的引用是可以变的,为了优化,可以将char[]的大小设置大一点,避免反复扩容

如果是String的话,因为不可修改的特性,要完成拼接,必然是需要创建新的String对象

  1. string 对象char[] value的大小应该是根据后面字符串的字符长度决定,如4个字符,string.length()就会得到4,
  2. 所以如果两个要拼接,因为a的char[] value的大小只有5,要拼接后面的势必要扩容,而又因为char[] 是final的,无法创建新char数组,重新赋值,只能新建1个string对象,让里面的char[] 构建9个字符的长度大小并将"helloabc"的引用赋予
 public AbstractStringBuilder append(String str) {
        if (str == null)
            return appendNull();
        int len = str.length();
        ensureCapacityInternal(count + len);
        //进行数组的copy赋值粘贴
        str.getChars(0, len, value, count);
        count += len;
        return this;
    }
1. new StringBuilder();
2. 调用append("hello");
3. 调用append("abc");
	```
	 以上两步都是调用str.getChars进行string维护的char[]属性数组的copy赋值粘贴
	 value是该builder自己的char[] ,即已经拼接好的char[]
	 append就是对stirng的char[]进行拼接
     str.getChars(0, len, value, count);
	```
4. 调用StringBuilder的tostring 
	1. return  new String(value, 0, count); 返回新的堆区String对象
String s1 = "hello"; 	
String s2 = "java";
String s5 = "hellojava"; //常量池
String s6 = (s1 + s2).intern(); //常量池
System.out.println(s5 == s6); //true 【s5 和 s6 的内容相同,并且都指向池】
System.out.println(s5.equals(s6));//true [内容,相等的.]

public class Test1 {
    String str = new String("good");
    char[] ch = { 't', 'e', 's', 't' };
    public void change(String str, char ch[]) {
        str = "test";
        ch[0] = 'g';
    }
    public static void main(String[] args) {
        Test1 ex = new Test1();
        ex.change(ex.str, ex.ch);
        System.out.print(ex.str + " and "); // good and 
        System.out.println(ex.ch);//gest
    }
} 

在这里插入图片描述
如图,因为每个方法都会开辟栈,所以在栈里面的局部变量作用域只在当前方法,
那么change方法将str重新指向 常量池的"test",不会对main 的str引用造成影响
而ch因为是将数组引用传递了过来,如果修改数组引用对象下标为0位置的元素,那么main方法的ch指向的数组也是同一个,肯定也修改。

String不可修改的优点

相对于可变对象,不可变对象有很多优势:

  1. 不可变对象可以提高String Pool的效率和安全性。如果你知道一个对象是不可变的,那么需要拷贝这个对象的内容时,就不用复制它的本身而只是复制它的地址,复制地址(通常一个指针的大小)需要很小的内存效率也很高。对于同时引用这个“ABC”的其他变量也不会造成影响。
  2. 不可变对象对于多线程是安全的,因为在多线程同时进行的情况下,一个可变对象的值很可能被其他进程改变,这样会造成不可预期的结果,而使用不可变对象就可以避免这种情况。
  3. 字符串不变性保证了hash码的唯一性,因此可以放心地进行缓存.这也是一种性能优化手段,意味着不必每次都去计算新的哈希码. 这就使得字符串很适合作为Map中的键,字符串的处理速度要快过其它的键对象。这就是HashMap中的键往往都使用字符串。在String类的定义中有如下代码:
    private int hash;//用来缓存HashCode 
    
  4. 安全性: String被许多的Java类(库)用来当做参数,例如 网络连接地址URL,文件路径path,还有反射机制所需要的String参数等, 假若String不是固定不变的,将会引起各种安全隐患。
    1. 如类加载器要用到字符串,不可变性提供了安全性,以便正确的类被加载。譬如你想加载java.sql.Connection类,而这个值被改成了myhacked.Connection,那么会对你的数据库造成不可知的破坏。

假若字符串对象允许改变,那么将会导致各种逻辑错误,比如改变一个对象会影响到另一个独立对象

Stringbuffer

  1. java.lang.StringBuffer代表可变的字符序列,可以对字符串内容进行增删。
  2. 很多方法与String相同,但StringBuffer是可变长度的。
  3. StringBuffer是一个容器。

优化

构成内容元素是父类的char数组,构造器可以自己设置char数组的大小,合适的大小设置可以增加效率,避免反复扩容带来的性能损耗。

当然stringbuilder也一样

//构成元素是父类的char数组
abstract class AbstractStringBuilder implements Appendable, CharSequence {
    /**
     * 该值用于字符存储。
     */
    char[] value;
public final class StringBuffer
    extends AbstractStringBuilder
    implements java.io.Serializable, CharSequence
{

    /**
     * toString返回的最后一个值的缓存。每当修改StringBuffer时清除。
     */
    private transient char[] toStringCache;

与string的区别

  1. String保存的是字符串常量,里面的值不能更改,每次String类的更新实际上就是更改地址,效率较低
  2. StringBuffer保存的是字符串变量,里面的值可以更改,每次StringBuffer的更新实际上可以更新内容,不用更新地址,效率较高

为什么拼接效率比String高

  1. Stringbuffer: char数组可以修改,且数组容量可以自己定义,那么可以String拼接在一个char[]上完成,如果容量不足,也只需要对char数组进行扩容
  private void ensureCapacityInternal(int minimumCapacity) {
        // overflow-conscious code
        if (minimumCapacity - value.length > 0) {
            value = Arrays.copyOf(value,
                    newCapacity(minimumCapacity));
        }
    }
  1. String: 而string因为不可修改,所以至少每次需要创建新的String对象才行(实际会调用StringBuilder进行拼接),效率很低

String和StringBuffer相互转换

// String——>StringBuffer
String s = "hello";
// 方式1:
StringBuffer b1 = new StringBuffer(s);
// 方式2:
StringBuffer b2 = new StringBuffer();
b2.append(s); 
System.out.println("b2=" + b2);  //? hello




// StringBuffer——>String
// 方式1:
String s2 = b1.toString(); //b1 [StringBuffer]
System.out.println("s2=" + s2);
// 方式2:
String s3 = new String(b1);

StringBuilder

  1. 一个可变的字符序列。此类提供一个与 StringBuffer 兼容的 API,但不保证同步[多并发+多线程]。该类被设计用作 StringBuffer 的一个简易替换,用在字符串缓冲区被单个线程使用的时候(这种情况很普遍)。如果可能,建议优先采用该类,因为在大多数实现中,它比 StringBuffer 要快。
  2. 在 StringBuilder 上的主要操作是 append 和 insert 方法,可重载这些方法,以接受任意类型的数据。每个方法都能有效地将给定的数据转换成字符串,然后将该字符串的字符追加或插入到字符串生成器中。append 方法始终将这些字符添加到生成器的末端;而 insert 方法则在指定的点添加字符。
  3. 总结:
    (1) StringBuilder 和 StringBuffer 的API(应用程序接口, 方法)是一样.
    (2) StringBuilder 没有线程同步和互斥的机制,因此他的速度快,但是有线程安全问题。当单线程或者并发要求不高时,选择使用StringBuilder , 如果多并发要求高,我们使用StringBuffer

与stringbuffer的区别

StringBuffer 的方法基本上都加了synchronized

  @Override
    public synchronized StringBuffer append(String str) {
        toStringCache = null;
        super.append(str);
        return this;
    }

StringBuilder 的方法上就没加synchronized

 @Override
    public StringBuilder append(String str) {
        super.append(str);
        return this;
    }

三者区别

StringBuilder 和 StringBuffer 非常类似,均代表可变的字符序列,而且方法也一样

  1. String:不可变字符序列,因为不可变,所以线程安全, 效率低,但是复用率高。
  2. StringBuffer:可变字符序列、效率较高(增删)、线程安全
  3. StringBuilder(JDK1.5):可变字符序列、效率最高、线程不安全

循环拼接时的区别

  • String如果进行多次的拼接来修改String字符串内容,那么会创建多个StringBuilder对象,然后每次循环都tostring(new String ),创建新的堆对象(对应有堆和常量池),会导致大量副本字符串对象存留在内存中,浪费内存,降低效率,影响性能。如下
    String a = "aaa";
    for(int i=0;i<5;i++){
        a+="ccc";
    }
    
    优化:
    只创建1个StringBuilder,且最后才会toString(new String)创建新的String堆对象
    	String a = "aaa";
    	StringBuilder sb = new StringBuilder(a);
    	for(int i=0;i<5;i++){
    	    sb.append("ccc");
    	}
    	System.out.println(sb.toString());
    
  • 那么如果String增和删除操作多,可以选择使用StringBuilder 或者StringBuffer 只会操作修改1个char数组对象
    • 如果要求线程安全,则使用 StringBuffer, 否则使用 StringBuilder
    • 只是为了读操作,选择使用String

效率测试

package com.test.StringBuilder;

public class PerformanceTest {

	public static void main(String[] args) {
		// TODO Auto-generated method stub

		String text = "";
		long startTime = 0L;
		long endTime = 0L;
		StringBuffer buffer = new StringBuffer("");
		StringBuilder builder = new StringBuilder("");
		startTime = System.currentTimeMillis();//获取当前的系统的毫秒数
		for (int i = 0; i < 2000000; i++) {
			buffer.append(String.valueOf(i));
		}
		endTime = System.currentTimeMillis();//获取当前的系统的毫秒数
		System.out.println("StringBuffer的执行时间:" + (endTime - startTime));
		
		
		startTime = System.currentTimeMillis();
		for (int i = 0; i < 2000000; i++) {
			builder.append(String.valueOf(i));
		}
		endTime = System.currentTimeMillis();
		System.out.println("StringBuilder的执行时间:" + (endTime - startTime));
		
		
		startTime = System.currentTimeMillis();
		for (int i = 0; i < 2000000; i++) {//javaEE 90%读
			text = text + i;
		}
		endTime = System.currentTimeMillis();
		System.out.println("String的执行时间:" + (endTime - startTime));

	}

}

参考

https://blog.csdn.net/qq_31615049/article/details/80891142
https://blog.csdn.net/u010887744/article/details/50844525 该篇个人觉得写的真的不错

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值