String Literal이란?
큰따옴표(“)로 감싸여져 있는 문자열을 의미합니다. 예를 들어 아래의 코드에서 “Hello, world!”가 String Literal입니다.
1
String str = "Hello, world!";
위의 코드에서 볼 수 있듯이, String Literal은 String 타입입니다. 그래서 아래와 같이 작성할 수도 있습니다.
1
"Hello".charAt(0) // H
String Literal 캐싱
String Literal은 String Pool에 캐싱됩니다. 즉, 같은 내용인 String Literal(equals가 true)들은 모두 같은 String 객체입니다.
String Pool은 초기에는 비어있는 상태로 시작합니다. 그리고 새로운 String Literal을 발견할 때 마다 String Pool에 캐싱해서 사용합니다. String Literal이 이미 String Pool에 존재하는 문자열이라면 캐싱된 문자열을 반환하고, 아니라면 String Pool에 새롭게 등록해서(생성하는 과정 포함) 반환합니다.
1
2
3
4
5
6
7
8
9
class Test {
public static void main(String[] args) {
String hello = "Hello";
System.out.println(hello == "Hello"); // true
System.out.println(Other.hello == hello); // true
System.out.println(Other.hello == "Hello"); // true
}
}
class Other { static String hello = "Hello"; }
코드의 어떤 위치에서 “Hello”를 써도, 모두 같은 String 객체임을 확인할 수 있습니다. String Literal은 이처럼 같은 JVM을 사용한다면, 어디에서든 모두 같은 객체임을 보장합니다.
String Pool은 메모리 어디에 위치할까요?
String Pool은 java7 이상 기준으로 JVM의 heap영역에 있습니다.
java6 까지는 Perm 영역에 있었지만, 이 영역은 size가 고정되어있습니다. 그래서 종종 OutOfMemory가 발생하는 문제가 있었습니다. 그래서 java7부터는 동적으로 확장이 가능한 heap영역에 string pool을 할당해서 사용합니다.
String은 불변 객체다.
String Literal은 캐싱된다는 성질때문에 불변(immutable)객체입니다. 만약 불변 객체가 아니라면, 한 곳에서 수정했을 때 다른 모든 코드에 영향을 줄 수 있겠죠?
불변 객체인데 문자열 조작 연산은 어떻게?
불변 객체인데 문자열 조작 연산(concat, replace 등)은 어떻게 이루어질까요? 정답은 “연산 결과를 새로운 String 객체에 담아 반환한다“입니다. 우리는 Java에서 문자열 합치는 연산을 할 때 아래와 같이 했었습니다.
1
2
3
4
5
String str1 = "Nice";
String str2 = "Good";
String newStr = str1 + str2;
System.out.println(str1 + str2); // NiceGood
System.out.println(newStr); // NiceGood
str1 + str2를 통해 concat한 결과를 newStr에 담는 방식으로 사용했었죠. 이때 String끼리의 덧셈 연산은 컴파일 후에 StringBuilder로 append하는 방식으로 변환됩니다. 즉 아래와 같이 바뀝니다. (JDK 8기준)
1
2
3
4
5
// 코드
String newStr = str1 + str2;
// compile 이후
String newStr = new StringBuilder(str1).append(str2).toString();
StringBuilder의 toString연산은 아래와 같이 캐싱없이 새로운 String 객체를 반환합니다.
1
2
3
4
public String toString() {
return isLatin1() ? StringLatin1.newString(value, 0, count)
: StringUTF16.newString(value, 0, count);
}
1
2
3
4
5
6
7
8
public static String newString(byte[] val, int index, int len) {
if (len == 0) {
return "";
}
// 이렇게 아얘 새롭게 String 객체를 만든다.
return new String(Arrays.copyOfRange(val, index, index + len), LATIN1);
}
이렇게 새로운 객체를 반환하는 방식은 불변 객체끼리 연산할 때 사용되는 전형적인 패턴입니다. 불변 객체를 바꿀 수는 없으니, 새롭게 객체를 만들어서 반환하는겁니다. 게다가 불변 객체이므로 Thread safe합니다.
참고로 JDK 9부터는 StringBuilder가 아니라 StringConcatFactory를 사용해서 문자열을 합친다고 합니다. (참고 링크)
StringBuilder는 문자열 합칠 때 마다 생성되는데, 이 문제를 해결하기 위해 도입한 방법인 것 같습니다.
String Literal Concatenation
바로 위에서 String끼리 concat하면 새로운 String객체가 반환된다고 했습니다. 그런데 아래 코드의 실행 결과를 보면 다소 이해 안 되는 부분이 있습니다.
1
2
3
String hello = "Hello", lo = "lo";
System.out.println(hello == ("Hel"+"lo")); // true
System.out.println(hello == ("Hel"+lo)); // false
두 번째 결과는 납득할 수 있습니다. 이전 챕터에서 살펴본 대로 문자열 concat은 새로운 String 객체를 반환하기 때문입니다. 하지만 첫 번째 결과는 이상하죠?
이유는 String Literal끼리의 concatenation은 컴파일 타임에 이루어지기 때문입니다. 위의 코드를 컴파일하면 아래와 같이 바뀝니다.
1
2
3
4
5
// compile 이전
System.out.println(hello == ("Hel"+"lo"));
// compile 이후
System.out.println(hello == ("Hello")); // true
그래서 결과가 false가 아닌 true가 나옵니다. 만약 “Hel”+lo의 결과도 String pool에 캐싱해서 사용하고 싶다면 intern을 사용하면 됩니다.
1
2
3
String hello = "Hello", lo = "lo";
System.out.println(hello == ("Hel"+lo)); // false
System.out.println(hello == ("Hel"+lo).intern()); // true
String.intern 메서드는 String Pool에 이미 있는 문자열이라면 그 문자열을 반환하고, 아니라면 새롭게 등록하고 등록한 문자열을 반환합니다.
위에서 살펴본 String Literal의 캐싱 과정과 동일하죠? 저는 이 글에서 “캐싱”이라고 표현했지만, 제가 설명했던 캐싱 과정을 intern이라고 표현하기도 합니다.
참고
1. https://docs.oracle.com/javase/specs/jls/se13/html/jls-3.html#jls-3.10.5 2. https://docs.oracle.com/javase/9/docs/api/java/lang/invoke/StringConcatFactory.html