들어가며

String의 불변성 - StringBuilder와 StringBuffer의 차이점을 다뤘습니다. 이번에는 String + 연산에 대해서 조금 더 자세하게 다뤄보고도록 하겠습니다. 이전 글을 보고 오시는것을 추천드립니다.

 

 

[JAVA] StringBuilder, StringBuffer 의 차이점과 주의사항

들어가며이 글에서는 StringBuilder, StringBuffer 차이점뿐만 아니라 사용 이유에 대해서도 다루고있습니다. [JAVA] String의 불변성(immutability)들어가며JAVA를 처음 접할 때, String은 불변객체라고 배우고

tmd8633.tistory.com

 

 

 

이 글을 쓰는 이유

지난시간에 String 의 불변성과 StringBuilder에 대해서 알아보았습니다. 이전 글들을 작성하면서 String + 연산이 최적화 되었다는 얘기를 수 없이 보았습니다.

 

        String[] arr = {"2", "3", "4"};
        String str = "1";

        for (String s : arr) {
            str += s;
        }

이렇게 String + 연산을 하게되면 String의 불변성 때문에 "12", "123", "1234"의 메모리가 heap 영역에 저장되고 GC 대상도 증가하게 되는 것은 이제 충분히 이해가 됩니다.

그런데 이 과정을 JDK 5 부터 컴파일 시에 내부적으로 StringBuilder를 사용해서 최적화를 하였고, 완전히 최적화가 되지않아서(매번 StringBuilder객체가 생성되는 문제 등) JDK 9부터 지금까지 StringConcatFactory가 사용되어 + 연산이 완전 최적화 되었다고 했습니다. 이를 실제로 알아보기 위해 bytecode를 까봤습니다.

 

 

