String and StringBuffer by fowler

String and StringBuffer

 

Immutable object는 한번 만들어지면 변하질 않는다는 말이지. Mutable 객체는 반대로 만들어진 후에 변한다는 말일테고. 이 부분에 대한 설명은 따로 하기로 하고, 우선 여기선 이런 객체타입으로 나누어 볼 수 있다고만 생각하고

 

Immutable mutable 객체로 나누어 본다면, String객체는 immutable, StringBuffer mutable이야

변할 수 없는 객체와 변할 수 있는 객체, 수정이 가능한 객체와 수정이 가능하지 않는 객체.

그래서, 둘 중 어떤 것을 사용할 것이냐는 조심스럽게 판단해야겠지. 좀 더 나은 성능을 보이고자 한다면 더더욱 말이야.

 

 

String 객체를 생성할 때, 이건 생각하며 하자

 

 

String 객체를 만들 때, 다음과 같이 할 수 있을거야.

 

String s1 = “hello”;

String s2 = “hello”;

 

String s3 = new String(“hello”);

String s4 = new String(“hello”);

 

 

첫 번째와 두 번째 중에 어떤 게 더 성능이 나을 거 같아?

두 종류를 실제로 테스트 해보면, String literal을 생성한 형태가 String객체를 생성한 형태보다 훨씬 빠른걸 알 수 있어.

 

* String Literal을 사용하는 형태? 이 표현보다는 상수를 표현하듯 이라는 표현이 더 적절할 수 있을 것 같아. 원래 constant expression의 형태를 지닌 string 객체 할당과 관련된 내용이니 말이야. 시간되면, JLS-language spec이나 VM spec을 보고 constant expression non-constant expression에 대한 약간의 지식도 알아두면 좋을 듯....

 

public class StringTest1 {

public static void main(String[] args){

          // create String literals

          long startTime = System.currentTimeMillis();

          for(int i=0;i<50000;i++){

          String s1 = "hello";

          String s2 = "hello";

          }

          long endTime = System.currentTimeMillis();

          System.out.println("Time taken for creation of String literals : "

                                          + (endTime - startTime) + " milli seconds" );

 

          // create String objects using 'new' keyword       

          long startTime1 = System.currentTimeMillis();

          for(int i=0;i<50000;i++){

          String s3 = new String("hello");

          String s4 = new String("hello");

          }

          long endTime1 = System.currentTimeMillis();

          System.out.println("Time taken for creation of String objects : "

                                          + (endTime1 - startTime1)+" milli seconds");

     }

}

 

두 타입의 생성이 왜 처리 속도 면에서도 차이를 보이냐 하면, s1 s2는 같은 객체에 대한 참조인 반면, s3,s4는 같은 객체에 대한 참조가 아니라서 그래. 무슨 얘기일까?

 

 

JVMString을 어떻게 처리할까요?

JVM은 String을 어떻게 처리할까요?

 

JVM heap 메모리 공간 안에 문자열 객체가 중복되지 않도록 하기 위해서 일정기간 관리되는 문자열’(interned Strings)들에 대한 레퍼런스 리스트를 가지고 유지하게 되거든? 이걸 string literal pool, pool of unique string 뭐 이런 등등 해서 표현하지.

 

* Interned Strings : 의미상 일정 기간/프로세스 동안은 유지되어 관리되어야 하는 문자열의 의미를 갖기 때문에 이렇게 표현한 거야. 앞으로는 interned string 혹은 intern이라고만 설명할 것임

 

JVM이 클래스 파일을 로드(load) 한 후에 string literal을 발견하게 되면, JVM은 그 string literal이 내부 리스트(앞에서 말한 문자열에 대한 레퍼런스 리스트)에 있는 것인지 아닌지를 검사하게 되는데, 있을 경우에는 새로운 문자열(String) 객체를 생성하지 않고, 이미 존재하는 문자열 객체를 그대로 사용하게 되는 거야.

 

아까 본 예제 코드를 보며 설명하자면,

string literal을 사용하는 형태(첫 번째)에서 hello가 이미 만들어져 있으니까(s1), s2에서는 새로운 문자열 객체를 생성하는 대신 이미 있는 객체를 참조하게 된다는 거지

 

근데, 이런 체크 과정이 string literal을 이용한 형태에서만 이루어지지 new 키워드를 이용한 문자열 생성에서는 이루어 지지 않는 다는 거야. 그러니 앞의 결과가 나왔겠지?

그럼, new 키워드를 이용해서 처리한 문자열 생성은 이런 체크 과정을 절대로 할 수 없는 것이냐? 그건 아니야. new 키워드를 사용해서 생성하였다 하더라도 앞의 체크 과정을 요청하고 싶을 때는 intern() 메소드를 이용하면 돼.

 

