String 클래스를 "+"로 반복해서 더하는 연산을 어떤 경우에 컴파일러에서 자동으로 최적화해주는지 알고 있다면 보다 융퉁성 있게 쓸 수가 있습니다.
몇년전에 javaservice.net에서의 String vs StringBuffer에 대한 논의에서도 이 이야기가 오고 갔었고, 그를 통해 제가 알게 되었던 바는 다음과 같습니다.
- 한줄에서 상수 String끼리만 더하는 것은 모두 합쳐진 문자열로 바꿔준다. 즉 String a= "a" + "b" + "c"; 라고 쓰면 String ="abc"; 로 알아서 컴파일해준다는 거죠.
- 한줄에서 상수와 다른 String 클래스를 더하는 것은 StringBuffer의 append, toString 메서드를 쓰는 코드로 준다. jdk 1.4 javadoc 의 StringBuffer API설명 에 명시되어 있네요.
String buffers are used by the compiler to implement the binary string concatenation operator . For example, the code:
x = "a" + 4 + "c"
is compiled to the equivalent of:
x = new StringBuffer().append("a").append(4).append("c").toString()
which creates a new string buffer (initially empty), appends the string representation of each operand to the string buffer in turn, and then converts the contents of the string buffer to a string. Overall, this avoids creating many temporary strings.
Java 1.5 이상에서는 String더하기가 StringBuilder로 치환된다는 것을 듣고나서, 이것을 직접 테스트 해보았습니다. jdk1.5의 API문서를 보시면 아시겠지만 StringBuilder는 동기화되지 않았다는 것이 SringBuffer와 차이점입니다.
참고로 컴파일은 이클립스에서 한 후 jad로 다시 역컴파일한 결과입니다.
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";
}
}
}
public class StringTest{
public 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 = str2 + "1111";
for(int i = 0; i < 10; i++) {
str2 = str2 + "1111";
str2 = str2 + "1111";
}
}
}
public class StringTest{
public 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 str1 = "It's" + " a string" + "...."; -> String str1 = "It's a string...."; 으로 양 버전 모두에서 바뀝니다. 상수와 상수가 아닌 것을 섞어서 더했는 때는 jdk1.4로 이클립스에서 컴파일한 결과로는 StringBuffer가 나타나지는 않네요. 그리고 1.5에서는 예상대로 StringBuilder가 나타납니다. jdk1.4에서 StringBuffer로 자동치환이 안되어서 나오는 것은 좀 이상하기는 해도, 반복문이 아닌 곳에서 스트링 한두개를 더하는 정도라면 최적화해 주지 않아도 한 두개의 객체가 더 생기는 정도일 것이니까 큰 성능의 차이는 없을 것 같습니다.
1.5에서는 반복문 안에서의 더하기도 StringBuilder로 바꿔주기는 하지만 매루프마다 새로운 StringBuilder 클래스를 생성하는 것이므로 String과 마찬가지로 필요없는 임시객체를 계속 만들게 됩니다. 즉 어떤 경우라도 반복문안에서 String 더하기에 "+"를 쓰지는 말아야 겠죠.
@apollyon4
StringBuffer로 변환되던건 JDK 1.4시절의 JavaDoc였습니다.
위의 본문에도 'jdk 1.4 javadoc' 라고 적혀있긴합니다.
JDK 1.5-8까지는 StringBuilder로 바뀝니다.
그리고 Java9부터는 최적화 방식이 완전히 달라졌습니다. InvokeDynamic을 이용해서 이미 컴파일된 코드라도 JDK버전업에 따른 최적화 여지를 더 남겨두었습니다. 그에 대해서는 https://dzone.com/articles/jdk-9jep-280-string-concatenations-will-never-be-t 에 설명되어 있습니다
디컴파일러의 옵션에는 따라서 원래의 코드에 가까운 복원을 위해 StringBuilder로 최적화된 코드를 안 보여주는 경우도 있습니다. cfr 이라는 디컴파일러도 그런 경우입니다.
http://www.benf.org/other/cfr/stringbuilder-vs-concatenation.html
즉, JDK의 버전과 디컴파일러의 옵션에 따라서 역컴파일된 소스는 다르게 보일수 있습니다.
바이트코드 분석을 하는것이 더 정확하기는 합니다.
직접 테스트도 해봤었는데요, 간단하게 아래 코드를 만들고,
컴파일후 바이트 코드를 보았습니다.
JDK 1.8.0_212
final
키워드가 없어도 원래 소스에서는 없었던 StringBuilder가 생성되는것이 확인이 됩니다.JDK 13.0.1
StringBuilder 대신 InvokeDynamic + StringConcatFactory가 보입니다.