美文网首页
字符串拼接的疑惑

字符串拼接的疑惑

作者: 南乡清水 | 来源:发表于2018-04-21 15:34 被阅读53次

    最近没事在玩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进行字符串拼接的差异

    相关文章

      网友评论

          本文标题:字符串拼接的疑惑

          本文链接:https://www.haomeiwen.com/subject/rdrxlftx.html