세 클래스의 공통점은 모두 문자열(String)을 저장하고 관리하는 클래스입니다.

String

String 클래스는 **immutable(불변)**하다는 특성이 있습니다. String 클래스의 문자열을 저장하는 char[] 을 보면 final 로 선언되어 있다는 것을 확인할 수 있습니다.

public final class String
    implements java.io.Serializable, Comparable<String>, CharSequence {
    /** The value is used for character storage. **/
    private final char value[];
    ...
}

그로 인해 한번 할당한 문자열을 변경하는 것은 불가능하며, 더하기 연산을 하여 붙일 시 새로운 객체가 생성되어 재할당됩니다.

String s = "hello";
System.out.println(s.hashCode());  // 99162322
s += " world";
System.out.println(s.hashCode());  // 1776255224

반복적으로 문자열을 이어 붙이면 Heap 영역에 참조를 잃은 문자열 객체가 계속해서 쌓이게 됩니다. 물론 나중에 GC에 의해 수거가 되지만, 메모리 관리 측면에서 이러한 코드는 결코 좋다고 할 수 없습니다. 또한 계속해서 객체를 생성하기 때문에 연산 속도적인 측면에서도 뒤떨어집니다.

이러한 String의 성능 이슈를 개선하기 위해 JDK 1.5 이상에서는 컴파일 단계에서 내부적으로 StringBuilder로 변경되 동작됩니다.

StringBuilder

반면 StringBuilder 클래스는 **mutable(가변)**합니다. 상속 받고 있는 AbstractStringBuilder 클래스의 내부를 보면 변경 가능하도록 선언되어 있습니다.

abstract class AbstractStringBuilder implements Appendable, CharSequence {
    /** The value is used for character storage **/
    char[] value;
    ...
}

append() 메소드를 호출하면, char[] 배열의 길이를 늘리고 같은 객체에 문자열을 더합니다. 아래의 코드를 보면 append() 호출 이후에도 StringBuilder 객체에 변함이 없음을 확인할 수 있습니다.

StringBuilder s = new StringBuilder("hello");
System.out.println(s.hashCode());  // 859417998
s.append("world");
System.out.println(s.hashCode());  // 859417998

String vs StringBuilder

위에서도 언급했듯이, String의 성능 이슈를 개선하기 위해 JDK 1.5 이상에서는 컴파일 단계에서 내부적으로 StringBuilder로 변경되어 동작됩니다.

예를 들어 아래와 같이 + 연산자를 이용한 코드는

String outside = str1 + str2 + str3;

내부적으로 다음과 같이 동작하게 됩니다.

String inside = new StringBuilder(String.valueOf(str1)).append(str2).append(str3).toString();