Javaコアテクノロジーインタビューエッセンシャル(講義5)| String、StringBuffer、StringBuilderの違いは何ですか?

今日は日常生活で使われている文字列についてお話します。見ないでください。とてもシンプルに見えますが、実際には、文字列は量や量に関係なくほとんどのアプリケーションで使用されているため、ほとんどすべてのプログラミング言語で特別な存在です。ボリューム。重要なコンポーネント。

今日お聞きしたいのは、Java文字列を理解するためのString、StringBuffer、StringBuilderの違いは何ですか?


典型的な答え

文字列はJava言語の非常に基本的で重要なクラスであり、文字列を構築および管理するためのさまざまな基本ロジックを提供します。これは典型的な不変クラスであり、finalクラスとして宣言されており、すべての属性がfinalです。また、その不変性により、文字列のスプライシングやカットなどのアクションにより、新しいStringオブジェクトが生成されます。文字列操作の普遍性により、関連する操作の効率がアプリケーションのパフォーマンスに大きな影響を与えることがよくあります。

StringBufferは、上記のスプライシングによって引き起こされる中間オブジェクトが多すぎるという問題を解決するために提供されるクラスです。appendまたはaddメソッドを使用して、既存のシーケンスの最後または指定された位置に文字列を追加できます。StringBufferは本質的にスレッドセーフな変更可能な文字シーケンスであり、スレッドセーフを保証し、パフォーマンスオーバーヘッドも追加するため、スレッドセーフが必要な場合を除いて、後継であるStringBuilderを使用することをお勧めします。

StringBuilderは、Java 1.5の新しい追加機能です。StringBufferと機能に本質的な違いはありませんが、スレッドセーフな部分を削除し、オーバーヘッドを効果的に削減します。ほとんどの場合、これは文字列スプライシングの最初の選択肢です。

テストサイト分析

ほとんどすべてのアプリケーション開発は、文字列の操作と切り離せません。文字列の設計と実装、およびスプライシングなどの関連ツールの使用を理解することは、高品質のコードを作成するのに非常に役立ちます。この質問に関して、私の以前の答えは一般的な要約の答えです。少なくとも、文字列は不変であり、不適切な文字列操作は多数の一時文字列を生成する可能性があり、スレッドセーフの違いを知っている必要があります。

さらに深く掘り下げていくと、インタビュアーは次のようなさまざまな視点からそれを見ることができます。

  • Stringおよび関連するクラスを通じて、基本的なスレッドセーフの設計と実装、およびさまざまな基本的なプログラミング手法を調べます。
  • JVMオブジェクトのキャッシュメカニズムの理解と、それを適切に使用する方法を調べます。
  • JVMによってJavaコードを最適化するためのいくつかの手法を調べます。
  • Java 9で実装された大幅な変更など、String関連のクラスの進化。

以上の点については、知識拡大の部分で詳しくお話しします。

知識の拡大

1.文字列の設計と実装に関する考慮事項

先に述べたように、StringはImmutableクラスの典型的な実装であり、内部データを変更できないため、基本的なスレッドセーフをネイティブに保証します。この便利さはコピーコンストラクタにも反映されます。不変性のため、Immutableオブジェクトは必要ありません。コピー時に追加データをコピーします。

StringBuffer実装の詳細を見てみましょう。スレッドセーフは、データを変更するさまざまなメソッドに同期キーワードを追加することで実現されます。これは非常に簡単です。実際、この単純で失礼な実装は、一般的なスレッドセーフな実装に非常に適しています。同期されたパフォーマンスについて心配する必要はありません。「時期尚早の最適化はすべての悪の根源である」と言う人もいます。信頼性、正確性、およびコードの可用性。読みやすさは、ほとんどのアプリケーション開発において最も重要な要素です。

文字シーケンスを変更する目的を達成するために、StringBufferとStringBuilderはどちらも下部に変更可能な(char、JDK 9の後のバイト)配列を使用します。どちらもAbstractStringBuilderを継承し、基本的な操作を含みます。違いは、最後のメソッドが追加されるかどうかだけです。同期。

