String、StringBuilder和StringBuffer的区别

String、StringBuilder和StringBuffer三个类都是用于操作字符串的java.lang包下的工具类。在字符串连接操作比较频繁情况下,StringBuilder、StringBuffer代替“+”操作符能带来更好的性能。另外,StringBuffer在多线程环境下比StringBuilder更安全,因为它给大部分方法加上了synchronized锁关键字,而在单线程环境下StringBuilder比StringBuffer性能更好,因为线程不必等待获取对象锁。此外,在结构上具有的共性、差异如图所示。
这里写图片描述

String

String类是编程中使用最频繁的工具类,以至于JVM将String类的实例创建和操作都进行了特殊处理,如”aaa”直接创建一个String实例,而不用通过new String构造函数的方式进行,此外还可以通过”+”连接符直接将两个字符串进行拼接,同时创建第三个字符串实例,同时进行了拼接和创建两个操作,如下面的示例代码所示。

public static void main(String[] args) {
    final String str1 = new String("Hello");
    final String str2 = String.valueOf("World");
    System.out.println(str1 + "," + str2);
}

三个字符串实例str1、str2和”,”通过内置的“+”连接(每一次连接都会创建一个全新的对象),最终在创建一个全新的字符串实例“Hello, World”。在上面的字符串实例创建过程有三种方式:new、String.valueOf工厂方法和字符串字面量等。字面量是我们平常编程使用最广泛的方式,但也只是在字符内容确定前提下,而类似于toString的方法创建字符串又很容易忽视NullPointerException检查,利用String.valueOf静态工厂方法来创建则可以获取到一个字符串,但对于Null值获取到的是一个”null”,这有时候会造成一定的程序问题,例如字符转成数值类型。

  • 内部实现
    String作为java.lang包中的工具类,它所操作的是字符char,而char是作为基本类型在JVM中存在,也就是char是一个基本单元,同时Character才是char所对应的包装类型。String操作的是char数组,这点在String的内部封装中可以看见,并且使用final修饰,也就是在一开始赋值以后便不能再变动。
public final String 
    implements Serializable, Comparable<String>, CharSequence {
    private final char value[];
    private int hash;
    private static final long seriaVersionUID = -68497944..;
    private static final ObjectStreamField[] serialPersistentFields =
        new ObjectStreamField[0];
    public String() {
        this.value = new char[0];
    }
    public void getChars(int paramInt1, int paramInt2, 
        char[] paramArrayOfChar, int paramInt3) {
        if (paramInt1 < 0) {
          throw new StringIndexOutOfBoundsException(paramInt1);
        }
        if (paramInt2 > this.count) {
          throw new StringIndexOutOfBoundsException(paramInt2);
        }
        if (paramInt1 > paramInt2) {
          throw new StringIndexOutOfBoundsException(
              paramInt2 - paramInt1);
        }
        //内部操作时通过System.arraycopy的底层native方法实现
        System.arraycopy(this.value, this.offset + paramInt1,  
            paramArrayOfChar, paramInt3, paramInt2 - paramInt1);
     }  
}
... ... ...

数组是一个类型一致的数据的聚合对象,而该对象是有序的、个数已知的、不可改变的对象,所以String对象是一个值不可变的对象。String的连接操作时合并数组,重现创建一个更长数组,以及String对象的过程,所以如果“+”太频繁时,会带来性能消耗。

  • 内存分析
    一个String对象的大小由value字段决定,它的长短就是一个char[]的大小。上面的字符连接操作在内存中的过程如下图所示。这其中$1、$2是一个匿名的内部指针命名,当然是我为了说明方便而加。

这里写图片描述

StringBuilder

在23中设计模式中,建造者Builder模式是创建对象的一种模式,它可以将复杂的对象创建过程隐藏,而只暴露给客户端一个调用接口。那么StringBuilder是否就是String的建造模式的具体实现?StringBuilder是采用char[16]数组作为默认存储容器,在append操作时会动态扩展内部数组,从而实现字符串的拼接,如下面代码所示。

public static void main(String[] args) {
    final String str1 = "hello";
    final String str2 = "world"
    final StringBuilder strBuilder = new StringBuilder();
    strBuilder.append(str1).append(",").append(str2);
}
  • 内部实现
    StringBuilder作为java.lang包中的工具类,它所操作的也是char。StringBuilder内部也是通过char[]数组作为字符串的存储容器,但不同于String工具类,它的char[]是可以动态扩展,也即char[]的长度在字符不断进行append时进行增加。StringBuilder内部实现如下所示。
