- String은
Heap
에 저장된다. Immutable
(불변성) 특징을 갖고 있다.
위 내용은 다음과 같이 증명할 수 있다.
public static void main(String[] args){
String str = "Hello";
String str1 = "Hello";
String str2 = new String("Hello");
String str3 = str1;
// new 연산자는 항상 새로운 인스턴스를 생성한다.
// == 연산은 객체의 주소값을 비교한다.
System.out.println(str1 == str2); // false
System.out.println(str1 == str3); // true
// literal로 생성된 객체는 string constant pool에 저장된다.
// pool 내에 같은 문자열이 있다면 해당 주소를 할당한다.
System.out.println(str == str1) // true
// String은 immutable하기 때문에 'Hi'라는 새로운 객체를 만든다!
str1 = "Hi";
System.out.println(str1 == str3); // false
System.out.println(str1); // Hi
System.out.println(str3); // Hello
}
-
string의 생성 방식은 두 가지가 있다.
new
연산자를 이용하는 방법literal
을 이용하는 방법
String operator = new String("hello"); String literal = "hello";
Java 7 이상 기준으로 두 방식 모두
Heap
영역에 존재한다.좀 더 자세히 설명하자면
new
연산자로 생성하면Heap
에 존재하게 되고 리터럴을 이용하면string constant pool
이라는 영역에 존재하게 된다.public static void main(String[] args){ String literal = "jophope"; String oper = new String("jophope"); String intern = oper.intern(); System.out.println(literal == oper); // false System.out.println(literal == intern); // true }
위 코드를 보면 기존에
new
로 생성된 String객체와리터럴
로 생성된 String객체를 == 연산하면false
를 반환한다.하지만
new
를 통해 생성된 String객체의intern()
메소드를 호출하여 새로운 String객체에 대입해보니리터럴
String 객체와==
연산에서true
를 반환한다.이는 리터럴로 이용하여 생성된 String은
string constant pool
에 저장되는데intern()
메소드를 호출하면서string constant pool
내부에 있는"jobhope"
라는 문자열을 검색하게 되고 해당 주소값을 반환하여true
가 성립하게 되는 것이다.string constant pool
은 Java 6 까지는Perm
이라는 영역에 존재했다가 Java 7부터Heap
영역으로 옮겨지게 되었는데 그 이유는 다음과 같았다.-
Perm은 Runtime에서 고정된 사이즈였기 때문에 String의 intern()을 호출하면 OOM(OutofMemoryException)을 발생시킬 수 있었다.
-
Heap으로 옮겨지면서 string constant pool의 모든 문자열도 GC(Garbage Collection)의 대상이 되었다!
앞서 설명한대로 String이 immutable 특징이었던데 반해 이들은 mutable한 특성을 갖고 있다.
즉 클래스를 한 번만 만들고(new) 연산이 필요할 때 크기를 늘리면서 문자열을 변경한다. 따라서 문자열 연산이 자주 있을 때 사용하면 성능이 좋다.
그럼 StringBuffer와 StringBuilder의 차이는 뭘까?
StringBuffer | StringBuilder |
---|---|
동기식 | 비동기식 |
Multe-thread도 가능 | Only Single-thread |
위 표처럼 가장 큰 특징은 동기화 여부이다.
StringBuffer의 경우 멀티스레드 환경에서 synchronized 키워드로 동기화가 가능하도록 되어있다.
public final class StringBuffer ...{
...
@Override
public synchronized StringBuffer append(Object obj) {
toStringCache = null;
super.append(String.valueOf(obj));
return this;
}
}
public final class StringBuilder ...{
...
@Override
public StringBuilder append(Object obj) {
return append(String.valueOf(obj));
}
}
요약하자면 다음과 같다.
- StringBuffer와 StringBuilder는 mutable한 특성을 갖는다.
- StringBuffer는 동기식이고 StringBuilder는 비동기식이다.
- 단순히 속도적인 측면만 봤을 때는 StringBuilder가 다소 빠르다.
- 초기 생성할 때 Buffer Size를 설정할 수 있는데 이에 의한 생성, 확장 오버로드가 생길 수 있기 때문에 Buffer Size를 잘못 지정할 경우 성능이 떨어질 수도 있다.
-
String 클래스의 덧셈 연산의 경우 초기 컴파일러 분석단계에서 리터럴 처리에 의해 최적화가 이루어져 오히려 빠른 결과를 보여줄 때도 있다.
String str = "Hello"+" "+"world"; >> String str = "Hello world"; String str2 = "My name is "+student.getName(); >> "My name is "+student.getName(); // 상수만 변환된다
-
JDK 1.5부터는 String 연산의 퍼포먼스를 위해 덧셈 연산도 내부적으로 StringBuilder를 사용하게 한다.
원본 소스1)
public class StringTest { public static void main(String[] args) { String str0 = "It's a string...."; String str1 = "It's" + " a string" + "...."; String str2 = "It's a string...." + str0 + "000"; str2 = str0 + str1 + "1111" ; str2 = str2 + "1111"; str2 += "1111"; for (int i=0;i<10;i++){ str2 = str2 + "1111"; str2 += "1111"; } } }
JDK 1.5 Compile1)
public class StringTest{ public static void main(String args[]) { String str0 = "It's a string...."; String str1 = "It's a string...."; String str2 = (new StringBuilder("It's a string....")).append(str0).append("000").toString(); str2 = (new StringBuilder(String.valueOf(str0))).append(str1).append("1111").toString(); str2 = (new StringBuilder(String.valueOf(str2))).append("1111").toString(); str2 = (new StringBuilder(String.valueOf(str2))).append("1111").toString(); for(int i = 0; i < 10; i++) { str2 = (new StringBuilder(String.valueOf(str2))).append("1111").toString(); str2 = (new StringBuilder(String.valueOf(str2))).append("1111").toString(); } } }
위와 같이 String의 덧셈 연산에서 컴파일러가 StringBuilder로 바꿔주는 모습을 볼 수 있다.
그렇다면 왜 아직도 String과 StringBuilder에서 속도 차이가 나는 것일까?
그것은 반복문을 사용해서 문자열을 더할 경우 StringBuilder 객체를 매번 새로 생성하기 때문이다. 따라서 반복문 안에서는 문자열 덧셈 연산을 해서는 안된다.