String 객체
자바의 참조 타입인 String 객체는 한 번 할당한 값을 바꿀 수 없는 불변(Immutable) 객체이다.
public final class String implements Serializable, Comparable<String>, CharSequence, Constable, ConstantDesc {
@Stable
private final byte[] value;
private final byte coder;
private int hash;
...
}
Java
복사
String 객체 내부는 이와 같이 구성되어있는데, 실제 문자를 저장하는 value 필드는 final 키워드를 사용하여 값을 바꿀 수 없게 되어있다.
Java에서 final 선언된 배열 내부의 값은 바꿀 수 있지만, String 객체에서는 배열 내부 값을 변경할 수 있는 메서드를 지원하지 않는다. 그 외에도 배열 크기의 변경이 불가능하니 불변 객체로 작성된 참조 타입 클래스이다.
JDK 8까지는 char[] 배열로 구성되어 있지만, JDK 9부터는 byte[] 배열을 사용하여 String compacting을 통한 성능과 heap 공간 효율(2byte → 1byte)을 향상 시켰다.
String 객체는 이와 같이 불변 객체라서, 한 번 생성된 이후에는 값 변경이 불가능하여 값 연산을 하거나 값을 바꾸게 되면 위와 같이 새로운 객체를 생성하고 스택 변수의 참조 주소를 바꾸어 동작하게 된다.
String string = "hello world";
string.toUpperCase();
System.out.println(string); // "hello world"
Java
복사
불변 객체이기 때문에 String 객체가 지원하는 여러 메서드들은 실제 문자열이 변경되는 것이 아닌, 문자열을 조작 후에 새로 객체를 생성해서 반환한다.
Java에서 String 객체를 불변 객체로 만든 이유는 다음 3가지 이유가 있다.
1. 캐싱 : String을 불변 객체로 만듦으로 인해 String pool에서 각 리터럴 문자열의 하나만 저장하여 캐싱하여 사용해 heap 공간을 절약한다.
2. 보안 : 가변 객체라면 객체를 생성해서 넣은다음, 해당 객체의 주소값을 알고 있는 모든 곳에서 값 수정이 가능하다.
3. 동기화 : 불변 객체이기 때문에 스레드 안전하다.
StringBuffer / StringBuilder
StringBuffer와 StringBuilder는 가변(Mutable) 객체로,
public final class StringBuffer extends AbstractStringBuilder implements Serializable, Comparable<StringBuffer>, CharSequence {
}
abstract class AbstractStringBuilder implements Appendable, CharSequence {
byte[] value;
byte coder;
}
Java
복사
이처럼 내부에 값 변경이 가능한 buffer(value 필드)를 두어 문자열을 저장해두고 문자열 추가, 수정, 삭제 작업을 수행할 수 있도록 설계되어 있다. 또한 문자열 저장 공간이 부족하다면 버퍼의 크기를 늘려서 확보한다.
StringBuffer와 StringBuilder는 가변 객체이기 때문에, 위와 같이 append나 delete 같은 수정 메서드를 통해 내부에 저장된 문자열을 수정할 수 있다. 때문에 문자열의 추가, 수정, 삭제가 빈번하게 발생한다면 String 클래스를 사용하는 것보다 훨씬 빠르다.
String vs StringBuffer/StringBuilder
성능 비교
String star = "*";
for (int i = 0; i < 10; i++) {
star += "*";
}
Java
복사
StringBuffer star = new StringBuffer("*");
for (int i = 0; i < 10; i++) {
star.append("*");
}
Java
복사
위의 두 방식의 차이를 살펴보면,
String 객체의 경우, 이처럼 매 연산마다 새로운 객체를 Heap 영역에 선언하고 참조를 바꾸어 추후 Garbage Collection에 의해 제거 된다.
하지만 StringBuffer를 사용하는 경우에는 내부 버퍼의 문자열을 수정한다.
때문에 문자열 수정을 자주하는 경우에 Heap 메모리 생성 - 문자열 복사 - GC의 단계를 거치는 String 객체보다 내부 문자열을 통해 수정하는 StringBuffer나 StringBuilder 객체를 사용하는 것이 훨씬 효율적이다.
이처럼 String 객체의 + 연산자를 사용하면 불필요한 객체들이 힙 메모리에 추가되기 때문에, 문자열을 합치기를 수행한다면 String 객체보다는 StringBuffer나 StringBuilder를 사용하는 것이 좋다.
String 객체에는 concat이라는 문자열을 합치는 메서드를 지원하는데, 이 메서드 역시 + 연산자보다는 빠를 뿐 호출할 때마다 새로운 문자열을 재구성하여 반환하기 때문에 StringBuffer나 StringBuilder에 비해 느릴 수 밖에 없다.
결론적으로 StringBuffer와 StringBuilder의 경우 초기에 크기를 설정하고 수정 중에 버퍼의 크기를 늘리고 줄이는 연산이 필요하므로, 문자열 수정이 빈번하게 발생하는게 아니라면 크기가 고정되어 있는 String 객체를 사용하는 것이 더 빠를 수 있다.
때문에 단순하게 조회하는 연산에서는 String 클래스를 사용하고, 문자열의 추가나 수정이 빈번하다면 StringBuffer나 StringBuilder를 사용하자.
동등 비교
String 객체에서의 동등 비교를 살펴보자.
String str1 = "Hello";
String str2 = "Hello";
System.out.println(str1 == str2); // false
System.out.println(str1.equals(str2)); // true
Java
복사
위처럼 단순 == 연산자를 통해 동등 비교를 하게 되면, 객체의 주소를 비교하여 실질적으로 서로 같은 객체인지를 비교한다. 하지만 String 객체는 equals 메서드를 재정의 해두었기 때문에, equals 메서드를 사용하면 내부의 문자열이 동일한지 비교하여 그 결과를 반환한다.
StringBuffer str1 = "Hello";
StringBuffer str2 = "Hello";
System.out.println(str1 == str2); // false
System.out.println(str1.equals(str2)); // false
Java
복사
하지만 StringBuffer와 StringBuilder 클래스는 String과 달리 equals 메서드를 재정의 해두지 않아서 단순 == 연산자를 사용한 것과 동일한 결과를 반환한다. 때문에 실제 문자열이 동일한지 여부를 비교하고 싶다면, 각각 toString 메서드를 호출하여 String 클래스로 바꾼 후 비교해야한다.
해시 코드
String 객체와 StringBuffer 객체 내부의 문자열이 무척 긴 상황에서 해시 코드를 계산하게 되면, 문자열의 길이만큼의 추가적인 연산을 수행하기 때문에 시간이 제법 소요된다.
public final class String implements Serializable, Comparable<String>, CharSequence, Constable, ConstantDesc {
@Stable
private final byte[] value;
private int hash;
public int hashCode() {
int h = hash;
if (h == 0 && value.length > 0) {
char val[] = value;
for (int i = 0; i < value.length; i++) {
h = 31 * h + val[i];
}
hash = h;
}
return h;
}
}
Java
복사
하지만 String 클래스는 위와 같이 내부의 hash 필드를 두고 캐싱하여, 해시값이 계산된 적이 있다면 연산 없이 캐싱된 값을 그대로 반환한다. 이는 String 클래스가 불변 객체이기 때문에 이와 같이 캐싱을 사용할 수 있다.
반면 StringBuffer나 StringBuilder의 경우 데이터가 가변이기 때문에 해시값을 캐싱하지 못한다.
이러한 차이로 인해 hash 자료구조를 사용하는 HashMap이나 HashSet 등의 구현체에서 호출될 때, String 객체를 사용하는 것이 성능적 이점이 있다.
StringBuffer vs StringBuilder
차이점
StringBuffer와 StringBuilder는 제공하는 메서드도 동일하고 그 사용 방법 또한 같다. 이 둘의 차이는 스레드 안전성(Thread-safe) 뿐이다. 스레드 안전성이란 여러 스레드에서 해당 자원에 동시 접근 했을 때, 데이터가 변질 되지않고 안전하게 유지되는 특성이다.
StringBuilder의 경우에는 동기화를 지원하지 않지만, StringBuffer의 경우에는 synchronized 키워드를 통해 동기화를 지원하기 때문에 멀티 스레드 환경에서도 안전하다. 다시말해, StringBuffer의 경우에는 스레드-안전 하지만, StringBuilder의 경우에는 스레드-안전 하지않다.
성능 비교
StringBuffer와 StringBuilder의 성능을 비교하면, 당연하게도 동기화를 수행하는 StringBuffer가 조금 더 느리다.
하지만 그 실질적인 차이는 미미하여, 1000만 번 정도의 연산이 넘어가야 유의미한 차이가 발생한다.
실질적으로 현업에서의 Java 애플리케이션은 대부분 멀티 스레드 환경에서 동작하기도 하고 그 성능 차이도 미미하기 때문에, 웬만하면 스레드 안전한 StringBuffer를 사용하는 것이 이상적이라고 할 수 있다.