백준 알고리즘 문제 [백준] 10989번 : 수 정렬하기 3를 풀다가 Java의 String
, StringBuffer
, StringBuilder
에 대해 정리하게 되었다.
우선 개념 정리에 앞서서 백준 문제에 대해 작성한 첫 코드는 아래와 같았다.
import java.io.*;
import java.util.Arrays;
public class Main {
public static void main(String[] args) throws IOException {
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
int N = Integer.parseInt(br.readLine());
int arr[] = new int[N];
for (int i = 0; i < N; i++) {
arr[i] = Integer.parseInt(br.readLine());
}
int[] sorted = Arrays.stream(arr).sorted().toArray();
for (int a : sorted) {
System.out.println(a);
}
}
}
로컬에서 테스트한 결과로는 문제 없었지만, 시간초과로 실패했다. 그러다가 검색을 통해 다른 사람의 코드([백준] 10989번 : 수 정렬하기 3 – JAVA [자바])로 실행해봤다.
import java.io.*;
import java.util.Arrays;
public class Main {
public static void main(String[] args) throws IOException {
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
int N = Integer.parseInt(br.readLine());
int arr[] = new int[N];
for (int i = 0; i < N; i++) {
arr[i] = Integer.parseInt(br.readLine());
}
Arrays.sort(arr);
StringBuilder sb = new StringBuilder();
for (int i = 0; i < N; i++) {
sb.append(arr[i] + "\n");
}
System.out.print(sb);
}
}
이 코드로는 시간초과 없이 통과되었다. 여기서 궁금해지는 부분은 두 가지였다.
Arrays.sort()
와 Arrays.stream().sorted.toArray()
의 차이StringBuilder
는 무엇인가첫 번째는 Arrays
객체에 대한 이해가 필요할 것 같다. 그러나 재밌는 건, Arrays.sort()
를 하건 Arrays.stream()
을 하건 관계 없이, arr
객체를 그대로 println()
했을 때에는 시간초과였지만 StringBuilder
를 통해 append()
했을 때에는 통과되었다는 것이다. 그래서 지금은 두 번째 StringBuilder
가 무엇인지에 대해 알아보려고 한다.
그러다가 검색을 통해 아래의 게시물을 확인하게 되었다.
→ [자바] String, StringBuilder, StringBuffer의 차이
위 게시물에서는 String
, StringBuilder
, StringBuffer
각각에 대해 설명하는데, 그 내용은 다음과 같다.
우선 String
클래스와 StringBuilder
, StringBuffer
클래스의 가장 기본적인 차이는 String
은 불변(Immutable)인 반면, 다른 클래스는 가변(mutable)이라는 것이다.
String
객체는 한번 생성되면 할당된 메모리 공간이 변하지 않는다. +
연산자 또는 concat
메서드를 통해 기존에 생성된 String
클래스 객체 문자열에 다른 문자열을 붙여도 기존 문자열에 새로운 문자열을 붙이는 것이 아니라, 새로운 String
객체를 만든 후, 새 String
객체에 연결된 문자열을 저장하고, 그 객체를 참조하도록 한다. 즉, String
클래스 객체는 Heap 메모리 영역(가비지 컬렉션이 동작하는 영역)에 생성되고, 한 번 생성된 객체의 내부 내용은 변경시킬 수 없으며, 기존 객체가 제거되면 Java의 가비지 컬렉션이 회수한다는 것이다.
String
객체의 이러한 특성에 의해 문자열 연산이 많은 경우에 그 성능이 좋지 않다. 하지만 불변(Immutable) 객체는 사용성이 좋고, 동기화에 대해 신경쓰지 않아도 되기 때문에(Thread-safe), 내부 데이터를 자유롭게 공유할 수 있다.
String
과는 달리, StringBuffer
, StringBuilder
는 문자열 연산 등으로 기존 객체의 공간이 부족하게 되는 경우, 기존의 버퍼 크기를 늘리며 유연하게 동작한다. 즉, 새로운 객체를 만들지 않기 때문에 복잡한 문자열 연산에서 String
보다 성능이 유리하다.
그렇다면 두 클래스의 차이는 무엇일까. 바로 동기화 여부이다.
StringBuffer
는 각 메서드별로 Synchronized Keyword가 존재하며, 멀티스레드 환경에서도 동기화를 지원
→ 멀티스레드 환경에 적합
StringBuilder
는 동기화를 보장하지 않음
→ 단일스레드 환경에 적합
이로 인해 멀티스레드 환경이라면 값 동기화 보장을 위해 StringBuffer
를 사용하고, 단일스레드 환경이라면 StringBuilder
를 사용하는 것이 좋다. 단일스레드 환경에서 StringBuffer
를 사용한다고 문제되는 것 까지는 아니지만, 동기화 관련 처리로 인해 StringBuilder
에 비해 성능이 좋지 않다.
추가로 책 <Java의 정석>에서도 해당 내용을 찾아봤다. 발췌 내용은 아래와 같다.
String
클래스는 인스턴스를 생성할 때 지정된 문자열을 변경할 수 없지만StringBuffer
클래스는 변경이 가능하다. 내부적으로 문자열 편집을 위한 버퍼(buffer)를 가지고 있으며,StringBuffer
인스턴스를 생성할 때 그 크기를 지정할 수 있다.
이 때, 편집할 문자열의 길이를 고려하여 버퍼의 길이를 충분히 잡아주는 것이 좋다. 편집 중인 문자열이 버퍼의 길이를 넘어서게 되면 버퍼의 길이를 늘려주는 작업이 추가로 수행되어야하기 때문에 작업효율이 떨어진다.
StringBuffer
클래스는String
클래스와 유사한 점이 많다. 아래의 코드에서 알 수 있듯이,StringBuffer
클래스는String
클래스와 같이 문자열을 저장하기 위한char
형 배열의 참조변수를 인스턴스 변수로 선언해 놓고 있다.StringBuffer
인스턴스가 생성될 때,char
형 배열이 생성되며 이 때 생성된char
형 배열을 인스턴스 변수value
가 참조하게 된다.public final class StringBuffer implements java.io.Serializable { private char[] value; ... }
즉, 연산이 많지 않으면 String
클래스가 유리하지만, 연산이 많은 경우에는 StringBuffer
가 유리하다는 것이다. 따라서 연산이 많은 경우에 대해 단순하게 성능 비교를 했을 때에는 StringBuilder > StringBuffer >>> String
이므로, 많은 연산이 필요했던 위 백준 문제에서는 StringBuilder
를 사용했는지 아닌지에 따라 통과 여부가 결정된 것이다.
String
: 불변 객체로, 짧은 문자열을 더하거나 연산이 많지 않은 경우 사용StringBuffer
: 스레드에 안전한 프로그램이 필요할 때나, 개발 중인 시스템의 부분이 스레드에 안전한지 모를 경우 사용StringBuilder
: 스레드에 안전한지 여부가 전혀 관계 없는 프로그램을 개발할 때 사용