さらに、この内部配列はどのくらいの大きさに作成する必要がありますか?小さすぎると、スプライシング時に十分な大きさの配列を再作成する必要がある場合があります。大きすぎると、スペースが無駄になります。現在の実装では、構築中に初期文字列の長さに16を追加します(つまり、オブジェクトの構築時に初期文字列が入力されていない場合、初期値は16になります)。スプライシングが何度も発生することが確実であり、おそらく予測可能である場合は、適切なサイズを指定して、多くの拡張のオーバーヘッドを回避できます。元の配列を破棄し、新しい(単純に複数と見なすことができる)配列を作成し、arraycopyを実行する必要があるため、拡張によって複数のオーバーヘッドが発生します。

特定のコード記述で話したコンテンツをどのように選択すればよいですか?

スレッドセーフの問題がなければ、すべてのスプライシング操作をStringBuilderで実装する必要がありますか?結局のところ、このように記述されたコードは多くの入力を必要とし、読みやすさは理想的ではありません。次の比較は非常に明白です。

String strByBuilder  = new
StringBuilder().append("aa").append("bb").append("cc").append
            ("dd").toString();
             
String strByConcat = "aa" + "bb" + "cc" + "dd";

 実際、通常の状況では、Javaがまだ非常にスマートであると信じるために、あまり心配する必要はありません。

実験を行い、次のコードをさまざまなバージョンのJDKでコンパイルしてから、逆コンパイルしてみましょう。たとえば、次のようになります。

public class StringConcat {
     public static String concat(String str) {
       return str + “aa” + “bb”;
     }
}

 異なるバージョンのJDKを使用するなど、最初にコンパイルしてから逆コンパイルします。

${JAVA_HOME}/bin/javac StringConcat.java
${JAVA_HOME}/bin/javap -v StringConcat.class

 JDK8の出力スニペットは次のとおりです。

         0: new           #2                  // class java/lang/StringBuilder
         3: dup
         4: invokespecial #3                  // Method java/lang/StringBuilder."<init>":()V
         7: aload_0
         8: invokevirtual #4                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
        11: ldc           #5                  // String aa
        13: invokevirtual #4                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
        16: ldc           #6                  // String bb
        18: invokevirtual #4                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
        21: invokevirtual #7                  // Method java/lang/StringBuilder.toString:()Ljava/lang/String;

 JDK 9では、逆コンパイルの結果は少し特殊になります。フラグメントは次のとおりです。

         // concat method
         1: invokedynamic #2,  0              // InvokeDynamic #0:makeConcatWithConstants:(Ljava/lang/String;)Ljava/lang/String;
         
         // ...
         // 实际是利用了MethodHandle,统一了入口
         0: #15 REF_invokeStatic java/lang/invoke/StringConcatFactory.makeConcatWithConstants:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/String;[Ljava/lang/Object;)Ljava/lang/invoke/CallSite;

 ご覧のとおり、非静的スプライシングロジックは、JDK 8のjavacによって自動的にStringBuilder操作に変換されます。JDK9では、考え方の変化を反映しています。Java 9は、InvokeDynamicを使用して、文字列スプライシングの最適化をjavacによって生成されたバイトコードから切り離します。JVM拡張機能の関連するランタイム実装が将来実装されると仮定すると、javacの変更に依存する必要はありません。

日常のプログラミングでは、プログラムの可読性と保守性を確保することが、いわゆる最適なパフォーマンスよりも重要であることがよくあります。実際のニーズに応じて、特定のコーディング方法を選択できます。

2.文字列キャッシュ

大まかな統計、一般的なアプリケーションのダンプヒープを実行してから、オブジェクトの構成を分析しました。平均して、オブジェクトの25%が文字列であり、それらの約半分が重複していることがわかります。重複する文字列の作成を回避できれば、メモリ消費とオブジェクト作成のオーバーヘッドを効果的に削減できます。

Stringは、Java 6の後にintern()メソッドを提供して、JVMに対応する文字列をキャッシュして再利用するように促します。文字列オブジェクトを作成してintern()メソッドを呼び出すと、キャッシュされた文字列がすでに存在する場合は、インスタンスがキャッシュに返されます。それ以外の場合は、キャッシュされます。一般的に、JVMは「abc」や文字列定数などのすべてのテキスト文字列をキャッシュします。

見栄えがいいですね。しかし、実際の状況はあなたを驚かせるでしょう。一般的に、Java 6の履歴バージョンが使用されており、インターンを広範囲に使用することはお勧めしません。なぜですか。悪魔は細部にあります。キャッシュされた文字列は、悪名高い「永続的な世代」であるいわゆるPermGenに格納されます。このスペースは非常に限られており、FullGC外のガベージコレクションでは処理されません。したがって、不適切に使用された場合、OOMはひいきになります。

