同事如此使用StringBuilder,我给他提了一个Bug

简介: 同事如此使用StringBuilder,我给他提了一个Bug

字符串的拼接在项目中使用的非常频繁,但稍不留意往往又会造成一些性能问题。最近Review代码时发现同事写了如下的代码,于是给他提了一个bug。

@Test
public void testForAdd() {
    String result = "NO_";
    for (int i = 0; i < 10; i++) {
        result += i;
    }
    System.out.println(result);
}

本文就带大家从表象到底层的来聊聊,为什么这种写法会有性能问题。

IDE的提示

如果你使用的IDE安装了代码检查的插件,会很轻易的看到上面代码中的“+=”操作会有黄色的背景,这是插件在提示,此处使用有问题。

下面来看一下关于“+=”,IDEA给出的提示详情:

String concatenation ‘+=’ in loop

Inspection info: Reports String concatenation in loops. As every String concatenation copies the whole String, usually it is preferable to replace it with explicit calls to StringBuilder.append() or StringBuffer.append().


这段提示简单翻译过来就是:循环中,字符串拼接使用了“+=”。检验信息:报告循环中的字符串拼接。每次String的拼接都会复制整个String。通常建议将其替换为StringBuilder.append()或StringBuffer.append()。


提示信息中给出了原因,并且给出了解决方案的建议。但事实真的如提示中这么简单吗?Java8以后使用String拼接JVM编译时不是已经默认优化构建成StringBuilder了吗,怎么还有问题?下面我们就来深入分析一下。


字节码的反编译

对上面的代码,我们通过字节码反编译一下,看看JVM在此过程中是否帮我们进行了优化,是否涉及到整个String的复制。


使用javap -c命令来查看字节码内容:



public void testForAdd();
Code:
   //从常量池引用#2并推向栈顶,操作了String初始化的变量“NO_”
   0: ldc           #2                  // String NO_
   2: astore_1
   3: iconst_0
   4: istore_2
   5: iload_2
   6: bipush        10
   //如果栈顶两个值大于等于0(此时0-10)则跳转36(code),这里开始进入for循环处理
   8: if_icmpge     36
   //创建StringBuilder对象,其引用进栈
  11: new           #3                  // class java/lang/StringBuilder
  14: dup
  //调用StringBuilder的构造方法
  15: invokespecial #4                  // Method java/lang/StringBuilder."<init>":()V
  18: aload_1
  19: invokevirtual #5                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
  22: iload_2
  //调用append方法
  23: invokevirtual #6                  // Method java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;
  //调用toString方法,并将产生的String存入栈顶
  26: invokevirtual #7                  // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
  29: astore_1
  30: iinc          2, 1
  33: goto          5
  36: getstatic     #8                  // Field java/lang/System.out:Ljava/io/PrintStream;
  39: aload_1
  40: invokevirtual #9                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
  43: return

上述反编译的字节码操作中已经将关键部分标注出来了。编号0处会加载定义的“NO_”字符串,编号8处开始进行循环的判断,符合条件(0-10)的部分便会执行后续的循环体中的内容。在循环体内,编号11创建StringBuilder对象,编号15调用StringBuilder的构造方法,编号23调用append方法,编号26调用toString方法。


经过上述的步骤我们能够发现什么?JVM在编译时的确帮我们进行了优化,将for循环中的字符串拼接转化成了StringBuilder,并通过appen方法和toString方法进行处理。这样有问题吗?JVM已经优化了啊!


但是,关键问题来了:每次for循环都会新创建一个StringBuilder,都会进行append和toString操作,然后销毁。这就变得可怕了,这与每次都创建String对象并复制有过之而无不及。


经过上述分析之后,上面的代码的效果相当于如下代码:


@Test
public void testForAdd1() {
    String result = "NO_";
    for (int i = 0; i < 10; i++) {
        result = new StringBuilder(result).append(i).toString();
    }
    System.out.println(result);
}

这样来看是不是更直观了?至此,想必大家已经明白为什么给那位同事提bug了吧。

方案改进

那么,针对上面的问题,代码该如何进行改进呢?直接上代码:

@Test
public void testForAppend() {
    StringBuilder result = new StringBuilder("NO_");
    for (int i = 0; i < 10; i++) {
        result.append(i);
    }
    System.out.println(result);
}

将StringBuilder对象的创建放在外面,for循环中直接调用append即可。再来看一下这段代码的字节码操作:

public void testForAppend();
Code:
   0: new           #3                  // class java/lang/StringBuilder
   3: dup
   4: ldc           #2                  // String NO_
   6: invokespecial #10                 // Method java/lang/StringBuilder."<init>":(Ljava/lang/String;)V
   9: astore_1
  10: iconst_0
  11: istore_2
  12: iload_2
  13: bipush        10
  15: if_icmpge     30
  18: aload_1
  19: iload_2
  20: invokevirtual #6                  // Method java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;
  23: pop
  24: iinc          2, 1
  27: goto          12
  30: getstatic     #8                  // Field java/lang/System.out:Ljava/io/PrintStream;
  33: aload_1
  34: invokevirtual #11                 // Method java/io/PrintStream.println:(Ljava/lang/Object;)V
  37: return

对照最开始的字节码内容,看看是不是简化了很多,问题完美解决。

for循环内的场景

上面介绍的使用场景主要针对通过for循环来获得一个整字符串,但某些业务场景中可能拼接字符串本身只在for循环当中,并不会在for循环外部处理,比如:

@Test
public void testInfoForAppend() {
    for (int i = 0; i < 10; i++) {
        String result = "NO_" + i;
        System.out.println(result);
    }
}

