第一章 String,StringBuffer,StringBuilder详解

1.定义

StringBuilder是从jdk1.5提供出来的新的String封装类,而StringBuffer是jdk1.0就已经存在了,这两个类的拼接效率远高于string。

2.String简介

String其实就是由若干个字符线性排列而成的,可以理解为字符Array,那么既然是数组实现的,那就需要考虑到数组的特性,数组在内存中是一块连续的地址空间块,即在定义数组的时候需要指定数组的大小
换言之, 数组就分为可变数组和不可变数组。可变数组能够动态插入和删除,而不可变数组一旦分配好空间后则不能进行动态插入或删除操作。
在实际的字符串应用场景中,涉及到多种操作,比如字符串的插入,删除,修改,拼接,查询,替换…

3.效率比对

现在我们对这三个类进行30w次的拼接,每次加上“-a”,测试效率,代码如下:

package com.hqa.design.test;

public class Test {
	public static void main(String[] args) {
		String str = "test";
		StringBuffer sbf = new StringBuffer(str);
		StringBuilder sbl = new StringBuilder(str);
		//string
		testString(str);
		//stringBuilder
		testStringBuilder(sbl);
		//stringBuffer
		testStringBuffer(sbf);

	}

	/**
	 * StringBuffer拼接
	 * @param str
	 */
	private static void testStringBuffer(StringBuffer str) {
		long start = System.currentTimeMillis();
		for(int i = 0;i < 300000;i++){
			str.append("-a");
		}
		System.out.println("StringBuffer拼接,耗时:" + (System.currentTimeMillis() - start) + "ms");
	}

	/**
	 * StringBuilder拼接
	 * @param str
	 */
	private static void testStringBuilder(StringBuilder str) {
		long start = System.currentTimeMillis();
		for(int i = 0;i < 300000;i++){
			str.append("-a");
		}
		System.out.println("StringBuffer拼接,耗时:" + (System.currentTimeMillis() - start) + "ms");
	}

	/**
	 * 原生String拼接
	 * @param str
	 */
	private static void testString(String str) {
		long start = System.currentTimeMillis();
		for(int i = 0;i < 300000;i++){
			str += "-a";
		}
		System.out.println("String拼接,耗时:" + (System.currentTimeMillis() - start) + "ms");
	}
}

输出:

String拼接,耗时:72122ms
StringBuilder拼接,耗时:5ms
StringBuffer拼接,耗时:9ms

可以看到 拼接效率 StringBuilder > StringBuffer >> String

4.String类详解

我们都知道string是不可变类,属性value为不可变数组,即String初始化构造器没有初始容量为16的概念,你定义多少,String中字符数组的长度就是多少,不存在字符数组扩容一说。

看一下类图:
在这里插入图片描述
我们来看一下string类的源码:


//类由final修饰,表明不可被继承
public final class String
    implements java.io.Serializable, Comparable<String>, CharSequence {
    //我们可以看到其内部是一个final修饰的char数组
    //这里的value变量其实就是存储了String字符串中的所有字符。
    private final char value[];
    private int hash; // Default to 0
    private static final long serialVersionUID = -6849794470754667710L;
    
    private static final ObjectStreamField[] serialPersistentFields =
        new ObjectStreamField[0];

    public String() {
        this.value = new char[0];
    }

    public String(String original) {
        this.value = original.value;
        this.hash = original.hash;
    }

既然String,不可变。我们再看下它的截取方法subString()实现

    public String substring(int beginIndex) {
        if (beginIndex < 0) {
            throw new StringIndexOutOfBoundsException(beginIndex);
        }
        int subLen = value.length - beginIndex;
        if (subLen < 0) {
            throw new StringIndexOutOfBoundsException(subLen);
        }
        return (beginIndex == 0) ? this : new String(value, beginIndex, subLen);
    }
    ...
      public String(char value[], int offset, int count) {
        if (offset < 0) {
            throw new StringIndexOutOfBoundsException(offset);
        }
        if (count < 0) {
            throw new StringIndexOutOfBoundsException(count);
        }
        // Note: offset or count might be near -1>>>1.
        if (offset > value.length - count) {
            throw new StringIndexOutOfBoundsException(offset + count);
        }
        //数组的拷贝方法就是重新构造一个新的char数组
        this.value = Arrays.copyOfRange(value, offset, offset+count);
    }
    

很明显,从源码我们可以发现,如果截取长度等于元字符串,那么返回this,否则就会重新new一个新的string对象。
类似的我们可以看到,String类的concat方法,replace方法,都是内部重新生成一个String对象的。

    public String concat(String str) {
        int otherLen = str.length();
        if (otherLen == 0) {
            return this;
        }
        int len = value.length;
        char buf[] = Arrays.copyOf(value, len + otherLen);
        str.getChars(buf, len);
        return new String(buf, true);
    }
...
    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++;
                }
                return new String(buf, true);
            }
        }
        return this;
    }

 

