본문 바로가기

Java

StringBuilder 살짝 까보기 (생성자, append)

728x90

StringBuilder (Buffer도 마찬가지)를 몇번 사용했지만, 어떻게 생긴지에 대해서 생각해본적이 없어서 살짝 까보려고한다.

 

참고로 jdk는 1.8

 

StringBuilder는 추상클래스인 AbstractStringBuilder을 상속받고있다. 이름부터만 봐도 StringBuilder에 대해서 정의한 클래스라는게 느껴진다.

 

 

StringBuilder 생성자 일부분

public StringBuilder() {
        super(16);
    }

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

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

아래 하나 더있는데 비슷한 맥락이라서 뺐다.

StringBuilder의 생성자는 super로 그 값을 쏘고있다. 그래서 AbstractStringBuilder의 생성자를 보면, 아래와 같이 생겼다.

 

abstract class AbstractStringBuilder implements Appendable, CharSequence {
    /**
     * The value is used for character storage.
     */
    char[] value;

    /**
     * The count is the number of characters used.
     */
    int count;

    /**
     * This no-arg constructor is necessary for serialization of subclasses.
     */
    AbstractStringBuilder() {
    }

    /**
     * Creates an AbstractStringBuilder of the specified capacity.
     */
    AbstractStringBuilder(int capacity) {
        value = new char[capacity];
    }

char[] value를 보면 StringBuilder는 char형 배열로 값을 담고 (String도 마찬가지), 글자 수인 length를 나타내는 count 변수가 있다.

 

그리고, StringBuilder에서 기본적으로 super(16)을 넘기기 때문에 기본 크기는 char[16]임을 알 수 있다.

 

new StringBuilder() => char[16]

new StringBuilder("abcd") 는 16 + 4 => char[20]

new StringBuilder(25) => char[25]

같은 방식이다.

 

내가 예상한거보다 기본값이 너무 작다.

그렇다면 만약에, new StringBuilder()로 char[16]를 메모리에 할당받고, 이후에 16글자가 넘는 문자열을 append하면 어떻게 될까?

ex) 

StringBuilder sb = new StringBuilder();
sb.append("sadajdasjdklajdklajdsjadjakdjaldashdkjahdjadad");

 

당연히 메모리 할당을 더 받아서 뭐 값을 넣고 하겠지만 그 과정을 append를 까보며 보는게 좋을 것 같다.

 

 

StringBuilder의 append()

@Override
    public StringBuilder append(CharSequence s, int start, int end) {
        super.append(s, start, end);
        return this;
    }

    @Override
    public StringBuilder append(char[] str) {
        super.append(str);
        return this;
    }

    /**
     * @throws IndexOutOfBoundsException {@inheritDoc}
     */
    @Override
    public StringBuilder append(char[] str, int offset, int len) {
        super.append(str, offset, len);
        return this;
    }

    @Override
    public StringBuilder append(boolean b) {
        super.append(b);
        return this;
    }

    @Override
    public StringBuilder append(char c) {
        super.append(c);
        return this;
    }

    @Override
    public StringBuilder append(int i) {
        super.append(i);
        return this;
    }

    @Override
    public StringBuilder append(long lng) {
        super.append(lng);
        return this;
    }

    @Override
    public StringBuilder append(float f) {
        super.append(f);
        return this;
    }

    @Override
    public StringBuilder append(double d) {
        super.append(d);
        return this;
    }

대략 조금씩 차이가 있지만, super.append를 호출하는걸 볼 수 있다. 때문에 AbstractStringBuilder의 append()가 핵심

 

AbstractStringBuilder의 append()

public AbstractStringBuilder append(String str) {
        if (str == null)
            return appendNull();
        int len = str.length();
        ensureCapacityInternal(count + len);
        str.getChars(0, len, value, count);
        count += len;
        return this;
    }

    // Documentation in subclasses because of synchro difference
    public AbstractStringBuilder append(StringBuffer sb) {
        if (sb == null)
            return appendNull();
        int len = sb.length();
        ensureCapacityInternal(count + len);
        sb.getChars(0, len, value, count);
        count += len;
        return this;
    }

    /**
     * @since 1.8
     */
    AbstractStringBuilder append(AbstractStringBuilder asb) {
        if (asb == null)
            return appendNull();
        int len = asb.length();
        ensureCapacityInternal(count + len);
        asb.getChars(0, len, value, count);
        count += len;
        return this;
    }

