Javaでfinalと宣言されている文字列と==の比較


220

Javaの文字列について簡単な質問があります。次の単純なコードのセグメントは、2つの文字列を連結し、それらをと比較するだけ==です。

String str1="str";
String str2="ing";
String concat=str1+str2;

System.out.println(concat=="string");

比較式のconcat=="string"戻りfalse明らかなように(私は違いを理解するequals()とし==)。


この2つの文字列がfinalそのように宣言されている場合、

final String str1="str";
final String str2="ing";
String concat=str1+str2;

System.out.println(concat=="string");

concat=="string"この場合、比較式はを返しますtrue。なぜfinal違いがあるのですか?それはインターンプールで何かをしなければならないのですか、それとも私はただ惑わされていますか?


22
equalsが等しいコンテンツをチェックするデフォルトの方法であるというのはばかげていると私はいつも気付いていました。
Davio 2013年

25
これは、「Javaで文字列を比較するにはどうすればよいですか?」の複製ではありません。とにかく。OPは間違いを理解equals()し、==文字列のコンテキストで、そしてより意味のある質問をされています。
arshajii 2013年

@Davioしかし、クラスがそうでない場合、それはどのように機能しStringますか?内容の比較をで行うのはばかげているのではなく、非常に論理的だと思いequalsます。オーバーライドして、2つのオブジェクトが等しいと見なした場合に通知し、アイデンティティの比較をで行うことができます==。内容の比較が行われた場合、==それをオーバーライドして「等しい内容」の意味を定義することはできず、の意味equalsとs ==についてのみ逆にStringすることはばかげたことになります。また、これに関係なく==、コンテンツの比較を行う代わりに、とにかく何の利点もないと思いますequals
SantiBailors 2017年

@SantiBailorsあなたは正しいです。これはJavaでの動作と同じです。C=も使用しています。ここでは、コンテンツの等価性のために==がオーバーロードされています。==を使用することの追加のボーナスは、nullセーフであることです:(null == "something")はfalseを返します。2つのオブジェクトにequalsを使用する場合、いずれかがnullになる可能性があるか、またはNullPointerExceptionがスローされるリスクがあるかどうかに注意する必要があります。
Davio

回答:


232

String不変の)変数をとして宣言finalし、コンパイル時の定数式で初期化すると、それもコンパイル時の定数式になり、その値は使用されるコンパイラーによってインライン化されます。したがって、2番目のコード例では、値をインライン化した後、文字列連結がコンパイラによって次のように変換されます。

String concat = "str" + "ing";  // which then becomes `String concat = "string";`

文字列リテラルがインターンされるため、これと比較する"string"と、次のようになります。true

JLS§4.12.4finalから- 変数

プリミティブ型またはtypeの変数String、つまりfinalコンパイル時の定数式(15.28)で初期化される変数は、定数変数と呼ばれます

また、JLS§15.28から-定数式:

型のコンパイル時定数式は、メソッドを使用して一意のインスタンスを共有するために、String常に「インターン」されますString#intern()


これは、String変数がでない最初のコード例には当てはまりませんfinal。したがって、これらはコンパイル時の定数式ではありません。そこでの連結操作は実行時まで遅延するため、新しいStringオブジェクトの作成につながります。これを確認するには、両方のコードのバイトコードを比較します。

最初のコード例(非finalバージョン)は、次のバイトコードにコンパイルされます。

  Code:
   0:   ldc     #2; //String str
   2:   astore_1
   3:   ldc     #3; //String ing
   5:   astore_2
   6:   new     #4; //class java/lang/StringBuilder
   9:   dup
   10:  invokespecial   #5; //Method java/lang/StringBuilder."<init>":()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:  ldc     #9; //String string
   31:  if_acmpne       38
   34:  iconst_1
   35:  goto    39
   38:  iconst_0
   39:  invokevirtual   #10; //Method java/io/PrintStream.println:(Z)V
   42:  return

