String 클래스 - 불변 객체

String은 불변 객체이다. 따라서 생성 이후에 절대로 내부의 문자열 값을 변경할 수 없다.

 

public class StringImmutable1 {
    public static void main(String[] args) {
        String str = "hello";
        str.concat(" java");
        System.out.println("str = " + str);
    }
}

실행 결과

  • String.concat() 메서드를 사용하면 기존 문자열에 새로운 문자열을 연결해서 합칠 수 있다.
  • 그러나 실행 결과를 보면 문자가 전혀 합쳐지지 않았다.

 

public class StringImmutable2 {
    public static void main(String[] args) {
        String str1 = "hello";
        String str2 = str1.concat(" java");
        System.out.println("str1 = " + str1);
        System.out.println("str2 = " + str2);
    }
}

실행 결과

String은 불변 객체이다. 따라서 변경이 필요한 경우 기존 값을 변경하지 않고, 대신에 새로운 결과를 만들어서 반환한다.

 

String이 불변으로 설계된 이유

  • String str3 = "hello"와 같이 문자열 리터럴을 사용하는 경우 자바는 메모리 효율성과 성능 최적화를 위해 문자열 풀을 사용한다.
  • 자바가 실행되는 시점에 클래스에 문자열 리터럴이 있으면 문자열 풀에 String 인스턴스를 미리 만들어둔다. 이때 같은 문자열이 있으면 만들지 않는다.
  • String str3 = "hello"와 같이 문자열 리터럴을 사용하면 문자열 풀에서 "hello"라는 문자를 가진 String 인스턴스를 찾는다. 그리고 찾은 인스턴스의 참조(x003)를 반환한다.
  • String str4 = "hello"의 경우 "hello" 문자열 리터럴을 사용하므로 문자열 풀에서 str3와 같은 x003 참조를 사용한다.
  • 문자열 풀 덕분에 같은 문자를 사용하는 경우 메모리 사용을 줄이고 문자를 만드는 시간도 줄어들기 때문에 성능도 최적화할 수 있다.
  • 그런데 만약 String 내부의 값을 변경할 수 있다면, 기존에 문자열 풀에서 같은 문자를 참조하는 변수의 모든 문자가 함께 변경되어 버리는 문제가 발생한다. 다음의 경우 str3이 참조하는 문자를 변경하면 str4의 문자도 함께 변경되는 사이드 이펙트 문제가 발생한다.
    • String str3 = "hello"
    • String str4 = "hello"
  • String 클래스는 불변으로 설계되어서 이런 사이드 이펙트 문제가 발생하지 않는다.

StringBuilder - 가변 String

불변인 String 클래스의 단점

String str = "A" + "B" + "C" + "D";
String str = String("A") + String("B") + String("C") + String("D"); 
String str = new String("AB") + String("C") + String("D");
String str = new String("ABC") + String("D");
String str = new String("ABCD");

 

불변인 String 클래스의 단점은 문자를 더하거나 변경할 때마다 계속해서 새로운 객체가 생성된다는 점이다. 문자를 자주 더하거나 변경해야 하는 상황이라면 더 많은 String 객체를 만들고, GC해야 한다. 결과적으로 컴퓨터의 CPU, 메모리를 더 많이 사용하게 된다. 그리고 문자열의 크기가 클수록, 문자열을 더 자주 변경할수록 시스템의 자원을 더 많이 소모한다.

 

StringBuilder

이런 문제를 해결하기 위해서는 가변 String을 사용하면 된다. 가변은 내부의 값을 바로 변경하면 되기 때문에 새로운 객체를 생성할 필요가 없다. 따라서 성능과 메모리 사용면에서 불변보다 더 효율적이다.

자바는 StringBuilder라는 가변 String을 제공한다. 물론 가변의 경우 사이드 이펙트에 주의해서 사용해야 한다.

 

StringBuilder 사용 예

