String vs StringBuilder vs StringBuffer
세 클래스의 공통점은 모두 문자열(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"; |
반복적으로 문자열을 이어 붙이면 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(" delf!");
System.out.println(s.hashCode()); // 859417998
String
vs StringBuilder
위에서도 언급했듯이, String
의 성능 이슈를 개선하기 위해 JDK 1.5 이상에서는 컴파일 단계에서 내부적으로 StringBuilder
로 변경되어 동작됩니다.
예를들어 아래와 같이 +
연산자를 이용한 코드는String outside = str1 + str2 ... + strN;
내부적으로 다음과 같이 동작하게 됩니다.String inside = new StringBuilder(String.valueOf(str1)).append(str2).[...].apend(strN).toString();
+
연산과 append()
메소드의 실질적인 성능 차이는 거의 없습니다만, 아래와 같은 코드는 매 루프마다 StringBuilder
객체가 생성되므로 기존(JDK 1.5 이하)의 String
의 +
연산과 같이 마찬가지로 불필요한 객체가 쌓이게 되고 성능에 악영향을 미칩니다.for (int i = 0; i < 100000; i++) {
string += value;
}
때문에 어떠한 경우라도 반복문에서는 String
의 +
연산은 피하는게 좋습니다.
반복문에서의
String
과StringBuilder
의 속도차이 실험 결과 약 만배의 속도 차이를 확인할 수 있었습니다. - code
StringBuffer
StringBuffer
는 synchronized
가 적용되어 멀티스레드 환경에서 Thread-safe하게 동작할 수 있습니다. 한마디로 동기화를 지원하는 StringBuilder
라고 이해하시면 편합니다.// StringBuffer 클래스의 append 메소드
public synchronized StringBuffer append(CharSequence s) {
toStringCache = null;
super.append(s);
return this;
}
StringBuilder
vs StringBuffer
하나의 StringBuilder
객체와 StringBuffer
객체에 10,000개의 스레드가 접근해서 append()
를 수행히는 실험을 해봤습니다.public class AppendTest {
private static StringBuilder builder = new StringBuilder("");
private static StringBuffer buffer = new StringBuffer("");
public static void main(String[] args) throws InterruptedException {
/* 1. StringBuilder에 문자열 붙이는 스레드 10000개 생성 */
Appenders[] appenders = new Appenders[10];
for (int i = 0; i < appenders.length; i++) {
appenders[i] = new Appenders(builder,10000, "a");
}
for (Appenders a : appenders) { // 스레드 시작
a.start(); //10000개의 스레드가 하나의 StringBuilder에 append
}
Thread.sleep(3000); // 3초 대기
System.out.println(builder.length());
/* 2. StringBuffer에 문자열 붙이는 스레드 10000개 생성 */
appenders = new Appenders[10];
for (int i = 0; i < appenders.length; i++) { // 스레드 10000개 생성
appenders[i] = new Appenders(buffer,10000, "a");
}
for (Appenders a : appenders) { // 스레드 시작
a.start(); // 10000개의 스레드가 하나의 StringBuffer에 append
}
Thread.sleep(3000); // 3초 대기
System.out.println(buffer.length());
}
}
class Appenders extends Thread {
private Appendable ap;
private int cnt;
private String s;
public Appenders(Appendable ap, int cnt, String s) {
this.cnt = cnt;
this.ap = ap;
this.s = s;
}
public void run() {
while (cnt-- > 0) {
try {
ap.append(s);
} catch (IOException e) {}
}
}
}
실행 결과93221 // 매 실행 마다 결과가 다름.
100000
Thread-safe하지 않은 StringBuilder
에 여러 스레드가 동시에 append()
할 경우에, 기존 문자열 뒤 인덱스 값이 동기화되지 않아 중복된 장소에 여러 번 값을 덮어 쓰는 경우가 발생하고, 목표 횟수만큼 문자열이 추가되지 않게됩니다.
심지어 char[]
의 버퍼를 늘리기 전에 문자 할당을 시도해서 ArrayIndexOutOfBoundsException
이 발생하기도 하지요.
반면, synchronized
키워드를 지원하는 StringBuffer
는 깔끔하게 목표 횟수만큼 문자열이 추가되었습니다.
정리
String
- 단순히 문자열을 참조하거나 탐색 및 검색이 잦을 때 좋습니다.
StringBuilder
- 런타임 때, 반복적인 문자열 추가 연산이 많을 때 좋습니다.
- 단일 스레드 환경이라면
StringBuffer
보다 성능이 좋을 수 있습니다.
StringBuffer
- 멀티 스레드 환경에서 반복적인 추가 문자 추가 연산이 많을 때 좋습니다.