java

[자바] String Class 의 불변성

하루이2222 2024. 8. 18. 19:37

자바의 String 클래스와 참조 주소 변경

1. String 클래스의 불변성(Immutable)

  • 불변성: 자바의 String 클래스는 불변 객체이다. 한 번 생성된 String 객체는 그 내용을 수정할 수 없다.
    • 예를 들어, String str = "Hello";"Hello"라는 문자열을 가리키는 str 참조 변수를 생성한다. str의 값을 변경하려면 새로운 String 객체가 만들어져야 한다.
    • 문자열을 수정할 때마다 새로운 String 객체가 생성되고, 기존의 객체는 그대로 유지된다.

2. 참조 주소의 변경

  • 참조 변경: 기존 String 객체를 수정하지 않고, 새로운 객체를 생성하여 참조 변수를 변경한다.
    • 예시:
      String str = "Hello";
      str = str + " World";
      • "Hello" 문자열 객체는 변경되지 않고, "Hello World"라는 새로운 객체가 생성되어 str이 이를 가리키도록 참조가 변경된다.

3. 배열에서의 참조 주소 변경

  • 배열의 수정 가능성: 배열 요소의 참조를 변경하는 것은 가능하지만, 배열 내의 String 객체 자체는 불변이기 때문에 수정할 수 없다.

    • 배열의 요소를 다른 String 객체로 교체할 수 있으며, 이 경우 배열의 참조 주소는 변경되지 않지만, 배열의 요소가 가리키는 객체가 변경된다.
  • 배열 크기 변경: 배열 크기를 변경하려면, 기존 배열을 복사하여 더 큰 배열을 생성하고, 참조를 새로운 배열로 변경해야 한다.

    • 예시:
      String[] strArray = {"Hello"};
      strArray = Arrays.copyOf(strArray, strArray.length + 1);
      strArray[1] = "World";
      • 이 과정에서 새로운 배열이 생성되고, strArray가 이를 가리키도록 참조가 변경된다.

4. 참조 주소 변경 확인 방법

  • 해시코드 사용: 참조 주소를 직접 확인하는 것은 불가능하지만, 객체의 해시코드를 통해 간접적으로 추적할 수 있다.

    • System.identityHashCode(Object obj) 메소드를 사용하면 객체의 고유 해시코드를 확인할 수 있다.
    • 해시코드가 달라지면 참조하는 객체가 변경된 것을 의미한다.
  • 예시 코드:

      public class Main {
          public static void main(String[] args) {
              String str = "Hello";
              System.out.println("Initial HashCode: " + System.identityHashCode(str));
    
              str = str + " World";
              System.out.println("After Modification HashCode: " + System.identityHashCode(str));
    
              String[] strArray = {"Hello"};
              System.out.println("Initial Array HashCode: " + System.identityHashCode(strArray));
    
              strArray = Arrays.copyOf(strArray, strArray.length + 1);
              System.out.println("After Array Copy HashCode: " + System.identityHashCode(strArray));
          }
      }
    • 이 코드를 통해 문자열이나 배열의 참조가 변경되었는지 확인할 수 있다.

5. 디버깅을 통한 참조 주소 확인

  • 인텔리J 디버거 사용:
    • 디버거에서 브레이크포인트를 설정하고, 문자열 및 배열의 해시코드를 단계별로 추적하여 참조가 변경되는지 확인할 수 있다.
    • 이는 객체의 참조 주소가 변경되었는지 확인하는 효과적인 방법이다.

좀더 근본적인 이유

