[자바] String, StringBuffer, StringBuilder 차이점

skyepodium·2022년 1월 12일
0
post-thumbnail

자바에서 문자열을 만드는 방법 3가지 String, StringBuffer, StringBuilder의 차이점에 대해 알아봅시다.

0. 요약

결론 부터 말하면 다음과 같은데 하나씩 알아봅시다.

1. 문자열 생성 방식

불변, 가변 여부를 알기 위해서는 생성 방식에 대해 알아야합니다.

1) String

String 클래스로 문자열을 만드는 방법은 크게 2가지 입니다. 1) 리터럴 방식 (쌍따옴표) 2) 생성자 방식

생성자 방식은 메모리를 비효율적으로 사용하기 때문에 잘 사용하지 않습니다.

그렇기 때문에 String은 리터럴 방식을 사용한다고 가정하겠습니다.`

리터럴 방식 (쌍따옴표)

자바에서 "" 쌍따옴표 문자열 생성방법을 리터럴 방식이라고 부릅니다.

리터럴 방식은 Heap 내부의 String Constant Pool을 먼저 조회하고 hello문자열을 검색합니다. 없으면 생성하고, 만약 있다면 또 생성하지 않고 주소값을 참조합니다.

class Main {
    public static void main(String[] args) {
        // 1) 리터럴 방식
        // String constant Pool에 hello 저장
        String a = "hello";
        // 이미 저장된 hello 참조
        String b = "hello";

        System.out.println(a == b); // 주소비교 true
        System.out.println(a.equals(b)); // 값 비교 true
    }
}

생성자 방식

매번 새로운 객체를 생성해서 Heap에 저장합니다.

String Constant Pool을 통한 메모리 관리또한 진행되지 못해 비효율적입니다.

class Main {
    public static void main(String[] args) {
        // 생성자 방식
        // heap에 hello 문자열 저장
        String a = new String("hello");
        // heap에 hello 문자열 저장
        String b = new String("hello");

        System.out.println(a == b); // 주소비교 false
        System.out.println(a.equals(b)); // 값 비교 true
    }
}

2) StringBuffer, StringBuilder

StringBuffer, StringBuilder의 생성방식은 같습니다.

처음에 16길이의 연속된 메모리 영역을 할당받습니다. 만약 해당 영역을 초과하면 더 긴 연속된 메모리 영역을 할당받고, 이전 내용을 복사합니다.

2. 불변, 가변

1) 정의

불변, 가변의 의미는 다음과 같습니다.

불변이란 현재 참조하고 있는 주소의 값이 변하지 않음을 의미하고, 가변이란 현재 참조하고 있는 값이 변할 수 있음을 의미합니다.

2) String

String은 새로운 문자열을 대입하는 경우 이전 참조값을 버리고, 새로운 주소를 참조합니다.

class Main {
    public static void main(String[] args) {

        // a - "hello" 참조
        String a = "hello";
        // b - "hello" 참조
        String b = a;
        // a - 새로운 객체 "hello world" 참조 - "hello"는 더이상 참조하지 않음
        a += " world";

        // a는 "hello world" 참조
        System.out.println("a " + a);
        // b는 "hello" 참조
        System.out.println("b " + b);
    }
}

3) StringBuffer, StringBuilder

현재 주소값에서 새로운 문자열이 추가되면 연속된 메모리 영역에 추가합니다.

ArrayList 처럼 구현됩니다.

class Main {
    public static void main(String[] args) {

        // a - "hello" 참조
        StringBuffer a = new StringBuffer("hello");
        // b - "hello" 참조
        StringBuffer b = a;
        // a - "hello world" 참조, 참조하는 주소의 값이 변경된 경우
        a.append(" world");
        
        // a는 "hello world" 참조
        System.out.println("a " + a);
        // b는 "hello world" 참조
        System.out.println("b " + b);
    }
}

4) 정리

불변의 장점

  • 중복 제거를 통한 효율적인 메모리 사용
    중복된 문자열을 여러번 생성하는 경우 새로운 객체를 생성하지 않고, 참조를 통해 메모리 효율성을 높일 수 있습니다.
  • 스레드 세이프
    여러 스레드가 동시에 String 값을 참조한다고 해도 업데이트 할 수 없기 때문에 항상 동일한 값임을 보장합니다.

불변의 단점

  • 문자열이 변경이 많은 경우, 지속적인 새로운 객체 생성으로 비효율적인 메모리 사용
    만약 기존 문자열에 새로운 문자열이 계속해서 더해지는 경우 String은 이전 참조 객체를 지속적으로 버리고, 새로운 객체를 생성해서 참조합니다.

    이를 통해 쓸모없는 객체가 많이 만들어져 메모리 비효율성이 높아집니다.

class Main {
    public static void main(String[] args) {
        String a = "";
        for(int i=0; i<100000; i++) {
            // 새로운 객체 참조
            a += "";
        }
    }
}

가변의 장점

  • 문자열의 변경이 필요한 경우, 추가를 통해 메모리 효율성 높임

3. 스레드 세이프

1) 정의

스레드 세이프란 여러 스레드가 동시에 접근해도 프로그램의 동작에 문제가 없음을 의미합니다.

2) 스레드 세이프 여부

String

불변이기 때문에 어떠한 상황에서도 동일한 값임을 보장합니다.

StringBuffer

synchronized 키워드를 통해 여러 스레드가 동시에 접근하는 경우 순차적으로 한번에 하나의 스레드만 사용할 수 있도록 처리하여 스레드 세이프를 보장합니다.

StringBuilder

전혀 스레드 세이프하지 않습니다.

4. 연산 속도

0) 요약

String - 느림 - O(xn^2) - x는 이어붙이는 문자의 길이, n은 횟수
StringBuffer - 중간 - O(xn)
StringBuilder - 빠름 - O(xn)

1) String

고작 백만번 추가연산에 1156ms 소요되었습니다.

String은 +연산이 발생할때마다 매번 새로운 문자열을 복사합니다.
1 + 2 + .... n = n(n+1) 이 되고, O(xn^2)이 됩니다.

class Main {
    public static void main(String[] args) {
        long beforeTime = System.currentTimeMillis();

        String a = "";
        for(int i=0; i<1000000; i++) {
            a += "a";
        }

        long afterTime = System.currentTimeMillis();
        long secDiffTime = (afterTime - beforeTime);
        System.out.println("시간차이(ms) : "+secDiffTime);
    }
}

2) StringBuffer

1억번 추가연산에 951ms 소요되었습니다.

class Main {
    public static void main(String[] args) {
        long beforeTime = System.currentTimeMillis();

        StringBuffer sb = new StringBuffer();
        for(int i=0; i<100000000; i++) {
            sb.append("a");
        }

        long afterTime = System.currentTimeMillis();
        long secDiffTime = (afterTime - beforeTime);
        System.out.println("시간차이(ms) : "+secDiffTime);
    }
}

3) StringBuilder

1억번 추가연산에 371ms 소요되었습니다.

class Main {
    public static void main(String[] args) {
        long beforeTime = System.currentTimeMillis();

        StringBuilder sb = new StringBuilder();
        for(int i=0; i<100000000; i++) {
            sb.append("a");
        }

        long afterTime = System.currentTimeMillis();
        long secDiffTime = (afterTime - beforeTime);
        System.out.println("시간차이(ms) : "+secDiffTime);
    }
}

5. 정리

그래서 언제 무엇을 사용해야하는지가 제일 중요한것 같습니다.

String

문자열 추가 연산이 적고, 중복제거가 필요한 스레드 세이프 환경

StringBuffer

문자열 추가 연산이 많은 스레드 세이프 환경

StringBuilder

멀티스레드 환경에서 스레드 세이프를 고려하지 않아도 되는 경우는 정말 스레드 하나만 실행되는 경우, 그런 경우 거의 없지만

정말 예를 든다면 알고리즘 푸는 경우

요즘은 잘 없기도 하지만, 예전에는 문자열 입력을 직접 받아서 처리하는 경우가 많았는데,

빠른 속도를 위해 BufferedReader를 사용했고 BufferedReader는 내부적으로 문자열을 입력받을 때 StringBuilder를 사용합니다.

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;

class Main {
    public static void main(String[] args) throws IOException {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));

        // 문자열 한줄 읽기
        int n = Integer.parseInt(br.readLine());
    }
}

profile
callmeskye

0개의 댓글