关键词:
哈喽,欢迎进来学习的小伙伴~
【学习背景】
本文会通过OpenJDK提供的Java性能测试工具
JMH
来测试下String
、StringBuilder
及StringBuffer
拼接字符串的效率如何~
关于JMH
的介绍及具体使用,我的这篇博文中有介绍:
Java–☀️面试官:LinkedList真的比ArrayList添加元素快?❤️本文通过Open JDK JMH带你揭开真相《⭐建议收藏⭐》
当然,除了验证三者的字符串拼接效率之外,还会对这三者的特性及常见面试问题进行分析和总结,希望加深自己对这三者的认知,分享出来,也希望能帮助到有需要的小伙伴~
进入正文~~
一、性能测试
1.1 代码实现
分别编写String、StringBuilder及StringBuffer的JMH基准单元测试方法:
StringAppendJmhTest.java
package com.justin.java;
import org.openjdk.jmh.annotations.*;
import org.openjdk.jmh.infra.Blackhole;
import org.openjdk.jmh.results.format.ResultFormatType;
import org.openjdk.jmh.runner.Runner;
import org.openjdk.jmh.runner.RunnerException;
import org.openjdk.jmh.runner.options.Options;
import org.openjdk.jmh.runner.options.OptionsBuilder;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.concurrent.TimeUnit;
@BenchmarkMode(Mode.AverageTime) //基准测试类型:time/ops(每次调用的平均时间)
@OutputTimeUnit(TimeUnit.NANOSECONDS) //基准测试结果的时间类型:微秒
@Warmup(iterations = 5) //预热:5 轮
@Measurement(iterations = 5) //度量:测试5轮
@Fork(3) //Fork出3个线程来测试
@State(Scope.Thread) // 每个测试线程分配1个实例
public class StringAppendJmhTest
@Param("2", "10", "100", "1000")
private int count; //指定添加元素的不同个数,便于分析结果
@Setup(Level.Trial) // 初始化方法,在全部Benchmark运行之前进行
public void init()
System.out.println("Start...");
public static void main(String[] args) throws RunnerException
//1、启动基准测试:输出普通文件
// Options opt = new OptionsBuilder()
// .include(ArrayAndLinkedJmhTest.class.getSimpleName()) //要导入的测试类
// .output("C:\\\\Users\\\\Administrator\\\\Desktop\\\\StringAppendJmhTest.log") //输出测试结果的普通txt文件
// .build();
//1、启动基准测试:输出json结果文件(用于查看可视化图)
Options opt = new OptionsBuilder()
.include(StringAppendJmhTest.class.getSimpleName()) //要导入的测试类
.result("C:\\\\Users\\\\Administrator\\\\Desktop\\\\StringAppendJmhTest.json") //输出测试结果的json文件
.resultFormat(ResultFormatType.JSON)//格式化json文件
.build();
//2、执行测试
new Runner(opt).run();
@Benchmark
public void stringAppendTest(Blackhole blackhole)
String str = new String();
for (int i = 0; i < count; i++)
str = str + "Justin";
blackhole.consume(str);
@Benchmark
public void stringBufferAppendTest(Blackhole blackhole)
StringBuffer strBuffer = new StringBuffer();
for (int i = 0; i < count; i++)
strBuffer.append("Justin");
blackhole.consume(strBuffer);
@Benchmark
public void stringBuilderAppendTest(Blackhole blackhole)
StringBuilder strBuilder = new StringBuilder();
for (int i = 0; i < count; i++)
strBuilder.append("Justin");
blackhole.consume(strBuilder);
@TearDown(Level.Trial) // 结束方法,在全部Benchmark运行之后进行
public void clear()
System.out.println("End...");
运行main
方法进行测试~
1.2 测试结果
1.2.1 普通展示
查看控制台输出的结果信息,拉到最后查看最后几行的Score
指标如下:
Benchmark (count) Mode Cnt Score Error Units
StringAppendJmhTest.stringAppendTest 2 avgt 15 43.029 ± 4.440 ns/op
StringAppendJmhTest.stringAppendTest 10 avgt 15 212.911 ± 22.882 ns/op
StringAppendJmhTest.stringAppendTest 100 avgt 15 9262.168 ± 431.742 ns/op
StringAppendJmhTest.stringAppendTest 1000 avgt 15 830811.924 ± 38227.519 ns/op
StringAppendJmhTest.stringBufferAppendTest 2 avgt 15 35.546 ± 1.159 ns/op
StringAppendJmhTest.stringBufferAppendTest 10 avgt 15 167.670 ± 4.900 ns/op
StringAppendJmhTest.stringBufferAppendTest 100 avgt 15 1698.781 ± 80.934 ns/op
StringAppendJmhTest.stringBufferAppendTest 1000 avgt 15 14059.694 ± 820.273 ns/op
StringAppendJmhTest.stringBuilderAppendTest 2 avgt 15 27.621 ± 1.745 ns/op
StringAppendJmhTest.stringBuilderAppendTest 10 avgt 15 154.621 ± 3.360 ns/op
StringAppendJmhTest.stringBuilderAppendTest 100 avgt 15 1488.514 ± 31.618 ns/op
StringAppendJmhTest.stringBuilderAppendTest 1000 avgt 15 12032.867 ± 69.878 ns/op
示例测试结果中的Score
指标,表示ns/op
即平均每次调用需要多少微秒,时间越低说明效率越高~
1.2.2 图形展示
程序运行完成后,会在控制台输出结果信息,还会将结果信息格式化成json
格式保存到了桌面的StringAppendJmhTest.json
文件中,将json
文件通过如下可视化工具生成图形:
JMH Visualizer
:https://jmh.morethan.io/JMH Visual Chart
:http://deepoove.com/jmh-visual-chart/
测试结果可视化如下:
1.3 结果分析
字符串拼接性能:StringBuilder
> StringBuffer
> String
通过
JMH
的测试结果,可以发现在少量拼接字符串10
个左右,效率区别不大,但是当字符串拼接的数据量比较大时,100
左右,String
比另外两者效率开始相差好几倍,当达到1000
时,此时String
的字符串拼接效率真的非常差非常差了,比另外两者效率低了即几十上百倍,这种情况应当避免使用String
来拼接字符串~
二、区别说明
2.1 String
2.1.1 String特性
- ⭐ 实现了序列化
Serializable
,Comparable
以及CharSequence
字符序列接口- ⭐
String
是Java
字符串对象,底层是基于char
字符数组,使用了final
修饰类,表示最终类,不能被继承和修改,线程安全~- ⭐ 每一次对
String
声明的对象的内容进行修改,得到的都是另外一个新的字符串常量
对象,如果字符串常量池
中已经存在该字符串常量
对象,则不会再创建~- ⭐
字符串常量
在JDK1.7
之前,存在于方法区运行时常量池
中的字符串常量池
,JDK1.7
时,字符串常量池
被移到堆区中,运行时常量池
还保留在方法区中- ⭐ JDK1.8时,取消了方法区(永久代),方法区被元空间替代,
字符串常量
拼接还被自动优化成了StringBuiler
,例如:
String s1 = “Justin”;
String s2 = “Jack”;
String s3 = s1 + s2;
//先javac
编译java源文件得到Class
,再经过javap -c ClassName
反编译查看汇编指令发现,发现s1+s2
等价于
String s4 = new StringBuffer().append(s1).append(s2).toString();- ⭐
String
重写了Object
类中的equals
、hashCode
方法,重写后equals
方法比较了字符串的每一个字符,而重写hashCode
方法则是由字符串的每一个字符计算出字符串的hashCode
值~
2.1.2 String常用API
常用方法 | 方法说明 |
---|---|
int length() | 求字符串长度 |
boolean isEmpty() | 判断字符串是否为空字符串,注意str.isEmpty() 调用时,要避免str 为null |
String valueOf(Object obj) | 转换Object 类型为字符串类型 |
String trim() | 去除字符串两端的空白 |
int indexOf(int ch) | 返回指定字符在字符串中第一次出现的索引,这里的ch 指的是char 字符对应的ASCII 码值 |
String replace(char oldChar, char newChar) | 替换字符串中的字符oldChar 为newChar |
String[] split(String regex) | 根据regex 分割字符串,返回一个分割后的字符串数组 |
byte[] getBytes() | 获取字符串的 byte 类型数组 |
char charAt(int index) | 获取指定索引处的字符 |
String toLowerCase() | 将字符串中的所有大写字母转成小写字母后返回新的字符串,注意原来的字符串没变 |
String toUpperCase() | 将字符串中的所有小写字母转成大写字母后返回新的字符串,注意原来的字符串没变 |
String substring(int beginIndex, int endIndex) | 截取字符串,第一位从0 开始,包含左边beginIndex ,不包含右边endIndex |
boolean equals(Object anObject) | 比较字符串内容是否相等 |
以上是比较常用的方法,更多可以查看java.lang.String
的源码~
2.1.3 String常见面试题(附参考答案)
(1)String重写equals、hashCode方法有什么用??
- 不重写默认是
Object
中的两个方法,equals
默认进行双等号判断,比较的是两个对象的堆区内存地址是否相等,而hashCode
则是一个native
本地方法,内部会自行计算出一个唯一随机整数值返回String
都重写了equals
、hashCode
方法,equals
重写后比较的是字符串中的每一个字符,hashCode
重写后则是通过数字31
与字符串中的每一个字符的ASCII
码值计算得到hashCode
值- 简单的说
String
重写equals
、hashCode
方法的主要目的是为了比较两个对象的内容是否相同,而不是比较对象的内存地址,因为两个内容一样的字符串,可能内存地址是不相同的,不是我们想要的结果。
(2)重写String中的
hashCode
方法时,为什么要用31
这个数字与字符串中的每一个字符的ASCII码值进行计算?
- 因为
31
是数学家们计算得到的一个优选质数(如果一个数只能够被1和本身整除,不能够被其他数字整除,这个数就是质数,最小质数是2,其他3,5,7,13,17…31…37…)
这个优选质数能够降低哈希算法的冲突率,而且31
能够被JVM
优化为1右移5位后再减去1即31 * i = (i << 5) - i
(2)new String(“Justin”)创建了几个对象?
- 一个或者两个,使用
new
实例化,首先肯定会在堆区创建一个新对象,至于new String
中指定的字符串常量,如果该字符串常量在字符串常量池
中不存在,则会再次创建字符串常量池中的对象,一共两个对象~- 需要注意的是
字符串常量池
是从JDK1.7开始,就从JVM
的方法区迁移到了堆区中了,不是JDK1.8才迁移,JDK1.8是永久代被取消,同时由元空间取代了方法区~
(3)定义String s1=null,String s2="",String s3 = new String(),String s4=new String("")有什么区别?
- 主要区别在于null没有分配内存,其他三种都分配了内存空间
空字符串
也属于字符串常量,定义的引用会直接指向字符串常量池
中的字符串,如果字符串常量池
不存在空字符串
,则该过程会在字符串常量池
中创建空字符串
的对象。new String()
由于使用了new
实例化,必然会在堆区创建一个新对象,而new String()
底层默认将空字符串
作为字符串对象的值,因此该过程可能创建了1
个对象或2
个对象- 同样
new String("")
跟new String()
一样也是可能创建了1
个对象或2
个对象~
(3)String、StringBuilder及StringBuffer最大的区别是什么?
- 最大的区别在于String使用
final
修饰,表示最终类,不可继承和修改,线程安全- 而StringBuilder和StringBuffer都是可修改对象,StringBuffer使用
synchronized
同步修饰方法,线程安全,StringBuilder非线程安全~String
在JDK1.8时字符串常量
拼接被自动优化成了StringBuiler
- 关于字符串拼接效率,我个人通过Open JDK基准性能测试工具
JMH
对三者的new实例化对象,进行字符串的拼接测试,发现效率始终是:
StringBuilder > StringBuffer > String
而且在少量拼接字符串10
个左右时,三者的拼接效率区别并不大,但是当字符串拼接的数据量比较大时,100
左右,String
比另外两者效率开始相差好几倍,当达到1000
时,此时String
的字符串拼接效率真的非常差非常差了,比另外两者效率低了即几十上百倍,这种情况应当避免使用String
来拼接字符串~
2.2 StringBuilder
2.2.1 StringBuilder特性
- ⭐ 底层继承了
AbstractStringBuilder
,实现了Serializable
、CharSequence
接口- ⭐ 底层基于char字符数组,可以修改操作对象,非线程安全
- ⭐ 实例化
new StringBuffer()
时默认字节数组初始化容量大小为16
,当容量大于当前字节数组容量时会自动进行1倍扩容再加2,每次扩容都会开辟新空间,并且进行新老字符数组的复制- ⭐ 源码底层通过调用
System
的一个native
本地方法arraycopy
实现新老字符数组的复制,该native方法底层会直接操作内存,比一般的for
循环遍历复制数组的效率要快很多~- ⭐ 如果要操作拼接字符串,并且拼接的字符串很长,又没有给
StringBuilder
指定合适的初始化容量大小,可能会导致底层的字符数组进行多次扩容,多次申请内存空间来完成新老字符数组的复制,性能开销比较大~
StringBuilder扩容机制的关键源码:
//扩容条件:当容量大于当前字节数组容量时
if (minimumCapacity - value.length > 0) expandCapacity(minimumCapacity);
...
//扩容多少:会自动进行1倍扩容再加2
int newCapacity = value.length * 2 + 2;
...
//新老字符数组的复制
value = Arrays.copyOf(value, newCapacity);
...
public static char[] copyOf(char[] original, int newLength)
char[] copy = new char[newLength];
//底层操作内存进行复制原字符数组的元素到新字节数组
System.arraycopy(original, 0, copy, 0, Math.min(original.length, newLength));
return copy;
...
public static native void arraycopy(Object src, int srcPos,
Object dest, int destPos,
int length);
2.2.2 StringBuilder常用API
常用方法 | 说明 |
---|---|
StringBuilder append(String str) | 拼接字符串 |
String toString() | 返回字符串内容 |
char charAt(int index) | 获取指定索引的字符 |
StringBuilder insert(int offset, String str) | 在指定位置offset 之前插入字符串 |
void setCharAt(int index, char ch) | 将指定位置index 的字符替换为ch |
StringBuilder insert(int offset, String str) | 在指定位置offset 之前插入字符串 |
StringBuilder delete(int start, int end | 删除起始位置start (含)到结尾位置end (不含)之间的字符串 |
其他方法请查看java.lang.StringBuilder
源码详情~
2.2.3 StringBuilder常见面试题(附参考答案)
(1)讲一下StringBuilder的扩容机制?
- 主要结合StringBuffer特性来回答即可~
- ⭐ 实例化
new StringBuffer()
时默认字节数组初始化容量大小为16
,当容量大于当前字节数组容量时会自动进行1倍扩容再加2,每次扩容都会开辟新空间,并且进行新老字符数组的复制- ⭐ 源码底层通过调用
System
的一个native
本地方法arraycopy
实现新老字符数组的复制,该native方法底层会直接操作内存,比一般的for
循环遍历复制数组的效率要快很多~- ⭐ 如果操作的字符串很长,又没有给
StringBuilder
指定合适的初始化容量大小,可能会导致底层的字符数组进行多次扩容,多次申请内存空间来完成新老字符数组的复制,性能开销比较大~
(2)String、StringBuilder及StringBuffer最大的区别是什么?
- 同String常见面试问题解答即可~
2.3 StringBuffer
2.3.1 StringBuffer特性
- ⭐ StringBuffer底层实现与StringBuffer最大的区别在于方法使用了
synchronized
(自创谐音:星可nice的,哈哈哈)同步修饰,因此是线程安全的,StringBuilder非线程安全~
2.3.2 StringBuffer常用API
跟StringBuilder常用API一样,只不过加了synchronized
修饰,线程安全~
2.3.3 StringBuffer常见面试题(附参考答案)
(1)StringBuffer为什么是线程安全的?
- StringBuffer底层使用synchronized同步修饰方法,因此是线程安全的~
(2)为什么StringBuffer使用synchronized修饰方法就能保证线程安全?
- synchronized是一个同步锁,在Java中每个类对象都可以作为锁,synchronized同步锁使用的关键在于对谁加锁~
- synchronized修饰普通方法
synchronized methodA()//操作
,是对当前对象加锁~- synchronized修饰静态方法
static synchronized void methodB()//操作
,是对当前类的class对象(所有此类的对象)加锁~- synchronized修饰代码块
methodC(obj)synchronized(obj) //操作
,是对括号中的对象加锁 ~- 因此,使用synchronized修饰方法时,会对方法中的相关对象进行加锁,如果某个线程抢先调用了该方法,那么将独占相关对象的锁,其他线程如果此时调用到该方法的相关对象时,会被阻塞~
(3)String、StringBuilder及StringBuffer最大的区别是什么?
- 最大的区别在于String使用
final
修饰,表示最终类,不可继承和修改,线程安全- 而StringBuilder和StringBuffer都是可修改对象,StringBuffer使用
synchronized
同步修饰方法,线程安全,StringBuilder非线程安全~String
在JDK1.8时字符串常量
拼接被自动优化成了StringBuiler
- 关于字符串拼接效率,我个人通过Open JDK基准性能测试工具
JMH
对三者的new实例化对象,进行字符串的拼接测试,发现效率始终是:
StringBuilder > StringBuffer > String
而且在少量拼接字符串10
个左右时,三者的拼接效率区别并不大,但是当字符串拼接的数据量比较大时,100
左右,String
比另外两者效率开始相差好几倍,当达到1000
时,此时String
的字符串拼接效率真的非常差非常差了,比另外两者效率低了即几十上百倍,这种情况应当避免使用String
来拼接字符串~
好了,本文的String、StringBuilder及StringBuffer区别分析就到这里结束啦,如有不妥和不足的地方,欢迎评论区指出纠正,非常感谢!!
原创不易,觉得有用的小伙伴来个一键三连(点赞+收藏+评论 )+关注
支持一下,非常感谢~
面试官:小伙子你给我讲一下integer和int的区别?
前言Integer和int最本质的区别就是:Integer是封装类,int是基本数据类型(这是废话)。本文是希望能对Integer和int的区别进行更详细的对比说明并加以举例Integer和int的区别Integer的默认初始值是null,而int的初始值是int也就是说Intege... 查看详情
面试官:小伙子,你给我讲一下java类加载机制和内存模型吧发布文章###类加载机制虚拟
类加载机制虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的java类型,这就是虚拟机的类加载机制。类的生命周期加载(Loading)验证(Verification)准备(Pr... 查看详情
面试官:讲一下jvm中如何判断对象的生死?(代码片段)
但凡问到JVM(Java虚拟机)通常有99%的概率一定会问,在JVM中如何判断一个对象的生死状态?判断对象的生死状态的算法有以下几个:1、引用计数器算法引用计算器判断对象是否存活的算法是这样的:给每一个对象设置一个引用... 查看详情
❤️爆肝!整理了一周的spring面试大全含答案,吊打java面试官建议收藏!❤️(代码片段)
对于Java的小伙伴来说,Spring是面试的必问环节,我研究Spring多年,甚至我的网名都叫SpringMeng。最新整理的数据结构和算法的值得收藏:❤️肝完了,一天掌握数据结构和算法面试题,吊打面试官,建... 查看详情
java--☀️面试官:linkedlist真的比arraylist添加元素快?❤️本文通过openjdkjmh带你揭开真相《⭐建议收藏⭐》(代码片段)
...,还是入行Java几年的小伙伴,相信很多小伙伴在面试Java工作岗位时,发现LinkedList和ArrayList这两者相关的问题基本是必面的~但是当面试官问到LinkedList和ArrayList的区别时,可能很多准备得不够充分 查看详情
java面试题⭐多线程篇⭐(万字总结,带答案,面试官问烂,跳槽必备,建议收藏)(代码片段)
个人主页:Java李小立后面会持续更新java面试专栏,请持续关注如果文章对你有帮助、欢迎关注、点赞、收藏(一键三连❤️❤️❤️)面试宝典列表(持续更新):序号内容链接地址1Java基础篇(点击跳转)ja... 查看详情
java面试题⭐多线程篇⭐(万字总结,带答案,面试官问烂,跳槽必备,建议收藏)(代码片段)
个人主页:Java李小立后面会持续更新java面试专栏,请持续关注如果文章对你有帮助、欢迎关注、点赞、收藏(一键三连❤️❤️❤️)面试宝典列表(持续更新):序号内容链接地址1Java基础篇(点击跳转)ja... 查看详情
面试官:小伙子,你给我说一下javaexception和error的区别吧?
...丝朋友私信问我Java的Exception和Error有什么区别呢?说他在面试的时候被问到这个问题卡壳了,最后还好也是有惊无险的过了。在恭喜这位粉丝的同时,我们再回过头来这个问题,其实在面试中这是个常见的连环问题了,大多数面... 查看详情
❤️爆肝!整理了一周的spring面试大全含答案,吊打java面试官建议收藏!❤️(代码片段)
对于Java的小伙伴来说,Spring是面试的必问环节,我研究Spring多年,甚至我的网名都叫SpringMeng。最新整理的数据结构和算法的值得收藏:❤️肝完了,一天掌握数据结构和算法面试题,吊打面试官,建... 查看详情
❤️大三的时候,看了这些java面试题附答案,我进华为了![建议收藏]❤️(代码片段)
...底层、项目经验都要刷,猛哥以后会给大家更新各种面试题……如果要进大厂,项目经验、底层算法、网络、数据机构等都要狂刷……原来我进大厂之前就是这么干的,刷项目,刷题。文末附面试资料。孟哥以后... 查看详情
❤️肝完了,一天掌握数据结构和算法面试题,吊打面试官,建议收藏❤️
最近有小伙伴面试,对数据结构和算法比较头疼,我整理了一波资料,帮助大家快速掌握数据结构和算法的面试,感觉有用的小伙伴,点赞支持哦!不叨叨,直接上干货。目录Q1:数据结构和算法的... 查看详情
❤️三万字《c/c++面试突击200题》四年面试官爆肝整合❤️(附答案,建议收藏)(代码片段)
... 辛苦搬砖「十年」的老码农,「四年」的C/C++面试官经验,整理出了一些当年用来「虐候选人」的题(不要打我🤣🤣🤣),先弄个「200」题吧,还有「8800」题,后面有时间再搞吧... 查看详情
❤️三万字《c/c++面试突击200题》四年面试官爆肝整合❤️(附答案,建议收藏)(代码片段)
... 辛苦搬砖「十年」的老码农,「四年」的C/C++面试官经验,整理出了一些当年用来「虐候选人」的题(不要打我🤣🤣🤣),先弄个「200」题吧,还有「8800」题,后面有时间再搞吧... 查看详情
❤️女朋友面试被问内联函数❤️一文讲的明明白白!(代码片段)
...猿🎈简介:CSDN博客专家🏆,C/C++、面试、刷题、算法尽管咨询我,关注我,有问题私聊!🎈关注专栏:C/C++面试通关集锦 (优质好文持续更新中……) 查看详情
只要你认真看完一万字☀️linux操作系统基础知识☀️分分钟钟都吊打面试官《❤️记得收藏❤️》(代码片段)
...完一万字☀️Linux操作系统基础知识☀️分分钟钟都吊打面试官《❤️记得收藏❤️》目录🏳️🌈开讲之前我们先庆祝我国赢得38金牌。🏳️🌈中国加油!!🏳️🌈😊开讲啦!ÿ... 查看详情
❤️学妹面试被问bfs和dfs的区别❤️,我一文讲的明明白白!
...猿🎈简介:CSDN博客专家🏆,C/C++、面试、刷题、算法尽管咨询我,关注我,有问题私聊!🎈关注专栏:C/C++面试通关集锦 (优质好文持续更新中……) 查看详情
:p6级面试
...通过这几篇博客总结,快速实现复习,从而达到面试资深Java开发岗拿到offer的总结,还算比较高频的一些面试题讲解吧,对于面试而已,一般的技术面试不管是二轮还是三轮面试,对于非大厂,中小企... 查看详情
想搞定大厂面试官?java思想编程电子版
美团一面(50分钟左右)进程和线程死锁的必要条件网络,七层协议TCP和UDP的区别hashmap详细讲一下hashmap底层是如何解决hash冲突的hashmap和linkedhashmap数据库的索引,为什么推荐自增id,有什么优点MySQL的引擎... 查看详情