JavaのString、StringBurer、StrigBuider

5879 ワード

一、Stringの解析
1.1問題の導入
多くの人がこのような面接問題に出会ったことがあると信じています。
String s1 = "ab";
String s2 = "cd";
String s3 = "abcd";
String s4 = s1 + s2;
String s5 = "ab" + "cd";

System.out.println(s4 == s3);
System.out.println(s5 == s3);
s 4が指している住所はs 3が指している住所と一致していますか?s 5が指している住所はs 3が指している住所と一致していますか?
第一の問題の答えはs 4が指している住所とs 3が一致していません。具体的にはなぜsmaliを使ってjvmのs 4=s 1+s 2のコードの実行ロジックを見てもいいですか?
new-instance v5, Ljava/lang/StringBuilder;
invoke-direct {v5}, Ljava/lang/StringBuilder;->()V
invoke-virtual {v5, v0}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;
move-result-object v5
invoke-virtual {v5, v1}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;
move-result-object v5
invoke-virtual {v5}, Ljava/lang/StringBuilder;->toString()Ljava/lang/String;
move-result-object v3
.line 12
.local v3, "s4":Ljava/lang/String;

JVMではs 4=s 1+s 2というコードは単純な2つの文字列だけではなく、smaliによって先にStringBuiderオブジェクトを新規作成したことが分かります。そして、apped方法でv 0(ab)、v 1(cd)の値を加算して、String割当値をs 4に呼び出します。したがってs 4は新規作成の対象となります。もちろん住所とs 3は一致しません。StringBuiderの概念については、次のようにします。
第二の問題の答えはs 5が指している住所とs 3が一致しています。この結果はまずs 5がjvmの中でどのような説明方法かを知っていますか?それともsmaliコードを見ますか?
const-string v4, "abcd"
.line 14
.local v4, "s5":Ljava/lang/String;
s 5="ab"+"cd"が直接s 5="abcd"に翻訳されているのが見えます。これはs 3と同じ定数文字を持っています。この2つのオブジェクトは常量池の同じ"abcd"アドレスを指しています。常量池の概念については、次のようにします。
1.2常量池の概念
上の第二の問題の結果から、私達は常量池という概念を引き出しました。Javaでコンパイルされたクラスファイルには、Contstant Poolというエリアがあります。配列構成のテーブルです。プログラム中の各種定数を記憶します。クラス、String、Integerなどの基本的なJavaタイプがあります。String Poolは、Costant PoolにString定数を格納するエリアです。動作中には、定数プールのStringは、新しいオブジェクトを作成します。変数が作成されたときに、定数プールの中に同じStringがあるかどうかを優先的に調べます。ある場合、どのように直接的に常量池のStringが指すアドレスに戻りますか?上のs 5==s 3を説明できます。
1.3 intern方法
常量池の概念を引き出すからには、私達もinternという方法を説明します。まず例を見て、上の問題をもとに、問題を出します。
 String s6 = s4.intern();
 System.out.println(s6 == s3);
