为什么不建议在循环体中使用+进行字符串拼接?

为什么不建议在循环体中使用+进行字符串拼接?

最近复习一下了阿里Java开发规范,并记录一下。
在这里插入图片描述
我们首先来看看在循环体中用 + 或者用 StringBuilder 进行字符串拼接的效率如何吧(jdk1.8)

public class StringBuilderAndAdd {
    
    
    public static void main(String[] args) {
    
    
        long s1 = System.currentTimeMillis();
        new StringBuilderAndAdd().addMethod();
        System.out.println(" + 拼接:" + (System.currentTimeMillis() - s1));
        s1 = System.currentTimeMillis();
        new StringBuilderAndAdd().stringBuilderMethod();
        System.out.println(" StringBuilder 拼接:" + (System.currentTimeMillis() - s1));
    }

    public String addMethod() {
    
    
        String result = "";
        for (int i = 0; i < 100000; i++) {
    
    
            result += "123";
        }
        return result;
    }

    public String stringBuilderMethod() {
    
    
        StringBuilder result = new StringBuilder();
        for (int i = 0; i < 100000; i++) {
    
    
            result.append("123");
        }
        return result.toString();
    }
}

执行结果,很明显采用 + 进行字符串拼接效果远远比不上StringBuilder的效果。
在这里插入图片描述
下面来分析一下原因:
1. 先来看看 ‘+’ 的分析

    public static void main(String[] args) {
    
    
        String a = "123";
        for (int i = 0; i < 20; i++) {
    
    
            a += "456";
        }
    }

如下反编译后的代码,
Java中的 + 对字符串的拼接,其实现原理是使用 StringBuilder 的 append() 来实现的,使用 + 拼接字符串,其实只是 Java 提供的一个语法糖。

// 程序反编译后的代码,反编译可以使用指令 jad -sjava StringBuilderAndAdd.class(jad指令首先要有jad.exe在jdk的bin目录下)
public static void main(String args[])
    {
    
    
        String a = "123";
        for(int i = 0; i < 20; i++)
            a = (new StringBuilder()).append(a).append("456").toString();

    }

而从Bytecode层面来看下,从idea中直接打开查看到,它在循环体中通过NEW java/lang/StringBuilder 创建对象。每次循环都创建对象很明显会花费很多时间。

