[Java] String, StringBuilder, StringBuffer에 대해 알아보자
자바의 String, StringBuilder, StringBuffer에 최대한 깊게 알아보자!
Constant pool
자바 클래스 파일 구성항목중 하나인 Constant pool은 리터럴 상수값을 저장하는 곳이다.
문자열뿐만 아니라, 모든종류의 숫자, 문자열, 식별자이름등에 대한 인덱스를 번호로 제공한다.
즉, 런타임시점에 "hello"의 리터럴을 사용하는 메소드를 여러번 호출한다해도 새로운 메모리공간을 할당하는것이 아니라, Constant pool의 인덱스를 참조한다.
단, Constant Pool은 클래스단위로 관리되기때문에 같은 리터럴이라 하더라도 서로다른 클래스에서 등장한다면 다른 인덱스값을 갖는다.
정리 :
CP를 사용해 이미 존재하는 문자열의 경우, 똑같은 문자열을 갖는 새로운 메모리공간을 매번 할당하는것이 아니라 CP에서 가져와 그대로 사용하기때문에 레퍼런스값을 비교하면 갖은 주소값을 갖는다.
String a = "Hello";
String b = "Hello";
System.out.println(a == b); // true, 레퍼런스의 비교
String Pool
public class StringPool {
public static void main(String[] args) {
//객체생성
StringPoolTest1 s1 = new StringPoolTest1();
StringPoolTest2 s2 = new StringPoolTest2();
// 메소드 호출
// str1과 str2는 서로다른 클래스로부터 생성된 같은문자열 "HelloWorld"를 의미함
String str1 = s1.stringTest("HelloWorld");
String str2 = s2.stringTest();
System.out.println( str1 == str2 ); // true
}
}
class StringPoolTest1 {
public String stringTest(String str){
return str;
}
}
class StringPoolTest2 {
public String stringTest(){
return "HelloWorld";
}
}
아까 위에서 분명 Constant Pool은 클래스단위로 실행되기때문에 서로다른 메모리공간에 할당되고, 다른 인덱스를 참조한다고 했다.
근데 위 코드를 보면 StringPoolTest1의 리터럴값과 StringPoolTest2의 리터럴값이 같은것을 볼수있다.
(equals가 아닌 ==을 이용했으므로 완전 같은 주소를 가리키고 있는것이다.)
이는 Java가 String처리에 String Pool을 이용하기 때문이다.
String은 기본자료형이 아니다 ( 객체를 생성하고 객체주소를 참조하기때문에 참조타입이다).
그러나 특수하게 리터럴을 이용해 값을 할당할 수 있다.
JVM은 리터럴이 할당된 String객체는 String Pool이라는 테이블에 저장하고 특정 리터럴이 값에 할당될때 StringPool에 있는지 확인하고 있다면 새로 메모리를 할당하지않고 기존의 인덱스를 참조하도록 한다.
다만, 리터럴이아닌 new()연산자를 통해 문자열을 생성하는경우 같은 문자열값이라도 다른 객체가 된다.
이렇게 ConstantPool과 StringPool이 존재하는 이유 결국 메모리공간을 낭비하지 않게 하기 위해서라고 생각한다.
만약 String type변수에 값을 할당할때 같은 String 리터럴값이 계속해서 메모리공간을 할당받게되면 같은 문자열임에도 불구하고 계속 런타임시점에 메모리에 로드되고 GC의 대상이 되기 때문이다.
String
// 생성자 - 내부적으로 char배열을 사용한다.
public final class String implements java.io.Serializable, Comparable, CharSequence {
private final char value[]; // 불변이다
public String(String original) {
this.value = original.value;
this.hash = original.hash;
}
.
.
.
}
- 새로운 값을 할당할 때마다 새로 클래스에 대한 객체가 생성된다 -> Garbage Collector의 대상이된다!
- String에서 저장되는 문자열은 private final char[]의 형태이기 때문에 String 값은 바꿀수 없다.
- private: 외부에서 접근 불가
- final: 초기값 변경 불가
- 객체가 불변 → 멀티쓰레드 환경에서 동기화에 신경 쓸 필요가 없다. -> 멀티쓰레드 환경에서 유리! (실제 클래스 주석에도 불변이기에 공유가 가능하다고 나와있다.)
- 문자열 연산이 많으면 오버헤드가 왜 발생하는가?
- String의 경우 불변객체이므로 문자열에 값이 더해지지가 않는다. 즉, 문자열이 새로 생성될때마다 새로운 객체가생성된다.
- 리터럴의 +연산과 참조변수를 통한 + 연산의 차이를 비교해보자.
public class StringTest {
public static void main(String[] args) {
String str1 = "HelloWorld";
// 리터럴 끼리의 + 연산
String str2 = "Hello" + "World";
System.out.println(str1 == str2); // true
// 참조변수를 통한 + 연산
String a = "Hello";
String b = "World";
String c = a + b; // -> new StringBuilder(String.valueOf(a)).append(b).toString();
System.out.println(str1 == c); // false
System.out.println(str1.equals(c)); // true
}
}
위 코드에서 위에는 true, 아래는 false가 나온다.
리터럴끼리의 + 연산은 StringPool에서 문자열이 있는지 확인하고 있으면 해당 인스턴스를 그대로 이용하기 때문이다.
반면 아래 참조변수의 + 연산은 a+b가 수행될때 내부적으로 StringBuilder클래스가 생성되고 그 인스턴스 주소값을 리턴한다.
따라서 str1와 c는 같은 논리적인 값을 갖지만 메모리공간을 서로다르게 할당받아있다.
문자열 연산의 오버헤드 문제로 Java 1.5에서부터 SB클래스들이 등장하게된다.
append() 메소드를 사용해 문자열을 결합하면, 새로운 인스턴스를 생성하지않고 하나의 메모리주소를 사용한다.
이러한 SB클래스들의 동작으로 Java1.5 이후부터는 + 연산방식이 바뀌겐된다.
+를 이용해서 문자열을 결합할때 내부적으로 StringBuilder를 생성, append메서드를 이용한다.
따라서 개발자는 그냥 이전방식을 써도 내부적으로 메모리 효율이 증가되었다! 찐천재!
다만 StringBuilder클래스의 인스턴스가 매번 생성되는것은 어쩔수 없으므로 반복문내부에서는 +연산을 사용해도 결국 StringBuilder인스턴스가 계속적으로 생성되기때문에 이 또한 오버헤드가 발생할 수 있다.
StringBuffer, StringBuilder
가변. 한번 만들고 필요할 때 크기를 변경하여 문자를 변경함 (append()와 같이)
0. 얘는 왜 가변적인데??
아까 String클래스의 필드를 보면 private final char[]의 형태인것을 볼 수 있었다.
그러나 얘는 private키워드도 final키워드도 없다. 즉 변경이 가능한 필드인것이다! 이 배열은 추후에 append메서드 내부를 보면 해당 배열의 끝 지점으로부터 새로운 문자열이 추가되는것을 확인할 수 있다.
1. 문자열 연산시 append메서드를 이용해 새로 객체를 만들지 않고 크기를 변경한다.
StringBuilder sb = new StringBuilder();
sb.append("Hello");
sb.append("World");
System.out.println(sb); // Hello World
Hello, World, Hello World 모두 같은 메모리공간을 사용 -> 메모리주소값 하나
append메서드를 확인해보면 내부적으로 sb의 append -> 부모(AbstractStringBuilder)의 append -> putStringAt() -> String클래스의 getBytes() 가 호출된다.
최종위치의 함수의 설명을 보면 현재 String의 char byte를 목적지 배열에 (dstBegin -> 복사할 부분첫주소) 복사한다고 되어있다.
이함수를 보면 append()가 왜 메모리공간을 추가로 할당하는것이 아닌 기존 메모리배열에 붙여넣는 식으로 동작하는지 알 수 있다.
2. SB클래스는 equals연산자 사용이 불가하다.
둘의 문자열값을 비교하고 싶으면 toString메소드로 String인스턴스를 얻은 후에 equals메소드를 사용하여야 한다고 한다.
sb에 바로 equals를 적용하면 Suspicious call 이라는문구가 뜨는데 검색해보니 StringBuilder는 Object메소드의 .equals()를 오버라이드 하지 않는다고 한다. 확인해보자.
왜 오버라이드하지 않는가에 대해서 생각해보았다..
가설1. 변함의 여지가 있기때문에 내부적으로 추가적인 공간을 가지는 배열을 가진다... 그래서 같은 값이여도 어차피 다르게 나올것이므로 오버라이드 하지 않는다..?,,,, 아직 잘 모르겠다.
일단 좀더 고민해 봐야겠다.
3. 둘의차이는 동기화 feat.Synchronized
StringBuffer의 경우 동기화를 제공하고 StringBuilder의 경우 동기화를 제공하지 않는다.
동기화를 제공하지 않는다는것은 여러 프로세스가 자원에 접근할 수 있고, 변경이 되었을때 프로세스(스레드)간 서로다른 값을 가져도 신경쓰지 않는다는 것을 의미한다. 이런점을 멀티스레드 환경에서 매우 위험하다. 같은 자원에 대해서 서로다른 값을 가지므로 예상하지 않은 결과가 나올수 있기 때문!
따라서 단순 성능으로는 동기화따위 신경쓰지않는 StringBuilder가 가장 좋은 성능을 보인다.