취뽀몽

[Java] String vs StringBuilder vs StringBuffer 본문

java

[Java] String vs StringBuilder vs StringBuffer

허몽구 2023. 4. 7. 22:55

알고리즘 문제를 풀다 보면 String, StringBuilder, StringBuffer 3가지를 접할 수 있다.

위 3가지의 차이점에 대해 알아보자.

 

1. String

String 클래스는 불변 객체이다.

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

 

문자열을 저장하는 char[]을 보면 final로 선언되어 있고, 해당 배열을 재할당하는 코드는 선언되어 있지 않다.

즉, 한 번 할당된 문자열을 변경하는 것은 불가능하며, + 연산으로 문자를 이어 붙일 때는 새로운 객체가 생성되어 재할당된다는 의미이다.

String str = "hello";
System.out.println(str.hashCode()); // 99162322

str += "안녕하세요";
System.out.println(str.hashCode()); // -834545163

hashCode의 값이 다르므로, 두 객체는 다른 객체이다.

 

여기서 생각해봐야할 점은,

반복적으로 문자열을 이어 붙이다 보면 Heap 영역에서 참조를 잃은 문자열 객체가 계속해서 쌓이게 된다.

물론 GC(Garbage Collection)에 의해 메모리 정리가 되겠지만, 메모리 관리 측면에서 이러한 코드는 좋은 코드라 할 수 없다.

 

String은 불변 객체이다.

자바는 String을 String Pool 에서 관리하는데, String이 불변 객체이기 때문에 가능하다.

만약 String mutable, 가변 객체였다면 해당 메모리에 값이 언제 어떻게 바뀔지 예상할 수 없기 때문에 String Pool 형태로 관리할 수 없게 된다. 

3개의 String 변수가 모두 같은 메모리를 가리킬 때, 하나의 값을 바꿔버리면 나머지 값 모두 바뀌는 문제가 발생할 수 있다. 

하지만, String이 불변 객체이기 때문에 값의 변경 가능성이 없어 멀티 스레딩 환경에서 안전하다고 할 수 있다.

따라서 String Pool의 재활용성을 이용하기 위해 String을 불변 객체로 설계한 것이다.

 

참고로 String Pool을 모르시는 분들은 밑의 블로그를 참고하면 좋을 것 같다! (본인 작성 ㅎㅎ)

 

 

[Java] == 연산자와 equals() 메서드

다음과 같이 두 개의 문자열이 있다. String s1 = "안녕하세요"; String s2 = new String("안녕하세요"); 이 문자열을 비교하고자 한다. 두 가지 방법을 사용할 것이다. 첫 번째로 == 연산자를 사용하여 비교

jiyoungmerong.tistory.com

 

2. StringBuilder

String이 불변 객체이기 때문에 장점이 많은 것도 맞지만, 문자열의 변화가 많다면 성능 이슈가 발생하게 된다.

이런 경우에는 String 객체를 하나로 두고, 내부 상태를 변경하는 가변 객체로 만드는 것이 좋다.

이때 사용할 수 있는 것이 StringBuilder와 StringBuffer이다.

abstract class AbstractStringBuilder implements Appendable, CharSequence {

    /** The value is used for character storage **/
    char[] value;
    ...
}

StringBuilder는 AbstractStringBuilder 클래스의 상속을 받는데, 내부 상태를 변경할 수 있도록 설계되어 있다.

 

    public AbstractStringBuilder append(String str) {
        if (str == null) str = "null";
        int len = str.length();
        ensureCapacityInternal(count + len);
        str.getChars(0, len, value, count);
        count += len;
        return this;
    }
		
    private void ensureCapacityInternal(int minimumCapacity) {
        if (minimumCapacity - value.length > 0)
            expandCapacity(minimumCapacity);
    }

    void expandCapacity(int minimumCapacity) {
        int newCapacity = value.length * 2 + 2; // value의 크기를 약 2배로 증가
        if (newCapacity - minimumCapacity < 0)
            newCapacity = minimumCapacity;
        if (newCapacity < 0) {
            if (minimumCapacity < 0)
                throw new OutOfMemoryError();
            newCapacity = Integer.MAX_VALUE;
        }
        value = Arrays.copyOf(value, newCapacity);
    }

 

StringBuilder는 문자를 이어 붙이기 위해서 append() 메소드를 호출한다.

append() 메소드는 char[] 배열의 길이를 늘리고 해당 배열에 문자열을 더하는 방식으로 설계되어 있다.

정확하게는 char[] 배열에 사용되지 않고 남은 공간에 새로운 문자열이 들어갈 정도의 크기가 있을 때는 그대로 문자열을 삽입하는 것이다.

그렇지 않다면 value의 크기를 약 2배로 증가하여 기존의 문자열을 복사하고 새로운 문자열을 삽입한다. 

 

StringBuilder str = new StringBuilder("안녕하세요");
System.out.println(str.hashCode());  // 2088051243

str.append("hello");
System.out.println(str.hashCode());  // 2088051243

StringBuilder는 객체 자체를 만드는 것이 아니기 때문에, String과 달리 객체의 주소가 유지되는 것을 볼 수 있다. 

따라서, 문자를 이어 붙이는 코드가 많아야 할 경우에는 StringBuilder를 쓰는 것이 좋다.

 

3. StringBuffer

StringBuffer는 대부분의 메소드에 synchronized(동기화)가 적용되어 일반적으로 멀티 스레드 환경에서 스레드 안전하게 동작한다.

synchronized에 대해서는 나중에 자세히 포스팅하도록 하겠다!

 

@Override
public synchronized StringBuffer append(CharSequence s) {
    toStringCache = null;
    super.append(s);
    return this;
}

StringBuffer의 append 메소드를 보면 synchronized가 적용되어 있는 것을 볼 수 있다.

 

그렇다면 모든 메소드에 synchronized을 적용시킨다면 모두 안전하게 동작하는 것일까?

 

synchronized 메소드 하나를 여러 스레드가 호출하는 것은 스레드 안전할 수 있지만,

여러 synchronized 메소드로 이루어진 하나의 메소드를 여러 스레드가 호출할 때는 스레드 안전하지 않을 수 있다.