자바

[Java] String vs StringBuffer vs StringBuilder

newwisdom 2021. 8. 2. 02:29
반응형

2021-02-15 글

자바의 불변 객체를 공부하다가 String 과 StringBuilder가 예시로 자주 등장함을 보게 되었다.
나도 Java를 제대로 공부한지 얼마 안되서,
사실 String, StringBuffer, StringBuilder의 명확한 차이를 잘모르고
그냥 가져다 썼는데, 자바의 정석을 읽어보니 이 String이 굉장히 흥미로워서 👀
이번 기회에 명확히 정리하고 가는 것이 좋다고 생각했다.

String

String은 참 특이한 존재이다.
다른 언어에서 문자열이란 대부분 char형의 배열로 다루는데,
자바에서는 이 문자열을 위한 클래스가 존재한다는 것이다.

이 String 클래스를 깊게 보기 위해 String 클래스가 어떻게 이루어져 있는지 들여다보자.

String.java 일부

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

    /** Cache the hash code for the string */
    private int hash; // Default to 0

    ...

    public String() {
        this.value = "".value;
    }

    /**
     * Initializes a newly created {@code String} object so that it represents
     * the same sequence of characters as the argument; in other words, the
     * newly created string is a copy of the argument string. Unless an
     * explicit copy of {@code original} is needed, use of this constructor is
     * unnecessary since Strings are immutable.
     *
     * @param  original
     *         A {@code String}
     */
    public String(String original) {
        this.value = original.value;
        this.hash = original.hash;
    }

    /**
     * Allocates a new {@code String} so that it represents the sequence of
     * characters currently contained in the character array argument. The
     * contents of the character array are copied; subsequent modification of
     * the character array does not affect the newly created string.
     *
     * @param  value
     *         The initial value of the string
     */
    public String(char value[]) {
        this.value = Arrays.copyOf(value, value.length);
    }

String 클래스는 인스턴스 변수로 몇 가지를 가지고 있는데
그 중 문자열을 저장하기 위해 문자형 배열 변수 char[] value가 있다.
또 생성자를 보면 인스턴스 생성 시 생성자의 매개변수로 입력 받는 문자열이
이 인스턴스 변수에 문자형 배열로 저장되는 것을 확인할 수 있다.
또한 final 키워드로 변경이 불가능한 불변 객체임을 확인할 수 있다.

또 생성된 String 인스턴스가 가지고 있는 문자열은 읽을 수만 있고, 변경할 수는 없다.(불변 객체)

만약 연산자를 통해 문자열을 결합하는 경우는 String 인스턴스 내의 value 값이 변하는게 아닌,
새로운 문자열이 담긴 String 인스턴스를 반환한다.
때문에 문자열을 결합할 때마다 새로운 문자열을 가진 String 인스턴스를 생성하는 것이다.
이는 무분별한 메모리 공간 차지의 문제를 일으킬 수 있다.

String의 비교

문자열을 만들 때 문자열 리터럴을 지정하는 방법과 String 클래스의 생성자를 이용해 만드는 법이 있다.

/*문자열 리터럴*/
String str1 = "amazzi";
String str2 = "amazzi";