new 키워드를 이용해서 생성하더라도 이미 heap 안에 그와 동일한 string객체가 있는 걸 피하고 싶다면, 그래서 성능을 개선하고 싶다면 intern() 메소드를 사용해 주면 된다는 얘기지.

 

옆의 그림의 경우엔 intern()을 사용하지 않은 경우의 문자열 생성을 나타낸거야. s1이나 s2 string literal을 이용한 생성이고, s3, s4 new 연산자를 이용해서 생성한 모습이니 보면서 차이점을 생각해 봐

 

 

 

연산자 == 이거와 equals() 메소드가 어떻게 다른 동작을 하는 지 기억하나?

== 연산자는 참조값(reference)이 동일한 객체를 가리키고 있으면 true return하고(하지만, 동일한 값을 가지고 있는지는 확인하지 않지), String.equals() 메소드는 문자열 객체의 내용이 동일하면 true return하잖아?

 

위에 있는 그림을 보면서 생각해 볼까?  s1==s2 true가 되고, s3.equals(s4) true가 될 것 같은데, 맞아? 그럼 s3==s4?

 

여기서 보면, 각각의 세 객체가 동일한 내용, hello를 가지고 있잖아? 메모리도 더 차지하게 되고 실행하는데 시간도 많이 걸리는 데 이렇게 해야만 하나?(뭔가 심오한 숨은 뜻이 있지 않고서는 말야)

 

좀 더 나은 성능을 위해서, 어떻게 하면 문자열 객체가 중복되지 않게 할 수 있을지 다음 나오는 내용들을 쭈~욱 읽어가면서 생각해 보자구~

 

 

 

String.intern()을 이용해서 최적화하기

String.intern()을 이용해서 최적화하기

 

문자열 객체가 불필요하게 중복될 것 같으면, String.intern() 메소드를 이용해서 이것을 피할 수가 있다고 방금 봤어. 아래 그림을 보면, intern()메소드가 실제 어떻게 동작하는 지 짐작할 수 있을거야. 앞 그림하고도 비교해보고

 

 

String.intern()메소드는 동일한 객체가 있는지 체크하고 있다면 새로운 문자열 객체를 생성하지 않고 이미 존재하는 객체로 포인터를 옮겨버리거든.

 

 

public class StringTest2 {

public static void main(String[] args){
          // create String references like s1,s2,s3...so on..
          String variables[] = new String[50000];
          for( int i=0;i<variables.length;i++){
                    variables[i] = "s"+i;
          }           // create String literals
          long startTime0 = System.currentTimeMillis();
          for(int i=0;i<variables.length;i++){
          variables[i] = "hello";
          }
         
          long endTime0 = System.currentTimeMillis();
          System.out.println("Time taken for creation of String literals : "
                                                       + (endTime0 - startTime0) + " milli seconds" );
        
          // create String objects using 'new' keyword       
          long startTime1 = System.currentTimeMillis();

          for(int i=0;i<variables.length;i++){
          variables[i] = new String("hello");
          }
 
          long endTime1 = System.currentTimeMillis();
          System.out.println("Time taken for creation of String objects with 'new' key word : "
                                                      + (endTime1 - startTime1)+" milli seconds");
 
          // intern String objects with intern() method         
          long startTime2 = System.currentTimeMillis();
          for(int i=0;i<variables.length;i++){
          variables[i] = new String("hello");
          variables[i] = variables[i].intern();
                     
          }
 
          long endTime2 = System.currentTimeMillis();
          System.out.println("Time taken for creation of String objects with intern(): "
                                                      + (endTime2 - startTime2)+" milli seconds");
  }

}

 

 

 

 

문자열을 연결시킬 때 최적화 할 수 있는 기술들

문자열을 연결시킬 때 최적화 할 수 있는 기술들 

 

 

연산자 + 를 이용해서 문자열을 연결하기도 하고 String.concat()이나 StringBuffer.append()를 이용하자나? 어떤게 성능 면에서 제일 나을까?

 

첫 번째로, 우선 다음 코드를 볼까?

 

public class StringTest3 {

    public static void main(String[] args){

 

        //Test the String Concatination

        long startTime = System.currentTimeMillis();

        for(int i=0;i<5000;i++){

        String result = "This is"+ "testing the"+ "difference"+ "between"+

                                "String"+ "and"+ "StringBuffer";

        }

        long endTime = System.currentTimeMillis();

        System.out.println("Time taken for string concatenation using + operator : "

                                          + (endTime - startTime)+ " milli seconds");

 

        //Test the StringBuffer Concatination

        long startTime1 = System.currentTimeMillis();

        for(int i=0;i<5000;i++){

            StringBuffer result = new StringBuffer();

            result.append("This is");

            result.append("testing the");

            result.append("difference");

            result.append("between");

            result.append("String");

            result.append("and");

            result.append("StringBuffer");

        }

        long endTime1 = System.currentTimeMillis();

        System.out.println("Time taken for String concatenation using StringBuffer : "

                                             + (endTime1 - startTime1)+ " milli seconds");

    }

 

}

 

