String 클래스
변경 불가능한 클래스
- String 클래스에는 문자열을 저장하기 위해서 문자형 배열 참조변수(byte[]) value를 인스턴스 변수로 정의해놓고 있다.
- 다른 블로그나 자료를 찾아보면 char 배열을 사용한다고 되어있는데 확인해보니 byte[]배열을 사용하고 있었다. 왜 변경된 것일까??
- 찾아보니 jdk 9부터 기존 char[]에서 byte[]을 사용하여 String Compacting을 통한 성능 및 heap 공간 효율(2byte -> 1byte)을 높이도록 수정되었다고 한다.
- 인스턴스 생성 시 생성자의 매개변수로 입력받는 문자열은 이 인스턴스변수에 문자형 배열로 저장되는 것이다.
public final class String implements java.io.Serializable, Comparable {
private final byte[] value;
}
- 한번 생성된 String 인스턴스가 갖고 있는 문자열은 읽어 올 수만 있고, 변경할 수는 없다.
- 아래처럼 +로 결합하는 경우 새로운 문자열("ab")이 담긴 String 인스턴스가 생성되는 것이다.
String str = new String("Hello");
str = str + "world";
이미지 출처
- 위처럼 문자열을 결합하는 것은 매 연산 시마다 새로운 문자열을 가진 String 인스턴스가 생성되어 메모리공간을 차지하게 되므로 가능한 한 결합횟수를 줄이는 것이 좋다.
- 즉, 문자열간의 결합이나 추출 등 문자열을 다루는 작업이 많이 필요한 경우에는 String 클래스 대신 StringBuffer 또는 StringBuilder 클래스를 사용하는 것이 좋다.
그렇다면 자바에서는 왜 String을 불변으로 했을까??
- 찾아보니 스트링을 불변하게 함으로써 캐싱, 보안, 동기화, 성능측면 이점이 있다고 한다.
- 캐싱 : String을 불변하게 함으로써 String pool에 각 리터럴 문자열의 하나만 저장하며 다시 사용하거나 캐싱에 이용가능하며 이로 인해 힙 공간이 많이 절약된다.
- 보안 : 아래와 같은 코드가 있다고 가정할 때 String이 변경 가능하다면 업데이트 쿼리를 실행할 때 까지 유효성 검사를 수행된 시점이라도 String이 안전할지 확신할 수 없다. 여전히 참조가 남아 있으며 SQL 주입에 노출되기 쉽다.
void criticalMethod(String userName) {
if (!isAlphaNumeric(userName)) {
throw new SecurityException();
}
initializeDatabase();
connection.executeUpdate("UPDATE Customers SET Status = 'Active' " +
" WHERE UserName = '" + userName + "'");
}
- 동기화 : 불변함으로써 동시에 실행되는 여러 스레드에서 공유가 가능하다. 또한 스레드가 값을 변경하면 String pool에 새 리터럴이 작성되기 때문에 안전하다.
- 이외에도 해시코드 캐싱에도 이점이 있어 String을 불변하게 한다면 힙 메모리를 절약하고 해시 구현의 액세스 속도를 높여 성능을 향상되기 때문에 불변으로 만든 이유이다.
문자열 연결을 위한 Java 컴파일러 최적화
- JDK 1.5 이상에서는 컴파일 단계에서 내부적으로 StringBuilder로 변경되어 동작된다.
- concat() 메서드는 해당사항이 없다.
- 만약 아래처럼 for문 안에서 문자열 연결 연산을 한다면 매번 StringBuilder 객체가 생성되어 GC는 엄청나게 낮은 성능을 보일 것이니 주의하자
public static void main(String[] args) {
String result = "";
for (int i = 0; i < 1e6; i++) {
result += "hello";
}
System.out.println(result);
}
public static void main(String[] args) {
String result = "";
for (int i = 0; i < 1e6; i++)
{
StringBuilder tmp = new StringBuilder();
tmp.append(result);
tmp.append("hello");
result = tmp.toString();
}
System.out.println(result);
}
문자열의 비교
- 문자열을 만들때는 두가지 방법이 있다.
- 문자열 리터럴을 지정하는 방법
- String 클래스의 생성자를 사용해서 만드는 방법
이미지 출처
- String 클래스의 생성자를 이용한 경우에는 new 연산자에 의해서 힙 영역에 메모리할당이 이루어지기 때문에 항상 새로운 String 인스턴스가 생성된다.
- 문자열 리터럴은 이미 존재하는 것을 재사용하는 것이다.
- 내부적으로 String.intern() 호출
- String Pool에 같은 값이 있는지 찾는다.
- 같은 값이 있으면 그 참조값이 반환된다.
- 같은 값이 없으면 String Pool에 문자열이 등록된 후 해당 참조값이 반환된다.
- 문자열 리터럴은 클래스가 메모리에 로드될 때 자동적으로 미리 생성
String str1 = "abc";
String str2 = "abc";
String str3 = new String("abc");
String str4 = new String("abc");
str == str2
st3 == str4
- equals()를 사용했을 때는 문자열의 내용을 비교하기 때문에 true이지만 인스턴스의 주소를 ==로 비교했을 때는 결과가 다르다.
StringBuffer vs StringBuilder
공통점
- 두 클래스 모두 는 String과 달리 mutable하다.
- 따라서 스트링 클래스와 달리 지정된 문자열을 변경할 수 있다.
- 내부적으로 문자열 편집을 위한 버퍼를 가지고 있으며 인스턴스를 생성할 때 그 크기를 지정할 수 있다.
- 기본 capacity 16
- 버퍼의 길이를 충분히 잡아주는 것이 좋다.
- 길이를 넘어서게 되면 버퍼의 길이를 늘려주는 작업이 추가로 수행되어야하기 때문이다.
- String 클래스처럼 문자열을 저장하기 위한 char 배열의 참조변수를 인스턴스 변수로 선언해놓고 있다.
public final class StringBuffer implements java.io.Serializable {
private byte[] value;
}
- 스트링 클래스와 달리 equals() 메서드를 오버라이딩하지 않아 '==' 로 비교한 것과 같은 결과를 얻는다.
차이점
- StringBuffer는 synchronized가 적용되어 멀티스레드 환경에서 Thread-safe하게 동작할 수 있다.
- 다음에는 synchronized를 이용한 동기화 관련도 찾아보자
- StringBuffer는 동기화로 인해 성능이 떨어지므로 상대적으로 속도가 느리다.
- StringBuilder 클래스는 쓰레드의 동기화만 빼고 StringBuffer와 똑같은 기능으로 작성되어 있다.
부족한 부분 및 궁금증
참고 출처