高性能条件下的StringBuilder使用及JAVA8新增StringJoiner类学习

StringBuilder and StringJoiner

  相信大家在平时工作中经常会使用到StringBuilder类,类似 sql条件查询语句拼接、简单字符串拼接之类的。我们经常会听到字符串拼接使用StringBuilder,不使用+或者StringBuffer、String字符串拼接编译后也是使用StringBuilder来完成的。我们真的了解StringBuilder类吗?不要知其然而不知起所以然。

一、合理初始化其长度,十分重要

  与String类不同的是StringBuilder类未实现Comparable接口,而是继承自AbstractStringBuilder类,该类实现了可变字符序列的一系列操作,下面我们将重点解释该下类的实现。

abstract class AbstractStringBuilder implements Appendable, CharSequence {
    /**
     * 在AbstractStringBuilder类中也封装了一个字符数组,但是它没有被final修饰(同String比较)
     */
    char[] value;
    /**
     * 与String不同,字符数组中的位置不一定都被使用,count实例变量用来表示数组中已经使用的字符个数
     */
    int count;
    /**
     * 指定容量的构造方法
     */
    AbstractStringBuilder(int capacity) {
        value = new char[capacity];
    }

    /**
     * 返回字符的长度(实际长度)
     */
    @Override
    public int length() {
        return count;
    }
    /**
     * 返回当前对象的容量。 容量存储的可用于新插入的字符,超过此将重新分配容量。
     */
    public int capacity() {
        return value.length;
    }
    //此处省略扩容方法的实现,后面会提及到。

  AbstractStringBuilder类实现了Appendable和CharSequence接口,Appendable接口主要是拼接操作,CharSequence接口提供一些操作字符相关的方法。
  下面我们继续看StringBuilder构造方法(4个)

  • 无参构造方法,默认容量:16
    public StringBuilder() {
        super(16);//此处调用的父类带容量的构造方法
    }
  • 指定容量(经常使用)
    public StringBuilder(int capacity) {
        super(capacity);//同上
    }
  • 接收一个String对象作为参数,设置了value数组的初始容量为String对象的长度+16,并把String对象中的字符添加到value数组中
    public StringBuilder(String str) {
        super(str.length() + 16);//此处同上
        append(str);//本类中方法(下)
    }
        @Override
    public StringBuilder append(String str) {
        super.append(str);//父类append方法,父类中设计到扩容的问题,后面说明
        return this;
    }
  • 接收一个CharSequence对象作为参数,设置了value数组的初始容量为CharSequence对象的长度+16,并把CharSequence对象中的字符添加到value数组中
    public StringBuilder(CharSequence seq) {
        this(seq.length() + 16);  //同第三个构造方法,仅形参不同
        append(seq);
    }

  由此我们先进行总结一下,初始化时,应优先使用第二个构造方法(依据场景不同而定,如果你知道初始化填充的内容且再次填充的内容不会超过16,你可以选择第三个构造方法,仅一次扩容。),如果你设定的容量和你要填充的相同或者容量设定稍微大那么一丁点,此时可以很好的避免扩容问题的出现,程序执行的效率肯定会提高。此处总结你可能不太明白,没有关系,继续往下看。

二、和效率相关的扩容问题

  根据上面的第三个和第四个构造方法我们可以看到append()方法简直是无处不在,下面我们介绍append方法的实现。方法支持的参数:(boolean、char、char[]、char[], int, int、CharSequence、CharSequence, int, int、double、float、int、long、Object、String、StringBuffer)
  我们以String类型参数为例子进行分析

  • StringBuilder中append()方法实现
    @Override
    public StringBuilder append(String str) {
        super.append(str);//调用父类的append方法
        return this;//???返回的是this???。。。由此我们可以进行链式操作。
    }
  • 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;
    }
	private AbstractStringBuilder appendNull() {
        int c = count;//得到实际长度
        ensureCapacityInternal(c + 4);//确保数组的长度足够容纳新添加的字符
        final char[] value = this.value;//定义为不可修改的字符数据
        value[c++] = 'n';
        value[c++] = 'u';
        value[c++] = 'l';
        value[c++] = 'l';
        count = c;
        return this;
    }
    private void ensureCapacityInternal(int minimumCapacity) {//形参接收实际的长度
        // overflow-conscious code
        if (minimumCapacity - value.length > 0) { //判断实际的长度和为扩容之前容量,如果大于o,代表需要扩容,小于等于0,终止执行
            value = Arrays.copyOf(value,
                    newCapacity(minimumCapacity));//分配一个足够长度的新数组,然后将原内容拷贝到这个新数组中,最后让内部的字符数组指向这个新数组
        }
    }
  private int newCapacity(int minCapacity) { 
        // overflow-conscious code
        int newCapacity = (value.length << 1) + 2;//扩容:"<<"左移",相当于*2的n次方, 自身长度*2+2
        if (newCapacity - minCapacity < 0) {//判断扩容的长度是否满足
            newCapacity = minCapacity;//不满足,直接扩容到需要的长度
        }
        return (newCapacity <= 0 || MAX_ARRAY_SIZE - newCapacity < 0)
            ? hugeCapacity(minCapacity)
            : newCapacity;//溢出判断,<0 是因为int是32位的二进制,最高位是符号位(0正数,1负数),如果newCapacity大于Integer最大值,那么首位被挤掉了,由0变成1,那么就变成了负数了
    }
  private int hugeCapacity(int minCapacity) {  //推断是否溢出,若溢出,则将容量设置为整型的最大值
            if (Integer.MAX_VALUE - minCapacity < 0) { // overflow
                throw new OutOfMemoryError();
            }
            return (minCapacity > MAX_ARRAY_SIZE)
                    ? minCapacity : MAX_ARRAY_SIZE;
        }
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是JVM底层提供的方法,native修饰的,用它来进行数组之间的拷贝。
        //StringBuilder底层是char[]数组
        //StringBuilder是动态扩容的,它是通过创建一个新的数组,然后把旧数组的数据拷贝到新数组当中,旧数组给gc回收
        System.arraycopy(value, srcBegin, dst, dstBegin, srcEnd - srcBegin);
    }

  至此,扩容问题分析完毕,其他参数类型的与String类型相似,扩容运算涉及到位运算和数组的复制,虽然运算的速度十分快,且算法也是相当完美,但是我们如果直接设定其容量,避免走扩容这一步,我们的程序将效率将有明显的提高

