StringBuffer: mutable(가변) 문자열을 다루는 고전적인 클래스
기본 개념
- 패키지:
java.lang
- 역할: 문자열을 수정 가능한 형태로 저장하고, 추가/삭제/수정 연산을 지원
- 특징
- 스레드 안전(Thread-safe): 주요 메서드들이
synchronized로 구현되어 있어서 멀티스레드 환경에서도 안전하게 사용 가능
- 불변 객체인
String과 달리 StringBuffer는 객체 내에서 문자열을 수정하므로 String 객체의 + 연산 반복 시 생기는 임시 객체 생성 오버헤드를 피할 수 있다.
주요 생성자
StringBuffer() // 초기 용량(capacity) 16
StringBuffer(int capacity) // 지정 용량으로 생성
StringBuffer(String str) // 초기 문자열을 지정
StringBuffer(CharSequence cs) // CharSequence 기반 생성
주요 메서드
| 메서드 |
설명 |
append(...) |
문자열, 숫자, 객체 등을 이어 붙임 |
insert(int offset, ...) |
지정 위치에 문자열 삽입 |
delete(int start, int end) |
구간 문자열 삭제 |
deleteCharAt(int index) |
특정 인덱스 문자 삭제 |
replace(int start, int end, String str) |
구간을 다른 문자열로 치환 |
reverse() |
문자열 뒤집기 |
capacity() |
현재 버퍼 용량 반환 |
ensureCapacity(int minCapacity) |
최소 용량 확보 |
charAt(int index) |
특정 인덱스 문자 반환 |
setCharAt(int index, char ch) |
특정 인덱스 문자 수정 |
toString() |
최종 문자열 반환 |
사용 예시
public class Main {
public static void main(String[] args) {
StringBuffer sb = new StringBuffer("Hello");
sb.append(" World");
System.out.println(sb); // Hello World
sb.insert(5, ",");
System.out.println(sb); // Hello, World
sb.replace(6, 11, "Java");
System.out.println(sb); // Hello, Java
sb.delete(5, 10);
System.out.println(sb); // Hello
sb.reverse();
System.out.println(sb); // olleH
}
}
성능과 활용
StringBuffer는 스레드 안전하지만 상대적으로 느리다.
- 단일 스레드 환경에서는
StringBuilder 사용이 권장된다.(StringBuilder는 동기화하지 않으므로 더 빠르다)
- 문자열이 자주 변하는 경우(삽입/수정/삭제)
String 대신 StringBuffer 또는 StringBuilder를 쓰는 것이 효율적
| 클래스 |
불변 여부 |
스레드 안전성 |
성능 |
String |
불변(immutable) |
안전(읽기 전용이라) |
가장 느림 (수정 시 객체 재생성) |
StringBuffer |
가변(mutable) |
스레드 안전 (synchronized) |
StringBuilder보다 느림 |
StringBuilder |
가변(mutable) |
스레드 안전하지 않음 |
가장 빠름 |
StringBuffer 객체 비교
StringBuffer의 equals 메서드
StringBuffer는 equals 메서드를 오버라이드하지 않는다.
StringBuffer의 equals는 Objects.equals 그대로이기 때문에 객체의 참조 동일성(==)만 비교하고 객체의 내용은 비교하지 않는다.
- 내용이 같더라도 서로 다른 인스턴스라면
equals 메서드의 반환값은 false이다.
왜 그럴까?
StringBuffer 클래스는 가변이고 내용이 바뀌면 객체의 해시/동등성의 의미가 불안정해진다.
- 따라서 애초에 내용 동등성 계약을 제공하지 않고 아이덴티티(참조)만 비교하도록 두었다.
- 이에 따라 해시도 마찬가지로 아이덴티티 기반의
Objects.hashCode를 그대로 쓴다. => Map의 키 값 타입으로 사용하면 위험하다.
올바른 StringBuffer 객체 내용 비교 방법
toString() 후 비교: 새 String 객체를 만들기 때문에 대량/고빈도 비교에서는 할당 비용이 크다.
StringBuffer sb1 = new StringBuffer("abc");
StringBuffer sb2 = new StringBuffer("abc");
boolean same = sb1.toString().equals(sb2.toString()); // true
String 클래스의 contentEquals(CharSequence) 활용(권장): contentEquals는 인자가 CharSequence면 문자 단위로 직접 순회 비교하기 때문에 "이미 String이 한 쪽에 있는 상황"에서 추가 할당을 막을 수 있다.
StringBuilder a = new StringBuilder("hello");
StringBuffer b = new StringBuffer("hello");
boolean same = "hello".contentEquals(a); // true
boolean same2 = a.toString().contentEquals(b); // true
- 공통 유틸로
CharSequence끼리 비교 => 무할당, 새 String 객체를 만들지 않고 CharSequence 인터페이스로 직접 비교
static boolean contentEquals(CharSequence x, CharSequence y) {
if (x == y) return true;
if (x == null || y == null) return false;
int n = x.length();
if (n != y.length()) return false;
for (int i = 0; i < n; i++) {
if (x.charAt(i) != y.charAt(i)) return false; // UTF-16 코드 유닛 기준
}
return true;
}
불변성: StringBuffer는 불변 클래스가 아닌 가변 클래스다.
- 불변(Immutable) 클래스의 특징
- 한 번 생성된 객체의 상태(내부 값)가 절대 변하지 않는다.
- 대표 예시:
String, Integer, LocalDateTime
String s = "Hello";
s.concat("World");
System.out.println(s); // 여전히 "Hello"
→ concat이 새로운 "HelloWorld" 객체를 만들고 반환할 뿐, 기존 "Hello"는 변하지 않음.
StringBuffer 객체 상태 특징
- 내부적으로 byte[] 배열을 관리하고 문자열 수정 시 배열 내용 자체를 변경한다.
- 따라서 동일 객체 안에서 값이 바뀐다. -> 가변(mutable)
StringBuffer sb = new StringBuffer("Hello");
sb.append("World");
System.out.println(sb); // HelloWorld
-> 새로운 객체가 만들어진 게 아니라, 기존 sb의 내부 배열에 "World"가 추가된 것이다.
StringBuffer는 멀티스레드 환경에서 안전하게 문자열을 수정할 수 있도록 synchronized가 붙은 가변 클래스다.
StringBuffer가 가변 클래스임에도 final로 선언된 이유
public final class StringBuffer
extends AbstractStringBuilder
implements Appendable, Serializable, Comparable<StringBuffer>, CharSequence
{
...
}
- 스레드 안전 계약 유지, 구현/최적화의 자유 보장, 그리고 보안/호환성
- 스레드 안전 계약 보호
StringBuffer의 핵심 가치는 모든 주요 연산이 동기화되어 있어 멀티스레드에서도 안전한다는 점이다.
- 만약 누군가가 하위 클래스로
append, insert 등을 동기화 없이 재정의하면, 외부에서는 여전히 “StringBuffer니까 thread-safe겠지”라고 믿고 쓰다가 계약(Contract) 위반이 발생한다.
final은 이런 “계약 파괴형 상속” 자체를 원천 차단한다.
- JIT 최적화·가정 유지
- 핫스팟 JIT은
StringBuffer 연산이 항상 동기화된다는 전제하에 인라이닝/탈가상화 같은 최적화를 적용하기 쉽다.
- 하위 클래스가 생기면 오버라이드 가능성 때문에 호출 지점이 다양해져(다형성) 최적화가 제한되거나, 동작 가정을 깰 위험이 생긴다.
final은 이 위험을 없앤다.
- 구현 세부 변경의 자유(후방 호환)
StringBuffer는 AbstractStringBuilder를 기반으로 내부 표현(버퍼, 용량 증가 전략 등)이 자바 버전에 따라 최적화될 수 있다.
- 상속을 허용하면 하위 클래스가 내부 동작에 의존하게되고, JDK가 내부를 바꾸기 어려워집니다.
final은 내부 바꿔도 API 계약만 지키면 OK라는 자유를 준다.
- 보안·안전성 강화
- 하위 클래스가 “부분적으로만 동기화”하거나, 예외 시 일관성 깨진 상태를 외부에 노출하게 만들 수도 있다.
- 또한 직렬화/역직렬화·
toString() 캐시 등 섬세한 경계에서 하위 클래스가 정보 노출/무결성 훼손을 만들 수 있다. final은 이런 공격면을 줄인다.
- 일관성:
StringBuilder도 final
- 비동기화 버전인
StringBuilder도 final
- 둘 다 “설계 의도(동기화 있음/없음) + 성능 특성”을 상속으로 바꿀 수 없게 고정해, 사용자가 클래스 이름만 보고도 동작을 확실히 추론하게 한다.
- final class로 상속 금지 여부를 결정하는 것은 객체가 불변이든 가변이든 상속을 막을 이유가 충분하다면
final로 설계된다.
StringBuffer를 final로 막은 건 불변성 때문이 아니라 스레드 안전 계약 보존, JIT 최적화, 내부 구현 변화의 자유, 보안/호환성을 위한 설계를 하기 위한 선택이다.
StringBuilder
기본 개념
- 패키지:
java.lang
- 역할:
StringBuffer와 거의 동일하게 가변(mutable) 문자열을 다루는 클래스
- 차이점:
StringBuffer는 모든 메서드가 synchronized -> 스레드 안전하지만 상대적으로 느리다. StringBuilder는 동기화를 제거해서 스레드 안전하지 않지만 더 빠르다.
- 추가된 시기: Java 5 (
JDK 1.5)
생성자
StringBuilder() // 초기 용량(capacity) 16
StringBuilder(int capacity) // 지정 용량으로 생성
StringBuilder(String str) // 초기 문자열 지정
StringBuilder(CharSequence cs) // CharSequence 기반 생성
주요 메서드
StringBuilder는 StringBuffer와 마찬가지로 equals 메서드를 오버라이드하지 않는다.
| 메서드 |
설명 |
append(...) |
문자열, 숫자, 객체 등을 이어 붙임 |
insert(int offset, ...) |
지정 위치에 문자열 삽입 |
delete(int start, int end) |
구간 삭제 |
deleteCharAt(int index) |
특정 인덱스 문자 삭제 |
replace(int start, int end, String str) |
구간을 다른 문자열로 치환 |
reverse() |
문자열 뒤집기 |
capacity() |
버퍼 용량 반환 |
ensureCapacity(int minCapacity) |
최소 용량 확보 |
setCharAt(int index, char ch) |
특정 인덱스 문자 변경 |
toString() |
최종 문자열 생성 |
사용 예시
public class Main {
public static void main(String[] args) {
StringBuilder sb = new StringBuilder("Hello");
sb.append(" World");
System.out.println(sb); // Hello World
sb.insert(5, ",");
System.out.println(sb); // Hello, World
sb.replace(7, 12, "Java");
System.out.println(sb); // Hello, Java
sb.delete(5, 11);
System.out.println(sb); // Hello
sb.reverse();
System.out.println(sb); // olleH
}
}
컴파일 시의 String 객체 덧셈 연산 최적화: StringBuilder 이용
StringBuilder는 내부적으로 가변적인 char[] 버퍼를 가지고 있기 때문에 append()할 때마다 새로운 객체를 만들 필요가 없다.
StringBuilder sb = new StringBuilder();
sb.append("a");
sb.append("b");
sb.append(variable);
String result = sb.toString();
StringBuilder 객체 하나만 생성되고, 내부 char[]에 직접 이어 붙이는 식으로 처리된다.
- 최종
toString()에서 단 한 번 String 객체를 만들 뿐이다.
StringBuilder가 도입된 Java 5부터 javac 자바 컴파일러가 문자열 연결 최적화를 StringBuilder를 기반으로 수행하기 시작했다.
StringBuilder는 동기화(synchronized)가 없는, 가벼운 가변 문자열 버퍼
- Java 5부터
javac는 컴파일 시 문자열의 + 연산을 전부 StringBuilder의 append 체인으로 변경한다.
StringJoiner
String 문자열 변환
String 클래스 valueOf 메서드
// String.java
public static String valueOf(boolean b) {
return b ? "true" : "false";
}
public static String valueOf(char c) {
if (COMPACT_STRINGS && StringLatin1.canEncode(c)) {
return new String(StringLatin1.toBytes(c), LATIN1);
}
return new String(StringUTF16.toBytes(c), UTF16); // 방어적 복사본 반환
}
public static String valueOf(int i) {
return Integer.toString(i);
}
public static String valueOf(long l) {
return Long.toString(l);
}
public static String valueOf(float f) {
return Float.toString(f);
}
public static String valueOf(double d) {
return Double.toString(d);
}
// Integer.java
public static String toString(int i) {
int size = stringSize(i);
if (COMPACT_STRINGS) {
byte[] buf = new byte[size];
StringLatin1.getChars(i, size, buf);
return new String(buf, LATIN1);
} else {
byte[] buf = new byte[size * 2];
StringUTF16.getChars(i, size, buf);
return new String(buf, UTF16); // 방어적 복사본 반환
}
}
// Long.java
public static String toString(long i) {
int size = stringSize(i);
if (COMPACT_STRINGS) {
byte[] buf = new byte[size];
StringLatin1.getChars(i, size, buf);
return new String(buf, LATIN1);
} else {
byte[] buf = new byte[size * 2];
StringUTF16.getChars(i, size, buf);
return new String(buf, UTF16); // 방어적 복사본 반환
}
}
int i = 100;
String str1 = i + "";
String str2 = String.valueOf(i);
String 클래스 join 메서드
// String.java
public static String join(CharSequence delimiter, CharSequence... elements) {
var delim = delimiter.toString();
var elems = new String[elements.length];
for (int i = 0; i < elements.length; i++) {
elems[i] = String.valueOf(elements[i]);
}
return join("", "", delim, elems, elems.length);
}
public static String join(CharSequence delimiter,
Iterable<? extends CharSequence> elements) {
Objects.requireNonNull(delimiter);
Objects.requireNonNull(elements);
var delim = delimiter.toString();
var elems = new String[8];
int size = 0;
for (CharSequence cs: elements) {
if (size >= elems.length) {
elems = Arrays.copyOf(elems, elems.length << 1);
}
elems[size++] = String.valueOf(cs);
}
return join("", "", delim, elems, size);
}
static String join(String prefix, String suffix, String delimiter,
String[] elements, int size) {
int icoder = prefix.coder() | suffix.coder();
long len = (long) prefix.length() + suffix.length();
if (size > 1) {
// when there are more than one element, size - 1 delimiters will be emitted
len += (long) (size - 1) * delimiter.length();
icoder |= delimiter.coder();
}
// assert len > 0L; // max: (long) Integer.MAX_VALUE << 32
// following loop will add max:
// (long) Integer.MAX_VALUE * Integer.MAX_VALUE to len
// so len can overflow at most once
for (int i = 0; i < size; i++) {
var el = elements[i];
len += el.length();
icoder |= el.coder();
}
byte coder = (byte) icoder;
// long len overflow check, char -> byte length, int len overflow check
if (len < 0L || (len <<= coder) != (int) len) {
throw new OutOfMemoryError("Requested string length exceeds VM limit");
}
byte[] value = StringConcatHelper.newArray(len);
int off = 0;
prefix.getBytes(value, off, coder); off += prefix.length();
if (size > 0) {
var el = elements[0];
el.getBytes(value, off, coder); off += el.length();
for (int i = 1; i < size; i++) {
delimiter.getBytes(value, off, coder); off += delimiter.length();
el = elements[i];
el.getBytes(value, off, coder); off += el.length();
}
}
suffix.getBytes(value, off, coder);
// assert off + suffix.length() == value.length >> coder;
return new String(value, coder); // 방어적 복사본 반환
}
- 사용 예시: 여러 문자열 사이에 구분자를 넣어서 결합
String animals = "dog, cat, bear";
String[] arr = animals.split(","); ["dog", "cat", "bear"]
String str = String.join("-", arr);
System.out.println(str); dog-cat-bear
Integer 클래스 valueOf, parseInt 메서드
// Integer.java
public static Integer valueOf(String s, int radix) throws NumberFormatException {
return Integer.valueOf(parseInt(s,radix));
}
public static Integer valueOf(int i) {
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)];
return new Integer(i);
}
public static int parseInt(String s) throws NumberFormatException {
return parseInt(s, 10);
}
public static int parseInt(String s, int radix)
throws NumberFormatException {
if (s == null) {
throw new NumberFormatException("Cannot parse null string");
}
if (radix < Character.MIN_RADIX) {
throw new NumberFormatException(String.format(
"radix %s less than Character.MIN_RADIX", radix));
}
if (radix > Character.MAX_RADIX) {
throw new NumberFormatException(String.format(
"radix %s greater than Character.MAX_RADIX", radix));
}
int len = s.length();
if (len == 0) {
throw NumberFormatException.forInputString("", radix);
}
int digit = ~0xFF;
int i = 0;
char firstChar = s.charAt(i++);
if (firstChar != '-' && firstChar != '+') {
digit = digit(firstChar, radix);
}
if (digit >= 0 || digit == ~0xFF && len > 1) {
int limit = firstChar != '-' ? MIN_VALUE + 1 : MIN_VALUE;
int multmin = limit / radix;
int result = -(digit & 0xFF);
boolean inRange = true;
/* Accumulating negatively avoids surprises near MAX_VALUE */
while (i < len && (digit = digit(s.charAt(i++), radix)) >= 0
&& (inRange = result > multmin
|| result == multmin && digit <= radix * multmin - limit)) {
result = radix * result - digit;
}
if (inRange && i == len && digit >= 0) {
return firstChar != '-' ? -result : result;
}
}
throw NumberFormatException.forInputString(s, radix);
}
int i = Integer.parseInt("100");
int i2 = Integer.valueOf("100");
Integer i3 = Integer.valueOf("100");
Long 클래스 valueOf, parseLong 메서드
// Long.java
public static Long valueOf(String s, int radix) throws NumberFormatException {
return Long.valueOf(parseLong(s, radix));
}
public static Long valueOf(long l) {
final int offset = 128;
if (l >= -128 && l <= 127) { // will cache
return LongCache.cache[(int)l + offset];
}
return new Long(l);
}
public static long parseLong(String s) throws NumberFormatException {
return parseLong(s, 10);
}
public static long parseLong(String s, int radix)
throws NumberFormatException {
if (s == null) {
throw new NumberFormatException("Cannot parse null string");
}
if (radix < Character.MIN_RADIX) {
throw new NumberFormatException(String.format(
"radix %s less than Character.MIN_RADIX", radix));
}
if (radix > Character.MAX_RADIX) {
throw new NumberFormatException(String.format(
"radix %s greater than Character.MAX_RADIX", radix));
}
int len = s.length();
if (len == 0) {
throw NumberFormatException.forInputString("", radix);
}
int digit = ~0xFF;
int i = 0;
char firstChar = s.charAt(i++);
if (firstChar != '-' && firstChar != '+') {
digit = digit(firstChar, radix);
}
if (digit >= 0 || digit == ~0xFF && len > 1) {
long limit = firstChar != '-' ? MIN_VALUE + 1 : MIN_VALUE;
long multmin = limit / radix;
long result = -(digit & 0xFF);
boolean inRange = true;
/* Accumulating negatively avoids surprises near MAX_VALUE */
while (i < len && (digit = digit(s.charAt(i++), radix)) >= 0
&& (inRange = result > multmin
|| result == multmin && digit <= (int) (radix * multmin -
limit))) {
result = radix * result - digit;
}
if (inRange && i == len && digit >= 0) {
return firstChar != '-' ? -result : result;
}
}
throw NumberFormatException.forInputString(s, radix);
}
StringJoiner 클래스
StringJoiner는 Java 8부터 추가된 문자열 결합 유틸리티 클래스이다.
- 여러 문자열을 연결하면서 구분자(delimeter), 접두사(prefix), 접미사(suffix)를 지정할 수 있기 때문에 반복문에서 문자열을 더하거나
StringBuilder를 직접 다루는 것보다 훨씬 깔끔하게 문자열을 조합할 수 있게 도와준다.
기본 개념
- 패키지:
java.util
- 핵심 역할: 지정한 구분자와 선택적인 접두사/접미사를 사용해 문자열을 효율적으로 결합한다.
생성자
StringJoiner(CharSequence delimeter)
StringJoiner(CharSequence delimeter, CharSequence prefix, CharSequence suffix)
delimiter: 요소 간에 넣을 구분자
prefix: 결과 문자열 앞에 붙일 접두사
suffix: 결과 문자열 뒤에 붙일 접미사주요 메서드
| 메서드 |
설명 |
add(CharSequence newElement) |
새로운 요소를 추가 |
merge(StringJoiner other) |
다른 StringJoiner 내용을 현재 객체에 병합 |
setEmptyValue(CharSequence emptyValue) |
요소가 없을 경우 반환할 기본 문자열을 지정 |
toString() |
최종 문자열 생성 |
사용 예시
StringJoiner sj = new StringJoiner(", ");
sj.add("Java").add("Kotlin").add("Scala");
System.out.println(sj.toString()); // Java, Kotlin, Scala
StringJoiner sj = new StringJoiner(", ", "[", "]");
sj.add("A").add("B").add("C");
System.out.println(sj.toString()); // [A, B, C]
StringJoiner sj = new StringJoiner(", ");
sj.setEmptyValue("EMPTY");
System.out.println(sj.toString()); // EMPTY
StringJoiner sj1 = new StringJoiner(", ");
sj1.add("One").add("Two");
StringJoiner sj2 = new StringJoiner(", ");
sj2.add("Three").add("Four");
sj1.merge(sj2);
System.out.println(sj1.toString()); // One, Two, Three, Four
관련 클래스와 비교
StringBuilder: 더 낮은 수준에서 문자열 조작 -> 유연하지만 구분자, 접두/접미사 처리는 직접 해야한다.
String.join(): 간단히 배열이나 컬렉션을 결합할 때 편리 -> 접두/접미사 기능은 없다.
Collertors.joining()(Stream API): 스트림 요소를 조합할 때 권장되는 방식이다. 내부적으로 StringJoiner를 활용한다.
public static Collector<CharSequence, ?, String> joining(CharSequence delimiter,
CharSequence prefix,
CharSequence suffix) {
return new CollectorImpl<>(
() -> new StringJoiner(delimiter, prefix, suffix),
StringJoiner::add, StringJoiner::merge,
StringJoiner::toString, CH_NOID);
// Collector 인터페이스 구현체 반환
}
List<String> list = Arrays.asList("a", "b", "c");
String result = list.stream().collect(Collectors.joining(", ", "{", "}"));
System.out.println(result); // {a, b, c}
StringJoiner는 단순 문자열 더하기보다 가독성, 구조적 제어(구분자/접두/접미사/빈 값 처리), 성능까지 고려된 고수준 API라고 볼 수 있다.불변성: StringJoiner는 불변 클래스가 아닌 가변 클래스이다.
- 왜 불변이 아닌가?
- StringJoiner는 내부적으로
String[] 버퍼를 사용한다.
add(), merge()를 호출할 때마다 String[] 버퍼가 직접 변경된다.
- 즉, 같은
StringJoiner 객체에 계속 요소를 추가하면 그 객체의 상태가 변경된다.
public final class StringJoiner {
private static final String[] EMPTY_STRING_ARRAY = new String[0];
private final String prefix;
private final String delimiter;
private final String suffix;
/** Contains all the string components added so far. */
private String[] elts; // 내부 버퍼: final이 아니다.
/** The number of string components added so far. */
private int size;
/** Total length in chars so far, excluding prefix and suffix. */
private int len;
private String emptyValue;
public StringJoiner add(CharSequence newElement) {
final String elt = String.valueOf(newElement);
if (elts == null) {
elts = new String[8]; // 객체 상태 변경
} else {
if (size == elts.length)
elts = Arrays.copyOf(elts, 2 * size); // 객체 상태 변경
len = checkAddLength(len, delimiter.length());
}
len = checkAddLength(len, elt.length());
elts[size++] = elt;
return this;
}
String sj = new StringJoiner(", ");
sj.add("A");
System.out.println(sj); // A
sj.add("B");
System.out.println(sj); // A, B
- 불변 클래스와 비교
String은 불변 -> concat() 메서드를 호출해도 원래 객체는 그대로 두고 새로운 객체를 반환
StringJoiner는 가변 -> add() 호출 시 기존 객체 자체가 수정
스레드 안정성: StringJoiner는 스레드 안전하지 않다.
StringJoiner는 불변 클래스가 아니고 어떤 메서드도 synchronized가 아니기 때문에 동시에 여러 스레드가 같은 인스턴스에 add()/merge()를 호출하면 데이터 경합과 깨진 출력이 발생할 수 있다.
- 스레드 안전하게 사용하는 방법
- 같은 인스턴스 공유 금지
- 필요하면 스레드별로 로컬 생성 후 합치기 또는 외부 동기화
- 단일 스레드:
StringBuilder가 가장 빠르다.
- 멀티 스레드에서 공유가 필요한 경우:
StringBuffer 사용을 고려
- 스트림:
Collectors.joining(delim, prefix, suffix)를 사용하면 파이프라인 수준에서 병렬 안전하게 동작하나 여전히 같은 StringJoiner 인스턴스를 직접 공유하면 안 된다.
- 메모리 가시성
- 조합이 끝난 뒤
toString()으로 얻은 String은 불변이라 여러 스레드가 읽어도 안전
- 하지만 조합 중인
StringJoiner를 공유하면 가시성과 원자성 모두 보장되지 않는다.
StringBuilder 사용 버전(java8)과 String[] 버퍼 사용 버전(java9 이후) 비교
StringJoiner 클래스는 최초 버전인 Java 8 도입 당시 구현부터 내부 상태가 변경되는 가변 클래스였다.
- JDK 8(초기 도입): 내부에
private StringBuilder value;를 두고 add()가 그 빌더에 계속 붙이는 구조 → 명백히 가변.
- JDK 9 최적화: 빌더 대신
String[] elts, int size, int len 등을 두고, toString()에서 한 번에 결과를 조립하도록 변경(성능 개선 목적). 여전히 add()가 내부 배열과 길이 카운터를 갱신하므로 가변.
StringJoiner는 API 설계상 요소를 점진적으로 추가하는 용도이므로, 설계 자체가 가변을 전제한다.
- 버전이 업그레이드되면서 바뀐 것은 “어떻게 저장/조립하느냐(StringBuilder → 배열/길이 누적)”이지, 가변성 여부가 아니다.
StringJoiner 클래스 JDK 8 버전 vs StringJoiner 클래스 JDK 9 버전
- 내부 상태
- JDK 8:
StringBuilder 하나에 계속 붙이는 구조
- JDK 9: 요소들을 배열에 모아두고(
elts, size, len) 최종 단계에서 한 번에 문자열 생성
add(...) 동작
- JDK 8:
add(newElement) → prepareBuilder().append(newElement) → (첫 원소면 prefix를 넣고, 그 이후엔 delimiter를 먼저 붙인 뒤 요소 append)
- JDK 9:
add(newElement) → 문자열로 바꿔 배열 elts에 저장, 길이 누계 len 갱신(첫 요소가 아니면 delimiter.length()도 더함). 실제 바이트/문자 복사는 안 함(최종 생성 때 한 번에).
toString() 동작
- JDK 8: 빈 상태면
emptyValue 반환. 아니면 value.append(suffix)로 결과를 만들고 다시 길이를 되돌려(suffix를 되빼서) 내부 버퍼 유지.
(코드 흐름: value.append(suffix).toString();value.setLength(initialLength);)
- JDK 9: 필요한 총 길이만큼
String[]를 만들고 prefix → elts[0..n-1] 사이사이에 delimiter → suffix를 한 번에 복사. 마지막엔 jla.newStringUnsafe(chars)로 String 생성.
- 변경 목적: 불필요한 중간 문자열/버퍼 조작을 줄여 GC 압력과 복사 비용을 낮추기 위함이다. 이 변경은 JDK 9에 반영된 구현 최적화다.