Heim >类库下载 >java类库 >Analyse der Java-String-Splicing-Effizienz und Best Practices

Analyse der Java-String-Splicing-Effizienz und Best Practices

高洛峰
高洛峰Original
2016-10-13 09:23:051839Durchsuche

Dieser Artikel basiert auf der Frage: Best Practices für die Java-String-Verbindung?

Es gibt viele Möglichkeiten, Strings in Java zu verbinden, z. B. Operatoren und die StringBuilder.append-Methode. Welche Vor- und Nachteile haben diese? Jede dieser Methoden (kann entsprechend erläutert werden) Implementierungsdetails verschiedener Methoden)?

Was sind nach dem Prinzip der Effizienz die Best Practices für die String-Verkettung in Java?

Welche anderen Best Practices? Gibt es für die String-Verarbeitung?

Ohne weitere Umschweife: Die Umgebung ist wie folgt:

CPU: i7 4790

Speicher: 16G

Spleißen direkt verwenden

Sehen Sie sich den folgenden Code an:

Im obigen Code verwenden wir das Pluszeichen Verbinden Sie vier Strings. Die Vorteile der Spleißmethode liegen auf der Hand: Der Code ist einfach und intuitiv, aber im Vergleich zu StringBuilder und StringBuffer ist er in den meisten Fällen niedriger als letzterer Javap-Tool zum Dekompilieren des vom obigen Code generierten Bytecodes. Sehen Sie, was der Compiler mit diesem Code macht.
@Test 
    public void test() { 
        String str1 = "abc"; 
        String str2 = "def"; 
        logger.debug(str1 + str2); 
    }

Den Dekompilierungsergebnissen zufolge werden Zeichenfolgen tatsächlich mithilfe von Operatoren gespleißt. Der Compiler optimiert den Code für die Verwendung der StringBuilder-Klasse und ruft die Append-Methode zum Spleißen von Zeichenfolgen auf. Und schließlich rufen Sie die toString-Methode auf, damit sie unter normalen Umständen direkt verwendet werden kann. Wie auch immer, der Compiler hilft mir, sie für die Verwendung von StringBuilder zu optimieren?
public void test(); 
    Code: 
       0: ldc           #5                  // String abc 
       2: astore_1 
       3: ldc           #6                  // String def 
       5: astore_2 
       6: aload_0 
       7: getfield      #4                  // Field logger:Lorg/slf4j/Logger; 
      10: new           #7                  // class java/lang/StringBuilder 
      13: dup 
      14: invokespecial #8                  // Method java/lang/StringBuilder."<init>":()V 
      17: aload_1 
      18: invokevirtual #9                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; 
      21: aload_2 
      22: invokevirtual #9                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; 
      25: invokevirtual #10                 // Method java/lang/StringBuilder.toString:()Ljava/lang/String; 
      28: invokeinterface #11,  2           // InterfaceMethod org/slf4j/Logger.debug:(Ljava/lang/String;)V 
      33: return

StringBuilder-Quellcode-Analyse

Die Die Antwort lautet natürlich: Nein, der Grund liegt darin, was die StringBuilder-Klasse intern tut.

Werfen wir einen Blick auf den Konstruktor der StringBuilder-Klasse

StringBuilder bietet neben dem parameterlosen Konstruktor auch drei weitere überladene Versionen Die super(int-Kapazität)-Konstruktionsmethode der übergeordneten Klasse wird intern aufgerufen. Ihre übergeordnete Klasse ist AbstractStringBuilder. Die Konstruktionsmethode lautet wie folgt:
public StringBuilder() { 
        super(16); 
    } 
 
    public StringBuilder(int capacity) { 
        super(capacity); 
    } 
 
    public StringBuilder(String str) { 
        super(str.length() + 16); 
        append(str); 
    } 
 
    public StringBuilder(CharSequence seq) { 
        this(seq.length() + 16); 
        append(seq); 
    }