/*String 클래스의 생성자*/
String str3 = new String("amazzi");
String str4 = new String("amazzi);

String 클래스 생성자

String 클래스 생성자는 new 연산자에 의해 새로운 메모리가 할당된다.
wmr gkdtkd tofhdns String 인스턴스가 생성되며 각각의 주소값을 갖는다.

문자열 리터럴

문자열 리터럴은 클래스가 메모리에 로드될 때 자동적으로 생성되어 이미 존재하는 것을 재사용한다.
때문에 같은 내용의 문자열 리터럴은 한번만 저장되며
같은 문자열은 하나의 인스턴스를 공유하고 참조변수만이 다를 뿐이다.

그렇다면 문자열 리터럴로 만든 str1, str2와
String 클래스의 생성자로 만든 str3, str4 각각의 비교 결과는 어떠할까?

str1.equals(str2); // true
str3.equals(str4); // true

str1 == str2; // true
str3 == str4 // false

equals() 사용을 통한 비교에서는 두 문자열의 내용을 비교하기 때문에 두 비교 결과 모두 true이다.
하지만 등가비교연산자 ==를 통해 주소를 비교했을 경우,
str3, str4는 각각 다른 주소값을 가지고 있기 때문에 false가 나온다.

StringBuffer

String 클래스는 불변 클래스이기 때문에 문자열을 변경할 수 없었으나,
StringBuffer는 변경이 가능하다.
StringBuffer는 할당된 값을 변경하더라도,
String 클래스처럼 새로운 객체를 만들지 않고 기존 할당된 값을 수정하는 것으로 처리한다.
내부적으로 문자열 편집을 위한 버퍼를 가지고 있으며 StringBuffer 인스턴스를 생성할 때
그 크기를 지정할 수 있다.

마찬가지로 StringBuffer 클래스를 들여다보자.

StringBuffer.java 일부

public final class StringBuffer
    extends AbstractStringBuilder
    implements java.io.Serializable, CharSequence
{

    /**
     * A cache of the last value returned by toString. Cleared
     * whenever the StringBuffer is modified.
     */
    private transient char[] toStringCache;

    /** use serialVersionUID from JDK 1.0.2 for interoperability */
    static final long serialVersionUID = 3388685877147921107L;

    /**
     * Constructs a string buffer with no characters in it and an
     * initial capacity of 16 characters.
     */
    public StringBuffer() {
        super(16);
    }

    /**
     * Constructs a string buffer with no characters in it and
     * the specified initial capacity.
     *
     * @param      capacity  the initial capacity.
     * @exception  NegativeArraySizeException  if the {@code capacity}
     *               argument is less than {@code 0}.
     */
    public StringBuffer(int capacity) {
        super(capacity);
    }

    /**
     * Constructs a string buffer initialized to the contents of the
     * specified string. The initial capacity of the string buffer is
     * {@code 16} plus the length of the string argument.
     *
     * @param   str   the initial contents of the buffer.
     */
    public StringBuffer(String str) {
        super(str.length() + 16);
        append(str);
    }

    /**
     * Constructs a string buffer that contains the same characters
     * as the specified {@code CharSequence}. The initial capacity of
     * the string buffer is {@code 16} plus the length of the
     * {@code CharSequence} argument.
     * <p>
     * If the length of the specified {@code CharSequence} is
     * less than or equal to zero, then an empty buffer of capacity
     * {@code 16} is returned.
     *
     * @param      seq   the sequence to copy.
     * @since 1.5
     */
    public StringBuffer(CharSequence seq) {
        this(seq.length() + 16);
        append(seq);
    }

문자열을 저장하기 위한 toStringCache 변수가 보인다.
String 클래스와 차이점은 이 변수는 final로 선언되어 있지 않다는 것이다.
인스턴스를 생성할 때는 StringBuffer(int capacity) 생성자를 통해 길이를 지정해 줄 수 있으나,
지정해주지 않는다면 16개 문자를 저장할 수 있는 크기의 버퍼를 생성한다.
만약 버퍼의 크기가 작업하려는 문자열의 길이보다 작을 때는 내부적으로 버퍼의 크기를 증가시키는 작업이 수행된다.

StringBuffer의 변경

String 클래스와는 달리 append() 메소드로 값을 변경, 추가할 수 있다.
append()는 반환 값이 StringBuffer이며 자신의 주소를 반환한다.
때문에 아래 코드는 sb에 새로운 문자열이 추가되고 sb 자신의 주소를 반환해 sb2에도 sb의 주소가 들어간다.

StringBuffer sb2 = sb.append("zzi");

StringBuffer의 비교

String 클래스에서는 equals 메서드를 오버라이딩 하고 있어서 내용 비교가 가능했으나,
StringBuffer는 그렇지 않다. 때문에 ==로 비교한 것과 같은 결과를 얻는다.

StringBuffer sb = new StringBuffer("abc");
StringVuffer sb2 = new StringVuffer("abc");
...
sb == sb2; // false
sb.equals(sb2); // false

하지만 toString()을 오버라이딩하고 있기 때문에 이를 이용해 인스턴스를 얻고 equals()를 통해 비교할 수 있다.

String s = sb.toString();
String s2 = sb2.toString();
...
s.equals(s2); // true

StringBuilder

StringBuffer는 멀티 쓰레드에 안전하도록 동기화되어 있다.
이에 대해 아직은 잘 모르지만 🥲 일단 이 동기화가 StringBuffer의 성능을 떨어뜨린다.
그래서 StringBuffer에서 스레드의 동기화만 뺀 것이 StringBuilder이고
둘의 기능은 완전이 동일하다.

반응형