public class StringBuilderMain1_1 {
    public static void main(String[] args) {
        StringBuilder sb = new StringBuilder();
        sb.append("A");
        sb.append("B");
        sb.append("C");
        sb.append("D");
        System.out.println("sb = " + sb);
        
        sb.insert(4, "Java");
        System.out.println("insert = " + sb);

        //delete(인덱스 시작점, 인덱스 끝점 + 1)
        sb.delete(4, 8); //4 ~ 7 삭제
        System.out.println("delete = " + sb);

        sb.reverse();
        System.out.println("reverse = " + sb);
        
        //StringBuilder -> String
        String string = sb.toString();
        System.out.println("string = " + string);
    }
}

실행 결과

  • StringBuilder 객체를 생성한다.
  • append() 메서드를 사용해 여러 문자열을 추가한다.
  • insert() 메서드로 특정 위치에 문자열을 삽입한다.
  • delete() 메서드로 특정 범위의 문자열을 삭제한다. delete(index 시작점, index 끝점 + 1)
  • reverse() 메서드로 문자열을 뒤집는다.
  • 마지막으로 toString 메서드를 사용해 StringBuilder의 결과를 기반으로 String을 생성해서 반환한다.

 

가변(Mutable) vs 불변(Immutable)
  • String은 불변하다. 즉, 한번 생성되면 그 내용을 변경할 수 없다. 따라서 문자열에 변화를 주려고 할 때마다 새로운 String 객체가 생성되고, 기존 객체는 버려진다. 이 과정에서 메모리와 처리 시간을 더 많이 소모한다.
  • 반면에, StringBuilder는 가변적이다. 하나의 StringBuilder 객체 안에서 문자열을 추가, 삭제, 수정할 수 있으며, 이때마다 새로운 객체를 생성하지 않는다. 이로 인해 메모리 사용을 줄이고 성능을 향상시킬 수 있다. 단, 사이드 이펙트를 주의해야 한다.

StringBuilder는 보통 문자열을 변경하는 동안만 사용하다가 문자열 변경이 끝나면 안전한(불변) String으로 변환하는 것이 좋다.

 


StringBuilder와 메서드 체인(Chain)

StringBuilder는 메서드 체이닝 기법을 제공한다.

 

StringBuilder의 append() 메서드를 보면 자기 자신의 참조값을 반환한다.

public StringBuilder append(String str) {
    super.append(str);
    return this;
}

 

메서드 호출의 결과로 자기 자신의 참조값을 반환하면, 반환된 참조값을 사용해서 메서드 호출을 계속 이어갈 수 있다. 참조값에 . 을 찍고 메서드를 계속 연결해서 사용하면 된다.

 

앞서 StringBuilder를 사용한 코드를 메서드 체이닝 기법을 적용해서 개선할 수 있다.

 

[메서드 체이닝 기법 적용 X]

public class StringBuilderMain1_1 {
    public static void main(String[] args) {
        StringBuilder sb = new StringBuilder();
        sb.append("A");
        sb.append("B");
        sb.append("C");
        sb.append("D");
        System.out.println("sb = " + sb);
        
        sb.insert(4, "Java");
        System.out.println("insert = " + sb);

        //delete(인덱스 시작점, 인덱스 끝점 + 1)
        sb.delete(4, 8); //4 ~ 7 삭제
        System.out.println("delete = " + sb);

        sb.reverse();
        System.out.println("reverse = " + sb);
        
        //StringBuilder -> String
        String string = sb.toString();
        System.out.println("string = " + string);
    }
}

실행 결과

 

[메서드 체이닝 기법 적용 O]

public class StringBuilderMain1_2 {
    public static void main(String[] args) {
        StringBuilder sb = new StringBuilder();
        String string = sb.append("A").append("B").append("C").append("D")
                        .insert(4,"Java")
                        .delete(4, 8)
                        .reverse()
                        .toString();

        System.out.println("string = " + string);
    }
}

실행 결과

 

+ Recent posts