1. ホーム
  2. ジャワ

Javaデータ型 - StringBuilderとStringBuffer

2022-02-23 06:38:04
<パス

StringBuilderとStringBufferの組み合わせ

StringBuilderとStringBufferは文字列を処理するためのクラスですが、Stringクラス自体にも文字列を処理するためのメソッドがたくさんあるのに、なぜこの2つのクラスを導入したのでしょうか。先に、Stringオブジェクトの不変性と新しいオブジェクトを作成する際の欠点について説明しましたが、その内容は以下の記事で見ることができます。 文字列の高度な不変性 イミュータブルなので、それを操作するロジックは別のオブジェクトに具現化され、それは新しく作られたオブジェクトに対する操作であることがおわかりいただけるかと思います。

このような操作は文字列の継ぎ足しで最もよく行われるので、数日前から勉強している2つのクラスはこの問題を解決するために作られています。では、なぜこの問題を解決するために作られたクラスなのに2つあるのか、後でゆっくり解析していきましょう

はじめに

まず、次の例を見てください。

    @Test
    public void testPerformance(){
        String str0 = "hello,world";

        long start = System.currentTimeMillis();
        for (int i = 0; i < 100000; i++) {
            str0 += i;
        }
        System.out.println(System.currentTimeMillis() - start);

        StringBuilder sb = new StringBuilder("hello,world");
        long start1 = System.currentTimeMillis();
        for (int i = 0; i < 100000; i++) {
            sb.append(i);
        }
        System.out.println(System.currentTimeMillis() - start1);

        StringBuffer sbf = new StringBuffer("hello,world");
        long start2 = System.currentTimeMillis();
        for (int i = 0; i < 100000; i++) {
            sbf.append(i);
        }
        System.out.println(System.currentTimeMillis() - start2);
    }


上記コードの3つのループは、同じ機能である文字列のスプライシングを実現し、以下のように実行されます。

38833
4
4


実行時間が大きく異なることがお分かりいただけると思います。そこで、Stringが大量の文字列を繋ぎ合わせることを苦手とするビジネスシナリオを解決するために、StringBufferとStringBuilderを導入しました。

まず、大規模な文字列のスプライシングシナリオでStringが遅い理由を分析しましょう。これについてはすでにお話しましたので、もう一度説明します。

String自体は不変なので、Stringに対して行う操作はすべて新しいオブジェクトを返し、現在のString変数は新しいオブジェクトを指すようになり、元のStringオブジェクトはGCで回収されるので、ループ内では 新しいオブジェクトが大量に素早く生成される そして、多くの元のオブジェクトがGCによって再利用され続け、恐ろしい時間を消費し、非常に大きなメモリフットプリントを持つことになります。

しかし、出力から別の問題が見えてきました。StringBufferとStringBuilderは基本的に同じ時間を使えるのに、なぜ同じような機能を持つ2つのクラスを定義する必要があるのでしょうか?

