Java--String、StringBuilder及StringBuffer区别及性能对比

哈喽,欢迎进来学习的小伙伴~

【学习背景】

本文会通过OpenJDK提供的Java性能测试工具JMH来测试下StringStringBuilderStringBuffer拼接字符串的效率如何~
关于JMH的介绍及具体使用,我的这篇博文中有介绍:

Java–☀️面试官:LinkedList真的比ArrayList添加元素快?❤️‍本文通过Open JDK JMH带你揭开真相《⭐建议收藏⭐》

当然,除了验证三者的字符串拼接效率之外,还会对这三者的特性及常见面试问题进行分析和总结,希望加深自己对这三者的认知,分享出来,也希望能帮助到有需要的小伙伴~

进入正文~~


学习目录

  • 一、性能测试
  • 1.1 代码实现
  • 1.2 测试结果
    • 1.2.1 普通展示
    • 1.2.2 图形展示
  • 1.3 结果分析
  • 二、区别说明
    • 2.1 String
      • 2.1.1 String特性
      • 2.1.2 String常用API
      • 2.1.3 String常见面试题(附参考答案)
    • 2.2 StringBuilder
      • 2.2.1 StringBuilder特性
      • 2.2.2 StringBuilder常用API
      • 2.2.3 StringBuilder常见面试题(附参考答案)
    • 2.3 StringBuffer
      • 2.3.1 StringBuffer特性
      • 2.3.2 StringBuffer常用API
      • 2.3.3 StringBuffer常见面试题(附参考答案)


一、性能测试

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/

测试结果可视化如下:
Java--String、StringBuilder及StringBuffer区别及性能对比_第1张图片

1.3 结果分析

字符串拼接性能:StringBuilder > StringBuffer > String

通过JMH的测试结果,可以发现在少量拼接字符串10个左右,效率区别不大,但是当字符串拼接的数据量比较大时,100左右,String比另外两者效率开始相差好几倍,当达到1000时,此时String的字符串拼接效率真的非常差非常差了,比另外两者效率低了即几十上百倍,这种情况应当避免使用String来拼接字符串~

二、区别说明

2.1 String

2.1.1 String特性

  • 实现了序列化SerializableComparable以及CharSequence字符序列接口
  • StringJava字符串对象,底层是基于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类中的equalshashCode方法,重写后equals方法比较了字符串的每一个字符,而重写hashCode方法则是由字符串的每一个字符计算出字符串的hashCode值~

2.1.2 String常用API

常用方法 方法说明
int length() 求字符串长度
boolean isEmpty() 判断字符串是否为空字符串,注意str.isEmpty()调用时,要避免strnull
String valueOf(Object obj) 转换Object类型为字符串类型
String trim() 去除字符串两端的空白
int indexOf(int ch) 返回指定字符在字符串中第一次出现的索引,这里的ch指的是char字符对应的ASCII码值
String replace(char oldChar, char newChar) 替换字符串中的字符oldCharnewChar
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都重写了equalshashCode方法,equals重写后比较的是字符串中的每一个字符,hashCode重写后则是通过数字31与字符串中的每一个字符的ASCII码值计算得到hashCode
  • 简单的说String重写equalshashCode方法的主要目的是为了比较两个对象的内容是否相同,而不是比较对象的内存地址,因为两个内容一样的字符串,可能内存地址是不相同的,不是我们想要的结果。

(2)重写String中的hashCode方法时,为什么要用31这个数字与字符串中的每一个字符的ASCII码值进行计算?

  • 因为31是数学家们计算得到的一个优选质数(如果一个数只能够被1和本身整除,不能够被其他数字整除,这个数就是质数,最小质数是2,其他3,5,7,13,17…31…37…)
    这个优选质数能够降低哈希算法的冲突率,而且31能够被JVM优化为1右移5位后再减去131 * 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,实现了SerializableCharSequence接口
  • 底层基于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区别分析就到这里结束啦,如有不妥和不足的地方,欢迎评论区指出纠正,非常感谢!!

原创不易,觉得有用的小伙伴来个一键三连(点赞+收藏+评论 )+关注支持一下,非常感谢~
在这里插入图片描述

你可能感兴趣的:(01丨Java,java,java面试,String,StringBuilder,StringBuffer)