// 程序Bytecode片段
  public static main([Ljava/lang/String;)V
   L0
    LINENUMBER 17 L0
    LDC "123"
    ASTORE 1
   L1
    LINENUMBER 18 L1
    ICONST_0
    ISTORE 2
   L2 //循环
   FRAME APPEND [java/lang/String I]
    ILOAD 2
    BIPUSH 20
    IF_ICMPGE L3
   L4
    LINENUMBER 19 L4
    NEW java/lang/StringBuilder // 创建新StringBuilder对象,这个是在循环体内,所以每循环一次就会创建一个对象。
    DUP
    INVOKESPECIAL java/lang/StringBuilder.<init> ()V
    ALOAD 1
    INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder;
    LDC "456"
    INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder;
    INVOKEVIRTUAL java/lang/StringBuilder.toString ()Ljava/lang/String;
    ASTORE 1
   L5
    LINENUMBER 18 L5
    IINC 2 1
    GOTO L2 //跳转到L2,继续循环
   L3
    LINENUMBER 21 L3
   FRAME CHOP 1
    RETURN

2. 再来看看 ‘StringBuilder’ 的分析

// 程序反编译后的代码
public static void main(String[] args) {
    
    
        StringBuilder a = new StringBuilder();
        for (int i = 0; i < 20; i++) {
    
    
            a.append("456");
        }
    }

如下可以看到循环只会调用append()方法操作字符串,不会重新创建StringBuiler对象。

    public static void main(String args[])
    {
    
    
        StringBuilder a = new StringBuilder();
        for(int i = 0; i < 20; i++)
            a.append("456");
    }

而从Bytecode层面来看下,从idea中直接打开查看到,它在进入循环体前就已经通过NEW java/lang/StringBuilder创建了对象,在循环体内只是调用了它的append()方法。

// 程序Bytecode片段
  public static main([Ljava/lang/String;)V
   L0
    LINENUMBER 11 L0
    NEW java/lang/StringBuilder // new StringBuilder对象,只会new一次
    DUP
    INVOKESPECIAL java/lang/StringBuilder.<init> ()V
    ASTORE 1
   L1
    LINENUMBER 13 L1
    ICONST_0
    ISTORE 2
   L2 // 循环
   FRAME APPEND [java/lang/StringBuilder I]
    ILOAD 2
    BIPUSH 20
    IF_ICMPGE L3
   L4
    LINENUMBER 14 L4
    ALOAD 1
    LDC "456"
    INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder;
    POP
   L5
    LINENUMBER 13 L5
    IINC 2 1
    GOTO L2  // 这里循环一次后的跳转到L2去
   L3
    LINENUMBER 16 L3
   FRAME CHOP 1
    RETURN

其实,StringBuilder内部也有一个char数组

char[] value;

但是与String不同的是,这个字符数组不是被final修饰的,所以是可以修改的。另外,与 String 不同,字符数组中不一定所有位置都已经被使用,它有一个实例变量,表示数组中已经使用的字符个数,定义如下:

int count;

StringBuilder类继承了 AbstractStringBuilder 类.
调用StringBuilder.append()实际上是调用了AbstractStringBuilder.append(),

public AbstractStringBuilder append(String str) {
    
    
        if (str == null)
            return appendNull();
        int len = str.length();
        ensureCapacityInternal(count + len);
        str.getChars(0, len, value, count);
        count += len; // 从这个操作来看可以很明显分析出,StringBuilder类是线程不安全的。
        return this;
    }

该方法首先会判断参数是否为 null ,如果为 null 就调用appendNull()方法。

扫描二维码关注公众号,回复: 13123933 查看本文章
private AbstractStringBuilder appendNull() {
    
    
        int c = count;
        ensureCapacityInternal(c + 4);
        final char[] value = this.value;
        // 这里不太懂为什么是这样的
        value[c++] = 'n';
        value[c++] = 'u';
        value[c++] = 'l';
        value[c++] = 'l';
        count = c;
        return this;
    }

ensureCapacityInternal()方法是判断拼接后的字符数组长度是否超过当前数组长度,如果超过,则调用 Arrays.copyOf() 方法进行扩容并复制

 /**
     * This method has the same contract as ensureCapacity, but is
     * never synchronized.
     */
    private void ensureCapacityInternal(int minimumCapacity) {
    
    
        // overflow-conscious code
        if (minimumCapacity - value.length > 0)
            expandCapacity(minimumCapacity);
    }

    /**
     * This implements the expansion semantics of ensureCapacity with no
     * size check or synchronization.
     */
    void expandCapacity(int minimumCapacity) {
    
    
    	// 首先尝试计算扩容后大小为现有value.length的2倍 + 2
        int newCapacity = value.length * 2 + 2;
        // 判断是否还是比“必须要有的容量大小” 要小
        // 小就直接用“必须要有的容量大小”
        if (newCapacity - minimumCapacity < 0)
            newCapacity = minimumCapacity;
        // 上面计算要是溢出就进入这个if
        if (newCapacity < 0) {
    
    
            if (minimumCapacity < 0) // overflow
                throw new OutOfMemoryError();
            newCapacity = Integer.MAX_VALUE;
        }
        // 扩容,并把之前的数据copy进去
        value = Arrays.copyOf(value, newCapacity);
    }

最后,将拼接的字符串 str 复制到目标数组 value 中。

str.getChars(0, len, value, count);

因此在循环体拼接字符串时,应该使用 StringBuilder 的 append() 去完成拼接。

猜你喜欢

转载自blog.csdn.net/qq_41257365/article/details/108828884