声明:本文首发于博客园,作者:后青春期的Keats;地址:https://www.cnblogs.com/keatsCoder/ 转载请注明,谢谢!

引言

都说 StringBuilder 在处理字符串拼接上效率要强于 String,但有时候我们的理解可能会存在一定的偏差。最近我在测试数据导入效率的时候就发现我以前对 StringBuilder 的部分理解是错误的。 后来我通过实践测试 + 找原理 的方式搞清楚了这块的逻辑。现在将过程分享给大家

测试用例

我们的代码在循环中拼接字符串一般有两种情况

  • 第一种就是每次循环将对象中的几个字段拼接成一个新字段,再赋值给对象
  • 第二种操作是在循环外创建一个字符串对象,每次循环向该字符串拼接新的内容。循环结束后得到拼接好的字符串

对于这两种情况,我创建了两个对照组

第一组:

在每次 For 循环中拼接字符串,即拼即用、用完即毁。分别使用 String 和 StringBuilder 拼接

    /**
* 循环内 String 拼接字符串,一次循环后销毁
*/
public static void useString(){
for (int i = 0; i < CYCLE_NUM_BIGGER; i++) {
String str = str1 + i + str2 + i + str3 + i + str4 ;
}
} /**
* 循环内 使用 StringBuilder 拼接字符串,一次循环后销毁
*/
public static void useStringBuilder(){
for (int i = 0; i < CYCLE_NUM_BIGGER; i++) {
StringBuilder sb = new StringBuilder();
String s = sb.append(str1).append(i).append(str2).append(i).append(str3).append(i).append(str4).toString();
}
}

第二组:

多次 For 循环拼接一个字符串,循环结束后使用字符串,使用后由垃圾回收器回收。也是分别使用 String 和 StringBuilder 拼接

    /**
* 多次循环拼接成一个字符串 用 String
*/
public static void useStringSpliceOneStr (){
String str = "";
for (int i = 0; i < CYCLE_NUM_LOWER; i++) {
str += str1 + str2 + str3 + str4 + i;
}
} /**
* 多次循环拼接成一个字符串 用 StringBuilder
*/
public static void useStringBuilderSpliceOneStr(){
StringBuilder sb = new StringBuilder();
for (int i = 0; i < CYCLE_NUM_LOWER; i++) {
sb.append(str1).append(str2).append(str3).append(str4).append(i);
}
}

为了保证测试质量,在每个测试项目进行前。线程休息 2s,之后空跑 5 次热身。最后执行 5 次求平均时间的方式计算时间

    public static int executeSometime(int kind, int num) throws InterruptedException {
Thread.sleep(2000);
int sum = 0;
for (int i = 0; i < num + 5; i++) {
long begin = System.currentTimeMillis(); switch (kind){
case 1:
useString();
break;
case 2:
useStringBuilder();
break;
case 3:
useStringSpliceOneStr();
break;
case 4:
useStringBuilderSpliceOneStr();
break;
default:
return 0;
} long end = System.currentTimeMillis(); if(i > 5){
sum += (end - begin);
}
}
return sum / num;
}

主方法

public class StringTest {
public static final int CYCLE_NUM_BIGGER = 10_000_000;
public static final int CYCLE_NUM_LOWER = 10_000;
public static final String str1 = "张三";
public static final String str2 = "李四";
public static final String str3 = "王五";
public static final String str4 = "赵六"; public static void main(String[] args) throws InterruptedException {
int time = 0;
int num = 5; time = executeSometime(1, num);
System.out.println("String拼接 "+ CYCLE_NUM_BIGGER +" 次," + num + "次平均时间:" + time + " ms"); time = executeSometime(2, num);
System.out.println("StringBuilder拼接 "+ CYCLE_NUM_BIGGER +" 次," + num + "次平均时间:" + time + " ms"); time = executeSometime(3, num);
System.out.println("String拼接单个字符串 "+ CYCLE_NUM_LOWER +" 次," + num + "次平均时间:" + time + " ms"); time = executeSometime(4, num);
System.out.println("StringBuilder拼接单个字符串 "+ CYCLE_NUM_LOWER +" 次," + num + "次平均时间:" + time + " ms"); }
}

测试结果

