`

StringBuilder StringBuffer --阅读源码从jdk开始

阅读更多

引言

 

最近我的同事分享了一个页面渲染过程中,字符串拼接的优化处理。我们系统的页面渲染是分模块渲染,每个模块渲染完成后都是一个String型的html片段,最终我们需要把所有模块的html片段拼接成一个完整html页面。老代码逻辑大致如下:

        List<String> moudleHtmls = new ArrayList<>(moduleSize);
        for(int i=0;i<moduleSize;i++){
            String tempHtml = null;
            //模块内容渲染 省略代码
            moudleHtmls.set(i, tempHtml);//把每个模块html片段放入List
        }
       
        String pageString="";
        for (int i=0;i <moduleSize; i++){
            pageString+=moudleHtmls.get(i);//遍历把所有模块的html片段拼接成一个页面
        }

通过反编译发现第二个for循环,每次拼接操作都会自动生成一个StringBuilder对象,反编译的代码如下(第二个for循环部分):

        String pageString="";
        for (int i=0;i <moduleSize; i++){
            pageString=new StringBuilder().append(pageString).append(moudleHtmls.get(i)).toString();
        }
 

 

每次for循环都会生成一个StringBuilder,其默认容量为16,超过16又需要自动扩容(后面源码详细讲解),势必会影响性能。改进后的代码如下:

StringBuilder pageString = new StringBuilder(pageTemplate.length * 2);//预估页面长度
for (int i=0;i <moduleSize; i++){
            pageString.append(moudleHtmls.get(i));//遍历把所有模块的html片段拼接成一个页面
        }

通过对比发现改造后的拼接性能大幅提升。我们系统每天都有上百万次的页面渲染,这个小小的改动带来的收益是可想而知的。

为了对StringBuilder有更深入的了解,决定阅读其相关源码,做一次全面的总结。

 

字符串连接的三种方式

 

1、字符串连接操作符(+),是把多个字符串合并为一个字符串的便利途径。

2 StringBuilder,创建该类对象,调用其append方法实现字符串连接,从jdk1.5开始支持。

3StringBuffer,用法和StringBuilder相同,从jdk1.0就开始支持。

 

通过阅读源码发现StringBuilderStringBuffer都是继承自AbstractStringBuilder,方式实现也是基本相同,只是StringBuffer的每个方式都是synchronized修饰的,也就是说StringBuffer适合使用在多线程并发环境下的字符串拼接。单线程环境下使用StringBuilder的性能会更好些。

 

String t=“123”+“456”

这种方式每次执行,本质上是创建一个新String,然后把两个String的的内容复制到这个新String,性能非常差。但从jdk1.5开始,该操作做了优化,执行过程中会自动new 一个StringBuilder,真实的执行过程变为:String t=new StringBuilder().append(“123”).append(“456”)

 

也就是说在拼接后的字符串总长度比较短的情况下(总长度不超过16),直接使用“(+)”符号拼接是最佳选择。

如果拼接后的字符串总长度大于16,最好新建一个指定预估容量的StringBuilder,调用其append方法进行拼接。如上述优化中:

StringBuilder pageString = new StringBuilder(pageTemplate.length * 2);//预估页面长度

 

另外如果在for循环中也不建议直接使用(+)操作,因为这会导致每次循环都会新创建一个StringBuilder,如上述优化中主要就是优化这个问题。

 

为什么要指定StringBuilder容量,其实跟ArrayListHashMap等原理相同,都存在自动扩容的问题,看下源码就一目了然。

 

StringBuilderStringBufferAbstractStringBuilder 源码解析

 

StringBuilderStringBuffer都是继承自AbstractStringBuilder抽象类,每个方法的实现基本相同,都是调用AbstractStringBuilder中的方法。唯一的差异,就是toString方法,方法也是AbstractStringBuilder中唯一的抽象方法。

 

成员变量是一个char性的数组,StringBuilderStringBuffer的所有操作基本都是围绕这个数组进行:

char[] value;//在AbstractStringBuilder中定义

该字符数组在StringBuilderStringBuffer的初始默认容量都是16,方法操作完全相同,以StringBuilder为例:

//默认构造方法
public StringBuilder() {
        super(16);//默认容量
}
//指定容量构造方法
public StringBuilder(int capacity) {
        super(capacity);
}
//指定初始字符串构造方法
public StringBuilder (String str) {
        super(str.length() + 16); //初始容量为字符串长度+16
        append(str);
}

 

这里通过super调用AbstractStringBuilder的构造方法:

AbstractStringBuilder(int capacity) {
        value = new char[capacity];//指定数组容量,初始化字节数组
}

 

成员变量字节数组value初始化完成。

 

append系列重载方法

 

AbstractStringBuilder中的append系列重载方法(StringBuilderStringBuffer 中的append方法通过super直接调用该系列方法),是其核心方法,可以处理处理所有的基础类型(比如booleanintlongdouble等)、引用类型的拼接(StringObjectAbstractStringBuilder自身等),这里以用得最多的append(String)进行讲解:

public AbstractStringBuilder append(String str) {
        if (str == null)
            return appendNull();//注意如果为str为null,会拼接一个"null"字符串
        int len = str.length();
        ensureCapacityInternal(count + len);//判断容量是否足够,如不够先扩容,再copy到新扩容后的数组
        str.getChars(0, len, value, count);//把str copy到 字符数组中
        count += len;//重新计算字符数组总长度
        return this;
    }
 
private void ensureCapacityInternal(int minimumCapacity) {
        // overflow-conscious code
        if (minimumCapacity - value.length > 0)
            expandCapacity(minimumCapacity);//容量不够,进行扩容
    }
   
    void expandCapacity(int minimumCapacity) {
        int newCapacity = value.length * 2 + 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); //把老数据复制到扩容后的新数组。
    }
 

 

StringBuilderStringBuffer的字符串拼接,实际上就是把待拼接的字符串,放到自己的字符数组,如果字符数组的容量不够,需要进行扩容。具体操作是新创建一个字符数组(容量为老数组的2+2),再把老数组中的内容copy到新数组。这就是为什么在拼接大量字符串(拼接后超过长度16),最好采用指定容量的方式创建StringBuilder(或StringBuffer),防止拼接过程中不断扩容带来的性能消耗。

 

toString方法

 

需要注意的是StringBuilderStringBuffer不是String类型,不能强制转换。只能通过调用其toString方法转换为String。前面已经提到StringBuilderStringBuffer重要区别就是toString方法的实现不同。

StringBuildertoString方法,采用自己成员变量value字符数组新建一个String

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

StringBuffertoString方法:

public synchronized String toString() {
        if (toStringCache == null) {
            toStringCache = Arrays.copyOfRange(value, 0, count);//缓存成员变量value字符数组
        }
        return new String(toStringCache, true);
}
 

 

其中toStringCache主要用作缓存。当StringBuffer对成员变量value字符数组有修改时,需要先清理缓存,如StringBuffer append(String)方法实现:

@Override
    public synchronized StringBuffer append(String str) {
        toStringCache = null;//清理缓存
        super.append(str);//调用父类AbstractStringBuilder的append方法
        return this;
    }
 

 

再来看下StringBuilderappend(String)方法实现:

@Override
    public StringBuilder append(String str) {
        super.append(str);
        return this; //调用父类AbstractStringBuilder的append方法
}
 

 

区别有两点:

1StringBuffer的方法是synchronized修饰。

2StringBuffer的方法需要清理缓存。

StringBuilderStringBufferappend系列方法不再一一讲解,具体操作都差不多,把待拼接类型转换中字符,依次放到其成员变量value字符数组中。

 

其他方法:

 

AbstractStringBuilderdelete方法,本质上也是对字符数组的copy操作:

public AbstractStringBuilder delete(int start, int end) {
        if (start < 0)
            throw new StringIndexOutOfBoundsException(start);
        if (end > count)
            end = count;
        if (start > end)
            throw new StringIndexOutOfBoundsException();
        int len = end - start;
        if (len > 0) {
            System.arraycopy(value, start+len, value, start, count-end);//本质上先复制到一个临时数组,再覆盖原数组 无内存泄漏问题
            count -= len;
        }
        return this;
    }
 

 

AbstractStringBuilderinsert系列方法,这里以String为例:

public AbstractStringBuilder insert(int offset, String str) {
        if ((offset < 0) || (offset > length()))
            throw new StringIndexOutOfBoundsException(offset);
        if (str == null)
            str = "null";
        int len = str.length();
        ensureCapacityInternal(count + len);//确认容量
        System.arraycopy(value, offset, value, offset + len, count - offset);//通过复制预留出待插入空间
        str.getChars(value, offset);//copy字符串 到指定开始位置
        count += len;
        return this;
    }

 

举个列子:“hello!”字符串中插入“pig”字符串,形成“hellopig!”, java代码如下:

StringBuilder sb = new StringBuilder;
sb.append(“hello!”)
sb.insert(4,“pig”);

对应的示意图:



 

 

AbstractStringBuilderreverse方法,本质上就是把字符数组里每个字符反序排列,不在累述。

 

最后总结下:

1、拼接完成的字符串长度短的情况下,直接使用(+)操作符即可:如String t = “123”+”456”

2、不要在for循环中使用(+)操作符,使用StringBuilder代替。

3、单线程环境下,使用StringBuilder拼接一个比较长的字符串,最好先预估容量,采用指定容量的StringBuilder构造方法,构造StringBuilder实例。

4、多线程环境下使用StringBuffer

 

 

  • 大小: 87.1 KB
0
1
分享到:
评论

相关推荐

    javabiginteger源码-read-jdk-source:阅读jdk

    String、StringBuffer、StringBuilder、StringJoiner、StringTokenizer(补充正则表达式的知识) CharacterIterator、StringCharacterIterator、CharsetProvider、CharsetEncoder、CharsetDecoder(较难) java.util...

    StringBuffer & StringBuilder 源码分析.docx

    StringBuffer & StringBuilder 源码分析 StringBuffer 和 StringBuilder 是 Java 语言中两个常用的字符串操作类,它们都是 CharSequence 接口的实现类,并且都继承了 AbstractStringBuilder 类。下面是对这两个类的...

    jdk8-source-code:jdk源码解析

    jdk1.8-source-analysis JDK1.8源码分析引入原始过程中的注意事项JDK1.8对应JDK版本下载: 码:49wi原始码在src目录下以下两个类手动添加的,解决编译过程中该包的丢失sun.font.FontConfigManager sun.awt....

    java8源码-jdk8:jdk8源码阅读理解

    String、StringBuffer、StringBuilder、StringJoiner、StringTokenizer(补充正则表达式的知识) CharacterIterator、StringCharacterIterator、CharsetProvider、CharsetEncoder、CharsetDecoder(较难) java.util...

    javajdk源码学习-JavaSourceLearn:JDK源码学习

    jdk源码学习 JavaSourceLearn 版本号 版本 corretto-1.8.0_275 方式 逐步阅读源码添加注释、notes文件夹添加笔记 计划学习任务计划 标题为包名,后面序号为优先级1-4,优先级递减 java.lang Object 1 String 1 ...

    jdk1.8-source-analysis:JDK1.8源码分析

    jdk1.8-source-analysis JDK1.8源码分析引入原始过程中的注意事项JDK1.8对应JDK版本下载: 码:49wi原始码在src目录下以下两个类手动添加的,解决编译过程中该包的丢失sun.font.FontConfigManager sun.awt....

    Collections源码java-jdk1.8-source-analysis:Java8源码分析,J.U.C、ThreadPool、Col

    很多java开发的小伙伴都会阅读jdk源码,然而确不知道应该从哪读起。以下为小编整理的通常所需阅读的源码范围。 标题为包名,后面序号为优先级1-4,优先级递减 1、java.lang 1) Object 1 2) String 1 3) ...

    jdk1.8源码+中文注释(chm格式)

    9. **String的优化**:JDK1.8对字符串进行了优化,例如新增了`String.join()`方法,以及`StringBuilder`和`StringBuffer`的改进,提升了字符串操作的效率。 10. **Optional类**:为了解决Java中的null安全问题,JDK...

    JDK 7 源码

    在JDK 7中,字符串连接操作进一步优化,使用StringBuilder或者StringBuffer时,如果只涉及到一个字符串变量,会直接返回该字符串,避免了不必要的内存分配和对象创建。 6. **新的File API** JDK 7的`java.nio....

    jdk1.7源码

    5. **字符串增强**:包括新的`String.join()`方法用于连接字符串数组,`String.split()`的改进,以及`StringBuilder`和`StringBuffer`的`appendCodePoint()`方法,提高了处理Unicode字符的效率。 6. **改进的集合...

    java源码剖析-JavaSourceLearn:JDK1.8源码的代码分析和学习

    很多java开发的小伙伴都会阅读jdk源码,然而确不知道应该从哪读起。以下为小编整理的通常所需阅读的源码范围。 标题为包名,后面序号为优先级1-4,优先级递减 1、java.lang 1) Object 1 2) String 1 3) ...

    jdk1.7源代码

    阅读并理解JDK源码,无疑能提升我们的技术水平,增强问题解决能力,但这需要一定的基础和耐心。本文将围绕JDK1.7版本的源代码,探讨其中的关键知识点,旨在帮助读者深入理解Java的核心机制。 1. 类加载机制:JVM...

    jdk1.8-source-analysis:Java 8源码分析,JUC,ThreadPool,Collection

    JDK1.8源码分析 引入原始过程中的注意事项 JDK1.8对应JDK版本下载: 码:49wi 原始码在src目录下 以下两个类手动添加的,解决编译过程中该包的丢失 sun.font.FontConfigManager sun.awt.UNIXToolkit 其中:1.请...

    java jdk 部分源码

    - 字符串处理:研究StringBuilder、StringBuffer的内部实现,优化字符串操作。 此外,源码还可以帮助开发者找到性能瓶颈,学习最佳实践,甚至为自定义工具或库提供灵感。尽管源码阅读可能有一定的难度,但对于提升...

    jdk:jdk源码阅读

    【文件名称列表】:这里的“jdk-master”可能是指JDK的源码仓库主分支,通常在Git等版本控制系统中,master分支代表了项目的主线开发。 **详细知识点**: 1. **虚拟机(JVM)**:JDK中的虚拟机实现是Java运行的...

    浅析我对 String、StringBuilder、StringBuffer 的理解

    在Java编程语言中,String、StringBuilder和StringBuffer都是用来处理字符串的类,但它们之间存在显著的区别和使用场景。下面是对这三个类的深入理解和分析。 首先,String类是最常见的字符串对象,它以其不可变性...

    JDK中的记事本demo源码

    例如,可以使用StringBuilder或StringBuffer类来处理字符串,或者使用正则表达式进行复杂文本匹配。 5. **多线程**:为了实现异步操作,比如在保存文件时不让用户界面冻结,可以使用Java的多线程。通过创建后台线程...

    Java_JDK_10_0_1.zip

    - `jdk-10.0.1_windows-x64_bin.exe`是JDK 10.0.1的Windows 64位安装程序,用户可以通过这个文件轻松地在Windows环境中安装和配置JDK,为开发Java应用程序提供必要的环境。 了解和掌握这些关键知识点,对于使用...

    java源码解读-jdk_reading:java源码日常阅读与注解

    2. 分层阅读:从简单的类开始,逐渐深入到复杂的系统组件。 3. 跟踪调试:通过IDE的调试功能,逐步跟踪代码执行过程,直观理解逻辑。 4. 编写测试:编写单元测试来验证源码的理解,同时也可以发现潜在的问题。 四、...

Global site tag (gtag.js) - Google Analytics