사람의 언어를 표현할 수 있는 string 은 가장 많이 사용하는 타입의 클래스 중 하나 일 것이다. 그렇기 때문에 이 클래스에 대한 이해도 조금 깊이 해볼 필요성이 있다.
우선, 간단하게 코드로 string class 가 가지는 특성과 jvm 이 string 을 어떤식으로 처리하는지에 대해 살펴보자
자바에서 문자열을 선언하는 방법은 2가지 이다.
// 1 - 리터럴
String literal = "hello";
// 2 - 생성자
String newString = new String("hello");
이 두 방법은 각각의 저장방식을 가지고 있다.
우선 생성자를 통해 생성하는 방식은 일반적인 방법과 동일하다.
Heap 영역을 선언해 값을 할당하고 newString 은 해당 영역을 참조하게 된다.
그에 반해 literal 방식을 사용하면 String constant pool 영역을 사용한다.
그렇다면 이 두 저장방식의 차이는 어떤 효과를 가져올까?
코드를 통해 확인해 보도록 하자
String s1 = "Hello";
String s2 = "Hello";
System.out.println(System.identityHashCode(s1));
System.out.println(System.identityHashCode(s2));
System.out.println(s1 == s2);
String s3 = new String("Hello");
String s4 = new String("Hello");
System.out.println(System.identityHashCode(s3));
System.out.println(System.identityHashCode(s4));
System.out.println(s3 == s4);
결과는 다음과 같다
1580066828
1580066828
true
491044090
644117698
false
위 과정을 세분화해서 이해해보도록 하자
String s1 = "Hello"; 를 통해 선언과 할당을 했다.
위 과정에서 "Hello" 라는 값이 string constant pool 영역에 존재하는지 확인한 후 없다면 해당 값을 풀 영역에 할당하고 s1 은 그 영역을 가리키게 된다.
다음으로 String s2 = "Hello"; 선언과 할당을 하는데,
이 때 "Hello"라는 값이 String constant pool 영역에 존재함으로 새로운 메모리 영역을 할당하는게 아니라 이미 존재하는 영역에 s2 를 매칭시켜주는 것이다.
그래서 s1 과 s2 는 동일한 주소값을 가지게 되는 것이다.
그에 반해서 s3, s4 는 new 키워드를 사용했음으로 당연히 해당 주소값은 서로 다른 값을 가리키게 된다.
이해가 잘 되지 않는다면, 밑의 이미지를 살펴보도록 하자.
리터럴 방식을 사용한다면, 같은 값을 위한 메모리를 중복하여 사용함으로, 메모리를 효과적으로 사용할 수 있다는 장점을 가지게 된다.
Immutable(불변)은 의미 그대로이다. 한번 할당한 값은 변경할 수 없다.
코드를 통해 이 의미가 무엇인지 알아보자
String s1 = "Hello";
String s2 = "Hello";
System.out.println( System.identityHashCode(s1));
System.out.println(s1);
System.out.println(System.identityHashCode(s2));
System.out.println(s2);
s1 = "hi";
System.out.println(System.identityHashCode(s1));
System.out.println(s1);
System.out.println(System.identityHashCode(s2));
System.out.println(s2);
다음의 결과이다.
1580066828
Hello
1580066828
Hello
491044090
hi
1580066828
Hello
코드에서 s1 의 값을 "hi" 로 바꿨지만, 참조하는 곳 즉, string pool constant 영역의 hello 를 hi 로 바꾼게 아니라 string pool constant 영역에 "hi"를 추가하고 s1을 그 새로운곳을 참조하게 하는 방식이다. 기존의 "hello" 는 불변한다는 것이다. 생각해보면 그 이유도 당연하다. 만약 hello 가 mutable 하다면 의도하지 않게 s2의 값도 변경될 수 있기 때문이다. 따라서 string 의 값은 immutable 하게 설계되었다.
직접 String 클래스를 들어가서 확인해봐도 char value[] 앞에 final 키워드를 붙여놓았음을 확인할 수 있다.
값이 immutable 한 덕분에 의도하지 않은 값의 변경에서 벗어날 수 있다.
하지만, 어떠한 상황에서는 이러한 특성이 메모리를 비효율적으로 사용하게 할 수 있다. 코드를 보며 생각해보도록 하자
public static void main(String[] args) {
String s1 = "안녕하세요";
System.out.println(System.identityHashCode(s1));
s1 = firstMethod(s1);
System.out.println(System.identityHashCode(s1));
s1 = secondMethod(s1);
System.out.println(System.identityHashCode(s1));
s1 = thirdMethod(s1);
System.out.println(System.identityHashCode(s1));
System.out.println(s1);
}
static String firstMethod(String s1){
return s1.concat(" 제 이름은");
}
static String secondMethod(String s1){
return s1.concat(" Jaden 입니다");
}
static String thirdMethod(String s1){
return s1.concat(" 반갑습니다");
}
----- 결과 ------
1580066828
491044090
644117698
1872034366
안녕하세요 제 이름은 Jaden 입니다 반갑습니다
여기서 문제점은 무엇일까?
결국 최종적으로 필요한 값은
"안녕하세요 제 이름은 Jaden 입니다 반갑습니다"
이고 해당 결과값을 만들기 위해 firstMethod, secondMethod, thirdMethod 의 과정을 거쳐야한다. 그런데 그 중간 과정을 거치면서 나온 값들은 ("안녕하세요 제 이름은" -> "안녕하세요 제 이름은 Jaden 입니다" -> "안녕하세요 제 이름은 Jaden 입니다 반갑습니다") 다음 과정 이후에는 쓸모없는 garbage 임에도, String 의 immutable 한 특성 때문에 메모리를 잡아먹고 있게 된다.
이런 비효율적인 메모리 사용을 피하기 위해 사용되는 class 들이 있다. 그것이 바로 StringBuilder 와 StringBuffer 인데 이것을 활용하면 쉽게 위와 같은 문제를 해결할 수 있다.
public static void main(String[] args) {
StringBuilder sb = new StringBuilder();
sb.append("안녕하세요");
System.out.println(System.identityHashCode(sb));
sb = firstMethod(sb);
System.out.println(System.identityHashCode(sb));
sb = secondMethod(sb);
System.out.println(System.identityHashCode(sb));
sb = thirdMethod(sb);
System.out.println(System.identityHashCode(sb));
System.out.println(sb.toString());
}
static StringBuilder firstMethod(StringBuilder sb){
return sb.append(" 제 이름은");
}
static StringBuilder secondMethod(StringBuilder sb){
return sb.append(" Jaden 입니다");
}
static StringBuilder thirdMethod(StringBuilder sb){
return sb.append(" 반갑습니다");
}
----- 결과 ------
1580066828
1580066828
1580066828
1580066828
안녕하세요 제 이름은 Jaden 입니다 반갑습니다
위와 같이 StringBuilder 혹은 StringBuffer 를 사용하면 값을 변화시킬 수 있음으로, 필요에 따라 사용한다면 메모리를 절약할 수 있다.
Builder 와 Buffer의 차이는 동기화 지원의 유무인데, StringBuffer의 경운 동기화를 지원해 멀티쓰레드 환경에서 thread-safe 하다는 장점이 있다. 물론, 멀티쓰레드 환경이 아니라면 동기화 작업을 하지 않는 StringBuilder의 성능이 더 뛰어나다.
코드를 확인해보면 StringBuilder 와 StringBuffer 모두 AbstractStringBuilder 를 상속한 클래스이고, AbstractStringBuilder 는 String 과 달리, final 키워드가 안붙어있음도 확인할 수 있다