티스토리 뷰
이전에 포스팅한 String클래스와 Constant Pool에 이어서
String, StringBuffer, StringBuilder의 차이에 대해 설명한 글이다.
String vs StringBuffer vs StringBuilder
Java에서는 문자열 객체를 생성하기 위해 String, StringBuffer, StringBuilder이라는 3가지 클래스를 제공한다.
세 클래스 모두 문자열을 관리하기 위한 클래스이지만 불변(Immutable)한지, Thread-safe한지에 따라
조금씩 차이가 있다.
String은 불변하며, 객체를 빠르고 작은 메모리로 생성할 수 있기 때문에
메모리 관리를 효율적으로 처리할 수 있다는 장점이 있었다.
하지만 불변하기 때문에 성능이 저하되는 문제도 가지고 있다.
String의 변경이 자주 일어난다면 ?
String의 변경이 자주 일어나게 된다면,
Constant Pool에서는 기존의 데이터의 변경이 아닌 새로운 리터럴의 추가가 계속해서 이루어 진다.
<noMenu/>
String str = "A";
str += "B";
str += "C";
str += "D";
위의 코드의 경우, "A" , "AB" , "ABC" , "ABCD"가 각각 String Constant Pool에 새로운 리터럴로 저장되며 아무도 참조하지 않은 리터럴은 GC가 회수해 가는 과정을 거치게 된다.
이러한 과정은 성능 저하의 원인이 된다.
어느 부분에서 성능이 떨어진다는 것일까?
성능을 따질때는 항상 시간복잡도나 공간복잡도를 따져야 한다.
결론만 말하자면 Java에서 String의 '+=' 또는 '+' 연산의 경우 $O(N+K)$의 시간복잡도를 가진다.
N은 기존의 문자열(lhs)의 길이, K는 더하려고 하는 문자열(rhs)의 길이다.
아래의 코드를 예시로 연산횟수에 대한 시간복잡도를 계산해보자.
s += "A"; 에서 i번째에서 s의 길이 + "A"의 길이1 만큼의 시간이 소요되고 그과정을 N번만큼 반복하므로
연산횟수는 N*(N+1)/2 * N 이고, 총 시간복잡도는 $O(N^3)$이 된다.
(N*(N+1)/2 는 1부터 N까지의 합, N은 반복문 횟수)
<noMenu/>
public class Main{
public static void main(String args []){
String s = "";
int n = 1000000;
for(int i=0; i<n; i++){
s += "A"; // ❗
}
}
}
// 출처 : https://velog.io/@ppnrn/%EC%8B%9C%EA%B0%84-%EB%B3%B5%EC%9E%A1%EB%8F%84-u1gjgoqn
이러한 '+' 연산은 StringBuffer 또는 StringBuilder를 이용하면 일반적인 경우 $O(1)$에 해결할 수 있으며
위의 코드를 아래처럼 바꾼다면 sb.append("A"); 의 시간복잡도는 $O(1)$이 되므로 전체 시간복잡도를 $O(N)$으로 줄일 수 있는 효과를 가져온다 !
(append의 더 자세한 시간복잡도는 아래에서 설명하겠다...!)
<noMenu/>
public class Main{
public static void main(String args []){
StringBuilder sb = new StringBuilder();
int n = 1000000;
for(int i=0; i<n; i++){
sb.append("A"); // ❗
}
}
}
// 출처 : https://velog.io/@ppnrn/%EC%8B%9C%EA%B0%84-%EB%B3%B5%EC%9E%A1%EB%8F%84-u1gjgoqn
StringBuilder와 StringBuffer의 차이점
StringBuilder와 StringBuffer의 기능을 알아보기 전에 차이점부터 확인하고 넘어가자.
차이점은 간단하다. 동기화(synchronization)가 되는지 안되는지의 차이만 있다.
StringBuffer는 동기화를 지원하고 StringBuilder는 지원하지 않는다.
동기화가 되어 있으면 멀티 스레드에서 안전하게 동작(thread-safe)한다는 장점이 있지만 동기화 과정에서 오버헤드가 발생하기 때문에 느리다는 단점이 있다.
StringBuilder와 StringBuffer의 기능과 시간복잡도
StringBuilder와 StringBuffer는 동기화 유무를 제외하고 기본적으로 동일한 기능을 가지고 있다.
따라서 StringBuffer의 기능설명으로 StringBuilder의 설명을 대신하겠다.
String과 다른점은 불변이 아니라 동적이라는 것인데 내부에 char형 배열을 가지고 있다.
배열의 크기는 객체를 생성할때 적당한 크기로 생성이 되거나 생성자를 통해 직접 크기를 지정해 줄 수도 있다.
문자열을 추가(append),삭제(delete),수정(insert)하는 연산이 가능하다.
기본적으로 추가연산은 $O(1)$의 시간복잡도를 가지며 삭제, 수정의 경우 $O(N)$의 시간복잡도를 가진다.
추가연산(append)의 경우, 할당된 메모리의 크기가 충분하지 않으면 현재 크기를 2배 늘린 후에 문자열을 추가하는데 이 과정에서 기존의 문자열과 추가되는 문자열의 내용을 새로 복사할 필요가 있다.
따라서 메모리의 크기를 늘리고 문자열을 복사하는 과정에서는 $O(N)$의 시간복잡도를 필요로 한다.
하지만 매번 메모리 크기를 늘리는 것이 아니기 때문에 추가연산의 시간복잡도는 $O(N)$이라고 볼 수 없다.
이러한 동적 메모리에서의 동작방식은 Amortized Analysis을 이용하여 시간복잡도를 계산하면 $O(1)$에 가까운 시간복잡도를 가진다고 알려져 있는데, 이를 Amortized $O(1)$ 이라고 한다.
즉, 추가연산의 시간복잡도는 $O(1)$이라고 알고 있으면 된다.
String vs StringBuffer vs StringBuilder
지금까지의 내용들을 대충 정리해보면 이렇다.
String을 이용하면 문자열을 리터럴로 초기화 할 수 있어 빠르게 생성이 가능하고
리터럴은 불변하기 때문에 재사용할 경우 메모리 소모가 적다는 장점이 있었다.
하지만 String의 문자열 추가 연산은 느리고 추가할 때마다 매번 새로운 문자열을 추가하기 때문에
잦은 변경이 이루어 진다면 StringBuffer또는 StringBuilder를 사용하는것이 좋다.
StringBuffer와 StringBuilder와의 차이점은 thread-safe의 유무이다.
StringBuffer가 thread-safe하다.
참고 링크
- https://zeddios.tistory.com/60
- https://velog.io/@ppnrn/%EC%8B%9C%EA%B0%84-%EB%B3%B5%EC%9E%A1%EB%8F%84-u1gjgoqn
'개발 공부 > Java' 카테고리의 다른 글
[Java] HashMap과 HashTable의 동작 방식 (0) | 2022.02.23 |
---|---|
[Java] Object클래스 - equals, hashCode, toString 메소드 (0) | 2021.08.09 |
[Java] String클래스와 String Constant Pool (0) | 2021.08.06 |
[Java] Thread-safe 클래스 (0) | 2021.08.06 |