본문 바로가기
자바

String 클래스

by 이상한나라의개발자 2024. 4. 4.

자바에서 문자열을 다루는 String 클래스는 자주 사용되는 중요한 클래스입니다. 이번 포스트에서는 String 클래스의 기본 개념, 불변성(immutability) 그리고 객체 생성 방식에 대해서 살펴 보겠습니다. 참고로 String 클래스의 비교는 equals입니다.

String 클래스의 불변성

String 클래스는 불변 클래스로, 한 번 생성된 String 객체의 상태를 변경할 수 없습니다. 즉, 문자열을 벼경하려고 하면 기존 객체를 수정하는 대신, 변경된 새로운 문자열을 포함하는 새로운 String 객체가 생성됩니다. 이러한 특성은 String 객체가 공유되거나 재사용될 때 유용합니다.

String a = "java";
a.concat(" coding");
System.out.println("a = " + a);

-- 결과
java

 

String 클래스는 final로 불변 객체입니다. 그러므로 기존 a 는 변경되지 않습니다. 대신 새로운 객체를 반환하여 사용됩니다.

내부적으로 new 로 생성하여 반환합니다.

static String newString(byte[] buf, long indexCoder) {
    // Use the private, non-copying constructor (unsafe!)
    if (indexCoder == LATIN1) {
        return new String(buf, String.LATIN1);
    } else if (indexCoder == UTF16) {
        return new String(buf, String.UTF16);
    } else {
        throw new InternalError("Storage is not completely initialized, " + (int)indexCoder + " bytes left");
    }
}
String a = "java";
String concat = a.concat(" coding");
System.out.println("a = " + a);
System.out.println("concat = " + concat);
System.out.println("a.hashCode() = " + a.hashCode());
System.out.println("concat.hashCode() = " + concat.hashCode());

// 결과
a = java
concat = java coding
a.hashCode() = 3254818
concat.hashCode() = 912619304

 

문자열 상수 풀(String Constant Poll)

자바에서 문자열 리터럴로 String 객체를 생성하면 JVM은 이 문자열을 문자열 상수 풀에 저장합니다. 이 풀은 힙 메모리 영역에 위치하며, 문자열 리터럴에 의해 생성된 모든 String 객체를 관리합니다. 문자열 상수 풀의 주요 목적은 메모리 사용을 최적화 하는 것입니다. 같은 문자열 리터럴에 의해 생성된 모든 String 객체는 풀에서 같은 메모리 주소를 참조합니다.

String s1 = "hello";
String s2 = "hello";
// s1과 s2는 문자열 상수 풀에서 동일한 "hello" 문자열을 가리킵니다.

 

위와 같이 리터럴 방식으로 문자열을 생성하는 경우가 대부분 입니다. 이럴 경우, 두 문자의 동일성, 동등성 비교는 모두 true입니다. 그 이유는 문자열 상수 풀로 같은 메모리 참조값을 사용하기 때문입니다.

System.out.println("s1 == s2) = " + (s1 == s2));
System.out.println("s1.equals(s2) = " + s1.equals(s2));

 

public boolean equals(Object anObject) {
    if (this == anObject) {
        return true;
    }
    return (anObject instanceof String aString)
            && (!COMPACT_STRINGS || this.coder == aString.coder)
            && StringLatin1.equals(value, aString.value);
}

 

위 코드는 String 클래스의 equals() 메서드를 재정의 하고 있습니다. 첫 번째 라인에서 동일성 비교를 합니다. 그렇다면 이런 생각이 들 수 있는데요. 리터럴로 생성한 문자는 equals가 필요 없는게 아닌가? 그럴수도 있습니다. 하지만, String 클래스의 equals 메서드는 다음과 같은 이유로 필요합니다.

  • 일관성 있는 비교 : 모든 객체는 기본적으로 Object 클래스의 equals() 메서드를 상속 받습니다.  이 메서드는 기본적으로 동일성(==) 비교만 수행합니다. 그러나 String와 같은 값 기반 클래스에서는 두 객체의 내용이 같은지를 비교하는 것이 더 유용합니다. 따라서 String 클래스는 equals() 메서드를 재정의 하여 두 문자열 내용이 같은지 비교합니다. 이는 러터럴 방식으로 생성된 문자 뿐만 아니라 new 키워드를 사용하여 생성된 문자열 객체들 사이에서도 일관된 동등성 비교를 가능하게 합니다.
  • new 키워드로 생성된 문자열 비교 : new String("value")를 사용하여 생성된 문자열 객체는 문자열 상수 풀을 사용하지 않고, 힙 메모리에 새로운 객체를 생성합니다. 이러한 경우, this == anObject는 false를 반환하지만 객체의 값은 동일할 수 있습니다. 따라서 equals 메서드는 이러한 객체들의 내용이 같은지를 정확하게 비교할 수 있어야 합니다.

객체 생성 방식

String 객체는 두 가지 방식으로 생성할 수 있습니다. 문자열 리터럴 방식과, new 키워드를 사용하는 것입니다.

  • 문자열 리터럴 : String 객체는 문자열 상수 풀에 의해 관리됩니다. 동일한 문자열 리터럴로 두 객체를 생성하면 두 객체는 상수풀에서 같은 메모리 위치를 공유합니다. 예를 들어 위 코드에서 s1 객체 주소 x0001 에 hello가 생성이 되었다면 s2는 hello 객체를 생성하지 않고 x0001을 참조하여 반환합니다.
  • new 키워드 : new 키워드로 String 객체를 생성하면, 각 객체는 힙 메모리의 별도 위치에 생성이 됩니다. 이 경우, 동일한 문자열 값을 가지더라도 두 객체는 서로 다른 메모리 주소를 가집니다.
String s3 = new String("hello");
String s4 = new String("hello");
// s3와 s4는 내용은 같지만, 서로 다른 메모리 주소를 가진 별도의 객체입니다.

 

String 클래스 내부

String 클래스의 내부 구현은 문자열 데이터를 저장하기 위해 char[] 배열을 사용합니다. 자바 9 부터는 char 배열 대신 byte 배열과 인코딩 플래그를 사용하여 메모리 사용량을 더 줄였습니다. 

public final class String implements java.io.Serializable, Comparable<String>, CharSequence {
    // 내부적으로 문자열 데이터를 저장하는 배열 (자바 8 이하)
    private final char value[];

    // 자바 9 이후는 byte 배열과 인코더를 사용
    // private final byte[] value;
    // private final byte coder;
    
    ...
}

 

'자바' 카테고리의 다른 글

자바에서의 불변 객체 (Immutable Object)  (1) 2024.04.03
자바에서 동일성, 동등성 비교하기  (1) 2024.04.03
Parallel Stream  (1) 2024.02.06
Optional<T>  (0) 2024.02.06
Stream API  (0) 2024.02.02