Java는 메모리 공간을 절약하기 위해 Heap 영역에 String Pool 영역을 만들고, 이 영역에 Constant 문자열을 저장한다.
그런데 이 String Pool 영역에 들어가는 조건이 애매하다.
예를 들어, 아래의 s2, s3, s4는 비슷해보이만 다른 결과가 나온다.
String s1 = "abc1"; // String Pool에 저장됨
int i = 1;
final int j = 1;
String s2 = "a" + "bc" + 1; // s1 == s2: true
String s3 = "a" + "bc" + i; // s1 == s3: false
String s4 = "a" + "bc" + j; // s1 == s4: true
이유가 뭘까? String이 String Pool에 저장되는 조건을 알아보자.
String이 String Pool에 저장되는 조건
String의 값이 결정되는데는 두 가지 방식이 있다.
하나는 Compile-time Resolution (컴파일 타임에 결정) 으로, String s = "ASD" + 1과 같이 상수만 이용해 초기화될 경우 컴파일 타임에 값이 결정된다. 이 경우엔 String이 Immutable (불변) 하다고 한다.
나머지 하나는 Runtime Resolution (런타임에 결정) 으로, String s = "Result: " + var 와 같이 변수의 값 등에 따라 바뀔 여지가 있어, 실행 후에 값이 결정된다.
Java는 Compile-time Resolution에 해당하는 String들만 String Pool에 저장한다.
아래 예시들을 통해 Compile-time Resolution과 Runtime Resolution의 차이를 구분해보자.
String 리터럴로 초기화하는 경우
String s1 = "abc4";
String s2 = "ab" + "c4"; // concat
System.out.println("s1 == s2 = " + (s1 == s2)); // true
"abc4"의 경우 리터럴이기 때문에 Compile-Time Resolution에 해당한다.
"ab", "c4" 역시 리터럴이기 때문에 "ab" + "c4" 또한 Compile-Time Resolution에 해당한다.
여기서 생각해봐야 할게 하나 있다.
"abc4"는 s1에 쓰이기도 하고 s2의 결과물이기도 해 String Pool에 들어가는게 맞다.
그런데 단순히 concat을 하기 위해 사용된 "ab"와 "c4" 리터럴도 String Pool에 들어갈까?
정답을 보기 전에 한번 생각해보자.
안들어간다.
javap 명령을 통해 disassembly 해보면 알 수 있다.
D:\workspace\Study\Java\out\production\Java> javap -v .\StringPoolTest.class
...
Constant pool:
#1 = Methodref #2.#3 // java/lang/Object."<init>":()V
#2 = Class #4 // java/lang/Object
#3 = NameAndType #5:#6 // "<init>":()V
#4 = Utf8 java/lang/Object
#5 = Utf8 <init>
#6 = Utf8 ()V
[String Pool 초기화]
#7 = String #8 // abc4
#8 = Utf8 abc4
#9 = Fieldref #10.#11 // java/lang/System.out:Ljava/io/PrintStream;
#10 = Class #12 // java/lang/System
#11 = NameAndType #13:#14 // out:Ljava/io/PrintStream;
#12 = Utf8 java/lang/System
#13 = Utf8 out
#14 = Utf8 Ljava/io/PrintStream;
#15 = InvokeDynamic #0:#16 // #0:makeConcatWithConstants:(Z)Ljava/lang/String;
#16 = NameAndType #17:#18 // makeConcatWithConstants:(Z)Ljava/lang/String;
#17 = Utf8 makeConcatWithConstants
#18 = Utf8 (Z)Ljava/lang/String;
...
8번 줄을 확인해보자. concat의 결과인 "abc4"는 String Pool에 들어갔지만, concat하기 위한 재료로 사용된 "ab", "c4"는 String Pool에 들어가지 않는 것을 볼 수 있다.
어차피 합쳐진 이후엔 쓸 일이 없으니 저장해둘 필요가 없어 String Pool에 넣지 않는 것 같다.
신기하다.
상수들만 사용해 초기화하는 경우
String s1 = "abc4";
String s2 = "ab" + 'c' + 4;
String s3 = "a" + (char)(7 * 7 * 2) + (char)('d' - 1) + (8 / 2);
System.out.println("s1 == s2 = " + (s1 == s2)); // true
System.out.println("s1 == s3 = " + (s1 == s3)); // true
위와 같이 상수들만 사용해 초기화할 경우에도 String Pool에 자동 변환되어 들어간다.
s3의 경우 조금 복잡해 보이는데, 별로 어려울것 없다. 식이 복잡해도 상수만 사용했기 때문에 Compile-Time Resolution에 해당한다.
변수를 사용해 초기화하는 경우
String s1 = "abc4";
int n1 = 4;
final int n2 = 4;
String s2 = "abc" + n1;
String s3 = "abc" + n2;
System.out.println("s1 == s2 = " + (s1 == s2)); // false
System.out.println("s1 == s3 = " + (s1 == s3)); // true
처음으로 false가 나왔다.
s2의 경우 수식에 n1이라는 변수가 들어갔다. 이렇게 수식에 변수가 들어가면, 변수의 값에 따라 String도 달라질 수 있기에 해당 코드를 실행해보기 전까진 값을 알 수 없다. 따라서 Runtime Resolution에 해당한다.
하지만 똑같이 수식에 변수를 썼더라도, 해당 변수에 final을 붙여 상수로 만들어주면 String의 Immutable이 유지된다. 따라서 s3의 경우엔 Compile-time Resolution에 해당한다.
예시를 하나 더 보자.
String s1 = "abc4";
String s2 = "ab";
String s3 = "c4";
String s4 = s2 + s3;
System.out.println("s1 == s4 = " + (s1 == s4)); // false
이 예제 역시 수식에 변수가 들어갔기 때문에 Runtime Resolution에 해당된다.
그럼 이 결과가 true로 나오게 하기 위해 어느 부분을 수정해야 할까?
변수 s2와 s3을 final로 만들면 된다.
이렇게 수정하면 String의 Immutable이 유지되기 때문에 Compile-time Resolution에 해당된다.
String.intern 메서드 사용
String엔 intern 메서드가 있다.
이 메서드를 사용하면 현재 String을 String Pool에 임의로 집어넣는다.
만약 String Pool에 같은 문자열이 있을 경우, 해당 문자열을 가리키게 만든다.
따라서 String 타입의 두 변수 a와 b가 있다고 할 때, a.equals(b)가 true라면, a.intern() == b.intern()도 항상 true다.
쓸 일이 많이 있을진 모르겠지만 알아두면 좋을 것 같다.
[ 관련 글 ]
[Java] String + 연산 원리와 성능 비교
Intro 개발하던 중 갑자기 String의 + 연산이 어떻게 되는지 궁금해져 찾아봤다. String + 연산 동작원리, StringBuilder, StringBuffer와의 성능 차이에 대해 알아보자. String + 연산 동작원리 String의 +연산은
devjaewoo.tistory.com
참고자료
'Study > Java' 카테고리의 다른 글
[Java] String + 연산 원리와 성능 비교 (0) | 2023.02.08 |
---|