石锤 Java String vs StringBuilder vs StringBuffer

在这里插入图片描述

结论

如果你在写字符串拼接的代码时,只用过 String,或者用过 StringBuilder, StringBuffer 不是很明白其优缺点。那么本文就是为你准备的。
先说结论:

  1. String: 操作方便,但高频次拼接字符串时性能低;
  2. StringBuilder: 高频次拼接字符串性能高出 String 类不少,但是并发情况下容易出现现成不安全;
  3. StringBuffer: 性能同样优异,略微比 StringBuilder 次一些,但是解决了 StringBuilder 并发下的线程不安全情况。

至此,你可以选择记住结论离开了。但我更希望你继续看下去,接下来会基于实验,演示 String 到底比 StringBuilder 慢多少,并且实践一下,写个精简(10行代码左右)的 StringBuilder 和 StringBuffer,明白其内在原理。

实验

首先准备一个辅助的时间统计类 Timer,计算一段代码的耗时。

// Timer.java
public class Timer {
    private long startTime = System.currentTimeMillis();

    public long finish() {
        return System.currentTimeMillis() - startTime;
    }
}

使用示例

Timer timer = new Timer();
// 需要计算耗时的代码
System.out.println("耗时" + timer.finish() + "毫秒");

String vs String Builder

这里主要来比较 String 与 StringBuilder 的性能差。测试内容:将10万个单词拼接词一个句子。

// StringTest.java
public class StringTest {
    private final int times = 100000;

    public static void main(String[] args) throws Exception {
        StringTest t = new StringTest();
        t.test1();
        t.test2();
    }

    /**
     * 10000次 String + String 方式的单词拼接耗时测试
     */
    private void test1() {
        String sentence = "";
        Timer timer1 = new Timer();
        for (long i = 0; i < times; i++) {
            String word = String.valueOf(i); // 这里使用阿拉伯数字代替单词
            sentence += word;
        }
        // System.out.println(sentence); // 打印出拼接结果
        System.out.println("String + String 方式拼接" + times + "次单词,耗时 " + timer1.finish() + " 毫秒");
    }

    /**
     * 10000次 StringBuilder::append() 方式的单词拼接耗时测试
     */
    private void test2() {
        StringBuilder sentence = new StringBuilder();
        Timer timer2 = new Timer();
        for (long i = 0; i < times; i++) {
            String word = String.valueOf(i); // 这里使用阿拉伯数字代替单词
            sentence.append(word);
        }
        // System.out.println(sentence.toString()); // 打印出拼接结果
        System.out.println("StringBuilder::append() 方式拼接" + times + "次单词,耗时 " +timer2.finish() + " 毫秒");
    }
}

实验输出(MacBook Air 2C8G)

String + String 方式拼接100000次单词,耗时 37150 毫秒
StringBuilder::append() 方式拼接100000次单词,耗时 13 毫秒

实验结论:拼接10万次字符串的耗时,String类要比StringBuilder多2800倍左右(仅本次实验)。

StringBuilder vs String Buffer

接下来我们比较并发情况下的 StringBuilder 和 StringBuffer。
实验方法如下,初始化一个StringBuilder 或 StringBuffer 对象,开两个线程,每个线程分别调用100000次 append() 方法,往里面塞长度为1的100000字符串。按照预期,该对象持有的字符串,长度应该为200000。

public class StringTest {
    private final int times = 100000;

    public static void main(String[] args) throws Exception {
        StringTest t = new StringTest();
        t.test3();
        t.test4();
    }

    /**
     * 并发环境测试 StringBuilder
     */
    private void test3() {
        StringBuilder sb = new StringBuilder();
        try {
            Thread t1 = new Thread(() -> { for (int i=0; i<times; i++) sb.append("a"); });
            Thread t2 = new Thread(() -> { for (int i=0; i<times; i++) sb.append("b"); });

            t1.start(); t2.start();
            t1.join(); t2.join();
        } catch (Exception e) {
            e.printStackTrace();
        }

        System.out.println("StringBuilder::append() " + times * 2 + "次后的长度: " + sb.length()); // 多线程情况下已经有问题了
    }

    /**
     * 并发环境测试 StringBuffer
     */
    private void test4() {
        StringBuffer sb = new StringBuffer();
        try {
            Thread t1 = new Thread(() -> { for (int i=0; i<times; i++) sb.append("a"); });
            Thread t2 = new Thread(() -> { for (int i=0; i<times; i++) sb.append("b"); });

            t1.start(); t2.start();
            t1.join(); t2.join();
        } catch (Exception e) {
            e.printStackTrace();
        }

        System.out.println("StringBuffer::append() " + times * 2 + "次后的长度: " + sb.length());
    }
}

实验输出

StringBuilder::append() 200000次后的长度: 100075
StringBuffer::append() 200000次后的长度: 200000

