Java--String、StringBuilder及StringBuffer区别及性能对比_java string 和 stringbuilder的性能对比

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化学习资料的朋友,可以戳这里获取

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!


学习目录

一、性能测试

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文件通过如下可视化工具生成图形:

测试结果可视化如下:
在这里插入图片描述

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位后再减去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,实现了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);


(1)Python所有方向的学习路线(新版)

这是我花了几天的时间去把Python所有方向的技术点做的整理,形成各个领域的知识点汇总,它的用处就在于,你可以按照上面的知识点去找对应的学习资源,保证自己学得较为全面。

最近我才对这些路线做了一下新的更新,知识体系更全面了。

在这里插入图片描述

(2)Python学习视频

包含了Python入门、爬虫、数据分析和web开发的学习视频,总共100多个,虽然没有那么全面,但是对于入门来说是没问题的,学完这些之后,你可以按照我上面的学习路线去网上找其他的知识资源进行进阶。

在这里插入图片描述

(3)100多个练手项目

我们在看视频学习的时候,不能光动眼动脑不动手,比较科学的学习方法是在理解之后运用它们,这时候练手项目就很适合了,只是里面的项目比较多,水平也是参差不齐,大家可以挑自己能做的项目去练练。

在这里插入图片描述

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化学习资料的朋友,可以戳这里获取

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值