Sie können sehen, dass StringBuilder intern tatsächlich ein char-Array verwendet . Daten (auch String und StringBuffer), wobei der Kapazitätswert die Größe des Arrays angibt. In Kombination mit dem Parameterlosen Konstruktor von StringBuilder können Sie erkennen, dass die Standardgröße 16 Zeichen beträgt.
AbstractStringBuilder(int capacity) { 
        value = new char[capacity]; 
    }

Das heißt, wenn die Gesamtlänge der zu spleißenden Zeichenfolgen nicht weniger als 16 Zeichen beträgt, gibt es keinen großen Unterschied zwischen direktem Spleißen und unserem manuellen StringBuilder, aber wir können die Größe angeben des Arrays, indem wir die StringBuilder-Klasse selbst erstellen, um zu vermeiden, dass zu viel Speicher zugewiesen wird.

Jetzt werfen wir einen Blick darauf, was in der StringBuilder.append-Methode geschieht:

Die Append-Methode der übergeordneten Klasse, die direkt aufgerufen wird:
@Override 
   public StringBuilder append(String str) { 
       super.append(str); 
       return this; 
   }

In diesem Fall wird die Methode „sichsureCapacityInternal“ innerhalb der Methode aufgerufen. Wenn die Gesamtgröße der gespleißten Zeichenfolge größer ist als die Größe des internen Array-Werts, muss dieser vor dem Spleißen erweitert werden:
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; 
        return this; 
    }

StringBuilder fügt beim Erweitern die Kapazität hinzu. Es ist schrecklich, die Kapazität auf das Doppelte der aktuellen Kapazität zu erhöhen. Wenn die Kapazität beim Erstellen nicht angegeben wird, ist es sehr wahrscheinlich, dass viel verschwendeter Speicherplatz belegt wird nach der Erweiterung. Zweitens wird die Methode Arrays.copyOf nach der Erweiterung aufgerufen. Diese Methode kopiert die Daten vor der Erweiterung in den erweiterten Bereich. Der Grund dafür ist: StringBuilder verwendet intern Char-Arrays, um Daten zu speichern, sodass nur Java-Arrays erweitert werden können Beantragen Sie erneut einen Speicherplatz und kopieren Sie die vorhandenen Daten in den neuen Speicherplatz. Hier wird schließlich die Methode System.arraycopy zum Kopieren aufgerufen. Die unterste Ebene betreibt den Speicher direkt und ist daher besser als die Verwendung von a Schleife zum Kopieren Es gibt viele Blöcke. Dennoch können die Auswirkungen der Beantragung einer großen Menge an Speicherplatz und des Kopierens von Daten nicht ignoriert werden.
void expandCapacity(int minimumCapacity) { 
        int newCapacity = value.length * 2 + 2; 
        if (newCapacity - minimumCapacity < 0) 
            newCapacity = minimumCapacity; 
        if (newCapacity < 0) { 
            if (minimumCapacity < 0) // overflow 
                throw new OutOfMemoryError(); 
            newCapacity = Integer.MAX_VALUE; 
        } 
        value = Arrays.copyOf(value, newCapacity); 
    }

Vergleich zwischen der Verwendung von Spleißen und der Verwendung von StringBuilder

Der obige Code ist so optimiert, dass er äquivalent ist zu:
@Test 
public void test() { 
    String str = ""; 
    for (int i = 0; i < 10000; i++) { 
        str += "asjdkla"; 
    } 
}

Sie können auf einen Blick erkennen, dass es zu viele gibt Es werden StringBuilder-Objekte erstellt, und str wird nach jedem Zyklus immer größer, was dazu führt, dass der angeforderte Speicherplatz jedes Mal größer und größer wird. Wenn die Länge von str größer als 16 ist, muss er jedes Mal zweimal erweitert werden! Die toString-Methode wird erstellt. Bei Verwendung des String-Objekts wird die Arrays.copyOfRange-Methode aufgerufen, um die Daten zu kopieren. Dies entspricht einer zweifachen Erweiterung der Kapazität und einer dreifachen Kopie der Daten.
@Test 
   public void test() { 
       String str = null; 
       for (int i = 0; i < 10000; i++) { 
           str = new StringBuilder().append(str).append("asjdkla").toString(); 
       } 
   }