这也就是为什么我们如果采用String对象频繁的进行拼接,截取,替换操作效率很低下的原因。

5.StringBuilder类详解

内部可变数组,存在初始化StringBuilder对象中字符数组容量为16,存在扩容。

同样先看一下类图:
在这里插入图片描述
StringBuilder继承自父类AbstractStringBuilder,其内部核心方法很多都是直接调用自父类。
构造函数:

abstract class AbstractStringBuilder implements Appendable, CharSequence {
    /**
     * The value is used for character storage.
     */
    char[] value;//此处的char数组没有final修饰,表明可以重新赋值,这一点与String类不同
    ....
    ....
    /**
     * Constructs a string builder with no characters in it and an
     * initial capacity of 16 characters.
     */
    public StringBuilder() {
        super(16);
    }

父类AbstractStringBuilder的构造:

 /**
     * Creates an AbstractStringBuilder of the specified capacity.
     */
    AbstractStringBuilder(int capacity) {
        value = new char[capacity];
    }

可以看到父类初始化了一个长度为16的char数组,除此以外StringBuilder还提供了初始容量大小的含参构造器

/**
     * Constructs a string builder with no characters in it and an
     * initial capacity specified by the {@code capacity} argument.
     *
     * @param      capacity  the initial capacity.
     * @throws     NegativeArraySizeException  if the {@code capacity}
     *               argument is less than {@code 0}.
     */
    public StringBuilder(int capacity) {
        super(capacity);
    }

以字符串String 作为参数的构造器

    /**
     * Constructs a string builder initialized to the contents of the
     * specified string. The initial capacity of the string builder is
     * {@code 16} plus the length of the string argument.
     *
     * @param   str   the initial contents of the buffer.
     */
    public StringBuilder(String str) {
        super(str.length() + 16);
        append(str);
    }

在参数Str 数组长度的基础上再增加16个字符长度,作为StringBuilder实例的初始数组容量,并将str字符串 append到StringBuilder的数组中。

我们在看下父类的append方法

    public AbstractStringBuilder append(String str) {
        if (str == null)
            return appendNull();
        int len = str.length();
        ensureCapacityInternal(count + len); //扩容逻辑
        str.getChars(0, len, value, count); //原char数组拷贝到新的数组,这行比较重要,看下面
        count += len;
        return this;
    }

  private void ensureCapacityInternal(int minimumCapacity) {
        // overflow-conscious code
        if (minimumCapacity - value.length > 0) //如果当前需要的长度大于当前char数组长度,则进行扩容
            expandCapacity(minimumCapacity);
    }

    /**
    *其具体的扩容逻辑,如果当前value数据的长度小于需要的最小长度,就会进行一次扩容,
    *先将现有的数组长度乘以二倍再加2,如果还是小于最小需要的长度,则扩容的长度为传入
    *的minCapacity,即最小需要的长度。最后还会判断最小需要的长度是否已经大于数组长度
    的最大值获取小于等于0,如果是的话就会取最大数组长度。
    */
    void expandCapacity(int minimumCapacity) {
        int newCapacity = value.length * 2 + 2;
        if (newCapacity - minimumCapacity < 0)
            newCapacity = minimumCapacity;
        if (newCapacity < 0) {
            if (minimumCapacity < 0) // overflow
                throw new OutOfMemoryError();
            newCapacity = Integer.MAX_VALUE;
        }
        value = Arrays.copyOf(value, newCapacity);
    }

看下append方法的关键:String的 getChars方法(从str的0位开始,到str的长度,当前StringBuilder对象的字符数组,当前数组已有的字符长度)

