Java에서 final로 선언 된 ==와 문자열 비교


220

Java의 문자열에 대한 간단한 질문이 있습니다. 다음 간단한 코드 세그먼트는 두 문자열을 연결 한 다음와 비교합니다 ==.

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

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

비교 표현 concat=="string"반환 false명백한로 (I 사이의 차이 이해 equals()==).


이 두 문자열이 final이렇게 선언 되면

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

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

concat=="string"이 경우 비교식이 반환 true됩니다. 왜 final차이가 있습니까? 인턴 풀과 관련이 있습니까? 아니면 잘못 인도되고 있습니까?


22
필자는 항상 equals가 동일한 내용을 확인하는 기본 방법이라는 것을 어리석은 것으로 알았습니다. == 그렇게하는 대신 referenceEquals 또는 비슷한 것을 사용하여 포인터가 같은지 확인하십시오.
Davio

25
입니다 하지 의 중복 "어떻게 내가 자바에서 문자열을 비교합니까?" 어떠한 방식으로. 영업 이익 사이의 차이를 이해 equals()하고 ==문자열의 맥락에서,보다 의미있는 질문을한다.
arshajii

@Davio 그러나 수업이 없을 때 어떻게 작동 String합니까? 나는 내용 비교를 수행하는 것이 매우 논리적이며, equals두 객체가 동일하다고 생각되는 시점을 알기 위해 재정의 할 수있는 방법이라고 생각합니다 ==. 내용 비교가 수행 된 경우 ==우리는 우리가 "동일한 내용"는 무엇을 의미하는 정의하는 것을 오버라이드 (override), 그리고 의미를 가지고 있지 수 equals==만 반전 String의 바보 될 것입니다. 또한 이것에 관계없이, ==대신 콘텐츠 비교 를 수행하는 데 아무런 이점이 없습니다 equals.
SantiBailors

@SantiBailors 당신은 이것이 Java에서 작동하는 방식이라는 것이 정확합니다. 또한 C #을 사용했습니다. 여기서 ==는 콘텐츠 평등에 대해 오버로드됩니다. ==를 사용하는 추가 보너스는 null 안전하다는 것입니다. (null == "something")은 false를 반환합니다. 2 개의 객체에 equals를 사용하는 경우 null이거나 NullPointerException이 발생할 위험이 있는지 알아야합니다.
Davio

답변:


232

String( 불변 인 ) 변수를로 선언하고 final컴파일 타임 상수 표현식으로 초기화하면 컴파일 타임 상수 표현식이되고 값은 사용되는 컴파일러에 의해 인라인됩니다. 따라서 두 번째 코드 예제에서 값을 인라인 한 후 문자열 연결은 컴파일러에 의해 다음과 같이 변환됩니다.

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

문자열 리터럴이 삽입 되어 있기 때문에 비교할 때 "string"제공됩니다 .true

에서 §4.12.4 JLS - final변수 :

프리미티브 유형 또는 유형의 변수 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

분명히 두 개의 개별 변수를 저장 str하고 연결 작업을 수행하는 데 사용하고 있습니다.ingStringBuilder

반면 두 번째 코드 예제 ( 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컴파일 타임에 문자열을 작성하기 위해 최종 변수를 직접 인라인하여 ldc단계의 조작으로 로드됩니다 0. 그런 다음 두 번째 문자열 리터럴이 ldc단계에서 작업에 의해로드됩니다 7. String런타임에 새로운 객체를 생성하지는 않습니다 . 문자열은 컴파일 타임에 이미 알려져 있으며 인턴됩니다.


2
다른 Java 컴파일러 구현이 최종 문자열을 인턴하지 못하게 막는 것이 있습니까?
Alvin

13
@Alvin JLS를 사용하려면 컴파일 타임 상수 문자열 표현식을 삽입해야합니다. 모든 적합한 구현은 여기서 동일한 작업을 수행해야합니다.
Tavian Barnes

반대로 JLS는 컴파일러 첫 번째 비 최종 버전 에서 연결을 최적화 하지 않아야 한다고 요구 합니까? 컴파일러가 코드를 생성하는 것이 금지되어 true있습니까?
phant0m

1
@ 현재 표현 복용 phant0m 사양은 " 목적은 새롭게 식은 상수 식 (§15.28)가 아닌 경우 (§12.5)를 생성한다. 문자 그대로 "최종 생성 된"문자열은 다른 객체 ID를 가져야하므로 최종 버전이 아닌 버전에서 최적화를 적용 할 수 없습니다. 이것이 의도적인지 모르겠습니다. 결국, 현재 컴파일 전략은 그러한 제한 사항을 문서화하지 않은 런타임 기능에 위임하는 것입니다. String
Holger

31

내 연구에 따르면 모든 것이 final StringJava로 인턴되었습니다. 블로그 게시물 중 하나에서 :

따라서 == 또는! =를 사용하여 두 개의 String을 비교해야하는 경우 비교하기 전에 String.intern () 메서드를 호출해야합니다. 그렇지 않으면 항상 문자열 비교를 위해 String.equals (String)를 선호하십시오.

따라서 호출 String.intern()하면 ==연산자를 사용하여 두 문자열을 비교할 수 있습니다 . 그러나 String.intern()Java final String에서는 내부적으로 인턴 되기 때문에 여기 에 필요하지 않습니다 .

String.intern () 메소드에 == 연산자 와 Javadoc을 사용하여 자세한 정보를 비교할 수 있습니다 .

자세한 내용 은이 Stackoverflow 게시물을 참조하십시오.


3
intern () 문자열은 가비지 수집되지 않으며 permgen 공간에 저장되어 있으므로 제대로 사용하지 않으면 메모리 부족 오류 와 같은 문제 가 발생 합니다.
Ajeesh

@Ajeesh-인턴 된 문자열은 가비지 수집 될 수 있습니다. 상수 식으로 인해 인턴 된 문자열조차도 일부 환경에서 가비지 수집 될 수 있습니다.
Stephen 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      
       ...

문자열이되지 않도록 경우 최종 컴파일러는 사용해야 할 것입니다 StringBuilder연결하는 str1str2그래서

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

스택 및 문자열 계속 풀 개념 여기에 이미지 설명을 입력하십시오


6
뭐? 나는 이것이 어떻게 공감했는지 이해하지 못한다. 답을 명확히 할 수 있습니까?
Cᴏʀʏ

str1 + str2가 문자열로 최적화되지 않았기 때문에 문자열 풀의 문자열과 비교하면 잘못된 조건이 발생합니다.
viki.omega9

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
}

여기에 두 가지가 String상수를 "str"하고 "ing"있는은과 런타임에 연결된해야합니다 StringBuilder.


0

Java의 String 리터럴 표기법을 사용하여 만들 때 해당 객체가 풀에없는 경우 자동으로 intern () 메서드를 호출하여 해당 객체를 String 풀에 넣습니다.

final은 왜 차이가 있습니까?

컴파일러 는 최종 변수가 절대 변경되지 않음을 알고 있습니다.이 최종 변수를 추가하면 str1 + str2표현식 출력으로 인해 출력이 문자열 풀로 이동합니다. 결과 출력도 변경되지 않으므로 결국 컴파일러는 위의 두 최종 변수의 출력 후에 인터 메소드를 호출합니다. 최종 변수가 아닌 변수 컴파일러의 경우 인턴 메소드를 호출하지 마십시오.

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.