明らかにそれが格納されているstring二つの別々の変数であり、使用StringBuilder連結演算を実行します。

一方、2番目のコード例finalバージョン)は次のようになります。

  Code:
   0:   ldc     #2; //String string
   2:   astore_3
   3:   getstatic       #3; //Field java/lang/System.out:Ljava/io/PrintStream;
   6:   aload_3
   7:   ldc     #2; //String string
   9:   if_acmpne       16
   12:  iconst_1
   13:  goto    17
   16:  iconst_0
   17:  invokevirtual   #4; //Method java/io/PrintStream.println:(Z)V
   20:  return

したがってstring、コンパイル時にStringを作成する最終変数を直接インライン化します。これはldc、ステップの操作によってロードされます0。次に、2番目の文字列リテラルがldcステップの操作によってロードされます7String実行時に新しいオブジェクトを作成する必要はありません。文字列はコンパイル時にすでにわかっており、インターンされています。


2
他のJavaコンパイラの実装が最終的なStringをインターンしないことを妨げるものは何もないのですか?
Alvin

13
JLSの@Alvinでは、コンパイル時の定数文字列式をインターンする必要があります。準拠する実装はすべて、ここでも同じことを行う必要があります。
Tavian Barnes 2013

逆に、JLSは、コンパイラーが最初の非最終バージョンで連結を最適化してはならないことを義務付けていますか?コンパイラはコードを生成することを禁じられていますか?true
phant0m 2018年

1
@ phant0mの現在の文言取っ仕様を、「式が定数式(15.28)でない限り、オブジェクトを新たに(§12.5)が作成されます。「新しく作成された」文字列は異なるオブジェクトIDを持つ必要があるため、文字通り、非最終バージョンで最適化を適用することはできません。これが意図的なものかどうかはわかりません。結局のところ、現在のコンパイル戦略は、そのような制限を文書化していないランタイム機能に委任することです。String
Holger、

31

私の調査によると、すべてfinal StringがJavaでインターンされています。ブログ投稿の1つから:

したがって、==または!=を使用して2つのStringを比較する必要がある場合は、比較を行う前に必ずString.intern()メソッドを呼び出してください。それ以外の場合、文字列の比較には常にString.equals(String)を優先します。

したがって、呼び出すString.intern()場合は、==演算子を使用して2つの文字列を比較できます。しかしString.intern()、Java final Stringは内部でインターンされているため、ここでは必要ありません。

==演算子String.intern()メソッドのJavadocを使用して、詳細な文字列比較を見つけることができます。

詳細については、このStackoverflowの投稿も参照してください。


3
intern()文字列はガベージコレクションされず、低いpermgenスペースに格納されるため、適切に使用しないと、メモリ不足エラーなどの問題が発生します。
Ajeesh 2013年

@Ajeesh-インターンされた文字列はガベージコレクションされる可能性があります。定数式の結果であるインターンされた文字列でさえ、状況によってはガベージコレクションされる可能性があります。
スティーブンC

21

この方法を見てみると

public void noFinal() {
    String str1 = "str";
    String str2 = "ing";
    String concat = str1 + str2;

    System.out.println(concat == "string");
}

public void withFinal() {
    final String str1 = "str";
    final String str2 = "ing";
    String concat = str1 + str2;

    System.out.println(concat == "string");
}

そして、javap -c ClassWithTheseMethods あなたが見るバージョンで逆コンパイルされます

  public void noFinal();
    Code:
       0: ldc           #15                 // String str
       2: astore_1      
       3: ldc           #17                 // String ing
       5: astore_2      
       6: new           #19                 // class java/lang/StringBuilder
       9: dup           
      10: aload_1       
      11: invokestatic  #21                 // Method java/lang/String.valueOf:(Ljava/lang/Object;)Ljava/lang/String;
      14: invokespecial #27                 // Method java/lang/StringBuilder."<init>":(Ljava/lang/String;)V
      17: aload_2       
      18: invokevirtual #30                 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
      21: invokevirtual #34                 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
      ...