次に、別のコードを見てみましょう。

    @Test
    public void testSafe() throws InterruptedException {
        String str0 = "hello,world";

        StringBuilder sb = new StringBuilder(str0);
        for (int i = 0; i < 100; i++) {
            new Thread(() -> {
                try {
                    TimeUnit.MILLISECONDS.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                sb.append("a");
            }).start();
        }


        StringBuffer sbf = new StringBuffer(str0);
        for (int i = 0; i < 100; i++) {
            new Thread(() -> {
                try {
                    TimeUnit.MILLISECONDS.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                sbf.append("a");
            }).start();
        }
        // Wait for the work thread to finish running
        while (Thread.activeCount()>2){

        }
        System.out.println("StringBuilder:"+sb.toString().length());
        System.out.println("StringBuffer:"+sbf.toString().length());
    }


出力結果

StringBuilder:109
StringBuffer:111


何が起こったかわかりますか?スプライシングの程度は111であるべきでしたが、今は109になっています。これはStringBuilderが安全ではないということで、マルチスレッド環境ではStringBuilderを使うべきということでしょう。

ここでは、String、StringBuffer、StringBuilderの違いを比較します。

<テーブル 文字列 StringBuffer ストリングビルダー final修飾子、継承不可 final-modified, 非相継続 最終修正、非遺伝的 文字列定数、生成後不変 文字列変数、動的に変更可能 文字列変数、動的に変更可能 スレッドセーフの問題なし スレッドセーフ、すべてのパブリックメソッドはsynchronizedによって変更されます。 スレッドはスレッドセーフではありません マス目縫いは最も効率が悪い 非常に高効率なマス・ストリング・ステッチ 大量の文字列を最も効率よく縫い合わせることができる

StringBufferはStringBuilderの実装と非常によく似ているので、ここではStringBuilderを使ったappend()メソッドの基本原理を簡単に説明します。

ソースコード分解

ここでは、StringBuilderを例にとって説明する。

1. StringBuilderのコンストラクタ

StringBuilder sb1 = new StringBuilder();
StringBuilder sb2 = new StringBuilder(100);


StringBuilderはchar[]を介して文字列を操作します。デフォルトのコンストラクタで作成されたStringBuilderは、内部で生成されるchar[]の長さがデフォルトで16になっていますが、もちろんオーバーロードのコンストラクタを呼び出して最初の長さを渡すことができます(配列の展開の回数が減り効率が良くなるので、この方法が推奨されています)。

     */
    public StringBuilder() {
        super(16);
    }

    /**
     * Constructs a string builder with no characters in it and an
     * initial capacity specified by the {@code capacity} argument.
     *
     * @param capacity the initial capacity.
     * @throws NegativeArraySizeException if the {@code capacity}
     * argument is less than {@code 0}.
     */
    public StringBuilder(int capacity) {
        super(capacity);
    }

    /**
     * Constructs a string builder initialized to the contents of the * specified string.
     * The initial capacity of the string builder is
     The initial capacity of the string builder is * {@code 16} plus the length of the string argument.
     The initial capacity of the string builder is * {@code 16} plus the length of the string argument.
     * @param str the initial contents of the buffer.
     */
    public StringBuilder(String str) {
        super(str.length() + 16);
        append(str);
    }

    /**
     * Constructs a string builder that contains the same characters
     * as the specified {@code CharSequence}. The initial capacity of
     * the string builder is {@code 16} plus the length of the
     The initial capacity of the string builder is {@code 16} plus the length of the * {@code CharSequence} argument.
     *
     * @param seq the sequence to copy.
     */
    public StringBuilder(CharSequence seq) {
        this(seq.length() + 16);
        append(seq);
    }



2. StringBuilderのappend()メソッド

append(str) メソッドを呼び出すたびに、まず配列が渡された文字列を追加するのに十分な長さがあるかどうかが判断されます。

/*
 * Appends the specified string to this character sequence.
 * 

* The characters of the {@code String} argument are appended, in * order, increasing the length of this sequence by the length of the * If {@code str} is {@code null}, then the four * characters {@code "null"} are appended. * @param str a string. * @param str a string. * @return a reference to this object. */

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; } /** * For positive values of {@code minimumCapacity}, this method * behaves like {@code ensureCapacity}, however it is never * synchronized. * If {@code minimumCapacity} is non positive due to numeric * overflow, this method throws {@code OutOfMemoryError}. */ private void ensureCapacityInternal(int minimumCapacity) { // overflow-conscious code if (minimumCapacity - value.length > 0) { value = Arrays.copyOf(value, newCapacity(minimumCapacity)); } }

渡された文字列の長さ+すでに配列に格納されている文字の長さ > が配列の長さであれば、データ展開が必要

/**
 * Returns a capacity at least as large as the given minimum capacity.
 * Returns the current capacity increased by the same amount + 2 if
 returns the current capacity increased by the same amount + 2 if * that suffices.
 * Will not return a capacity greater than {@code MAX_ARRAY_SIZE}
 * unless the given minimum capacity is greater than that.
 * @param
 * @param minCapacity the desired minimum capacity
 * @throws OutOfMemoryError if minCapacity is less than zero or
 * greater than Integer.MAX_VALUE
 MAX_VALUE */
private int newCapacity(int minCapacity) {
    // overflow-conscious code
    int newCapacity = (value.length << 1) + 2;
    if (newCapacity - minCapacity < 0) {
        newCapacity = minCapacity;
    }
    return (newCapacity <= 0 || MAX_ARRAY_SIZE - newCapacity < 0)
        ? hugeCapacity(minCapacity)
        : newCapacity;
}


展開ルールは以下の通りです。デフォルトでは、配列の長さを " に設定します。 (現在の配列の長さ * 2) + 2 "ですが、このルールでも新しい文字列を追加できるほど配列が大きくない場合は、配列の長さを " に設定する必要があります。 配列の文字数 + 渡された文字列の長さ "。

つまり、縫い合わせる文字列のおおよその長さが100文字以上と分かっている場合、初期長を150や200に設定することで、配列を展開する回数を回避または減らすことができ、効率が上がるのです。

要約

  1. StringBuilderとStringBufferは、大量の文字列をスティッチングする際のパフォーマンスの問題を解決するために設計されています。実際、スティッチングの際にStringクラスが生成する大量のオブジェクトは、内部メモリの割り当てやGCの問題を引き起こす可能性があり、この問題を解決するために設計されています
  2. StringBuilderとStringBufferの大きな違いは、スレッドセーフかどうかで、StringBufferはメソッドにsynchronizedキーワードを追加することでスレッドセーフにしている。
  3. StringBuilderとStringBufferは共に文字配列に依存しています。Stringと異なり、基礎となる文字配列は不変、すなわちfinalであるため、StringBuilderとStringBufferはMutableで、文字配列を動的に拡張して、The
  4. StringBuilderとStringBufferのパフォーマンスを向上させるために、適切な容量を設定することによって、配列ライブラリの容量を回避することができます。