测试结果如下

结果分析

第一组

10_000_000 次循环拼接,在循环内使用 String 和 StringBuilder 的效率是一样的!为什么呢?

使用 javap -c StringTest.class 反编译查看两个方法编译后的文件:

可以发现 String 方法拼接字符串编译器优化后使用的就是 StringBuilder、因此用例1 和用例2 的效率是一样的。

第二组

第二组的结果就是大家喜闻乐见的了,由于 10_000_000 次循环String 拼接实在太慢所以我采用了 10_000 次拼接来分析。

分析用例3:虽然编译器会对 String 拼接做优化,但是它每次在循环内创建 StringBuilder 对象,在循环内销毁。下次循环他有创建。相比较用例4在循环外创建,多了 n 次 new 对象、销毁对象的操作、n - 1 次将 StringBuilder 转换成 String 的操作 。效率低也是理所应当了。

扩展

第一组的测试还有一种写法:

    /**
* 循环内 使用 StringBuilder 拼接字符串,一次循环后销毁
*/
public static void useStringBuilderOut(){
StringBuilder sb = new StringBuilder();
for (int i = 0; i < CYCLE_NUM_BIGGER; i++) {
// sb.setLength(0);
sb.delete(0, sb.length());
String s = sb.append(str1).append(i).append(str2).append(i).append(str3).append(i).append(str4).toString();
}
}

循环外创建 StringBuilder 每次循环开始的时候清空 StringBuilder 的内容然后拼接。这种写法无论使用 sb.setLength(0); 还是 sb.delete(0, sb.length()); 效率都比直接在循环内使用 String / StringBuilder 慢。奈何才疏学浅我一直想不明白为什么他慢。我猜测是 new 对象的速度比重置长度慢,于是这样测试了以下:

    public static void createStringBuider() {
for (int i = 0; i < CYCLE_NUM_BIGGER; i++) {
StringBuilder sb = new StringBuilder();
}
} public static void cleanStringBuider() {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < CYCLE_NUM_BIGGER; i++) {
sb.delete(0, sb.length());
}
}

但是结果是 cleanStringBuider 更快。让我摸不着头脑

如果有大神看到希望可以帮忙分析分析

结论

  • 编译器会将 String 拼接优化成使用 StringBuilder,但是还是有一些缺陷的。主要体现在循环内使用字符串拼接,编译器不会创建单个 StringBuilder 以复用

  • 对于多次循环内拼接一个字符串的需求:StringBuilder 很快,因为其避免了 n 次 new 对象、销毁对象的操作,n - 1 次将 StringBuilder 转换成 String 的操作

  • StringBuilder 拼接不适用于循环内每次拼接即用的操作方式。因为编译器优化后的 String 拼接也是使用 StringBuilder 两者的效率一样。后者写起来还方便...

