Java SE基礎強化(二):String類


Stringの使用頻度は非常に高く、大規模でも小型でも多くのアプリケーションでStringクラスが使用されます.したがって,Stringを理解して高性能に使用することが重要である.
Stringクラスは、substring()、indexOf()、lastIndexOf()など、多くの機能豊富なAPIを提供します.Stringは可変クラスであり、substring()のように文字列メソッドを変更するように見えてもインスタンスを実際に変更することはなく、新しいStringオブジェクトを作成して返します.Stringも継承されず、継承は柔軟性を高めるが、親の論理(書き換えメカニズムのため)を破壊する可能性があり、Stringクラスを非常に危険にさらすため、Stringの設計者が継承できないように設計するには十分な理由がある.
次に、ソースコードから上記の特性を徐々に分析します.
1 Stringクラスの豊富なAPI
Stringクラスには様々な機能のAPIがあり、一つ一つ詳しくは言えませんが、indexOf()メソッドを選んで詳しく議論しました.
public int indexOf(int ch) {
    return indexOf(ch, 0);
}

public int indexOf(int ch, int fromIndex) {
    final int max = value.length;
    if (fromIndex < 0) {
        fromIndex = 0;
    } else if (fromIndex >= max) {
        // Note: fromIndex might be near -1>>>1.
        return -1;
    }

    if (ch < Character.MIN_SUPPLEMENTARY_CODE_POINT) {
        // handle most cases here (ch is a BMP code point or a
        // negative value (invalid code point))
        final char[] value = this.value;
        for (int i = fromIndex; i < max; i++) {
            if (value[i] == ch) {
                return i;
            }
        }
        return -1;
    } else {
        return indexOfSupplementary(ch, fromIndex);
    }
}

indexOf()には、列挙された2つのほかに、5つのリロード形式がありますが、あまりよく使われていません.比較的よく使われているのがこの2つです.1つのパラメータだけを受け入れるのは多くありません.彼は余分な論理を持っていないので、indexOf(int ch,int fromIndex)を直接呼び出しました.indexOf(int ch,int fromIndex)メソッドの最初のパラメータは、検索する文字であり、intタイプであり、このパラメータについてJDKドキュメントでは次のように記述されています.
a character (Unicode code point).
つまりUnicode符号化された文字(JDKの注釈ドキュメントを見ると1つの方法を理解する最も速い方法)であり、コンピュータの基礎知識を持っている友人は、intタイプであるにもかかわらず、実際に私たちが使用しているときにcharタイプを直接伝えることができる理由を理解するのは難しくないはずです.以下のようにします.
String s = "hello";
int i = s.indexOf('h',0);

2番目のパラメータfromIndexは、パラメータchが1つしかないAPIが呼び出された場合、fromIndexの値のデフォルトは0であり、文字列の先頭から検索されます.
次にif-else構造です.ここではfromIndexを検証します.fromIndexが0未満の場合は0に等しくなり、fromIndexがmaxより大きい場合は-1に戻ります.つまり、見つからないことを示します.
次のコードこそindexOfのコアです.
final char[] value = this.value;
for (int i = fromIndex; i < max; i++) {
    if (value[i] == ch) {
        return i;
    }
}
return -1;