1. 안전성 (Thread Safety)

  • 설명: 자바의 String 객체는 불변이기 때문에, 생성된 후에 그 내용이 절대 변경되지 않는다. 이는 특히 멀티스레드 환경에서 매우 중요한 특성이다.

  • 멀티스레드 환경:

    • 여러 스레드가 동일한 String 객체를 공유할 때, 만약 이 객체가 수정 가능(mutable)하다면, 한 스레드가 객체를 변경할 경우 다른 스레드에 영향을 미칠 수 있다. 이는 동기화 문제를 발생시킬 수 있으며, 프로그램의 예측 불가능한 동작을 초래할 수 있다.
    • 그러나 String이 불변이므로, 어떤 스레드도 String 객체의 내용을 변경할 수 없다. 따라서 여러 스레드가 동일한 String 객체를 안전하게 사용할 수 있다. 불변 객체는 이러한 동기화 문제를 회피할 수 있게 해주며, 스레드 간의 상호작용을 단순화한다.
  • 예시:

    public class StringExample {
        public static void main(String[] args) {
            String sharedString = "Hello";
            Runnable task = () -> {
                System.out.println(sharedString);
                // sharedString은 불변이므로, 어떤 스레드도 이 문자열을 수정할 수 없다.
            };
    
            Thread thread1 = new Thread(task);
            Thread thread2 = new Thread(task);
    
            thread1.start();
            thread2.start();
        }
    }
    • 이 예제에서 두 스레드가 동일한 sharedString을 안전하게 사용할 수 있다.

2. 캐싱 (Caching)

  • 설명: 자바에서는 동일한 String 리터럴이 여러 곳에서 사용될 때, 메모리에서 동일한 객체를 재사용한다. 이 메모리 절약 기법을 String interning이라고 한다.

  • String Pool:

    • 자바 런타임은 String 리터럴을 내부적으로 String Pool이라는 메모리 영역에 저장한다. 동일한 문자열이 여러 곳에서 사용될 때, 새로운 객체를 생성하지 않고, 이미 생성된 객체를 재사용한다.
    • 이는 메모리 사용량을 줄이고, 애플리케이션의 성능을 향상시키는 데 기여한다.
  • 불변성이 필요한 이유:

    • String이 불변이기 때문에, 동일한 객체를 여러 곳에서 안전하게 재사용할 수 있다. 만약 String이 가변적이었다면, 객체가 재사용될 때 그 내용을 변경할 수 있었을 것이고, 이는 프로그램의 의도하지 않은 동작을 유발할 수 있었다.
  • 예시:

    String str1 = "Hello";
    String str2 = "Hello";
    
    // str1과 str2는 동일한 객체를 참조
    System.out.println(str1 == str2); // true
    • 여기서 str1str2는 동일한 객체를 가리키고 있다. 이는 String의 불변성 덕분에 가능하다.

3. 해시 코드 최적화 (Hash Code Optimization)

  • 설명: String 객체의 해시코드는 그 내용에 기반하여 계산된다. 불변 객체의 해시코드는 한 번 계산되면 그 후에는 절대 변하지 않는다.

  • 해시 기반 컬렉션:

    • 자바의 HashMap, HashSet 등과 같은 해시 기반 컬렉션은 내부적으로 객체의 해시코드를 사용하여 객체를 저장하고 검색한다. 해시코드가 변경되지 않으므로, 이러한 컬렉션에서의 검색 작업이 일관성을 유지하고, 효율적으로 수행될 수 있다.
  • 효율성:

    • String이 불변이기 때문에, 해시코드를 여러 번 계산할 필요 없이, 한 번 계산한 해시코드를 캐싱해두고 필요할 때마다 이를 사용할 수 있다. 이는 컬렉션의 성능을 크게 향상시킬 수 있다.
  • 예시:

    String str = "example";
    int hashCode1 = str.hashCode();
    
    // 어떤 작업을 수행하더라도, str의 해시코드는 변경되지 않음
    int hashCode2 = str.hashCode();
    
    System.out.println(hashCode1 == hashCode2); // true
    • 이 예제에서 str의 해시코드는 처음에 계산된 이후로 변경되지 않으며, 해시 기반 컬렉션에서 빠르고 일관된 성능을 제공한다.