public class StringBuilder extends AbstractStringBuilder
    implements Serializable, CharSequence {
    public StringBuilder() {
        super(16);
    }
    public StringBuilder(int capacity) {
        super(capacity)
    }
    public StringBuilder(CharSequence paramCharSequence) {
        this(paramCharSequence.length() + 16);
        append(paramCharSequence);
    }
    public StringBuilder append(String paramString) {
        super.append(paramString);
        return this;
    }
    // ... ... ...
}
//StringBuilder、StringBuffer的父类
abstract class AbstractStringBuilder 
    implements Appendable, CharSequence {
    char[] value;
    int count;
    static final int[] sizeTable = {9, 99, 999, 9999, 99999, 
        999999, 9999999, 99999999, 999999999, 2147483647};
    AbstractStringBuilder(int paramInt) {
        this.value = new char[paramInt];
    }
    void expandCapacity(int paramInt) {
        int i = (this.value.length + 1) * 2;
        if (i < 0)
           i = 2147483647;
        else if (paramInt > i) {
           i = paramInt;
        }
        char[] arrayOfChar = new char[i];
        System.arraycopy(this.value, 0, arrayOfChar, 0, this.count);
        this.value = arrayOfChar;
    }
    public AbstractStringBuilder append(String paramString) {
        if(paramString == null) paramString = "null";
        int i = paramString.length();
        if(i == 0) return this;
        int j = this.count + i;
        if(j > this.value.length) {
            expandCapacity(j);
        }
        paramString.getChars(0, i, this.value, this.count);
        this.count = j;
        return this;
    }
    //... ... ... 
}

在进行append操作时,AbstractStringBuilder的append是StringBuilder的底层实现,其逻辑基础是附加数组前提是判断当前value是否能够容纳paramString,如果value.length长度不够,则调用expandCapacity扩展value,然后再有native方法System.arraycopy将paramString的值填充到value字符数组中,这也是StringBuilder附加字符串的逻辑实现。此外,StringBuilder不同于String工具类的根本原因在于AbstractStringBuilder的value是非终结的字段,而String的value是final修饰的终结字段,所以一个可以扩展而另一个不能扩展。

StringBuffer

StringBuilder可以动态的附加字符到内部存储容器char[]中,但是它的各项操作都是非线程安全的,在多线程环境下,可能会抛出意想不到异常,如下面代码所示。

public static void main(String[] args) {
    final StringBuilder strBuilder = new StringBuilder();           
    //final StringBuffer strBuffer = new StringBuffer();
    new Thread(new Runnable() {
        public void run() {
            for(int i = 1; i <= 10000; i++) {
                //多线程不安全操作
                strBuilder.append(i);
                System.out.print(strBuilder.charAt(i)+ " ");

                //线程安全操作
                //strBuffer.append(i);
                //System.out.print(strBuffer.charAt(i)+ " ");
            }
        }               
    }).start();         
    new Thread(new Runnable() {
        public void run() {
            for(int i = 1; i <= 10000; i++) {
                strBuilder.append(i);
                System.out.print(strBuilder.charAt(i)+ " ");

                //线程安全操作
                //strBuffer.append(i);
                //System.out.print(strBuffer.charAt(i)+ " ");
            }
        }
    }).start();
}

在运行该段代码时,抛出异常的可能性很大,大概的异常信息为下面所示。而造成异常的原因主要是append操作和chaAt操作非原子操作,它们在两个线程里存在潜在的污染(异步性),在第一个线程append时检测value的容量不够,进行扩展,但还未扩展完成时,第二个线程已经检测到改value的容量足够,所以chatAt获取字符时报错。

Exception in thread "Thread-0" java.lang.StringIndexOutOfBoundsException: 
    String index out of range: 1
at java.lang.AbstractStringBuilder.charAt(
    AbstractStringBuilder.java:177)
at TestStringBuilder$1.run(TestStringBuilder.java:11)
at java.lang.Thread.run(Thread.java:595)
  • 内部实现
    StringBuffer可以保证在多线程环境下附加字符串不会抛出ArrayIndexOutOfException问题,而它保证的前提是操作的原子性,也即方法加上synchronized关键字,其内部代码如下所示。
public final class StringBuffer extends AbstracStringBuilder
    implements Serializable,CharSequence {
    private static final ObjectStreamField[] serialPersisentFields = 
        { new ObjectStreamField("value", [C.class), 
          new ObjectStreamField("count", Integer.TYPE), 
          new ObjectStreamField("shared", Boolean.TYPE) };
    public StringBuffer() {
        super(16);
    }
    public StringBuffer(int capacity) {
        super(capacity);
    }
    public synchronized char charAt(int paramInt) {
        if ((paramInt < 0) || (paramInt >= this.count))
           throw new StringIndexOutOfBoundsException(paramInt);
        return this.value[paramInt];
    }
    public synchronized StringBuffer append(String paramString) {
        super.append(paramString);
        return this;
    }
    ... ... ...
}

结论

String、StringBuilder和StringBuffer都是操作字符的工具类,或者String对象是大小固定的、StringBuilder和StringBuffer对象是大小可扩展的工具类。在字符串使用上String是最常见的工具类,但在字符拼接比较频繁的环境下,StringBuilder具有较高的性能,而在多线程环境下,StringBuffer比StringBuilder安全。

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值