以降のバージョンでは、このキャッシュはヒープに配置されるため、永続的な生成がいっぱいになる問題が大幅に回避され、永続的な生成でさえJDK 8のMetaSpace(メタデータ領域)に置き換えられます。さらに、デフォルトのキャッシュサイズは、最初の1009から7u40の後の60013まで、絶えず拡大しています。次のパラメータを使用して特定の数値を直接印刷できます。また、独自のJDKを使用してすぐに試すことができます。

-XX:+PrintStringTableStatistics

 次のJVMパラメータを使用してサイズを手動で調整することもできますが、ほとんどの場合、サイズが操作の効率に影響を与えていることが確実でない限り、調整する必要はありません。

-XX:StringTableSize=N

 Internは明示的な重複排除メカニズムですが、開発者がコードを作成するときに明示的に呼び出す必要があるため、特定の副作用もあります。1つは不便であり、それぞれを明示的に呼び出すのは非常に面倒です。もう1つは効率を保証することは困難であり、アプリケーション開発段階で文字列の繰り返しを明確に予測することは困難です。これはコードを汚染する慣行であると考える人もいます。

幸い、Oracle JDK 8u20の後に、新しい機能、つまりG1GCでの文字列の並べ替えが導入されました。これは、同じデータの文字列を同じデータにポイントすることによって行われます。これは、JVMの下部での変更であり、Javaクラスライブラリを変更する必要はありません。

この機能は現在デフォルトで無効になっていることに注意してください。有効にするには次のパラメータを使用する必要があり、G1GCの使用を指定することを忘れないでください。

-XX:+UseStringDeduplication

 上記のいくつかの側面は、Javaの下部にある文字列のさまざまな最適化のほんの一部です。実行時に、文字列の基本的な操作の一部は、JVM内の組み込みメカニズムを直接使用し、特別に最適化されたネイティブコードを実行することがよくあります。バイトコードではありません。 Javaコードによって生成されます。イントリンシックは、ネイティブメソッドを使用した一種のハードコードされたロジックとして簡単に理解できます。これは特別な種類のインライン化です。多くの最適化では、特定のCPU命令を直接使用する必要があります。詳細については、関連するソースコードを参照して、関連する固有の定義を見つけるための「文字列」。もちろん、次のパラメータを使用して、実験アプリケーションを開始するときの本質的な発生の状態を理解することもできます。

-XX:+PrintCompilation -XX:+UnlockDiagnosticVMOptions -XX:+PrintInlining
    //样例输出片段    
        180    3       3       java.lang.String::charAt (25 bytes)  
                                  @ 1   java.lang.String::isLatin1 (19 bytes)   
                                  ...  
                                  @ 7 java.lang.StringUTF16::getChar (60 bytes) intrinsic 

 文字列の実装だけでも、Javaプラットフォームのエンジニアや科学者による非常に大規模でわかりにくい作業が必要であり、私たちが得る便利さの多くはこれに由来していることがわかります。

コラムの最後で、JVMやパフォーマンスなどのトピックでJVM内部最適化のいくつかの方法を詳しく紹介します。興味がある場合は、詳細を学ぶことができます。JVM開発を行っていない場合、または特別なパフォーマンス最適化をまだ使用していない場合でも、この知識は技術的な深さを増すのに役立ちます。

3.文字列自体の進化

Java文字列を注意深く観察した場合、過去のバージョンでは、char配列を使用してデータを格納します。これは非常に簡単です。ただし、Javaの文字のサイズは2バイトです。ラテン語の文字には、幅が広すぎる文字はまったく必要ないため、無差別に実装すると、ある程度の無駄が生じます。最終的な分析では、ほとんどのタスクはデータを操作することであるため、密度はプログラミング言語プラットフォームの永遠のトピックです。

実際、Java 6では、Oracle JDKが文字列を圧縮する機能を提供していましたが、この機能の実装はオープンソースではなく、実際にいくつかの問題が明らかになったため、最新のJDKバージョンに移動されました。

Java 9では、コンパクト文字列の設計を導入し、文字列を大幅に改善しました。データの保存方法がchar配列からbyte配列に変更され、識別コード付きのいわゆるコーダーが追加され、関連する文字列操作クラスが変更されます。さらに、関連するすべてのイントリンシックなども、パフォーマンスの低下がないように書き直されています。