実はとても簡単で、value配列を遍歴して、それから1つ1つ比較して、もし見つけたら直接返して、遍歴が終わってからまだ見つけていないで、-1を返して、見つけていないことを表します.
その他の方法は多くありませんが、具体的にはJDK APIドキュメントを見たり、IDEを使って、IDEで直接注釈ドキュメントを見たりすることをお勧めします.(IDEAならCtrl/cmd+左クリックでアクセスできます).
2 Stringの不変性
Stringは可変ではありませんが、なぜ可変に設計されているのでしょうか.主に以下の点を考慮します.
  • は、定数プールを容易に実現します.定数プール内の文字列定数が可変であれば、多くのセキュリティ問題が発生します.
  • ネットワークセキュリティ.ネットワーク接続のパラメータは文字列の形で現れることが多いが、文字列が可変であれば、パラメータが改ざんされる可能性があることを意味し、これは明らかに私たちが見たいものではない.
  • スレッドは安全です.可変クラスはスレッドが安全であるに違いありません.複数のスレッドが共有変数を変更することはありません(文字列が変更できないため).
  • 文字列処理を高速化します.文字列は可変であるため、hashcodeはオブジェクトの作成時に一度計算するだけでよい.たとえばStringをMapのKeyとする.

  • では、Stringはどのようにして可変を実現しますか?可変化を実現するには、少なくとも次のステップが必要です.
  • はクラスをfinalと宣言し、継承できません.(継承はクラスの動作を変更する可能性が高い)
  • は、すべての可変状態をfinalおよびプライベートとして宣言する.
  • は、setterのような状態を変更する方法がないことを保証する.
  • getterメソッドで返されるステータスは、それ自体ではなくコピーを返すべきである.
  • ステータスを変更する方法をいくつか提供しなければならない場合、これらの方法の戻り値は新しいオブジェクトであり、ソースオブジェクト上で直接変更することはできません.

  • Stringがどのように実現されているかを見てみましょう.
    ソースコードからStringはクラスにfianl修飾があり、クラス全体のすべての方法がfinalメソッドであり、継承されて書き換えられないことがわかります.
    public final class String
        implements java.io.Serializable, Comparable, CharSequence {
        /** The value is used for character storage. */
        private final char value[];
        ......
    }
    

    同時にhash以外のメンバー変数はfinal修飾であることがわかる.次のようになります.
    public static final Comparator CASE_INSENSITIVE_ORDER
                                             = new CaseInsensitiveComparator();
    
    private int hash; // Default to 0
    private static final long serialVersionUID = -6849794470754667710L;
    private final char value[];
    private static final ObjectStreamField[] serialPersistentFields =
            new ObjectStreamField[0];
    

    これは私たちが最初に話した2つ目の原則とは少し異なることを発見しました.しかし大丈夫です.それは比較的強硬なルールで、finalに設定しなくても問題がないことを証明できればいいです.CASE_INSENSITIVE_ORDERはなぜpublicなのでしょうか?実はこのメンバー変数はStringの内部状態とは言えず、定数としか言えず、外部にアクセスされてもあまり影響はありません.
    Stringがsetterメソッドを提供しているかどうかを見てみましょう.APIドキュメントを参照してみましたが、ステータスのsetterメソッドは見つかりませんでした.次にgetterメソッドを見てみると、Stringの内部状態value配列にアクセスし、以下に示すようにバイト配列に符号化するgetBytes()メソッドがあります.
    public byte[] getBytes() {
        return StringCoding.encode(value, 0, value.length);
    }
    
    //StringCoding.encode  
    static byte[] encode(char[] ca, int off, int len) {
        String csn = Charset.defaultCharset().name();
        try {
            // use charset name encode() variant which provides caching.
            return encode(csn, ca, off, len);
        } catch (UnsupportedEncodingException x) {
            warnUnsupportedCharset(csn);
        }
        try {
            return encode("ISO-8859-1", ca, off, len);
        } catch (UnsupportedEncodingException x) {
            // If this code is hit during VM initialization, MessageUtils is
            // the only way we will be able to get any kind of error message.
            MessageUtils.err("ISO-8859-1 charset not available: "
                             + x.toString());
            // If we can not find ISO-8859-1 (a required encoding) then things
            // are seriously wrong with the installation.
            System.exit(1);
            return null;
        }
    }
    

    Encodeメソッドはvalue配列の内容を変更せず,コンテンツを取得し,コンテンツに基づいて一定の符号化方式で符号化しbyte配列に結果を格納し,最後に戻る.ここから,この方法はString内部状態を修正していないことが分かる.
    最後にStringを修正する可能性のあるAPIがあるかどうかを見て、APIドキュメントをざっと見てみると、substring、replaceなど、Stringを修正する方法がたくさんあることがわかりました.しかし、実際には、これらの方法は最終的にStringオブジェクトの値を変更するのではなく、元のStringの内容に基づいて次のように新しいStringオブジェクトを生成します.
     public String substring(int beginIndex) {
            if (beginIndex < 0) {
                throw new StringIndexOutOfBoundsException(beginIndex);
            }
            int subLen = value.length - beginIndex;
            if (subLen < 0) {
                throw new StringIndexOutOfBoundsException(subLen);
            }
            return (beginIndex == 0) ? this : new String(value, beginIndex, subLen);
        }
    

    最後の戻り文を見てみましょう.beginIndexが0の場合、オブジェクトを再作成する必要はありません.直接自分に戻ります.0でない場合は、オブジェクト自体を変更するのではなく、新しいStringオブジェクトを作成します.replaceなどの方法も同様であり,余計なことは言わない.
    Stringクラスは基本的に上記の5つのステップを満たしており、hashがfinal修飾ではないなどのコードの一部だけが従っていないが、hash値を変更できる外部ルートがないことを保証できれば大丈夫であることがわかる.
    3'+'のリロード
    「+」記号を使用して、次のような2つの文字列をつなぎ合わせることがよくあります.
    String s1 = "hello,";
    String s2 = "world";
    String res = s1 + s2;
    System.out.println(res);
    

    前節では、Stringは可変ではないと言いましたが、なぜここはStringが「可変」に見えるのでしょうか.実はここはStringの不変性に違反していません.ただ、コンパイラは私たちと小さな「トリック」をしました.生成されたバイトコードを見てみましょう.まずjavacを使用してコンパイルし、javap-verbose xxxを使用します.classコマンドはバイトコードを表示し、結果は以下のようになります(他の内容は省略).
    ......
    Code:
          stack=2, locals=4, args_size=1
             0: ldc           #2                  // String hello,
             2: astore_1
             3: ldc           #3                  // String world
             5: astore_2
             6: new           #4                  // class java/lang/StringBuilder
             9: dup
            10: invokespecial #5                  // Method java/lang/StringBuilder."":()V
            13: aload_1
            14: invokevirtual #6                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
            17: aload_2
            18: invokevirtual #6                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
            21: invokevirtual #7                  // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
            24: astore_3
            25: getstatic     #8                  // Field java/lang/System.out:Ljava/io/PrintStream;
            28: aload_3
            29: invokevirtual #9                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
            32: return
     ....
    

    「+」号はコンパイラによってStringBuilder.に翻訳されていることがわかります.append()メソッド(シーケンス番号14と18の命令を参照)は、言い換えれば、"+"番号がリロードされ、最終的にパッチが完了した後、StringBuilder.toString()メソッドを呼び出して新しいパッチされたStringオブジェクト(シーケンス番号21の命令を参照)を返す.
    したがって、Stringは変更されず、StringBuilderを呼び出す.append()メソッドは、元の文字列の結合を支援し、新しい文字列を生成します.
    演算子のリロードはC++で許可されていますが、Javaでは許可されていません.何が原因なのかはよくわかりませんが、演算子の再ロードがC++の複雑さの原因の一つだと思います.
    4 Integer.String()とString.valueOf()の関係
    私たちは直接ソースコードを見ます(分からないときは直接ソースコードを見るのが一番いい方法で、ネットで調べるよりも速く、直観的です):
    //String.valueOf(int)
    public static String valueOf(int i) {
        return Integer.toString(i);
    }
    
    //Integer.toString(int i)
    public static String toString(int i) {
        if (i == Integer.MIN_VALUE)
            return "-2147483648";
        int size = (i < 0) ? stringSize(-i) + 1 : stringSize(i);
        char[] buf = new char[size];
        getChars(i, size, buf);
        return new String(buf, true);
    }
    

    明らかにStringvalueOf(int)メソッドはIntegreを直接呼び出した.toString()メソッドは、文字列で表される整形数字を返します.IntegerではtoString()メソッドでは、最後に新しいStringオブジェクトが返されます.
    String.valueOf()には他にもリロード形式があり、valueOf(int)と非常に似ている.例えば、valueOf(long)はLongを呼び出す.toString()、valueOf(double)はDoubleを呼び出します.TOString()ですが、Booleanは必要ありません.直接「true」または「false」に戻ればいいです.具体的な違いはソースコードを見ることをお勧めします.
    5 Java 9とJava 8 Stringクラスの違い
    Java 9はStringを大きく変更しましたが、最も根本的な違いはcharタイプの配列を使用して文字を格納するのではなく、byteタイプの配列を変更することです.charタイプの占有スペースは2バイト、byteは1バイトで、多くのスペースを節約できます.他の修正はすべてこの変更に基づいて変更され、具体的な変更はネット上で検索することをお勧めします.これは私が見た文章です.JAVA 9 Stringの新しい特性、あなたの知らないものを言いますです.
    6まとめ
    Stringクラスは非常に重要で、Javaプログラムの各方面で使用されます.本文は簡単にStringについて話した.indexOf()メソッド、「+」号のリロード、Stringの不変性など、ついでにソースコードの読み方を見失うことが一番重要だと思います.JDKのすべてのクラスとそのAPIをメモすることはできないので、ソースコード、ドキュメントを見る方法を身につけるだけで、いつでもこのクラスとそのAPIの機能と使用方法を迅速に理解することができます.だからマスターする方法はとても重要です!!!出典を明らかにする.