この出力はs 6とs 3が同じ対象で、つまりイコールが成立します。internのこの方法の公式doc注釈を見ます。
 * When the intern method is invoked, if the pool already contains a
 * string equal to this {@code String} object as determined by
 * the {@link #equals(Object)} method, then the string from the pool is
 * returned. Otherwise, this {@code String} object is added to the
 * pool and a reference to this {@code String} object is returned.
この方法が起動されると、先にString poolに等しい文字列があるかどうか調べ、ある場合はそのままString poolのアドレスに戻ります。そうでない場合は、String poolに新しい文字列を作成します。
1.4 Stringは可変タイプではない。
ソースコードを見ることによって、Stringクラスで文字列を格納する配列がfinalタイプであることが分かります。
/** The value is used for character storage. */
private final char value[];
一度だけコピーされて、その後は変更できません。文字列の拡張操作は必ずオブジェクトを新規作成するので、Stringは可変されないタイプです。Stringが可変でない理由は、StringがJavaで類似の基本的なタイプとして使用されているためであり、使用頻度が非常に高い場合は、指すメモリの値を任意に変更することができ、ソースデータの変更によって、予期せぬ動作結果をもたらしやすく、例えば、ネットワーク伝送時に、ある部分文字列を送信する過程で、Stringの前に指していたメモリ値が修正され、データが変更されると思わぬ結果になります。そのため、Stringの修正毎に新しいオブジェクトが作成され、このような状況を避けるために新しいメモリアドレスが開発されました。
二、SteringBuffer
2.1 StringBufferは何ですか?
StringBufferとは、Stringに対して変更可能なオブジェクトであり、上に述べたように、Stringは可変的ではないオブジェクトであるが、一部のシーンによっては変動しないオブジェクトが現れると効率が低下するため、String Bufferのオブジェクトが必要となり、String Burerの不足を補うために、String Burerのメンバー変数を見て文字列を格納することができます。(AbstractStrigBuiderで宣言されている)変数はもうfinalではありません。これは拡張、削除できることを意味します。
 /**
 * The value is used for character storage.
 */
char[] value;
この他にも、StringBufferはスレッドセキュリティであり、すべての公開方法にsynchronizedフィールドが付加されているので、マルチスレッド動作による予期せぬ結果を回避することができる。
2.2なぜStringBufferが必要ですか?
次のコードを見てください。
String s1 = "";
for (int i = 0; i < 1000; i++) {
    s1 += "ab";
}
このようなサイクルは文字列の長さを増加させます。Stringの不変性によって、新しいオブジェクトを呼び出すたびに、効率が非常に低いです。第四節では、このような効率の影響をテストしてみます。
StringBuffer stringBuffer = new StringBuffer(2000);
for (int i = 0; i < 1000; i++) {
    stringBuffer.append("ab");
}
オブジェクトを一度作成するだけで、StringBuiderを直接拡張することで+=の効果が得られます。拡大容量といえば、StringBuiderのapped方法を紹介します。一番の核心はString Buiderで、デフォルトでは長さ16の文字配列が作成されます。appedを呼び出した時に配列が大きくないと、元の長さの2倍+2の長さが増加します。:
/**
 * This implements the expansion semantics of ensureCapacity with no
 * size check or synchronization.
 */
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);
}
このため、StringBuiderの大きさを事前に見積もることができれば、拡張機能の損失を減らすことができます。StringBuffer stringBuffer = new StringBuffer(2000);のように宣言すればいいです。
三、StringBuider
3.1 StringBuiderは何ですか?
StringBuiderは、StringBufferの無同期ロックバージョンであり、StringBufferと唯一の違いは、すべての公開方法にsynchronizedがないことである。
3.2なぜStringBuiderを使いますか?
ロックをかけると性能にロスがありますので、マルチスレッド操作ができないと確定した場合は、StringBuiderを使うほうが、StrigBufferを使うより効率的です。
四、String、StrigBurer、StrigBuider
4.1効率の違い
String、StrigBuffer、StringBuiderについて説明しましたが、これらの3つのクラスの効率の違いはどれぐらいかを見てみましょう。以下は、3つのキャラクターをそれぞれ1000個ずつ拡張していく10000文字に対して、100000文字を必要とする時間のテスト結果です。
テストオブジェクト
1000
10000
100000
100000万円
String
7 ms
173 ms
5033 ms
StringBuffer
0 ms
1 ms
1 ms
395 ms
StringBuider
0 ms
1 ms
1 ms
146 ms
StringBufferとStrigBuiderはStringよりどれだけ効率が高いかが見られます。100000万のサイクルの後に、String BufferとStrigBuiderの違いが分かります。大体2倍ぐらいの差があります。だから、文字列拡張に対する操作効率はStringBuider>StrigBufferです。String
4.2まとめ
使用中にマルチスレッドの操作がない場合は、できるだけStringBuiderを使って文字列の操作を行い、StringBufferは通常、マルチスレッドで同じ文字を操作する場合に用いられますが、文字列操作を行う際には「+=」「=」などの使用を避けることで、プログラムの動作効率を大幅に向上させることができます。