そして

  public void withFinal();
    Code:
       0: ldc           #15                 // String str
       2: astore_1      
       3: ldc           #17                 // String ing
       5: astore_2      
       6: ldc           #44                 // String string
       8: astore_3      
       ...

文字列が最終でないのであればコンパイラが使用する必要がありますStringBuilderCONCATENATEにstr1してstr2そう

String concat=str1+str2;

コンパイルされます

String concat = new StringBuilder(str1).append(str2).toString();

つまり、これはconcat実行時に作成されるため、文字列プールからは取得されません。


また、文字StringBuilder列が最終的なものである場合、コンパイラはそれらが決して変更されないと想定できるため、使用する代わりに安全に値を連結できます。

String concat = str1 + str2;

に変更することができます

String concat = "str" + "ing";

と連結

String concat = "string";

これはconcate、文字列プールにインターンされ、ifステートメント内のそのプールからの同じ文字列リテラルと比較される文字列リテラルになることを意味します。


15

スタックと文字列のcontsプールの概念 ここに画像の説明を入力してください


6
何?私はこれがどのように賛成票を持っているのか理解できません。あなたの答えを明確にできますか?
2013年

意図された答えは、str1 + str2がインターンされた文字列に最適化されていないため、文字列プールからの文字列と比較すると、誤った状態になるということだと思います。
viki.omega9 2013年

3

final例のいくつかのバイトコードを見てみましょう

Compiled from "Main.java"
public class Main {
  public Main();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return

  public static void main(java.lang.String[]) throws java.lang.Exception;
    Code:
       0: ldc           #2                  // String string
       2: astore_3
       3: getstatic     #3                  // Field java/lang/System.out:Ljava/io/PrintStream;
       6: aload_3
       7: ldc           #2                  // String string
       9: if_acmpne     16
      12: iconst_1
      13: goto          17
      16: iconst_0
      17: invokevirtual #4                  // Method java/io/PrintStream.println:(Z)V
      20: return
}

0:2:String "string"(一定のプールからの)スタックにプッシュされ、ローカル変数に格納されているconcat直接。コンパイラーがString "string"コンパイル時にそれ自体を作成(連結)していると推測できます。

finalバイトコード

Compiled from "Main2.java"
public class Main2 {
  public Main2();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return

  public static void main(java.lang.String[]) throws java.lang.Exception;
    Code:
       0: ldc           #2                  // String str
       2: astore_1
       3: ldc           #3                  // String ing
       5: astore_2
       6: new           #4                  // class java/lang/StringBuilder
       9: dup
      10: invokespecial #5                  // Method java/lang/StringBuilder."<init>":()V
      13: aload_1
      14: invokevirtual #6                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/Stri
ngBuilder;
      17: aload_2
      18: invokevirtual #6                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/Stri
ngBuilder;
      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: ldc           #9                  // String string
      31: if_acmpne     38
      34: iconst_1
      35: goto          39
      38: iconst_0
      39: invokevirtual #10                 // Method java/io/PrintStream.println:(Z)V
      42: return
}

ここには2つのString定数が"str"あり"ing"、実行時にStringBuilder。で連結する必要があります。


0

ただし、Javaの文字列リテラル表記を使用して作成する場合、オブジェクトがまだプールに存在していなければ、オブジェクトは自動的にintern()メソッドを呼び出してそのオブジェクトを文字列プールに入れます。

なぜfinalは違いを生むのですか?

コンパイラーは最終的な変数が変化しないことを知っています。これらの最終的な変数を追加すると、str1 + str2式の出力も変化しないため、出力は文字列プールに送られます。そのため、コンパイラーは最後に上記の2つの最終的な変数の出力後にinterメソッドを呼び出します。非最終変数コンパイラの場合は、インターンメソッドを呼び出さないでください。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.