    public void getChars(int srcBegin, int srcEnd, char dst[], int dstBegin) {
        if (srcBegin < 0) {
            throw new StringIndexOutOfBoundsException(srcBegin);
        }
        if (srcEnd > value.length) {
            throw new StringIndexOutOfBoundsException(srcEnd);
        }
        if (srcBegin > srcEnd) {
            throw new StringIndexOutOfBoundsException(srcEnd - srcBegin);
        }
        //
        System.arraycopy(value, srcBegin, dst, dstBegin, srcEnd - srcBegin);
    }
    
    //System class
    public static native void arraycopy(Object src,  int  srcPos,
                                        Object dest, int destPos,
                                        int length);

其实是调用了System的arraycopy方法 参数如下:

value 为str的内部不可变字符数组,

srcBegin 为从str 字符串数组的0下标开始,

srcEnd 为str字符串数组的长度,

dst 为StringBuilder对象的内部可变字符数组,

dstBegin 则为StringBuilder对象中已有的字符长度(char[] 已有的元素长度)

即整个StringBuilder的append方法,本质上是调用System的native方法,直接将String 类型的str字符串中的字符数组,拷贝到了StringBuilder的字符数组中

最后看一下stringBuilder的toString()方法

 @Override
    public String toString() {
        // Create a copy, don't share the array
        return new String(value, 0, count);
    }

这里的toString方法直接new 一个String对象,将StringBuilder对象的value进行一个拷贝,重新生成一个对象,不共享之前StringBuilder的char[]

可以发现没有像StringBuilder一样去重新new 对象,所以在频繁的拼接字符上,StringBuilder的效率远远高于String类。

6.StringBuffer

同样搞一张类图
在这里插入图片描述
其实StringBuffer和StringBuilder非常类似,可以说是StringBuilder的翻版,但是StringBuffer是在jdk1.0就已经存在了,所以其实StringBuilder的设计应该是借鉴自StringBuffer

看下append方法:

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

可以看到这里就是在append方法上加了同步锁,来实现多线程下的线程安全。其他的和StringBuilder一致。
多了一个toStringCache全局变量,这里的作用简单介绍一下,就是去缓存toString的。

可以看下StringBuffer的toString方法

    @Override
    public synchronized String toString() {
        if (toStringCache == null) {
            toStringCache = Arrays.copyOfRange(value, 0, count);
        }
        return new String(toStringCache, true);
    }

这里的作用就是如果StringBuffer对象此时存在toStringCache,在多次调用其toString方法时,其new出来的String对象是会共享同一个char[] 内存的,达到共享的目的。但是StringBuffer只要做了修改,其toStringCache属性值都会置null处理。这也是StringBuffer和StringBuilder的一个区别点。

7.总结

String 类不可变,内部维护的char[] 数组长度不可变,为final修饰,String类也是final修饰,不存在扩容。字符串拼接,截取,都会生成一个新的对象。频繁操作字符串效率低下,因为每次都会生成新的对象。

StringBuilder 类内部维护可变长度char[] , 初始化数组容量为16,存在扩容, 其append拼接字符串方法内部调用System的native方法,进行数组的拷贝,不会重新生成新的StringBuilder对象。非线程安全的字符串操作类, 其每次调用 toString方法而重新生成的String对象,不会共享StringBuilder对象内部的char[],会进行一次char[]的copy操作。

StringBuffer 类内部维护可变长度char[], 基本上与StringBuilder一致,但其为线程安全的字符串操作类,大部分方法都采用了Synchronized关键字修改,以此来实现在多线程下的操作字符串的安全性。其toString方法而重新生成的String对象,会共享StringBuffer对象中的toStringCache属性(char[]),但是每次的StringBuffer对象修改,都会置null该属性值。

部分内容参考自:https://www.jianshu.com/p/64519f1b1137

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值