사실 이 외에도 몇가지 오버로딩된 append()들이 있는데 내가 보고싶은 공통적인 핵심 로직이 있다.

 

ensureCapacityInternal(count + len);
sb.getChars(0, len, value, count);

 

1. ensureCapacityInternal()

먼저 ensureCapacityInternal 메서드는 AbstractStringBuilder에 필드 변수로 있던 char[] value를 append 하기 위해 필요한 일정 양만큼 메모리를 할당한다.

 

private void ensureCapacityInternal(int minimumCapacity) {
        // overflow-conscious code
        if (minimumCapacity - value.length > 0) {
            value = Arrays.copyOf(value,
                    newCapacity(minimumCapacity));
        }
    }

 

Arrays.copyOf를 통해 기존에 value를 재선언 하는걸 볼 수 있다.

newCapacity는 추가할 일정 양이 얼만큼인지 정하는 메서드라고 생각하면 된다.

 

private int newCapacity(int minCapacity) {
        // overflow-conscious code
        int newCapacity = (value.length << 1) + 2;
        if (newCapacity - minCapacity < 0) {
            newCapacity = minCapacity;
        }
        return (newCapacity <= 0 || MAX_ARRAY_SIZE - newCapacity < 0)
            ? hugeCapacity(minCapacity)
            : newCapacity;
    }

쓰윽 보면 int newCapacity = (value.length << 1) + 2; 부분이 중요한데

새로 append할 값이 기존에 가지고 있던 value의 크기에 * 2 + 2 한 값보다 크면 새로 append할 값의 length를, 아니면 value의 크기에 * 2 + 2로 값을 정하는 정도?로 보면 될 것 같다.

 

너무 큰 값(Integer.MAX_VALUE를 벗어나는 오버플로우 값)이면 hugeCapacity에서 OutOfMemoryError를 날린다.

 

앞에서 사용한 예시를 대입해서 다시 보면

StringBuilder sb = new StringBuilder();
sb.append("sadajdasjdklajdklajdsjadjakdjaldashdkjahdjadad");

 

처음에 16바이트만큼 받았지만 sadsadasd어쩌구저쩌구 46글자를 append하게되서 23바이트가 초과되는 상황이다.

 

이는 ensureCapacityInternal(0 + 46)를 호출하게 되고 (0인 이유는 생성자에서 char[16]를 new했지만 count는 따로 초기화 안했음)

Arrays.copyOf를 통해 char[16] 이었던 value가 char[46]로 되는것이다.

 

 

2. getChars

getChars는 간단하게 생각하면, char[46]로 value가 늘어났지만 아직 값을 넣기 전이다.

getChars는 여기에 값을 넣는 메서드다.

 

String을 예시로 보면

public AbstractStringBuilder append(String str) {
        if (str == null)
            return appendNull();
        int len = str.length();
        ensureCapacityInternal(count + len);
        
        str.getChars(0, len, value, count);
        
        count += len;
        return this;
    }

getChars의 첫번째 인자 0은 str에서 어디부터 가져올지

len은 어디까지 가져올지 (위 소스에서는 0 ~ len이니까 전부)

세번째 인자 value는 가져온 char[]를 넣을 target이다

네번째 인자 count는 target의 몇번째 인덱스부터 넣을지 offset을 나타낸다. 

(아직 count를 초기화 한적 없기 때문에 0이다.)

 

단순하게 생각해서 기존에 있던 value에 append할 string값을 char[]로 바꿔 그 뒤에 붙이는 과정이다.

 

마지막으로 count에 값을 넣어준다.

 

디버깅모드로 값 체크

728x90