보통, 프로그래머들은 StringBuffer.append()를 사용하는 게 (+)연산자를 사용하거나 String.concat()을 사용하는 것보다 빠를 것이라고 생각하지만, 어떤 경우에선 그게 아닐 수가 있지. 위에 선언된 클래스에서의 경우가 바로 그건데, 연산자 (+)를 쓰는게 StringBuffer.append()보다 빠르거던?

 

왜 그럴까?

아까 잠깐 constant expression non-constant expression에 대해 언급했었지? 그것과도 연관이 있는데 말야.

 

* constant expression non-constant expression : constant expression이 뭐냐면... 컴파일시에 결정되어질 수 있는 expression을 말하는 거야. 컴파일 시에 그 값이 결정되어 질 수 없다면 non-constant expression이 되겠지

 

여기서는 컴파일러가 중요한 역할을 해. 컴파일러가 컴파일 하면서(compile time) 문자열들을 연결시켜 버리거던.

컴파일 시에 문자열이 결정되어 진다는 얘기지.

 

컴파일 전에

 

String result = "This is"+ "testing the"+ "difference"+ "between"+ "String"+ "and"+ "StringBuffer";

 

모양이던게, 컴파일을 하고 나면,

 

String result = "This is testing the difference between String and StringBuffer";

 

이런 값이 된다는 거야.

 

StringBuffer 객체가 실행 시에 분석/결정되는 데 반해서, String 객체는 컴파일 시에 분석/결정된다는 얘기이기도 하고, ‘컴파일시 분석/결정이 문자열의 값을 미리 알 때 이루어지는 데 반해, ‘실행시 분석/결정은 문자열의 값을 미리 알고 있지 않을 때 이루어진다는 얘기도 되는 거고..... 예로,

 

public String getString(String str1,String str2) {

            return str1+str2;

}

이런 코드가 있다고 봐. 이걸 컴파일하고 나면,

 

return new StringBuffer().append(str1).append(str2).toString();

 

이렇게 되거든. 이건 실행시(runtime)에 결정되겠지? 시간도 더 걸릴테고

 

 

말하자면, 실행시켜보지 않아도 그 값을 알 수 있을 경우엔 컴파일 시에 그 값을 결정해 버린다는 거야. 그럴 경우엔 오히려 연산자(+)를 써서 문자열을 조합하는 게 StringBuffer.append()를 이용하는 것보다도 빠를 수 있다는 거고.

 

 

하지만, 실행시(runtime)에 모든 게 결정되는 구조라면, String을 이용하는 것보다는 StringBuffer를 사용하는 게 낫지. 바로 전 코드에서 String을 사용한 예가 컴파일시에 결정되도록 한 것이라면 다음 예제는 String, StringBuffer 모두 실행시 결정되도록 한 예야.

 

public class StringTest4 {

     public static void main(String[] args){

 

          //Test the String Concatenation using + operator

          long startTime = System.currentTimeMillis();

          String result = "hello";

          for(int i=0;i<1500;i++){

               result += "hello";

          }

 

          long endTime = System.currentTimeMillis();

          System.out.println("Time taken for string concatenation using + operator : "

                                          + (endTime - startTime)+ " milli seconds");

 

          //Test the String Concatenation using StringBuffer

          long startTime1 = System.currentTimeMillis();

          StringBuffer result1 = new StringBuffer("hello");

          for(int i=0;i<1500;i++){

               result1.append("hello");

          }

          long endTime1 = System.currentTimeMillis();

          System.out.println("Time taken for string concatenation using StringBuffer :  "

                                          + (endTime1 - startTime1)+ " milli seconds");

     }

}

 

 

이번엔 둘 다 실행시(runtime)에 값이 결정되는 구조지? 그래서, StringBuffer.append()를 이용한 게 훨씬 빠른 결과가 나온거야.

 

 

StringBuffer를 사용할 때엔 반드시 초기화 해서 적절한 크기로 사용합시다.

 

StringBuffer 얘기가 나온김에

 

StringBuffer는 기본적으로 16-character 크기에서부터 시작해서 모자라게 되면 두배로 늘어나는 구조야. 크기를 초기화 하지 않고 사용하다 모자라면 (2*old_size +2) 이런 식으로 증가하거던....

 

16 -> 34 -> 70 -> 142 -> 286 이렇게 늘어나겠네?

 

그래서, 반드시 적절한 양으로 초기화해서 쓰자. 낭비하지 말고^^


[영문원본] 출처: http://www.precisejava.com/javaperf/j2se/StringAndStringBuffer.htm