字符串拼接的疑惑

最近没事在玩ASM框架,于是乎想将业务代码中的PO对象中的toString方法 在编译期间,自动转换了基于StringBuilder 拼接的代码。发现了一个奇怪的问题: 实体类如下

@Getter
@Setter
@EqualsAndHashCode(of = "id")
@ApiModel("活动")
public class Banner implements Serializable{

    private static final long serialVersionUID = 191609922585601269L;

    @ApiModelProperty(value = "ID", position = 1)
    private Integer id;

    @ApiModelProperty(value = "显示次序", position = 2)
    private Integer orderNo;

    @ApiModelProperty(value = "关联文件", position = 3)
    private Integer fileId;

    @ApiModelProperty(value = "跳转链接", position = 4)
    private String forwardLink = "";

    @ApiModelProperty(value = "创建时间", position = 5)
    private Long createDateline;

    @ApiModelProperty(value = "是否可用:1 可用,0 不可用", position = 6)
    private Integer isenable;

    @ApiModelProperty(value = "标题", position = 7)
    private String title = "";

    @ApiModelProperty(value = "备注", position = 8)
    private String remark = "";

    @ApiModelProperty(value = "banner类型:1.抢单app 2.借款端app", position = 9)
    private Integer bannerType;

    @ApiModelProperty(value = "图片链接", position = 10)
    private String url = "";

    @Override
    public String toString() {
        return "Banner{" + "id=" + id + ", orderNo=" + orderNo + ", fileId=" + fileId + ", forwardLink='" + forwardLink
                + '\'' + ", createDateline=" + createDateline + ", isenable=" + isenable + ", title='" + title + '\''
                + ", remark='" + remark + '\'' + ", bannerType=" + bannerType + ", url='" + url + '\'' + '}';
    }
}

我的目的是将toString方法变成StringBuilder的方式:

 @Override 
    public String toString() {
        final StringBuilder sb = new StringBuilder(1 << 8);
        sb.append("Banner{");
        sb.append("id=").append(id);
        sb.append(", orderNo=").append(orderNo);
        sb.append(", fileId=").append(fileId);
        sb.append(", forwardLink=").append(forwardLink);
        sb.append(", createDateline=").append(createDateline);
        sb.append(", isenable=").append(isenable);
        sb.append(", title=").append(title);
        sb.append(", remark=").append(remark);
        sb.append(", bannerType=").append(bannerType);
        sb.append(", url=").append(url);
        sb.append('}');
        return sb.toString();
    }

我想基于修改字节码来实现它,于是乎我对里了前后两者的字节码差异通过beyondCompare工具比较: tostring.JPG 上面红色部分就是ASM解析class文件出来的差异性字节码 可是看第90行代码的时候,突然发现 字符串拼接 + 其底层的实现竟然用的StringBuilder,凌乱了。java编译器做了优化 既然这样为什么大家还建议使用StringBuilder, 直接使用 + 拼接好了么,出于无法理解,我测了下字符串凭借不同方式的效率

public class Person {

    private String name = "1";

    private int age = 2;

    @Override
    public String toString() {
        testAdd(100000);
        testConcat(100000);
        testStringBuilder(100000);
        return "";
    }

    public static void main(String args[]) {
        Person p = new Person();
        p.toString();
    }

    public static void testAdd(int num){
        long start = System.currentTimeMillis();
        String str = "";
        for(int i = 0; i < num; i++){
            str += i;
        }
        System.out.println("字符串拼接使用 + 耗时:" + (System.currentTimeMillis() - start) + "ms");
    }

    public static void testConcat(int num){
        long start = System.currentTimeMillis();
        String str = "";
        for(int i = 0; i < num; i++){
            str.concat(String.valueOf(i));
        }
        System.out.println("字符串拼接使用 concat 耗时:" + (System.currentTimeMillis() - start) + "ms");
    }

    public static void testStringBuffer(int num){
        long start = System.currentTimeMillis();
        StringBuffer stringBuffer = new StringBuffer();
        for(int i = 0; i < num; i++){
            stringBuffer.append(String.valueOf(i));
        }
        stringBuffer.toString();
        System.out.println("字符串拼接使用 StringBuffer 耗时:" + (System.currentTimeMillis() - start) + "ms");
    }

    public static void testStringBuilder(int num){
        long start = System.currentTimeMillis();
        StringBuilder stringBuilder = new StringBuilder();
        for(int i = 0; i < num; i++){
            stringBuilder.append(String.valueOf(i));
        }
        stringBuilder.toString();
        System.out.println("字符串拼接使用 StringBuilder 耗时:" + (System.currentTimeMillis() - start) + "ms");
    }

}

代码网上随便copy的,就测试下 +拼接 , concat拼接, StringBuilder拼接 测试结果让我更疑惑了:

字符串拼接使用 + 耗时:30117ms
字符串拼接使用 concat 耗时:11ms
字符串拼接使用 StringBuilder 耗时:7ms

这是在逗我么,既然 + 做了优化这么效率差这么多,编译器开发人员不可能做无用功的,再次看了下字节码

