👩🏻‍💻 Programming/Java

Java9 부터는 String이 StringBuilder보다 빠를 수 있다

한국의 메타몽 2024. 6. 12. 17:06

개요

  1. 작성 계기
  2. Java 9 이전 : String < StringBuilder
  3. Java 9 이후 : String > StringBuilder
  4. 진짜일까?
  5. 결론

 

0. 작성 계기

회사에서 작업 도중 위와 같은 코드 리뷰가 들어왔다.
개인적으로 나는 Java랑 그렇게 친하다고 생각하진 않는다.
코딩 테스트 언어도 C++로 선택했었고, 회사 외부에서 하는 프로젝트는 Kotlin 또는 Dart를 사용한다.
Java는 현재 회사 업무랑 병행하면서 조금이라도 익숙해지고자 알고리즘 문제를 풀때 사용하는데, 애정이 없다보니 무언가 궁금한 키워드가 있을때는 수박 겉핥기 식으로 대충 이해하고 넘기는것 같다.

이 기회에 모르는 지식이 있으면 제대로 짚고 넘어가자는 생각이 들었다.

 

1. Java 9 이전 : StringStringBuilder

Java로 알고리즘 문제를 풀때 대부분의 사람들은 문자열 관련 로직, 특히 문자열의 변경이 필요되는 로직에서는 StringBuilder를 사용한다.
예전에 정리했던 Github 자료 (Click) 에서도 String은 문자열의 변경이 생길 경우, Heap 영역에 새로운 객체가 생성되어 재할당되는 것으로 언급했었다.

때문에 final이 아닌 mutable(가변)의 특징이 있는 StringBuilder로 문자열 변수를 선언하는 것이 권장됐다.

여기서 Synchronized가 적용되는, 이를테면 멀티 스레드 환경에서 Thread-safe한 동작을 원할 경우 StringBuffer를 사용하는 것이 권장됐던걸로 알고있다.

조금더 공부를 해보니, Java 9이후 버전부터는 String 객체에 변경이 생겼다는 것을 알 수 있어다.

 

2. Java 9 이후 : StringStringBuilder

String 클래스의 문자열 연결 연산이 StringBuilder로 사용되었던 이전과는 달리, Java 9부터는 StringConcatFactory 클래스의 makeConcatWithConstants를 사용하는데, runtime 시점에 바이트 코드를 생성하기 때문에 StringBuilder를 사용하는 것 보다 오버헤드가 적다고 한다.

This class provides two forms of linkage methods: a simple version (makeConcat(java.lang.invoke.MethodHandles.Lookup, String, MethodType)) using only the dynamic arguments, and an advanced version (makeConcatWithConstants(java.lang.invoke.MethodHandles.Lookup, String, MethodType, String, Object...) using the advanced forms of capturing the constant arguments. The advanced strategy can produce marginally better invocation bytecode, at the expense of exploding the number of shapes of string concatenation methods present at runtime, because those shapes would include constant static arguments as well.

 

 

3. 진짜일까?

글로만 보니 딱히 와닿지가 않았다.
그래서 아래와 같이 테스트 코드를 돌려봤다.

 

(1) 계속 해서 문자열을 더하기

  @Test
  void testStringPerf1() {

    long start = System.currentTimeMillis();
    StringBuilder builder = new StringBuilder("test");

    for (int i = 0; i < 10000; i++) {
      builder.append("  StringBuilder");
    }
    long end = System.currentTimeMillis();

    System.out.println("## 실행 시간 StringBuilder : " + (end - start));

    String builder2 = "test";
    long start2 = System.currentTimeMillis();

    for (int i = 0; i < 10000; i++) {
      builder2 += "  StringBuilder";
    }
    long end2 = System.currentTimeMillis();

    System.out.println("## 실행 시간 String : " + (end2 - start2));
  }
## 실행 시간 StringBuilder : 0
## 실행 시간 String : 84

 

(2) 한 번만 문자열을 더하기

  @Test
  void testStringPerf2() {
    long start = System.currentTimeMillis();
    for (int i = 0; i < 100_000_000; i++) {
      StringBuilder builder = new StringBuilder("test");
      builder.append("  StringBuilder");
    }
    long end = System.currentTimeMillis();

    System.out.println("## 실행 시간 StringBuilder : " + (end - start));

    long start2 = System.currentTimeMillis();
    for (int i = 0; i < 100_000_000; i++) {
      String builder2 = "test";
      builder2 += "  StringBuilder";
    }
    long end2 = System.currentTimeMillis();

    System.out.println("## 실행 시간 String : " + (end2 - start2));
  }
## 실행 시간 StringBuilder : 1090
## 실행 시간 String : 101

 

결론

  • 문자열을 한 번만 더할때 : String > StringBuilder
  • 문자열을 여러번 더할때 : String < StringBuilder