public static main([Ljava/lang/String;)V
   L0
    LINENUMBER 13 L0
    ICONST_3
    ANEWARRAY java/lang/String
    DUP
    ICONST_0
    LDC "2"
    AASTORE
    DUP
    ICONST_1
    LDC "3"
    AASTORE
    DUP
    ICONST_2
    LDC "4"
    AASTORE
    ASTORE 1
   L1
    LINENUMBER 14 L1
    LDC "1"
    ASTORE 2
   L2
    LINENUMBER 16 L2
    ALOAD 1
    ASTORE 3
    ALOAD 3
    ARRAYLENGTH
    ISTORE 4
    ICONST_0
    ISTORE 5
   L3
   FRAME FULL [[Ljava/lang/String; [Ljava/lang/String; java/lang/String [Ljava/lang/String; I I] []
    ILOAD 5
    ILOAD 4
    IF_ICMPGE L4
    ALOAD 3
    ILOAD 5
    AALOAD
    ASTORE 6
   L5
    LINENUMBER 17 L5
    ALOAD 2
    ALOAD 6
    INVOKEDYNAMIC makeConcatWithConstants(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String; [
      // handle kind 0x6 : INVOKESTATIC
      java/lang/invoke/StringConcatFactory.makeConcatWithConstants(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/String;[Ljava/lang/Object;)Ljava/lang/invoke/CallSite;
      // arguments:
      "\u0001\u0001"
    ]
    ASTORE 2
   L6
    LINENUMBER 16 L6
    IINC 5 1
    GOTO L3
   L4
    LINENUMBER 19 L4
   FRAME CHOP 3
    RETURN
   L7
    LOCALVARIABLE s Ljava/lang/String; L5 L6 6
    LOCALVARIABLE args [Ljava/lang/String; L0 L7 0
    LOCALVARIABLE arr [Ljava/lang/String; L1 L7 1
    LOCALVARIABLE str Ljava/lang/String; L2 L7 2
    MAXSTACK = 4
    MAXLOCALS = 7
}

 

StringConcatFactory.makeConcatWithConstants 메소드가 실제로 String + 연산에 사용되고 있었습니다.

 

그럼 직접 StringBuilder를 사용하지 않고 String + 연산을 해도 최적화가 되는건가? 라는 생각에 테스트를 진행했습니다.

속도테스트는 100배가량 +연산이 느렸고, 메모리도 낭비되는 것을 확인할 수 있었습니다.

 

그럼 무슨 최적화가 되었다는걸까요? 그 결과를 글로 적고싶어서 이렇게 남깁니다.

 

 

Concat

String의 concat() 메소드를 사용해보셨나요? 앞문자열과 뒷문자열을 합쳐주는 기능을 수행하는 메소드입니다.

StringConcatFactory는 그 concat 메소드처럼 여러 문자열을 하나로 합쳐주는 역할을 수행하는 것이었습니다.

 

String str1 = "1";
String str2 = str1 + "2" + "3" + "4" + "5" + "6" + "7" + "8" + "9";

 

JDK5 이전에는 이런 String + 연산을 하면 "12", "123", "1234" ... 이런 데이터가 저장이 되었지만, StringConcatFactory는 이런 연속적인 문자열 합치는 것을 한번에 수행하면서 최적화가 되는 것입니다.

 

   L0
    LINENUMBER 8 L0
    LDC "1"
    ASTORE 1
   L1
    LINENUMBER 9 L1
    ALOAD 1
    INVOKEDYNAMIC makeConcatWithConstants(Ljava/lang/String;)Ljava/lang/String; [
      // handle kind 0x6 : INVOKESTATIC
      java/lang/invoke/StringConcatFactory.makeConcatWithConstants(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/String;[Ljava/lang/Object;)Ljava/lang/invoke/CallSite;
      // arguments:
      "\u000123456789"
    ]
    ASTORE 2

저는 str1 + "2" + "3' ... + "9" 까지 각각 문자열을 더했지만 이를 StringConcatFactory 에서 "23456789" 문자열로 하나로 합쳐 불필요한 메모리 낭비를 최적화 한겁니다. 그러니까 배열에서 하니씩 꺼내며 + 연산할때에는 최적화가 안되었던것이지요.

 

그러니까 String + 연산 은 절대하지말고 StringBuilder, StringBuffer나 쓰십쇼

들어가며

이 글에서는 StringBuilder, StringBuffer 차이점뿐만 아니라 사용 이유에 대해서도 다루고있습니다.

 

[JAVA] String의 불변성(immutability)

들어가며JAVA를 처음 접할 때, String은 불변객체라고 배우고 지나갔습니다. 하지만 실제 String을 다루다보면 어째서 불변객체인지 의문이 들곤합니다.  String str = "apple"; str = "banana";왜냐하면, 재할

tmd8633.tistory.com

 

이전 글에서 String의 불변성에 대해서 읽어보시는걸 추천드립니다.

 

 

불변성

String 은 불변성입니다.

StringBuffer, StringBuilder 는 가변성입니다.

 

        String str = "1";
        System.out.println("hashCode = " + str.hashCode());
        System.out.println("====================================");
        String[] arr = {"2", "3", "4"};
        for (String s : arr) {
            str += s;
            System.out.println("hashCode = " + str.hashCode());
        }
//        hashCode = 49
//        ====================================
//        hashCode = 1569
//        hashCode = 48690
//        hashCode = 1509442

 

String은 불변객체이기때문에 String Constant Pool에서 str += s 될때마다 불필요한 새로운 데이터를 heap 영역에 저장합니다.

System.out.println("12".hashCode()); // 1569

 

실제로 중간과정에서 만들어진 String의 hashCode를 가져와보면 Pool에서 불필요한 데이터가 저장된걸 볼 수 있습니다.

 

 

여기에서 StringBuffer or StringBuilder를 사용하면 어떻게 될까요?

        StringBuilder sb = new StringBuilder("1");
        System.out.println("hashCode = " + sb.hashCode());
        System.out.println("====================================");
        String[] arr = {"2", "3", "4"};
        for (String s : arr) {
            sb.append(s);
            System.out.println("hashCode = " + sb.hashCode());
        }
//        hashCode = 918221580
//        ====================================
//        hashCode = 918221580
//        hashCode = 918221580
//        hashCode = 918221580

가변객체이기때문에 hashCode가 변하지 않은것을 확인할 수 있습니다.

 

String의 불변특성때문에 문자열 더하기의 연산은 불필요한 데이터를 저장하게되며, hashCode의 캐싱에도 불필요한 데이터가 저장된다는 문제가 있습니다. 따라서 String의 문자열 더하기는 StringBuilder 나 StringBuffer를 사용하도록 해야합니다.

 

그러면 StringBuilder와 StringBuffer는 무슨 차이가 있을까요?

 

 

StringBuilder vs StringBuffer

 

Builder와 Buffer는 같은 추상클래스를 상속받고 있고, 그 구조가 완벽하게 동일합니다. 그 차이점은 구현체에서 찾을 수 있습니다.

 

StringBuffer는 동기화를 지원하고 (Thread-Safe),

StringBuilder는 동기화를 지원하지 않습니다.

 

 

 

StringBuffer 대부분의 메소드에는 synchronized 키워드가 붙어있습니다.

 

 

 

언제 어떻게 사용하면 될까?

 

StringBuffer

동기화를 지원하기때문에 멀티스레드 환경에서 안전하게 동작합니다. 동시성 문제를 고려해야한다면 StringBuffer를 사용해야합니다.

 

StringBuilder

StringBuilder는 StringBuffer 보다 일반적으로 성능상 빠릅니다. 동기화 처리가 없기 때문입니다. 따라서 단일스레드 환경에서는 불필요한 동기화 처리를 하지 않는 StringBuilder를 사용해야합니다.

 

 

 

사용 시 주의점

 

1. capacity

StringBuilder, StringBuffer 둘다 용량증가정책은 동일하게 설정되어있습니다. 이 capacity는 문자열의 수용량입니다. 기본 capacity는 16입니다.

 

 

 

Collection List를 최적화해보자

Collection List Collection중에 가장 많이 사용하는 것은 아마도 List일것입니다. List에는 여러 구현체가 있습니다. ArrayList, LinkedList, Vector 가 있는데 오늘 알아볼것은 ArrayList와 LinkedList입니다. 최적화

tmd8633.tistory.com

이전 글에서 List의 최적화를 다뤘는데 List의 initialCapacity와 같은 개념입니다.

 

 

append 메소드를 사용할때, capacity가 가득차면 추상클래스에서 ensureCapacityInternal 메소드를 통해 capacity를 2배증가시킵니다.

 

이 과정에서 StringBuffer는 동기화 처리과정에서 오버헤드가 발생할 수 있습니다. (락 획득과 해제, 락에 의한 대기시간 증가 등)

 

capacity는 생성자에서 사용자가 조절할 수 있습니다.

 

 

 

2. null 처리

        String str = null;

        StringBuilder sb = new StringBuilder("1");
        sb.append(str);
        System.out.println("sb = " + sb); // sb = 1null

        StringBuffer sb2 = new StringBuffer("1");
        sb2.append(str);
        System.out.println("sb2 = " + sb2); // sb2 = 1null

        String a = "1";
        a += str;
        System.out.println("a = " + a); // a = 1null

 

StringBuilder, StringBuffer 에 null이 append 되었을 때 따로 처리해주지 않습니다. null 이 들어오면 "null" 문자열로 인식되어 append 됩니다. 사용하기 전에 null 처리를 반드시 해야됩니다.

+ Recent posts