Java

Java char과 String의 차이 (feat. Constant Pool)

jwKim96 2022. 2. 15. 01:51

Java char과 String의 차이 (feat. Constant Pool)

  • char과 String 간단 정리표
- char String
타입 Primitive Reference
저장위치 Stack Stack + Heap
초기값 타입에 따른 초기값 제공 null

char

char은 Primitive-type입니다.
Primitive-type 변수는 Stack Frame에 저장되며, call-by-value라는 특징이 있는데요.
call-by-value를 간단한 예제 코드로 알아보겠습니다.

class CallByValueTest {
    public static void main(String[] args) {
        // stack의 charA변수 영역에 'A' 저장됨
        char charA = 'A';
        // stack의 newchar변수 영역에 'A' 저장됨
        char newChar = charA;

        // charA를 'B'로 변경
        charA = 'B';

        // newChar 출력
        System.out.println(newChar) // 'A'
    }
}

charA에 'A'를 넣은 다음 newchar에 charA를 대입하고,
다시 charA의 값을 변경한 다음, newChar를 출력해보면 'A'가 나옵니다.

이렇게 되는 이유는, Primitive-type은 변수에는 주소값이 아닌, 실제 데이터가 저장되기 때문입니다.
실제 데이터리터럴이라고도 부르는데요.
리터럴이 들어있는 변수를 다른 변수에 대입하면, 데이터가 복사가 되어 저장됩니다.
그래서 charA를 변경해도 newChar은 영향을 받지 않게 됩니다.

String

String은 Java에서 기본 자료형으로 제공하지만, Reference-type 입니다.
Reference-type 변수에 데이터를 저장하면, Stack과 Heap 모두 사용하게 되는데요.
실제 데이터는 Heap 영역에 저장되고, 그 주소를 Stack 영역에 저장합니다.
그래서 우리가 해당 변수를 사용하면, 먼저 Stack으로 가서 주소값을 확인하고
그 주소로 Heap에 있는 실제 데이터를 참조해서 사용하게 됩니다.
아래 예제를 통해 알아보겠습니다.

class ReferenceType  {
    private int num;

    public void setNum(int num) {
        this.num = num;
    }

    public int getNum() {
        return this.num;
    }
}

public class ReferenceTypeStackHeapTest {
    private static ReferenceType myRef;

    public static void main(String args[]) {
        // ReferenceType의 ref 인스턴스 생성
        ReferenceType ref = new ReferenceType();
        ref.setNum(10); // ref.num = 10

        // myRef에 ref의 참조값을 삽입
        myRef = ref; 

        // ref.num = 20
        ref.setNum(20);

        // myRef.num 출력
        System.out.println(myRef.getNum()); // 20
    }
}

image

Java는 기본적으로 모든 것이 Call-by-value로 동작합니다.
Call-by-value는 A변수의 값을 복사해서 B에 넣어주는데요.
여기서는 myRefref를 대입했으니, ref 변수에 들어있는 값(주소값)을 myRef에 복사해서 넣어주게 됩니다.
그러면 refmyRef 는 Heap 영역에 있는 같은 ReferenceType 인스턴스를 가리키는 포인터 변수가 됩니다.

위 예제의 ReferenceType 처럼 String 또한 Reference-type 이기 때문에 위와 같은 방식으로 동작할까요?
String은 일반 클래스들과는 다르게 String Constant Pool을 사용할 수 있게 특별 대우를 받는 녀석인데요.
이에 대해서 아래에서 알아보겠습니다.

String Constant Pool(SCP)

SCP에 대해 말씀드리기에 앞서, String의 두 가지 선언 방식에 대해 짚고 넘어가겠습니다.

// Literal 선언 방식
String name1 = "hello";

// Reference 선언 방식
String name2 = new String("hello");

name1과 name2는 모두 "hello"라는 문자열 값을 갖습니다.
하지만, Reference 선언 방식은 아주 비효율적으로 동작하는데요.
이를 SCP와 함께 알아보겠습니다.

위 예제의 메모리가 할당되는 모습은 다음과 그림과 같습니다.

image

첫 번째 name1 의 초기화 단계에서 "hello"가 대입되었습니다.
그러면 먼저 SCP에 "hello"가 있는지 찾고, 없으면 새로 저장합니다.

두 번째 name2의 초기화 단계 중 String 클래스의 생성자의 파라미터로 "hello"가 대입되었습니다.
이 역시 Literal 이기 때문에 역시 SCP에서 찾고, 발견된 "hello"는 String 생성자 내부로 복사되어, String 인스턴스 내부에 저장됩니다.
그런데 원래 모든 인스턴스의 정보는 Heap에 저장되죠.
그래서 "hello"라는 데이터가 Heap에 복사된 셈이 됩니다.

SCP에 대한 다른 예제를 살펴보겠습니다.

String name1 = "Hello";
String name2 = "Hello";
String name3 = "Jay";

image

첫 번째의 name1 의 "Hello" 라는 Literal String이 대입됩니다.
그래서 SCP에서 먼저 찾지만 없어서 SCP에 저장하고, name1 은 해당 주소를 참조합니다.

두 번째의 name2 에서 또 "Hello"가 대입됩니다.
마찬가지로 SCP에서 찾고, 있어서 해당 주소를 참조합니다.

세 번째의 name3 에서는 이전과 다른"Jay"가 대입됩니다.
SCP에 없기 때문에 저장하고, name3 은 해당 주소를 참조합니다.

Java의 Call-by-reference (추가)

위에서 스쳐지나가듯 Java는 모든것이 Call-by-value로 동작한다고 설명했었는데요.
이에 대해서 조금더 자세히 짚어보겠습니다.

Call-by-reference해당 변수의 주소를 그대로 사용하는 방식 입니다.

Java는 참조 변수가 가리키는 객체의 주소를 가진 새로운 참조변수를 생성하는 방식이기 때문에, 두 변수가 같은 인스턴스를 참조하게 됩니다.
그저 변수에 들어있는 값을 복사한 것 뿐인데, 그 값이 객체의 주소인 것 입니다.

위에서 봤던 ReferenceTypeStackHeapTest 예제를 다시한번 살펴보겠습니다.

class ReferenceType  {
    private int num;

    public void setNum(int num) {
        this.num = num;
    }

    public int getNum() {
        return this.num;
    }
}

public class ReferenceTypeStackHeapTest {
    private static ReferenceType myRef;

    public static void main(String args[]) {
        // ReferenceType의 ref 인스턴스 생성
        ReferenceType ref = new ReferenceType();
        ref.setNum(10); // ref.num = 10

        // myRef에 ref의 참조값을 삽입
        myRef = ref; 

        // ref.num = 20
        ref.setNum(20);

        // myRef.num 출력
        System.out.println(myRef.getNum()); // 20
    }
}

마치 Call-by-reference인 것 같은 착각이 듭니다만, 더 자세히 살펴보겠습니다.

new Reference() 이 인스턴스의 주소를 저장하는 ref변수가 있는데요.
myRefref를 대입하면, ref의 값이 myRef복사됩니다.
변수의 값이 다른 변수에 복사되는 방식은 Call-by-value 방식입니다.
그런데 하필 복사된 값이 주소값이기 때문에 같은 인스턴스를 가리키게 되고, 그래서 마치 Call-by-reference처럼 동작하게 되는 것입니다.

참고자료