public static testAdd(I)V
   L0
    LINENUMBER 27 L0
    INVOKESTATIC java/lang/System.currentTimeMillis ()J
    LSTORE 1
   L1
    LINENUMBER 28 L1
    LDC ""
    ASTORE 3
   L2
    LINENUMBER 29 L2
    ICONST_0
    ISTORE 4
   L3
   FRAME APPEND [J java/lang/String I]
    ILOAD 4
    ILOAD 0
    IF_ICMPGE L4
   L5
    LINENUMBER 30 L5
    NEW java/lang/StringBuilder
    DUP
    INVOKESPECIAL java/lang/StringBuilder.<init> ()V
    ALOAD 3
    INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder;
    ILOAD 4
    INVOKEVIRTUAL java/lang/StringBuilder.append (I)Ljava/lang/StringBuilder;
    INVOKEVIRTUAL java/lang/StringBuilder.toString ()Ljava/lang/String;
    ASTORE 3
   L6
    LINENUMBER 29 L6
    IINC 4 1
    GOTO L3
   L4
    LINENUMBER 32 L4
   FRAME CHOP 1
    GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
    NEW java/lang/StringBuilder
    DUP
    INVOKESPECIAL java/lang/StringBuilder.<init> ()V
    LDC "\u5b57\u7b26\u4e32\u62fc\u63a5\u4f7f\u7528 + \u8017\u65f6\uff1a"
    INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder;
    INVOKESTATIC java/lang/System.currentTimeMillis ()J
    LLOAD 1
    LSUB
    INVOKEVIRTUAL java/lang/StringBuilder.append (J)Ljava/lang/StringBuilder;
    LDC "ms"
    INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder;
    INVOKEVIRTUAL java/lang/StringBuilder.toString ()Ljava/lang/String;
    INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/String;)V
   L7
    LINENUMBER 33 L7
    RETURN
   L8
    LOCALVARIABLE i I L3 L4 4
    LOCALVARIABLE num I L0 L8 0
    LOCALVARIABLE start J L1 L8 1
    LOCALVARIABLE str Ljava/lang/String; L2 L8 3
    MAXSTACK = 6
    MAXLOCALS = 5

发现 + 拼接 每次循环 GOTO L3 接下来继续创建在L5环节中new一个StringBuilder不断在生成新的StringBuilder; 而StringBuilder的没有

public static testStringBuilder(I)V
   L0
    LINENUMBER 55 L0
    INVOKESTATIC java/lang/System.currentTimeMillis ()J
    LSTORE 1
   L1
    LINENUMBER 56 L1
    NEW java/lang/StringBuilder
    DUP
    INVOKESPECIAL java/lang/StringBuilder.<init> ()V
    ASTORE 3
   L2
    LINENUMBER 57 L2
    ICONST_0
    ISTORE 4
   L3
   FRAME APPEND [J java/lang/StringBuilder I]
    ILOAD 4
    ILOAD 0
    IF_ICMPGE L4
   L5
    LINENUMBER 58 L5
    ALOAD 3
    ILOAD 4
    INVOKESTATIC java/lang/String.valueOf (I)Ljava/lang/String;
    INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder;
    POP
   L6
    LINENUMBER 57 L6
    IINC 4 1
    GOTO L3
   L4
    LINENUMBER 60 L4
   FRAME CHOP 1
    ALOAD 3
    INVOKEVIRTUAL java/lang/StringBuilder.toString ()Ljava/lang/String;
    POP
   L7
    LINENUMBER 61 L7
    GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
    NEW java/lang/StringBuilder
    DUP
    INVOKESPECIAL java/lang/StringBuilder.<init> ()V
    LDC "\u5b57\u7b26\u4e32\u62fc\u63a5\u4f7f\u7528 StringBuilder \u8017\u65f6\uff1a"
    INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder;
    INVOKESTATIC java/lang/System.currentTimeMillis ()J
    LLOAD 1
    LSUB
    INVOKEVIRTUAL java/lang/StringBuilder.append (J)Ljava/lang/StringBuilder;
    LDC "ms"
    INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder;
    INVOKEVIRTUAL java/lang/StringBuilder.toString ()Ljava/lang/String;
    INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/String;)V
   L8
    LINENUMBER 62 L8
    RETURN
   L9
    LOCALVARIABLE i I L3 L4 4
    LOCALVARIABLE num I L0 L9 0
    LOCALVARIABLE start J L1 L9 1
    LOCALVARIABLE stringBuilder Ljava/lang/StringBuilder; L2 L9 3
    MAXSTACK = 6
    MAXLOCALS = 5

循环每次 GOTO L3 在L3之前已经创建了StringBuilder对象,所以一直使用同一个对象,从而减少GC成本

结论 >因为在编写代码中可能涉及到循环体内的字符串拼接,所以还是建议使用StringBuilder并且要指定初始化容量

Reference

深入分析Java使用+和StringBuilder进行字符串拼接的差异

文章目录
-----------------------
最新评论

[评论][COMMENTS]