结论: StringBuilder 的结果不符合预期,也就是我们说的并发情况下的线程不安全。

分析

String

java.lang.String.java 部分源码

public final class String implements java.io.Serializable, Comparable<String>, CharSequence {
    /** The value is used for character storage. */
    private final char value[];
    
    ...
}

留意 String 类里的 value[] 属性,“The value is used for character storage.” 表示,此属性主要用来存储字符串的字符数组。
且是 final,不可被修改的。每次执行

sentence += word;

jvm 都会优化为

sentence = new StringBuilder(sentence).append(word).toString();

每次都会产生一个新的字符串赋值给 sentence, 且由于这个sentence越来越长,每次新产生字符串都要发生字节拷贝,非常耗时。

StringBuilder

StringBuilder 和 StringBuffer 类似,为了避免每次都要产生新的字符串,都预先使用一个字符数组,每次拼接字符串,都是把新串合并到预先准备好的字符数组,避免了大量的字符拷贝。
具体实现,就是 StringBuilder 和 StringBuffer 都实现了 java.lang.Appendable 接口

// java.lang.AppendAble
public interface Appendable {
    Appendable append(CharSequence csq) throws IOException;
    Appendable append(CharSequence csq, int start, int end) throws IOException;
    Appendable append(char c) throws IOException;
}

String 对象则实现了 CharSequence 接口,所以 StringBuilder 的 append() 方法可以接受一个 String 类型的传参。接下来再具体看下 append() 的具体实现

// java.lang.AbstractStringBuilder  
abstract class AbstractStringBuilder implements Appendable, CharSequence {
    // 存储当前字符串的所有字符
    char[] value; 

    public AbstractStringBuilder append(String str) {
        if (str == null)
           return appendNull();
        int len = str.length();
        // 扩容 value 字符数组的长度,每次扩容会发生字符数组拷贝。
        // 为了减少扩容次数,扩容算法大致思路为,不断翻倍 16->32->64->128->256... 注:这不是精准的扩容算法,具体请看源码
        ensureCapacityInternal(count + len); 
        // 拷贝 str 的字符数组,至 value 字符数组的结尾
        str.getChars(0, len, value, count); 
        count += len;
        return this;
    }
}

由此可以看出,StringBuilder 和 StringBuffer 均持有一个字符数组,来让新的子串加入进来。通过翻倍扩容的方式,避免大字符数组频繁发生拷贝。

StringBuffer

StringBuffer 和 StringBuilder 均继承自 AbstractStringBuilder, 使用方法几乎一致。以下主要分析为啥 StringBuilder 是线程不安全,而 StringBuffer 是线程安全的。
我们分别看下这两个类的 append 方法源码

// java.lang.StringBuilder
public final class StringBuilder extends AbstractStringBuilder implements java.io.Serializable, CharSequence {
    @Override
    public StringBuilder append(String str) {
        super.append(str);
        return this;
    }
}
// java.lang.StringBuffer
public final class StringBuffer extends AbstractStringBuilder implements java.io.Serializable, CharSequence {
    @Override
    public synchronized StringBuffer append(Object obj) {
        toStringCache = null;
        super.append(String.valueOf(obj));
        return this;
    }
}

可以看出,它们的相同之处。都是以委托方式,具体逻辑交由父类的 append() 方法处理,而这个方法,我们再上文有讲解。
重点说下不同之处,StringBuffer 的 append() 方法多了用 synchronized 关键词去描述。表示这是一个同步方法,只允许单线程去访问,其他线程需要去等待。这样也就保证了 append 方法的线程安全。

DIY

有了以上的知识储备,我们可以练练手,实现一个自己的 StringBuilder 和 StringBuffer,这里直接贴代码了。

MyStringBuilder

// MyStringBuilder.java
class MyStringBuilder {
    private char[] chars;
    private int count;

    public MyStringBuilder() {
        chars = new char[100000*10];
        count = 0;
    }

    public void append(String substr) {
        substr.getChars(0, substr.length(), chars, count);
        count = substr.length() + count;
    }

    public int length() {
        return count;
    }

    @Override
    public String toString() {
        return new String(chars, 0, count);
    }
}

MyStringBuffer

class MyStringBuffer extends MyStringBuilder  {
    @Override
    public synchronized void append(String substr) {
        super.append(substr);
    }
}

本文完。

附本文全部代码

package string;

public class StringTest {
    private final int times = 100000;

    public static void main(String[] args) throws Exception {
        StringTest t = new StringTest();
        t.test1();
        t.test2();
        t.test3();
        t.test4();
        t.test5();
        t.test6();
        t.test7();
        t.test8();
    }

    /**
     * 10000次 String + String 方式的单词拼接耗时测试
     */
    private void test1() {
        String sentence = new String("");
        Timer timer1 = new Timer();
        for (long i = 0; i < times; i++) {
            String word = String.valueOf(i); // 这里使用阿拉伯数字代替单词
            sentence += word;
        }
//        System.out.println(sentence); // 打印出拼接结果
        System.out.println("String + String 方式拼接" + times + "次单词,耗时 " + timer1.finish() + " 毫秒");
    }

