위처럼 문자열을 결합하는 것은 매 연산 시마다 새로운 문자열을 가진 String 인스턴스가 생성되어 메모리공간을 차지하게 되므로 가능한 한 결합횟수를 줄이는 것이 좋다.
즉, 문자열간의 결합이나 추출 등 문자열을 다루는 작업이 많이 필요한 경우에는 String 클래스 대신 StringBuffer 또는 StringBuilder 클래스를 사용하는 것이 좋다.
그렇다면 자바에서는 왜 String을 불변으로 했을까??
찾아보니 스트링을 불변하게 함으로써 캐싱, 보안, 동기화, 성능측면 이점이 있다고 한다.
캐싱 : String을 불변하게 함으로써 String pool에 각 리터럴 문자열의 하나만 저장하며 다시 사용하거나 캐싱에 이용가능하며 이로 인해 힙 공간이 많이 절약된다.
보안 : 아래와 같은 코드가 있다고 가정할 때 String이 변경 가능하다면 업데이트 쿼리를 실행할 때 까지 유효성 검사를 수행된 시점이라도 String이 안전할지 확신할 수 없다. 여전히 참조가 남아 있으며 SQL 주입에 노출되기 쉽다.
void criticalMethod(String userName) {
// perform security checks
if (!isAlphaNumeric(userName)) {
throw new SecurityException();
}
// do some secondary tasks
initializeDatabase();
// critical task
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++)
{
StringBuilder tmp = new StringBuilder();
tmp.append(result);
tmp.append("hello");
result = tmp.toString();
}
System.out.println(result);
}
public static void main(String[] args) {
String result = "";
for (int i = 0; i < 1e6; i++) {
result += "hello";
}
System.out.println(result);
}
java9에서는 재컴파일을 피하고 바이트 코드를 변경하지 않도록 하기 위해 각 문자열 연결은 JDK Enhancement Proposal 280에 설명된 대로 invokedynamic 에 대한 호출로 변경되었다고 한다.