基盤となる実装にはこのような大きな変更が加えられていますが、Java文字列の動作はまったく変更されていないため、この機能はほとんどのアプリケーションに対して透過的であり、ほとんどの場合、既存のコードを変更する必要はありません。

もちろん、極端な場合には、最大の文字列のサイズなど、文字列にもいくつかの劣化が見られます。考えてみてください。char配列の元の実装では、文字列の最大長は配列自体の長さの制限ですが、バイト配列に置き換えると、同じ配列長でストレージ容量が2倍になります。幸い、これは理論上存在する制限であり、実際のアプリケーションがこれによって影響を受けることはわかっていません。

一般的なパフォーマンステストと製品実験では、コンパクトな文字列の利点、つまり、メモリフットプリントが小さく、動作速度が速いことがはっきりとわかります。

今日は、String、StringBuffer、StringBuilderの主な設計と実装機能から始め、文字列キャッシングのインターンメカニズム、非侵入型仮想マシンレベルの重み付け解除、Java 9でのコンパクト文字の改善、および最初の連絡先を分析しました。 JVMを使用する場合基礎となる最適化メカニズムは本質的です。実用的な観点から、コンパクト文字列であろうと基礎となる組み込み最適化であろうと、それらはすべてJava基本クラスライブラリを使用する利点を示しています。JDKをアップグレードする限り、多くの場合、最高の程度と最高品質の最適化を得ることができます。バージョンでは、これらをゼロコストで楽しむことができます。


他の古典的な答え

以下は、ネチズンのビンによって提起された質問から来ています。

jdk1.8では、stringは標準の不変クラスですが、そのハッシュ値はfinalで変更されません。そのハッシュ値は、ハッシュコードメソッドが初めて呼び出されたときに計算されますが、メソッドはロックされておらず、変数はロックされません。 volatileキーワードを使用します。レタッチはその可視性を保証できません。複数のスレッドから呼び出されると、ハッシュ値が複数回計算される場合があります。結果は同じですが、jdkの作成者が最適化しないのはなぜですか。

著者の回答:これらの「最適化」は、一般的なシナリオでは継続的なコストになる可能性があります。揮発性の読み取りには明らかなオーバーヘッドがあります。
競合が一般的でない場合は、読み取りがより一般的であり、単純なキャッシュがより効率的です。

以下は、ネチズンの公式アカウントからの回答です-Technology Sleeplessly:

今日のString / StringBuffer / StringBuilderの経験:
1 String

(1)文字列作成メカニズム
StringはJavaの世界で頻繁に使用されるため、Javaは、システムで多数のStringオブジェクトが生成されないようにするために文字列定数プールを導入しました。その操作メカニズムは次のとおりです。文字列を作成するときは、最初にプールに同じ値の文字列オブジェクトがあるかどうかを確認します。ある場合は、プールから直接見つかったオブジェクトへの参照を作成する必要はありません。そうではなく、新しい文字列オブジェクトを作成してオブジェクト参照を返し、新しく作成したオブジェクトをプールに入れます。ただし、newメソッドで作成されたStringオブジェクトは、文字列プールをチェックせず、ヒープ領域またはスタック領域に直接新しいオブジェクトを作成し、オブジェクトをプールに配置しません。上記の原則は、直接測定によって文字列オブジェクトに値を割り当てる場合にのみ適用されます。

例:String str1 = "123"; //直接値を割り当てることにより、文字列定数プールに入れます
String str2 = new String( "123"); //新しいメソッド割り当てにより、文字列定数プールに入れません

注:String inter()メソッドを提供します。このメソッドを呼び出すときに、このStringオブジェクトに等しい文字列が定数プール(equalsメソッドによって決定される)に含まれている場合、プール内の文字列が返されます。それ以外の場合、このStringオブジェクトはプールに追加され、このプール内のオブジェクトへの参照が返されます。

(2)文字列
[A]の特性は不変です。これは、Stringオブジェクトが生成されると、変更できないことを意味します。不変性の主な機能は、オブジェクトを複数のスレッドで共有する必要があり、頻繁にアクセスされる場合、同期とロックの待機時間を省略できるため、システムパフォーマンスが大幅に向上することです。不変モードは、マルチスレッドプログラムのパフォーマンスを向上させ、マルチスレッドプログラムの複雑さを軽減できる設計モードです。

[B]定数プールの最適化。2つのStringオブジェクトが同じ値を持つ場合、それらは定数プール内の同じコピーのみを参照します。同じ文字列が繰り返し表示される場合、この手法によりメモリスペースを大幅に節約できます。

