String

원화가·2024년 4월 3일

string 자료형에 대하여,

리터럴과 new

스트링 자료형의 선언에는 두가지 방법이 있다.
1. 리터럴
2. new String() 생성자 호출

String imString = "Yes, I am"; //리터럴
String imStringObj = new String("Yes, I am"); //생성자 호출

두 생성 방식은 어떤 차이가 있을까?

1.리터럴

리터럴(literal)의 사전적 정의를 먼저 확인해보자.

Literal : (형)문자그대로의, (형)(번역이)직역인,

리터럴은 쉽게 생각해서 변하지 않는 데이터 그 자체를 의미한다.

"HI!" 'A' 1,2,0.4 .....

Q. 🤔 그렇다면 리터럴은 상수인가?
A. 상수와 리터럴은 다르다.

상수는 클래스와 같은 객체도 포함할 수 있지만, 리터럴은 말 그대로 값을 의미하기 때문에
엄밀히 다르다.

리터럴, 어디에 저장되는가?

String imString = "나는 스트링이에요.";

String 객체를 명시해주고, 문자열을 리터럴로 입력했다.

| stack : 컴파일시 메모리 할당, 함수 호출과 관련된 변수와 매개변수등이 할당된다.
함수 블록의 소멸과 함께 할당된 메모리도 소멸.

| heap : 런타임 메모리 할당, 사용자의 동적 할당영역. 직접 할당 해제 필요.

그러면 위와 같이 메모리 공간에 할당이된다.
여기서 리터럴인 ["나는 스트링이에요."]문자열은 Heap 공간 중에서도 String Constant Pool이라는 위치에 할당된다.
(String 자료형 자체가 불변(immutable)하기 때문에 String constant pool이라는 용어가 틀렸다는 말도 존재한다. string pool이라고 작성해도 되지만, 본 문서는 String constant pool을 사용하도록 하겠다.)

어떤 이점이 있는가?
이렇게 String constant pool에 저장된 리터럴 문자열은 해당 데이터를 공유한다는 장점이 있다.
예시를 살펴보자.

String str1 = "Hello!";
String str2 = "Hello!";

위와 같이 작성된 리터럴 문자열이 있으면 다음과 같이 메모리를 사용한다.

String 리터럴로 생성된 객체는 intern()이라는 내부함수를 호출해 constant pool에 같은 값이 있는지 확인한 후, 없다면 string constant pool에 저장하고, 이후에 같은 내용을 생성하고자하는 String 리터럴은 이미 만들어진 string constant pool을 참조하게된다.

즉, String 객체가 리터럴을 참조하도록 하면 메모리상 이득을 볼 수 있다.

2 new 생성자에 의한 String

new 생성자에 의해 생성되는 String을 작성해보자.

String str1 = new String("Hi!");
String str2 = new String("Hi!");

생성자에 의해 만들어지는 String은 하나의 객체로서 등록된다.
따라서 String자료형에 주어지는 문자열이 무엇이든, 만들어지는 객체는 각각 별도의 메모리 공간을 차지한다.

3 리터럴 문자열 vs new String()

그렇다면, 누가 더 사용하기에 이득인가?
공통점
자바의 String클래스는 우선 불변(immutable)을 보장한다.
이는 한번 만들어진 문자열은 절대 바꿀 수 없음을 의미한다.
차이점
서로 저장되는 메모리 영역이 다르고, 그 참조 방식이 다르다.
다만 동일한 내용을 작성해도 new String()은 메모리 공간을 계속 할당하고, 리터럴 문자열 객체는 이미 있는 값을 참조한다는 차이점이 있다.

그렇기에 자바에서는 리터럴문자열사용을 권장한다.

리터럴 문자열은 컴파일 단계에서 생성되기 때문에 최적화에서도, 메모리 공간 효율면에서도 우세를 점하고있다.

Q 그러면 new String()은 그냥 안쓰면 되는거 아닌가?

그럼 new 생성자를 사용하는 경우는 어떤 경우일까?
리터럴 문자열은 컴파일 단계에서 모두 수행된다. 즉 런타임에서는 작성할 수 없다.
런타임 단계에서 문자열을 동적으로 처리하거나, 사용자의 입력에 의한 문자열을 처리해야 한다면, new 연산자를 사용한다.

그 외의 경우는 모두 리터럴선언을 권장한다.

4 ==, 그리고 equals()

문자열 두개를 비교해보자, 우선 다음과 같이 코드를 작성한다.

String str1 = new String("hello");
String str2 = new String("hello");

str1 == str2 => ?

str1과 str2는 둘다 동일하게 Hello라는 값을 가지고 있다.
일반적으로 둘의 값은 동일하니 True라는 결과가 나올 것이라 생각할 수 있다.

하지만 자바의 [==]연산자가 어떤 의미인지 알고 갈 필요가 있다.

‘ == ’ :
1. 기본 데이터 타입간의 비교 -> 둘이 동등한 값인지 확인한다(전통적인 방식)
2. 참조 데이터 타입간의 비교 -> 두 참조변수가 같은 객체를 가리키는지 확인한다. 즉, 동일성 비교.

자 그러면 결과를 예상해 볼 수 있다.
str1과 str2는 모두 참조 변수 타입이며, 둘은 각자 다른 객체를 참조하고 있다.
당연하게도, 다음 결과는 false가 나오게 된다.

str1 == str2 => false

리터럴 문자열은 어떨까?

String str1 = "hello";
String str2 = "hello";

System.out.println(str1 == str2); // true (리터럴 문자열은 상수 풀에서 동일한 문자열을 가리킴)

리터럴 문자열은 String constant pool에 등록된 같은 객체를 참조한다.
따라서 참조 변수 동일성 비교에서 True가 나오게 된다.

리터럴 문자열 값 == new 생성자 문자열 값

그러면 다음의 경우는 어떨까?

String str1 = "hello";
String str2 = new String("hello");

System.out.println(str1 == str2); // false

당연하지만, false가 나온다. 둘은 다른 객체를 참조하고 있기 때문이다.

값끼리만 비교, equals()

equals()메소드는 사실 두 참조변수가 같은 객체를 가리키고 있는지에 대한 동일성 비교 메소드다.
근본적으로는 ‘==’연산자와 동일하게 동작하도록 작성되었다.
다만 많은 클래스들이 해당 메소드를 상속받은 후, 객체 간 값의 동일성을 비교하도록 재정의하여 오버라이딩한다.

대표적으로 String()클래스가 그러하다.

즉 String객체에서 equals()를 사용하면, 참조변수가 어떤 객체를 참조하던 간에 값의 동일성을 비교해준다.

물리적인 주소가 다르더라도, 논리적으로 값이 같다면 True를 반환해준다.

따라서 다음 구문은 True이다.

public class Main {
    public static void main(String[] args) {
        String str1 = "hello";
        String str2 = "hello";
        String str3 = new String("hello");
        String str4 = new String("world");

        // equals() 메서드를 사용하여 두 문자열 객체의 내용 비교
        System.out.println("str1 equals str2: " + str1.equals(str2)); // true
        System.out.println("str1 equals str3: " + str1.equals(str3)); // true
        System.out.println("str1 equals str4: " + str1.equals(str4)); // false
    }
}

마무리

자바의 String 클래스 중 리터럴 생성, 생성자 생성, 그리고 문자열 비교와 메모리 공간 상 위치까지 간략하게 다루어 보았다. 이후에 추가적인 공부를 통해 더 깊은 단계를 이해하고자 한다.


오류 및 잘못된 내용이 있다면 가감없이 말씀해주세요.

profile
은하수의 히치하이커

0개의 댓글