Die Ausführungszeit dieses Codes auf meinem Computer beträgt sowohl 0 ms (weniger als 1 ms) als auch 1 ms, während der obige Code etwa 380 ms beträgt!
public void test() { 
        StringBuilder sb = new StringBuilder("asjdkla".length() * 10000); 
        for (int i = 0; i < 10000; i++) { 
            sb.append("asjdkla"); 
        } 
        String str = sb.toString(); 
    }

Derselbe Code oben dauert beim Anpassen der Anzahl der Schleifen auf 1000000 auf meinem Computer etwa 20 ms, wenn die Kapazität angegeben ist, und etwa 29 ms, wenn die Kapazität nicht angegeben ist. Dieser Unterschied unterscheidet sich jedoch von dem von direkte Verwendung Der Operator wurde erheblich verbessert (und die Anzahl der Schleifen wurde um das Hundertfache erhöht), löst jedoch weiterhin mehrere Erweiterungen und Replikationen aus.

Ändern Sie den obigen Code, um StringBuffer zu verwenden. Dies liegt daran, dass StringBuffer den meisten Methoden ein gewisses Maß an Reduzierung verleiht.

Verwenden Sie String.concat zum Spleißen

Sehen Sie sich nun diesen Code an:

@Test 
   public void test() { 
       String str = ""; 
       for (int i = 0; i < 10000; i++) { 
           str.concat("asjdkla"); 
       } 
   }

这段代码使用了String.concat方法,在我的机器上,执行时间大约为130ms,虽然直接相加要好的多,但是比起使用StringBuilder还要太多了,似乎没什么用。其实并不是,在很多时候,我们只需要连接两个字符串,而不是多个字符串的拼接,这个时候使用String.concat方法比StringBuilder要简洁且效率要高。

public String concat(String str) { 
        int otherLen = str.length(); 
        if (otherLen == 0) { 
            return this; 
        } 
        int len = value.length; 
        char buf[] = Arrays.copyOf(value, len + otherLen); 
        str.getChars(buf, len); 
        return new String(buf, true); 
    }

上面这段是String.concat的源码,在这个方法中,调用了一次Arrays.copyOf,并且指定了len + otherLen,相当于分配了一次内存空间,并分别从str1和str2各复制一次数据。而如果使用StringBuilder并指定capacity,相当于分配一次内存空间,并分别从str1和str2各复制一次数据,最后因为调用了toString方法,又复制了一次数据。

结论

现在根据上面的分析和测试可以知道:

Java中字符串拼接不要直接使用+拼接。

使用StringBuilder或者StringBuffer时,尽可能准确地估算capacity,并在构造时指定,避免内存浪费和频繁的扩容及复制。

在没有线程安全问题时使用StringBuilder, 否则使用StringBuffer。

两个字符串拼接直接调用String.concat性能最好。

关于String的其他最佳实践

用equals时总是把能确定不为空的变量写在左边,如使用"".equals(str)判断空串,避免空指针异常。

第二点是用来排挤第一点的.. 使用str != null && str.length() != 0来判断空串,效率比第一点高。

在需要把其他对象转换为字符串对象时,使用String.valueOf(obj)而不是直接调用obj.toString()方法,因为前者已经对空值进行检测了,不会抛出空指针异常。

使用String.format()方法对字符串进行格式化输出。

在JDK 7及以上版本,可以在switch结构中使用字符串了,所以对于较多的比较,使用switch代替if-else。


Stellungnahme:
Der Inhalt dieses Artikels wird freiwillig von Internetnutzern beigesteuert und das Urheberrecht liegt beim ursprünglichen Autor. Diese Website übernimmt keine entsprechende rechtliche Verantwortung. Wenn Sie Inhalte finden, bei denen der Verdacht eines Plagiats oder einer Rechtsverletzung besteht, wenden Sie sich bitte an admin@php.cn