三、Delete删除操作的实现

    @Override
    public StringBuilder deleteCharAt(int index) {
        super.deleteCharAt(index);//调用父类的方法
        return this;//返回当前对象
    }
    public AbstractStringBuilder deleteCharAt(int index) {
        if ((index < 0) || (index >= count))
            throw new StringIndexOutOfBoundsException(index);
        System.arraycopy(value, index+1, value, index, count-index-1);// 拷贝数组【同一个数组之间的拷贝】
        count--;//实际长度-1
        return this;
    }

四、java8新增StringJoiner类

  我们平时开发时经常使用StringBuilder类进行拼接,jdk1.8为我们新提供了一个拼接类StringJoiner类。

  • 应用演示
        StringJoiner sj=new StringJoiner(",","id in (",")");
        sj.add("1").add("2").add("3");
        System.out.println(sj.toString());//id in (1,2,3)
  • 源码解读
package java.util;
*/
public final class StringJoiner {
    private final String prefix;//拼接字符串的前缀
    private final String delimiter;//拼接字符串的分隔符
    private final String suffix;//拼接字符串的后缀

    private StringBuilder value;//字符的值

    private String emptyValue;//字符为空值
    
    
    public StringJoiner(CharSequence delimiter) {
        this(delimiter, "", "");//默认前缀和后缀为"",重载调用下一个构造方法
    }
    
    public StringJoiner(CharSequence delimiter,
                        CharSequence prefix,
                        CharSequence suffix) {
        Objects.requireNonNull(prefix, "The prefix must not be null");
        Objects.requireNonNull(delimiter, "The delimiter must not be null");
        Objects.requireNonNull(suffix, "The suffix must not be null");
        // 给成员变量设定值
        this.prefix = prefix.toString();
        this.delimiter = delimiter.toString();
        this.suffix = suffix.toString();
        this.emptyValue = this.prefix + this.suffix;//空值默认设置为:前缀+后缀
    }
	//设置空值
    public StringJoiner setEmptyValue(CharSequence emptyValue) {
        this.emptyValue = Objects.requireNonNull(emptyValue,
            "The empty value must not be null").toString();
        return this;
    }
	//重写父类toString()方法,
    @Override
    public String toString() {
        if (value == null) {//没有值将返回空值或者后续设置的空值
            return emptyValue;
        } else {
            if (suffix.equals("")) {//后缀为""直接返回字符串,不用添加
                return value.toString();
            } else { //后缀不为"",添加后缀,然后直接返回字符串,修改长度
                int initialLength = value.length();
                String result = value.append(suffix).toString();
                // reset value to pre-append initialLength
                value.setLength(initialLength);
                return result;
            }
        }
    }
	//初始化,先添加前缀,有了之后每次先添加间隔符,StringBuilder后续append字符串
    public StringJoiner add(CharSequence newElement) {
        prepareBuilder().append(newElement);
        return this;
    }
	//合并StringJoiner,注意后面StringJoiner 的前缀就不要了,后面的appen进来
    public StringJoiner merge(StringJoiner other) {
        Objects.requireNonNull(other);
        if (other.value != null) {
            final int length = other.value.length();
            StringBuilder builder = prepareBuilder();
            builder.append(other.value, other.prefix.length(), length);
        }
        return this;
    }
	//初始化,先添加前缀,有了之后每次先添加间隔符
    private StringBuilder prepareBuilder() {
        if (value != null) {
            value.append(delimiter);
        } else {
            value = new StringBuilder().append(prefix);
        }
        return value;
    }
    //返回长度(+后缀的长度)
    public int length() {
        return (value != null ? value.length() + suffix.length() :
                emptyValue.length());
    }
}
  • 此外,String类中也添加了join()方法,其其内部也是使用StringJoiner实现的。同StringJoiner区别:不能指定拼接字符串的前缀和后缀、
        StringJoiner sj=new StringJoiner(",");
        sj.add("1").add("2").add("3");
        System.out.println(sj.toString());//1,2,3

        String []array={"1","2","3"};
        //不需指定开头和结尾时,用String.join(),其内部也是使用StringJoiner实现的。
        System.out.println(String.join(", ", array));//1, 2, 3
  • 源码
    public static String join(CharSequence delimiter, CharSequence... elements) {
        Objects.requireNonNull(delimiter);
        Objects.requireNonNull(elements);
        // Number of elements not likely worth Arrays.stream overhead.
        StringJoiner joiner = new StringJoiner(delimiter);
        for (CharSequence cs: elements) {
            joiner.add(cs);
        }
        return joiner.toString();
    }

补充一个问题

1.为什么扩容是原数组的长度*2(指数)扩容算法?为什么要+2?

  • 指数扩容是一种折中的算法,因为一方面要减少内存分配次数,另一方面要避免浪费内存。
  • 为什么要+2?因为StringBuilder提供了一个构造函数,可以指定初始数组的大小public StringBuilder(int capacity).
    如果capacity = 0的情况下就不能正常扩容了。所以+2。

完结撒花

但凡通过点滴复出,累计出来的结果,都是平淡无声的。

你可能感兴趣的:(java基础)