String의 문자열은 아래와 같이 value에 저장된다. (Java 버전에 따라 방식이 달라짐)
// Java9 이전
private final char[] value;
// Java9 이후
private final byte[] value;
※ 자바에서 문자를 표현할때는 2byte를 사용. 영어 및 숫자는 1byte로 표현이 가능
그래서 영어, 숫자로만 이뤄져있을시 1byte를 사용하고 그렇지않으면 2byte로 사용하기로 되어 효율적으로 변경
String은 참조형타입이다.
- 문자열 비교를 할때는 ==가 아닌 equals() 메서드를 사용하여야 한다.
String str1 = new String("hello");
String str2 = new String("hello");
System.out.println("new String() == 비교: " + (str1 == str2));
System.out.println("new String() equals 비교: " + (str1.equals(str2)));
String str3 = "hello";
String str4 = "hello";
System.out.println("리터럴 == 비교" + (str3 == str4));
System.out.println("리터럴 equals 비교" + (str3.equals(str4)));
//new String() == 비교: false
//new String() equals 비교: true
//리터럴 == 비교: true
//리터럴 equals 비교: true
- new String()의 경우 서로 다른 인스턴스이므로 == 비교는 false, equals() 비교는 같은 문자열이기에 true
- 문자열 리터럴을 사용하는 경우 자바에서는 메모리 효율성과 성능 최적화를 위해 문자열 풀 사용
- 문자열 풀은 Heap 영역을 사용하며 해시 알고리즘을 사용하기 때문에 속도가 빠름
String은 불변객체이다.
String str = "hello";
str.concat(" java");
System.out.println("str = " + str);
String str1 = "hello";
String str2 = str1.concat(" java");
System.out.println("str1 = " + str1);
System.out.println("str2 = " + str2);
//str = hello
//str1 = hello
//str2 = hello java
- 불변 객체이기 때문에 변경이 필요한 경우 새로운 결과를 만들어서 반환한다. (str2와 같이)
리터럴과 new String() 비교
String str = "hello";
- 이 방식은 문자열 리터럴을 사용
- Java는 문자열 리터럴을 생성할 때, 먼저 문자열 풀에서 해당 문자열이 이미 존재하는지 확인
- 만약 풀에 존재하면, 해당 문자열에 대한 참조를 반환
- 존재하지 않으면, 새로운 문자열 객체를 생성하고 풀에 추가
- 이 방법은 메모리 사용을 최적화하고, 같은 문자열 리터럴을 재사용하기 때문에 성능이 우수
String str = new String("hello");
- 이 방식은 new 키워드를 사용하여 새로운 String 객체를 생성
- 문자열 리터럴 "hello"는 먼저 문자열 풀에 저장
- 그 다음에, new 키워드는 힙 영역에 새로운 String 객체를 생성
- 이 과정에서 문자열 풀에 있는 "hello" 문자열을 복사하여 새로운 객체를 생성하는데, 이는 불필요한 객체 생성을 초래
성능 차이
- 메모리 사용
- String str = "hello";는 동일한 리터럴이 여러 번 사용될 때 메모리를 절약
- String str = new String("hello");는 매번 새로운 객체를 생성하여 메모리를 더 사용
- 속도
- String str = "hello";는 기존 풀에 있는 문자열을 재사용하기 때문에 더 빠름
- String str = new String("hello");는 매번 새로운 객체를 생성해야 하므로 더 느림
결론
대부분의 경우, String str = "hello";를 사용하는 것이 성능 면에서 더 효율적이다. new String("hello")는 특별한 이유가 없는 한 사용을 피하는 것이 좋다. 예를 들어, 문자열의 새로운 인스턴스를 꼭 생성해야 하는 특별한 경우가 아니라면, 일반적으로 리터럴 방식을 사용하는 것이 권장
String 연산 과정
1. "A" + "B"
2. String("A") + String("B")
3. String("A").concat(String("B"))
4. new String("AB")
※String은 불변이라 내부 값을 변경할 수 없다. 그래서 변경이 될때마다 새로운 String 객체를 생성한다.
예시) 아래의 3,4에서 new String한 객체는 최종으로 사용되지도 않고 GC 대상이 된다.
1. String str = "A" + "B" + "C" + "D";
2. String str = String("A") + String("B") + String("C") + String("D");
3. String str = new String("AB") + String("C") + String("D");
4. String str = new String("ABC") + String("D");
5. String str = new String("ABCD");
위 String 객체가 생성되는 문제를 해결하는 방법은 StringBuilder이다.
StringBuilder
가변 String으로 불리며, 내부의 값을 변경할 수 있어서 새로운 객체를 생성할 필요가 없다.
성능과 메모리 사용면에서 효율적이다.
StringBuilder sb = new StringBuilder();
sb.append("A");
sb.append("B");
sb.append("C");
sb.append("D");
sb.append("E");
System.out.println("sb = " + sb);
// ABCDE
String 최적화
String 단순 연산은 시스템에서 최적화 해준다
str1 + str2;
String result = new StringBuilder().append(str1).append(str2).toString();
하지만
루프안에서는 최적화가 이루어지지 않는다.
아래와 같이 차이가 엄청나다.
long startTime = System.currentTimeMillis();
String result = "";
for(int i = 0; i < 100000; i++){
result += "Hello Java ";
}
long endTime = System.currentTimeMillis();
System.out.println("time = " + (endTime - startTime) + "ms");
// time = 6082ms
long startTime = System.currentTimeMillis();
StringBuilder sb = new StringBuilder();
for(int i = 0; i < 100000; i++){
sb.append("Hello Java ");
}
long endTime = System.currentTimeMillis();
String result = sb.toString();
System.out.println("time = " + (endTime - startTime) + "ms");
// time = 6ms
StringBuilder vs StringBuffer
StringBuilder와 같은 기능을 수행하는 StringBuffer
StringBuffer는 내부 동기화가 되어 있어, 멀티 쓰레드에 안전하지만 동기화 오버헤드로 인해 성능이 느림
StringBuilder는 멀티 쓰레드에는 안전하지 않지만 동기화 오버헤드가 없어서 속도가 빠름
'공부 > Java' 카테고리의 다른 글
메모리 가시성 (0) | 2024.09.20 |
---|---|
스레드 기본 정보 (1) | 2024.09.18 |
스레드 생성과 실행 (0) | 2024.09.16 |
프로세스와 스레드란 (0) | 2024.09.16 |
래퍼, Class 클래스란 (0) | 2024.05.30 |