上述代码中for循环内部的字符串拼接还可能会更复杂,我们已经知道JVM会优化成上面提到的StringBuilder进行处理。同时,每次都会创建StringBuilder对象,那么针对这种情况,只能听之任之吗?


其实,还可以考虑另外一个思路,那就是在for循环外部创建一个StringBuilder,然后在内部使用完之后进行清空处理。有两种方式可以实现清空:delete方法删除和setLength方法。


直接上两种方法的示例代码:


@Test
public void testDelete() {
    StringBuilder result = new StringBuilder();
    for (int i = 0; i < 10; i++) {
        result.delete(0,result.length());
        result.append(i);
        System.out.println(result);
    }
}
@Test
public void testSetLength() {
    StringBuilder result = new StringBuilder();
    for (int i = 0; i < 10; i++) {
        result.setLength(0);
        result.append(i);
        System.out.println(result);
    }
}

关于上述示例的验证和底层操作,感兴趣的朋友可以继续深挖一下,这里只说结论。经过试验,这两种方法的性能都要比默认的处理方式要好很多。同时delete操作的方式略微优于setLength的方式,推荐使用delete的方式。


小结

通过IDE的一个提示信息,我们进行底层原理深挖及实现的验证,竟然发现这么多可提升的空间和隐藏知识点,是不是很有成就感?最后,我们再来稍微总结一下String和StringBuilder涉及到的知识点(基于Java8及以上版本):


没有循环的字符串拼接,直接使用+就可以,JVM会帮我们进行优化。

并发场景进行字符串拼接,使用StringBuffer代替StringBuilder,StringBuffer是线程安全的。

循环内JVM的优化存在一定的缺陷,可在循环体外构建StringBuilder,循环体内进行append操作。

对于纯循环体内使用的字符串拼接,可在循环体外构建StringBuilder,使用完进行清除操作(delete或setLength)。



目录
相关文章
|
10天前
|
存储 关系型数据库 分布式数据库
PostgreSQL 18 发布,快来 PolarDB 尝鲜!
PostgreSQL 18 发布,PolarDB for PostgreSQL 全面兼容。新版本支持异步I/O、UUIDv7、虚拟生成列、逻辑复制增强及OAuth认证,显著提升性能与安全。PolarDB-PG 18 支持存算分离架构,融合海量弹性存储与极致计算性能,搭配丰富插件生态,为企业提供高效、稳定、灵活的云数据库解决方案,助力企业数字化转型如虎添翼!
|
9天前
|
存储 人工智能 Java
AI 超级智能体全栈项目阶段二:Prompt 优化技巧与学术分析 AI 应用开发实现上下文联系多轮对话
本文讲解 Prompt 基本概念与 10 个优化技巧,结合学术分析 AI 应用的需求分析、设计方案,介绍 Spring AI 中 ChatClient 及 Advisors 的使用。
403 130
AI 超级智能体全栈项目阶段二:Prompt 优化技巧与学术分析 AI 应用开发实现上下文联系多轮对话
|
3天前
|
存储 安全 前端开发
如何将加密和解密函数应用到实际项目中?
如何将加密和解密函数应用到实际项目中?
197 138
|
9天前
|
人工智能 Java API
AI 超级智能体全栈项目阶段一:AI大模型概述、选型、项目初始化以及基于阿里云灵积模型 Qwen-Plus实现模型接入四种方式(SDK/HTTP/SpringAI/langchain4j)
本文介绍AI大模型的核心概念、分类及开发者学习路径,重点讲解如何选择与接入大模型。项目基于Spring Boot,使用阿里云灵积模型(Qwen-Plus),对比SDK、HTTP、Spring AI和LangChain4j四种接入方式,助力开发者高效构建AI应用。
377 122
AI 超级智能体全栈项目阶段一:AI大模型概述、选型、项目初始化以及基于阿里云灵积模型 Qwen-Plus实现模型接入四种方式(SDK/HTTP/SpringAI/langchain4j)
|
3天前
|
存储 JSON 安全
加密和解密函数的具体实现代码
加密和解密函数的具体实现代码
196 136
|
21天前
|
弹性计算 关系型数据库 微服务
基于 Docker 与 Kubernetes(K3s)的微服务:阿里云生产环境扩容实践
在微服务架构中,如何实现“稳定扩容”与“成本可控”是企业面临的核心挑战。本文结合 Python FastAPI 微服务实战,详解如何基于阿里云基础设施,利用 Docker 封装服务、K3s 实现容器编排,构建生产级微服务架构。内容涵盖容器构建、集群部署、自动扩缩容、可观测性等关键环节,适配阿里云资源特性与服务生态,助力企业打造低成本、高可靠、易扩展的微服务解决方案。
1347 8
|
8天前
|
监控 JavaScript Java
基于大模型技术的反欺诈知识问答系统
随着互联网与金融科技发展,网络欺诈频发,构建高效反欺诈平台成为迫切需求。本文基于Java、Vue.js、Spring Boot与MySQL技术,设计实现集欺诈识别、宣传教育、用户互动于一体的反欺诈系统,提升公众防范意识,助力企业合规与用户权益保护。
|
20天前
|
机器学习/深度学习 人工智能 前端开发
通义DeepResearch全面开源!同步分享可落地的高阶Agent构建方法论
通义研究团队开源发布通义 DeepResearch —— 首个在性能上可与 OpenAI DeepResearch 相媲美、并在多项权威基准测试中取得领先表现的全开源 Web Agent。
1460 87