この記事は、「Java での文字列接続のベスト プラクティスは?」という質問から来ています。
Java では、+ 演算子や StringBuilder.append メソッドなど、文字列を接続する方法がたくさんあります。それぞれの長所と短所は何ですか。これらのメソッド (各メソッドの実装は詳細を適切に説明できます)?
効率の原則によれば、Java での文字列連結のベスト プラクティスは何ですか?
文字列処理には他にどのようなベスト プラクティスがありますか?
なし詳しい説明は次のとおりです:
JDK バージョン: 1.8.0_65
CPU: i7 4790
メモリ: 16G
以下のコードを見てください:
@Test public void test() { String str1 = "abc"; String str2 = "def"; logger.debug(str1 + str2); }
上記の中でコードでは、プラス記号を使用して 4 つの文字列を接続します。この文字列結合方法の利点は明らかです。コードはシンプルで直感的ですが、StringBuilder や StringBuffer と比較すると、ほとんどの場合、後者よりも低くなります。ほとんどの場合、javap ツールを使用して上記のコードを比較します。生成されたバイトコードは逆コンパイルされて、コンパイラーがこのコードに対して何をしたかを確認します。
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 クラスを使用するようにコードを最適化し、文字列の結合に append メソッドを呼び出し、最後に toString メソッドを呼び出します。通常の状況では、実際に + を直接使用でき、とにかく StringBuilder を使用するようにコンパイラーが最適化を支援してくれるようですか?
StringBuilder ソース コード分析
答えは当然ノーです。理由は の内部にあります。何かが行われたときの StringBuilder クラス。
StringBuilder クラスのコンストラクターを見てみましょう
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); }
StringBuilder は、引数なしのコンストラクターに加えて、4 つのデフォルト コンストラクターを提供し、他の 3 つのオーバーロードされたバージョンも提供し、親クラス int の super( を内部で呼び出します。 Capacity) 構築メソッド、その親クラスは AbstractStringBuilder です。構築メソッドは次のとおりです:
AbstractStringBuilder(int capacity) { value = new char[capacity]; }
StringBuilder が実際に内部で char 配列を使用してデータ (String、StringBuffer も同様) を格納していることがわかります。ここでは、capacity の値が値を指定しています。配列のサイズ。 StringBuilder のパラメーターなしのコンストラクターと組み合わせると、デフォルトのサイズが 16 文字であることがわかります。
つまり、結合する文字列の合計の長さが 16 文字以上の場合、直接結合するのと StringBuilder を手動で記述するのとの間には実際にはほとんど違いはありませんが、配列のサイズを指定することができます。 StringBuilder クラスを自分たちで構築することで、過剰なメモリの割り当てを回避します。
次に、StringBuilder.append メソッド内で何が行われるかを見てみましょう:
@Override public StringBuilder append(String str) { super.append(str); return this; }
直接呼び出される親クラスの 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; return this; }
このメソッド内で ensureCapacityInternal メソッドが呼び出されます。結合された文字列が次より大きいです。 内部配列値のサイズが決定されたら、結合する前にまず拡張する必要があります。 拡張コードは次のとおりです:
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); }
StringBuilder は、拡張中に容量を現在の容量の 2 倍 + 2 に増やします。これは非常に恐ろしいことです。容量が指定されていない場合、拡張後に大量のメモリ領域が占有され、無駄になる可能性が非常に高くなります。次に、Arrays.copyOf メソッドが拡張後に呼び出されます。このメソッドは、拡張前のデータを拡張された領域にコピーします。その理由は、StringBuilder はデータを格納するために内部で char 配列を使用するため、拡張できるのは Java 配列だけです。メモリ領域を再申請し、既存のデータを新しい領域にコピーします。ここでは、最後に System.arraycopy メソッドを呼び出してメモリを直接操作するため、メソッドを使用するよりも優れています。それでも、大量のメモリ領域を適用してデータをコピーする影響は無視できません。
+ splicing の使用と StringBuilder の使用の比較
@Test public void test() { String str = ""; for (int i = 0; i < 10000; i++) { str += "asjdkla"; } }
上記のコードは次と同等になるように最適化されています:
@Test public void test() { String str = null; for (int i = 0; i < 10000; i++) { str = new StringBuilder().append(str).append("asjdkla").toString(); } }
作成される StringBuilder オブジェクトが多すぎて、各ループ後に str がどんどん大きくなっていることが一目でわかります。その結果、毎回要求されるメモリ空間はますます大きくなり、str の長さが 16 を超える場合は、毎回 2 回拡張する必要があります。実際、toString メソッドは String オブジェクトを作成するときに、 Arrays.copyOfRange メソッドを使用してデータをコピーすると、実行するたびに容量を 2 回拡張し、データを 3 回コピーすることになり、かなりのコストがかかります。
public void test() { StringBuilder sb = new StringBuilder("asjdkla".length() * 10000); for (int i = 0; i < 10000; i++) { sb.append("asjdkla"); } String str = sb.toString(); }
私のマシンでは、このコードの実行時間は 0 ミリ秒 (1 ミリ秒未満) と 1 ミリ秒でしたが、上記のコードは約 380 ミリ秒でした。効率の違いは明らかです。
上記の同じコードで、ループ数を1,000,000に調整する場合、私のマシンでは、容量を指定した場合は約20ms、容量を指定しなかった場合は約29msかかりましたが、この差は+演算子を直接使用した場合とは異なります。は大幅に改善されましたが (サイクル数は 100 倍に増加しました)、それでも複数の拡張とレプリケーションがトリガーされます。
StringBuffer を使用するように上記のコードを変更します。これは、StringBuffer がスレッドの安全性と実行効率をある程度短縮するために、ほとんどのメソッドに synchronized キーワードを追加するためです。
String.concat を使用して結合します
次に、このコードを見てください:
@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。