Java 에서 문자열을 다루를 대표적인 클래스로 String , StringBuffer, StringBuilder 가 있다.
String 클래스에는 문자열을 저장하기 위해서 문자형 배열 변수(char[]) value를 인스턴스 변수로 정의한다.
String은 불변(immutable)의 속성을 갖는다. 한번 생성한 String 인스턴스가 갖고있는 문자열은 읽어올 수만 있고, 변경할 수는 없다는 것.
이렇게 string을 더해도, 우리는 눈에는 "hello world"로 합쳐진 것처럼 보이지만, 사실은 기존에 "hello" 값이 들어가있던 String 클래스의 참조변수 str이 "hello world"라는 값을 가지고 있는 새로운 메모리영역을 가리키게 변경된 것이다. 처음 선언했던 "hello"로 값이 할당되어 있던 메모리 영역은 Garbage로 남아있다가 GC에 의해 사라지게 된다.
String 클래스는 불변하기 때문에 문자열을 수정하는 시점에 새로운 String 인스턴스가 생성된 것이다.
그래서 문자열 추가,수정,삭제 등의 연산이 빈번하게 발생하는 알고리즘에 String 클래스를 사용하면, 힙 메모리(Heap)에 많은 임시 가비지(Garbage)가 생성되어 힙메모리 부족으로 어플리케이션 성능에 치명적인 영향을 끼치게 되버림..
👩 늘 그렇듯 우리는 문제를 타파하기 위해 답을 찾는다.
이를 해결하기 위해 Java에서는 가변(mutable)성을 가지는 StringBuffer / StringBuilder 클래스를 도입했다!
String 클래스는 인스턴스를 생성할 때 지정된 문자열을 변경할 수 없지만 StringBuffer클래스는 변경이 가능하다. 내부적으로 문자열 편집을 위한 버퍼(Buffer)를 가지고 있으며, StringBuffer 인스턴스를 생성할 때 그 크기를 지정할 수 있다.
요놈도 String 처럼 문자형 배열(char[])을 내부적으로 가지고 있다.
public final StringBuffer implements java.io.Serializable{
private char[] value;
...
}
however! String과 달리 내용을 변경할 수 있다.
이렇게 .append()로 문자열을 합칠 수 있다는 것이다.
기본적으로 StringBuffer의 버퍼(데이터 공간) 크기의 기본값은 16개의 문자를 저장할 수 있는 크기이며, 생성자를 통해 그 크기를 별도로 설정할 수도 있다.
이때 편집할 문자열의 길이를 고려하여 버퍼의 길이를 충분히 잡아주는 것이 좋다. 편집 중인 문자열이 버퍼의 길이를 넘어서게 되면 버퍼의 길이를 늘려주는 작업이 추가로 수행되어야하기 때문에 작업효율이 떨어진다.(자동으로 늘려주니 걱정하지마라. but! 효율성이 떨어지니 넉넉히 잡아줘라)
우리는 배열의 길이는 변경이 불가하다는 것을 알고있다. 길이를 늘리는 척 하려면 배열을 새로 만들어서 복사하는 과정을 거친다. StringBuffer 이놈도 문자형 배열(char[]) 을 가지고 있으니 자동으로 늘어날때 그런 과정이 필요한거다.
StringBuffer sb = new StringBuffer(); // 기본 16 버퍼 크기로 생성
// sb.capacity() - StringBuffer 변수의 배열 용량의 크기 반환
System.out.println(sb.capacity()); // 16
sb.append("1111111111111111111111111111111111111111"); // 40길이의 문자열을 append
System.out.println(sb.capacity()); // 40 (추가된 문자열 길이만큼 늘어남)
이 append()는 지정된 내용을 StringBuffer 에 추가한 후, StringBuffer 참조를 반환한다.
StringBuffer sb = new StringBuffer("abc");
StringBuffer sb2 = sb.append("ZZ"); //sb에 ZZ추가한 배열의 참조값이 리턴되어 sb2에 담기게된다.
System.out.println(sb); //abcZZ
System.out.println(sb2); //abcZZ
그래서 같은 객체에 append()를 계속해서 쓸수 있다.
StringBuffer sb = new StringBuffer("abc");
sb.append("123");
sb.append("ZZ");
요거를 다음과 같이 쓸 수 있다.
StringBuffer sb = new StringBuffer("abc");
sb.append("123").append("ZZ");
이놈은 또 String 객체와 달리 equals() 메서드를 오버라이딩하지 않아 '==' 로 비교한 것과 같은 결과를 얻게 되어 버린다.
StringBuffer sb = new StringBuffer("hello");
StringBuffer sb2 = new StringBuffer("hello");
System.out.println(sb == sb2); // false
System.out.println(sb2.equals(sb)); // false
StringBuffer는 멀티쓰레드에 안전(thread safe) 하도록 동기화 되어있다. 동기화는 StringBuffer의 성능을 떨어트린다. 멀티쓰레드로 작성된 프로그램이 아닌경우, StringBuffer의 동기화는 불필요하다.
🤷♀️ 그래서 또 우리 석박사들이 만들어낸게 여기서 동기화만 뺀 StringBuilder 다. 요놈들은 완전히 똑같은 기능이라 그냥 단어만 바꿔주면 된다.
StringBuilder sb = new StringBuilder("abc");
sb.append("123");
StringBuilder는 동기화를 지원하지 않기 때문에 멀티스레드에서 안전하지 않지만(thread unsafe), 단일쓰레드에서의 성능은 StringBuffer 보다 뛰어나다.
그래서 정리해보면 상황에따라 요놈들을 잘 사용하면 되는 것이다.
String : 문자열 연산이 적고 멀티쓰레드 환경일 경우
StringBuffer : 문자열 연산이 많고 멀티쓰레드 환경일 경우
StringBuilder : 문자열 연산이 많고 단일쓰레드이거나 동기화를 고려하지 않아도 되는 경우
참고자료 :