2 StringBuffer / StringBuilder

StringBufferとStringBuilderはどちらもAbstractStringBuilder抽象クラスを実装し、外部に提供される呼び出しインターフェイスはほぼ同じです。メモリ内の基になるストレージは、文字列の順序付けられたシーケンス(charタイプの配列)でStringと同じです。ストレージの違いは、StringBuffer / StringBuilderオブジェクトの値を変更でき、値を変更した後、オブジェクト参照は変更されないことです。2つのオブジェクトの構築中に、最初にデフォルトに従って文字配列を適用します。サイズ、追加され続けるため新しいデータがデフォルトのサイズを超えると、より大きな配列が作成され、元の配列の内容がコピーされ、古い配列は破棄されます。したがって、より大きなオブジェクトの拡張には、多くのメモリコピー操作が必要になります。サイズを事前に評価できれば、パフォーマンスを向上させることができます。

注意すべき唯一のことは、StringBufferはスレッドセーフですが、StringBuilderはスレッドセーフではないということです。Java Standard Class Libraryのソースコードを参照できます。StringBufferクラスのメソッド定義には、その前にsynchronizeキーワードがあります。このため、StringBufferのパフォーマンスはStringBuilderよりもはるかに低くなります。

3アプリケーションシナリオ

[A]文字列クラスは、文字列の内容が頻繁に変更されないビジネスシナリオで推奨されます。例:定数宣言、少数の文字列スプライシング操作など。文字列コンテンツのスプライシングが多数ある場合は、文字列と文字列の間で「+」操作を使用しないでください。これにより、無駄な中間オブジェクトが多数生成され、スペースが消費され、パフォーマンスが低下します(新しいオブジェクト、オブジェクトのリサイクルには多くの時間がかかります)時間)。

[B]文字列操作(スプライシング、置換、削除など)を頻繁に実行し、マルチスレッド環境で実行する場合は、XML解析、HTTPパラメータ解析、カプセル化などのStringBufferを使用することをお勧めします。

[C]文字列操作(スプライシング、置換、削除など)を頻繁に実行し、シングルスレッド環境で実行する場合は、SQLステートメントアセンブリ、JSONカプセル化などのStringBuilderを使用することをお勧めします。

以下は、Huaxiangの時代のネチズンYuyueの答えから来ています:

getBytesとStringの間で変換する場合は、ビジネスニーズに応じてエンコード方式を指定することをお勧めします。指定しない場合は、JVMパラメーターにfile.encodingパラメーターが指定されているかどうかを確認してください。オペレーティングオペレーティングシステム環境のエンコーディング。、その後、エンコーディングは不確実になります。一般的なエンコーディングiso8859-1はシングルバイトエンコーディングであり、UTF-8は可変長エンコーディングです。

以下は、ネチズンからの穏やかな答えです。

public class StringConcat {         public static void main(String [] args){              String myStr = "aa" + "bb" + "cc" + "dd";              System.out.println( "My String:" + myStr);         }   } 。作者によって与えられる。この例では、まず、最初の文の文字列myStr =「AA」+「BB」+「CC」+「DD」を以下の説明に混乱して、コンパイラは、myStr =にそれをマージしました」 「aabbccdd」、いわゆるStringBuilderの背後にあるのは、System.out.println ..に文字列のスプライシングがあるためです。





著者の返信:ありがとう、それは少しよく考えられていません

次の答えはnetizenzhihai.tuから来ています:

1.実行速度:StringBuilder> StringBuffer> String
2. StringBuilderはスレッドセーフ、StringBufferはスレッドセーフ(同期)
3。文字列には2つの特性があります:1)不変2)文字列定数プールの使用(新しいString(xxx)を含まない) )
4。StringBuilderとStringBufferの最下層はchar配列です。まず、デフォルトのサイズに従って文字配列を適用します。追加後、デフォルトの配列サイズを超えると、より大きな配列が作成され、次に古い配列が作成されます。コンテンツがコピーされますここに来て、古い配列を破棄します。
5. Stringは、少数の文字列を接続するのに適しています。StringBuilderは、シングルスレッドの場合に多数の文字列を接続するのに適しています。StringBufferは、マルチスレッドの場合に多数の文字列を接続するのに適しています。

 

 

 

おすすめ

転載: blog.csdn.net/qq_39331713/article/details/114087610