StringBuider 在什么条件下、如何使用效率更高?的更多相关文章

  1. Effective Java 第三版——69. 仅在发生异常的条件下使用异常

    Tips 书中的源代码地址:https://github.com/jbloch/effective-java-3e-source-code 注意,书中的有些代码里方法是基于Java 9 API中的,所 ...

  2. Atitit 如何创新 创新只有在两种条件下发生:自由、效率。

    Atitit 如何创新 创新只有在两种条件下发生:自由.效率. 创新是如何发生的呢? 创新只有在两种条件下发生:自由.效率.在自由的环境下,对效率的追逐等于创新.如果你不自由,你的思想不够开阔,你脑洞 ...

  3. java中继承条件下构造方法的执行过程

    继承条件下构造方法的调用规则如下: 情况1:如果子类的构造方法中没有通过super显式调用父类的有参构造方法,也没有通过this显式调用自身的其他构造方法,则系统会默认先调用父类的无参构造方法.在这种 ...

  4. 使用n2n在没有公网IP条件下访问树莓派

    实现:在树莓派2和客户机都没有公网IP条件下实现远程访问控制 不足:暂时没实现网页代理 因为校园网环境没有公网IP,无法直接访问树莓派.之前有想过SSH反向代理:使用VPN,ddns(花生壳.no-i ...

  5. .NET DLL 保护措施详解(五)常规条件下的破解

    为了证实在常规手段破解下能有效保护程序核心功能(演示版本对AES加解密算法及数据库的密钥(一段字符串)进行了保护),特对此DLL保护思路进行相应的测试,包含了反编译及反射测试,看是否能得到AES加解密 ...

  6. 《Python CookBook2》 第四章 Python技巧 - 若列表中某元素存在则返回之 &amp;&amp; 在无须共享引用的条件下创建列表的列表

    若列表中某元素存在则返回之 任务: 你有一个列表L,还有一个索引号i,若i是有效索引时,返回L[i],若不是,则返回默认值v 解决方案: 列表支持双向索引,所以i可以为负数 >>> ...

  7. 如何在已安装Python条件下,安装Anaconda,,并将原有Python添加到Anaconda中

    在安装Anaconda之前,有的已经安装过一个Python版本了,但是又不想删除这个Python版本,该怎么办呢? 概括:轻松两步--在系统环境变量中找到对应之前安装Python的路径并删除:直接将你 ...

  8. C#本质论第四版-1,抄书才能看下去,不然两三眼就看完了,一摞书都成了摆设。抄下了记忆更深刻

    C#本质论第四版-1,抄书才能看下去,不然两三眼就看完了,一摞书都成了摆设.抄下了记忆更深刻 本书面向的读者 写作本书时,我面临的一个挑战是如何持续吸引高级开发人员眼球的同时,不因使用assembly ...

  9. 最长公共子序列-LCS问题 (LCS与LIS在特殊条件下的转换) [洛谷1439]

    题目描述 给出1-n的两个排列P1和P2,求它们的最长公共子序列. 输入 第一行是一个数n, 接下来两行,每行为n个数,为自然数1-n的一个排列. 输出 一个数,即最长公共子序列的长度 输入样例 5 ...

随机推荐

  1. Codeforces 543D. Road Improvement (树dp + 乘法逆元)

    题目链接:http://codeforces.com/contest/543/problem/D 给你一棵树,初始所有的边都是坏的,要你修复若干边.指定一个root,所有的点到root最多只有一个坏边 ...

  2. 生成XML文件,通过实体生成XML文件

    实体 using System;using System.Collections.Generic;using System.Linq;using System.Text;using System.Xm ...

  3. Common Git command and mean (Windows)

    Config: git config --system git config --global git config --global merge.tool vimdiff Check config: ...

  4. Spring通过AOP实现对Redis的缓存同步

    废话不多说 说下思路:使用aop注解,在Service实现类添加需要用到redis的方法上,当每次请求过来则对其进行拦截,如果是查询则从redis进行get key,如果是update则删除key,防 ...

  5. TCP长连接与短连接的原理及区别

    一.当网络通信时采用TCP协议时:  1.过程: 第一步:(在真正的读写操作之前)Server 和Client 之间必须建立一个连接,连接的建立需要三次握手 经典的三次握手示意图: 第二步:进行读写操 ...

  6. C++智能指针(auro_ptr...)

    写的很好,忍不住转了: 博文原址:http://blog.csdn.net/xt_xiaotian/article/details/5714477 一.简介 由于 C++ 语言没有自动内存回收机制,程 ...

  7. 工作随笔—Java容器基础知识分享(持有对象)

    1. 概述 通常,程序总是运行时才知道的根据某些条件去创建新对象.在此之前,不会知道所需对象的数量,甚至不知道确切的类型,为解决这个普遍的编程问题:需要在任意时刻和任意位置创建任意数量的对象,所以,就 ...

  8. MySQL--批量KILL连接

    ============================================== 使用SELECT INTO OUTFILE方式获取到要删除的连接ID并保存为文件,在通过SOURCE执行 ...

  9. How To Scan QRCode For UWP (3)

    这一节主要介绍如何去设置MediaCapture拍照的分辨率. MediaCapture 包含一个 VideoDeviceController对象,凭借它可以控制摄像头的很多设置,其中包括设置拍照的分 ...

  10. [POJ1655]Balancing Act

    思路:DP求树的重心.对于每个结点$i$,$d_i=\displaystyle{\sum_{j\in s(i)}}d_j+1$.删去这个点能得到的最大子树大小即为$\max(\max\limits_{ ...