    /**
     * 10000次 StringBuilder::append() 方式的单词拼接耗时测试
     */
    private void test2() {
        StringBuilder sentence = new StringBuilder();
        Timer timer2 = new Timer();
        for (long i = 0; i < times; i++) {
            String word = String.valueOf(i); // 这里使用阿拉伯数字代替单词
            sentence.append(word);
        }
//        System.out.println(sentence.toString()); // 打印出拼接结果
        System.out.println("StringBuilder::append() 方式拼接" + times + "次单词,耗时 " +timer2.finish() + " 毫秒");
    }

    /**
     * 并发环境测试 StringBuilder
     */
    private void test3() {
        StringBuilder sb = new StringBuilder();
        try {
            Thread t1 = new Thread(() -> { for (int i=0; i<times; i++) sb.append("a"); });
            Thread t2 = new Thread(() -> { for (int i=0; i<times; i++) sb.append("b"); });

            t1.start(); t2.start();
            t1.join(); t2.join();
        } catch (Exception e) {
            e.printStackTrace();
        }

        System.out.println("StringBuilder::append() " + times * 2 + "次后的长度: " + sb.length()); // 多线程情况下已经有问题了
    }

    /**
     * 并发环境测试 StringBuffer
     */
    private void test4() {
        StringBuffer sb = new StringBuffer();
        try {
            Thread t1 = new Thread(() -> { for (int i=0; i<times; i++) sb.append("a"); });
            Thread t2 = new Thread(() -> { for (int i=0; i<times; i++) sb.append("b"); });

            t1.start(); t2.start();
            t1.join(); t2.join();
        } catch (Exception e) {
            e.printStackTrace();
        }

        System.out.println("StringBuffer::append() " + times * 2 + "次后的长度: " + sb.length());
    }

    /**
     * 使用自己写的 MyStringBuilder 去测试字符串拼接
     */
    private void test5() {
        MyStringBuilder s = new MyStringBuilder();
        Timer timer2 = new Timer();
        for (long i = 0; i < times; i++) {
            s.append(String.valueOf(i));
        }
//        System.out.println(s.toString());
        System.out.println("MyStringBuilder::append() 方式拼接" + times + "次单词,耗时 " +timer2.finish() + " 毫秒");
    }

    /**
     * 使用自己写的 MyStringBuffer 去测试字符串拼接
     */
    private void test6() {
        MyStringBuffer s = new MyStringBuffer();
        Timer timer2 = new Timer();
        for (long i = 0; i < times; i++) {
            s.append(String.valueOf(i));
        }
//        System.out.println(s.toString());
        System.out.println("MyStringBuffer::append() 方式拼接" + times + "次单词,耗时 " +timer2.finish() + " 毫秒");
    }

    /**
     * 使用自己写的 MyStringBuilder 去演示并发情况下的线程不安全
     */
    private void test7() {
        MyStringBuilder sb = new MyStringBuilder();
        try {
            Thread t1 = new Thread(() -> {
                for (int i=0; i<times; i++) {
                    sb.append("a");
                }
            });
            Thread t2 = new Thread(() -> {
                for (int i=0; i<times; i++) {
                    sb.append("b");
                }
            });

            t1.start();
            t2.start();
            t1.join();
            t2.join();
        } catch (Exception e) {
            e.printStackTrace();
        }


        System.out.println(sb.length()); // 多线程情况下已经有问题了
    }

    /**
     * 使用自己写的 MyStringBuffer 去演示并发情况下的线程安全
     */
    private void test8() throws Exception {
        int times = 100000;
        MyStringBuilder sb = new MyStringBuilder();
        Thread t1 = new Thread(() -> {
            for (int i=0; i<times; i++) {
                sb.append("a");
            }
        });
        Thread t2 = new Thread(() -> {
            for (int i=0; i<times; i++) {
                sb.append("b");
            }
        });

        t1.start();
        t2.start();
        t1.join();
        t2.join();

        System.out.println(sb.length());
    }
}

class MyStringBuilder {
    private char[] chars;
    private int count;

    public MyStringBuilder() {
        chars = new char[100000*10];
        count = 0;
    }

    public void append(String substr) {
        substr.getChars(0, substr.length(), chars, count);
        count = substr.length() + count;
    }

    public int length() {
        return count;
    }

    @Override
    public String toString() {
        return new String(chars, 0, count);
    }
}

class MyStringBuffer extends MyStringBuilder  {
    @Override
    public synchronized void append(String substr) {
        super.append(substr);
    }
}

class Timer {
    private long startTime = System.currentTimeMillis();

    public long finish() {
        return System.currentTimeMillis() - startTime;
    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值