멘토링을 진행하면서 String과 다르게 StringBuilder와 StringBuffer이 가변의 속성을 가지고 있음을 알았다. 그리고 StringBuilder가 어떻게 내부적으로 동작할까? ArrayList와 비슷하게 동작하지 않을까? 라는 궁금증이 생겼고 이에 대해 한번 알아봤다.
1. StringBuilder
StringBuilder는 StringBuffer와는 다르게 멀티쓰레드에 안전하지 않은 문자열 클래스이다. 하지만 단일 쓰레드 환경에서는 속도가 더 빠르게 때문에 동기화를 고려하지 않는 환경에서 사용하는 것이 좋다.
그리고 StringBuilder의 객체는 String 처럼 String constant pool을 사용하지 않고 heap 영역에 저장된다.
그렇다면 StringBuilder은 연산할때 어떻게 동적으로 크기를 늘리고 String보다 빠를까?
1.1 동작 원리
StringBuilder은 내부적으로 ArrayList와 같은 원리로 크기가 동적으로 늘어난다.
StringBuilder sb = new StringBuilder("Hello");
System.out.println(sb.capacity()); //21
기본적으로 StringBuilder 객체 생성 시 내부적으로 default capacity가 16 bytes의 버퍼 크기를 가진다. 따라서 16 + 5해서 21의 용량을 가진다.
sb = sb.append("World");
System.out.println(sb.capacity()); //21
만약 해당 StringBuilder에 크기가 5인 문자열을 추가해도 용량은 그대로 21이다. 이는 StringBuilder이 기존 남은 용량(= 16)을 사용했다는 의미이다.
그렇다면 만약에 16 이상의 문자열을 더하면 어떻게 될까?
sb = sb.append("WorldWorldWorldhh"); //길이가 17인 문자열을 더함
System.out.println(sb.capacity()); //44
이때는 남은 용량인 16을 넘어가 (기존 용량 크기 + 1) * 2 즉 (21 +1) * 2 = 44의 용량을 가진 문자열이된다.
즉 StringBuilder은 ArrayList처럼 갖고있는 버퍼 용량이 찼을 때 새로운 용량의 크기를 늘려 새로운 공간에 할당하는것을 알 수 있다.
다음은 JDK16의 StringBuilder 코드이다.
@IntrinsicCandidate
public StringBuilder() {
super(16); //default caapcity
}
AbstractStringBuilder(int capacity) {
if (COMPACT_STRINGS) {
value = new byte[capacity];
coder = LATIN1;
} else {
value = StringUTF16.newBytesFor(capacity);
coder = UTF16;
}
}
StringBuilder 생성 시 용량이 16인 byte 배열이 내부적으로 생성됨을 알 수 있다.
/**
* Appends the specified string to this character sequence.
* <p>
* The characters of the {@code String} argument are appended, in
* order, increasing the length of this sequence by the length of the
* argument. If {@code str} is {@code null}, then the four
* characters {@code "null"} are appended.
* <p>
* Let <i>n</i> be the length of this character sequence just prior to
* execution of the {@code append} method. Then the character at
* index <i>k</i> in the new character sequence is equal to the character
* at index <i>k</i> in the old character sequence, if <i>k</i> is less
* than <i>n</i>; otherwise, it is equal to the character at index
* <i>k-n</i> in the argument {@code str}.
*
* @param str a string.
* @return a reference to this object.
*/
public AbstractStringBuilder append(String str) {
if (str == null) {
return appendNull();
}
int len = str.length();
ensureCapacityInternal(count + len);
putStringAt(count, str);
count += len;
return this;
}
/**
* For positive values of {@code minimumCapacity}, this method
* behaves like {@code ensureCapacity}, however it is never
* synchronized.
* If {@code minimumCapacity} is non positive due to numeric
* overflow, this method throws {@code OutOfMemoryError}.
*/
private void ensureCapacityInternal(int minimumCapacity) {
// overflow-conscious code
int oldCapacity = value.length >> coder;
if (minimumCapacity - oldCapacity > 0) {
value = Arrays.copyOf(value, //value는 byte[], 새로만든 byte[]에 복사후 리턴
newCapacity(minimumCapacity) << coder);
}
}
그리고 append()메소드 호출 했을 때 minimumCapacity- oldCapacity 이 0보다 클때 즉, minimumCapacity (count + 새로운 문자열의 길이) 가 기존의 byte[]의 길이보다 길다면 기존 byte[]에 추가하지 못하고 새로운 byte[]만들어야하니 (기존용량 크기 + 1) * 2배의 용량의 byte[]을 만들어 복사한다.
- Arrays.copyOf(원본배열, 복사할길이) : 원본 배열을 0부터 원하는 길이만큼 복사한다.
- count : 현재 byte[]의 사이즈
- miminumCapacity: 최소한으로 필요한 용량,
- oldCapacity : 기존 용량크기
'Java' 카테고리의 다른 글
[JAVA] synchronized를 이용한 동기화와 락의 단위 (0) | 2024.02.08 |
---|---|
[JAVA] ArrayList의 로드팩터는 1인데 HashMap의 로드팩터는 0.75일까? (0) | 2024.01.26 |
[JAVA] float과 double은 왜 정확한 숫자계산에 쓸 수 없을까? (0) | 2024.01.19 |
[JAVA] GC 과정에서 왜 survivor 한곳을 비워야할까? (0) | 2024.01.19 |
[JAVA] 자바의 hashcode는 무엇이고, 어디에 사용할까? (0) | 2024.01.18 |