String、StringBuffer和StringBuilder

1.三者对比

public final class String
    implements java.io.Serializable, Comparable<String>, CharSequence {

String是Immutable类,不可变类。因此对于修改的动作,都会产生新的String对象。

 public final class StringBuffer
    extends AbstractStringBuilder
    implements java.io.Serializable, CharSequence
{

    @Override
    public synchronized int length() {
        return count;
    }

    @Override
    public synchronized int capacity() {
        return value.length;
    }

    @Override
    public synchronized StringBuffer append(String str) {
        toStringCache = null;
        super.append(str);
        return this;
    }

StringBuffer是可以修改的,通过添加synchronized,保证了线程安全。

public final class StringBuilder
    extends AbstractStringBuilder
    implements java.io.Serializable, CharSequence
{

    @Override
    public StringBuilder append(String str) {
        super.append(str);
        return this;
    }

StringBuilder在功能上与StringBuffer没有本质区别,但是其去掉了synchronized,在无并发修改的情况下是拼接的首选。

2.拼接

        String str = "aa" + "bb" + "cc";
        String str1 = str + "dd";

对应字节码:

         0: ldc           #2                  // String aabbcc
         2: astore_1
         3: new           #3                  // class java/lang/StringBuilder
         6: dup
         7: invokespecial #4                  // Method java/lang/StringBuilder."<init>":()V
        10: aload_1
        11: invokevirtual #5                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
        14: ldc           #6                  // String dd
        16: invokevirtual #5                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
        19: invokevirtual #7                  // Method java/lang/StringBuilder.toString:()Ljava/lang/String;

可见:

  • 1)"aa" + "bb" + "cc"直接优化成了"aabbcc"
  • 2)普通的+拼接优化成了通过StringBuilder进行拼接

+和StringBuilder直接拼接两种方式性能对比:

        String test1 = "0";
        long current = System.currentTimeMillis();
        for (int i = 1; i <= 200_000; i++) {
            if (i % 10_000 == 0) {
                long temp = System.currentTimeMillis();
                System.out.println(temp - current);
                current = temp;
            }
            test1 += i;
        }
        System.out.println();

        StringBuilder test2 = new StringBuilder();
        current = System.currentTimeMillis();
        for (int i = 1; i <= 200_000; i++) {
            if (i % 10_000 == 0) {
                long temp = System.currentTimeMillis();
                System.out.println(temp - current);
                current = temp;
            }
            test2.append(i);
        }
    }

测试结果如下:

452
1245
1555
2048
2899
3381
4524
4756
5379
5962
6720
3664
3721
4067
4431
4933
5349
5922
6192
6505

1
0
1
0
1
0
0
1
0
0
0
1
0
0
0
1
0
0
0
0

-XX:+PrintGCDetails开启GC日志,可以看到大量新建的临时对象触发频繁GC回收:

[GC (Allocation Failure) [PSYoungGen: 33280K->760K(38400K)] 33280K->768K(125952K), 0.0010430 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[GC (Allocation Failure) [PSYoungGen: 34029K->776K(38400K)] 34037K->784K(125952K), 0.0032711 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[GC (Allocation Failure) [PSYoungGen: 34056K->715K(38400K)] 34064K->723K(125952K), 0.0028864 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[GC (Allocation Failure) [PSYoungGen: 33995K->728K(71680K)] 34003K->736K(159232K), 0.0006853 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[GC (Allocation Failure) [PSYoungGen: 67288K->814K(71680K)] 67296K->822K(159232K), 0.0009245 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[GC (Allocation Failure) [PSYoungGen: 67374K->775K(134144K)] 67382K->783K(221696K), 0.0015878 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 

反编译字节码如下,可以看出通过+在循环内会重复新建StringBuilder对象,这样会快速消耗内存又频繁触发GC。

         0: ldc           #2                  // String 0
         2: astore_1
         3: invokestatic  #3                  // Method java/lang/System.currentTimeMillis:()J
         6: lstore_2
         7: iconst_1
         8: istore        4
        10: iload         4
        12: ldc           #4                  // int 200000
        14: if_icmpgt     70
        17: iload         4
        19: sipush        10000
        22: irem
        23: ifne          44
        26: invokestatic  #3                  // Method java/lang/System.currentTimeMillis:()J
        29: lstore        5
        31: getstatic     #5                  // Field java/lang/System.out:Ljava/io/PrintStream;
        34: lload         5
        36: lload_2
        37: lsub
        38: invokevirtual #6                  // Method java/io/PrintStream.println:(J)V
        41: lload         5
        43: lstore_2
        44: new           #7                  // class java/lang/StringBuilder
        47: dup
        48: invokespecial #8                  // Method java/lang/StringBuilder."<init>":()V
        51: aload_1
        52: invokevirtual #9                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
        55: iload         4
        57: invokevirtual #10                 // Method java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;
        60: invokevirtual #11                 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
        63: astore_1
        64: iinc          4, 1
        67: goto          10
        70: getstatic     #5                  // Field java/lang/System.out:Ljava/io/PrintStream;
        73: invokevirtual #12                 // Method java/io/PrintStream.println:()V
        76: new           #7                  // class java/lang/StringBuilder
        79: dup
        80: invokespecial #8                  // Method java/lang/StringBuilder."<init>":()V
        83: astore        4
        85: invokestatic  #3                  // Method java/lang/System.currentTimeMillis:()J
        88: lstore_2
        89: iconst_1
        90: istore        5
        92: iload         5
        94: ldc           #4                  // int 200000
        96: if_icmpgt     140
        99: iload         5
       101: sipush        10000
       104: irem
       105: ifne          126
       108: invokestatic  #3                  // Method java/lang/System.currentTimeMillis:()J
       111: lstore        6
       113: getstatic     #5                  // Field java/lang/System.out:Ljava/io/PrintStream;
       116: lload         6
       118: lload_2
       119: lsub
       120: invokevirtual #6                  // Method java/io/PrintStream.println:(J)V
       123: lload         6
       125: lstore_2
       126: aload         4
       128: iload         5
       130: invokevirtual #10                 // Method java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;
       133: pop
       134: iinc          5, 1
       137: goto          92
       140: return

3.拼接字节码在不同JDK版本的演进

        String str = "aa" + "bb" + "cc";
        String str1 = str + "dd";

如下为JDK10:

         0: ldc           #2                  // String aabbcc
         2: astore_1
         3: aload_1
         4: invokedynamic #3,  0              // InvokeDynamic #0:makeConcatWithConstants:(Ljava/lang/String;)Ljava/lang/String;
         9: astore_2
        10: return

BootstrapMethods:
  0: #25 REF_invokeStatic java/lang/invoke/StringConcatFactory.makeConcatWithConstants:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/String;[Ljava/lang/Object;)Ljava/lang/invoke/CallSite;
    Method arguments:
      #26 \u0001dd

JDK8中拼接+会被javac转换成StringBuilder操作,在JDK10中利用了InvokeDynamic,将字符串拼接的优化与java生成的字节码解耦,假设未来JVM增强相关运行时实现,并不需要依赖javac的任何修改。

拼接在JDK10下面的测试,也发现GC回收很频繁,则表明JDK10未根本改变拼接实现方式。

88
136
266
358
581
424
499
567
653
711
827
953
1038
1141
1247
1318
1412
1529
1601
1771

2
1
0
0
0
1
0
0
1
0
0
1
0
0
0
1
0
0
0
1

4.String自身的演化

JDK8使用char数组来存数据,拉丁语系语言的字符,根本不需要太宽的char。

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

JDK9引入了Compact Strings设计,如下为JDK10的源码:

public final class String
    implements java.io.Serializable, Comparable<String>, CharSequence {

    /**
     * The value is used for character storage.
     *
     * @implNote This field is trusted by the VM, and is a subject to
     * constant folding if String instance is constant. Overwriting this
     * field after construction will cause problems.
     *
     * Additionally, it is marked with {@link Stable} to trust the contents
     * of the array. No other facility in JDK provides this functionality (yet).
     * {@link Stable} is safe here, because value is never null.
     */
    @Stable
    private final byte[] value;

    /**
     * The identifier of the encoding used to encode the bytes in
     * {@code value}. The supported values in this implementation are
     *
     * LATIN1
     * UTF16
     *
     * @implNote This field is trusted by the VM, and is a subject to
     * constant folding if String instance is constant. Overwriting this
     * field after construction will cause problems.
     */
    private final byte coder;

在JDK9之后的String类中,维护了属性coder,它是一个编码格式的标识,使用LATIN1还是UTF-16,这个是在String生成的时候自动的,如果字符串中都是能用LATIN1就能表示的就是0,否则就是UTF-16。另外,所有相关的Intrinsic都进行了重写。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,547评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,399评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,428评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,599评论 1 274
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,612评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,577评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,941评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,603评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,852评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,605评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,693评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,375评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,955评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,936评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,172评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,970评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,414评论 2 342

推荐阅读更多精彩内容