이번 포스팅에서는 지난 포스팅 JAVA - 타입과 타입 변환편에서 잠깐 이야기했던 참조 타입(Reference type)에 대한 내용을 이어서 설명하려고 한다.
참조 타입은 일반적으로 배열(Array)과 클래스(Class), 열거(Enum), 인터페이스(Interface)로 생성된 객체나 배열의 메모리 상 주소를 참조하는 자료형을 의미한다.
말로 설명하기 애매하지만 이전 포스팅에서 설명했던 자바의 기본형 데이터 타입(Primitive type)을 제외한 모든 데이터 타입이라고 보면 된다.
- 기본 타입(Primitive type)
boolean
char
byte, int, long
float, double
참조 타입을 이해하기 위해 먼저 메모리가 어떤 구조를 이루고 있는지 그리고 각 자료형들로 선언된 변수들이 메모리 상에서 어떻게 저장되는지 이해할 필요가 있다.
메모리는 스택 영역(STACK)과 힙 영역(HEAP)으로 나뉜다.
- 스택 영역
코드 상에서 선언한 모든 변수들이 저장된다. 즉, 스택 영역에는 기본형 변수들과 참조형 변수들이 저장된다.- 힙 영역
배열과 클래스, 열거, 인터페이스로 생성된 객체나 배열의 "실제값"들이 저장된다.- 메모리 구조
조금 더 자세하게 설명하자면,
스택 영역의 경우 기본형 변수가 실제값과 함께 저장되고 참조형 변수가 주소값과 함께 저장된다.
힙 영역의 경우 스택 영역에 저장된 참조형 변수의 "주소값"이 가리키고 있는 위치에 "실제값"을 저장한다.
글로만 설명하면 이해하기 어려우니 예시와 그림으로 보자.
int x = 10;
int[] A = {10, 20, 30, 40, 50};
String S = new String("abc");
- 변수 x는 int형(기본 타입)으로 선언되어 있고 값 10을 저장하고 있다.
- 변수 A는 int형 배열(참조 타입)으로 선언되어 있고 총 5개의 값을 저장하고 있다.
- 변수 S는 String(클래스 - 참조 타입)으로 선언되어 있고 값 "abc"를 저장하고 있다.
이를 메모리 상에서 표현하면 다음과 같다.(실제값은 빨간색으로 표현하였다.)
다음으로 참조형 변수를 다룰 때 중요한 Equality 연산자("==" 연산자와 "!=" 연산자) 사용에 대해 알아보자.
Equality 연산자는 스택에 저장된 변수들 간의 비교를 진행한다.
따라서, 기본형 변수에서 Equality 연산자의 사용은 실제값의 비교로 사용자의 의도에 맞게 비교가 진행된다.
하지만, 참조형 변수의 경우, Equality 연산자의 사용은 주소값의 비교로 진행된다.
특히, Equality 연산자 사용 시 가장 주의해야 하는 상황은
이다.
String 간의 비교 시 Equality 연산자를 사용하게 되면 주소값 비교를 진행하므로 사용자의 의도와 다르게 비교가 진행된다.😭
이럴 경우 ✅equals() 메소드나 ✅compareTo() 메소드를 사용하면 실제값(문자열)의 비교를 진행하게 된다.
- boolean equals()
두 문자열이 같은 true를 return, 다르면 false를 returnString A = new String("abc"); String B = new String("abc"); System.out.println(A.equals(B)); // 출력: true
- int compareTo(String target)
비교하려는 문자열이 target 문자열과 같으면 0을 return
target 문자열보다 "사전에서" 앞에 나온다면 음수를 return
target 문자열보다 "사전에서" 뒤에 나온다면 양수를 returnString A = new String("abc"); String B = new String("abc"); String C = new String("hij"); String D = new String("def"); System.out.println(A.compareTo(B)); // 출력: 0 System.out.println(A.compareTo(C)); // 출력: 음수 System.out.println(C.compareTo(D)); // 출력: 양수
추가로, Java에서 String을 다룰 때 신경써야 할 것이 있다.
그것은 바로 🔎 String으로 선언된 변수를 어떻게 초기화하느냐인데
String 변수를 초기화하는 방법은 2가지가 있다.
- new 키워드를 통한 String 객체를 생성하여 초기화하는 방법
- String 리터럴을 이용하여 초기화하는 방법
이처럼, 각 방법에 따라 메모리 상(특히, Heap 영역)에서
👉 String 변수가 가진 값이 어떻게 저장되는지 알아야 한다.
먼저, 첫 번째 방법을 살펴보자.
new 키워드를 통해 초기화하는 방법은 앞서, 예제로 보여준 것과 같은데 다시 한번 보도록 하자.
String A = new String("abc");
String B = new String("abc");
위 코드는 메모리 상에서 다음과 같이 나타난다.
위와 같이, new 키워드를 통해 Heap 영역에서 각각 별도의 메모리 공간을 할당 받아 값을 저장한다.
다음으로, String 리터럴을 통해 초기화하는 방법을 보자.
String A = "abc";
String B = "abc";
위 코드는 메모리 상에서 다음과 같이 나타난다.
위 그림을 보면, String 리터럴을 통해 초기화하는 경우 new 키워드와는 달리 A와 B 모두 Heap 영역에서 같은 공간을 가리키고 있다.
보다 더 구체적으로 말하자면,
String 리터럴은 Heap 영역에서 특별히 할당된 공간인
에 메모리를 할당 받아 값을 저장한다.
이때, String pool에 저장된 값과 동일한 값으로 String 변수를 초기화하는 경우(예제 코드에서 변수 B의 경우)
새로운 공간을 할당 받아 저장하는 것이 아니라
String pool에서 동일한 값이 저장된 공간을 서로 공유하게 되는 것이다.
이처럼, String을 초기화하는 방법에 따라 메모리에 저장되는 방식이 다르기 때문에 String 간의 비교 시 equals()나 compareTo() 함수의 사용을 필요하다는 것을 명심해야 한다.
만약, 위의 예제와 같이 동일한 String 리터럴로 초기화를 하고 Equality 연산자를 통해 비교한다면
같은 주소값을 비교하게 되어 결과적으로 사용자의 의도에 맞게 출력되지만,
이는 본래 사용자가 진짜로 원하던 비교가 아니기 때문에 꼭 주의해야 한다.
마지막으로 참조 타입 또한 타입 변환(Type Casting)이 가능한데
이와 관련된 내용은 Java의 클래스(Class)에서 상속 개념을 먼저 학습하고 다루어 보도록 하자.
너무 유익한 자료네요. 잘보고 가요~~^^