자바라는 언어는 객체 지향 언어이다. 본격적으로 객체를 학습하기 전에 먼저 참조 타입의 종류와 참조 변수의 역활에 대해서 알아야 한다.
자바에서 타입은 크게 기본 타입과 참조 타입으로 두 가지로 나뉜다. 앞선 글에서는 기본 타입에 관련해서 간략히 알아 보았으니 이번에는 참조 타입에 대해 알아보겠다.
기본 타입으로 선언된 변수와 참조 타입으로 선언된 변수의 차이점으로는 저장되는 값에 있어서 차이점이 존재한다. 기본 타입은 실제 값 안에 변수를 저장하지만, 참조 타입은 메모리의 번지를 변수 안에 저장을 한다는 점이다.
예를 들어 아래와 같이 변수가 있다고 하자.
int price = 100;
double tariff = 1.5;
String name = "철수";
String hobby = "독서";
기본 타입 변수인 price와 tariff는 직접 값을 저장하고 있지만, String클래스 변수인 name과 hobby는 힙 영역의 String객체의 번지 값을 갖고 있다.
참조 타입에 대해서 알아보기 전 JVM이 사용하는 메모리 영역에 대해서 알아야 이해하기 쉽다.
JVM이란?
JVM(Java Virtual Machine)은 자바 프로그램이 실행되는 가상의 컴퓨터입니다. 자바 소스 코드를 컴파일하면 생성되는 바이트코드(.class 파일)를 실행하는 역할을 합니다. 플랫폼마다 다른 JVM이 존재하지만, 같은 바이트코드를 실행할 수 있어 자바의 “한 번 작성하면 어디서나 실행된다”는 특징을 가능하게 합니다. 또한, 메모리 관리, 쓰레드 제어, 가비지 컬렉션 등의 기능도 수행합니다.
JVM은 운영체제에서 할당 받은 메모리 영역(Runtime Data Area)을 다음과 같이 세부 영역으로 구분하여 사용한다.

메서드 영역 (Method Area)
메서드 영역은 JVM이 시작할 때 생성되고 모든 쓰레드가 공유하는 영역이다.
해당 영역은 코드에서 사용되는 클래스들을 클래스 로더로 읽어 클래스별로 정적필드와 상수 메서드 코드 생성자 등을 분류하여 저장한다.
힙 영역 (Heap Area)
객체와 배열이 생성되는 영역이다. 여기에서 생성되는 객체와 배열을 JVM스택 영역의 변수나 다른 객체의 필드에서 참조를 한다. 만일 참조하는 변수나 필드가 없다면 의미없는 객체가 되기 때문에 이것을 쓰레기로 취급하고, 쓰레기 수집기를 실행하여 제거하게 된다.
JVM 스택 영역
JVM스택은 메서드를 호출할 때마다 프레임을 추가하고 메서드가 종효된다면 해당 프레임을 제거하는 동작을 하게된다.
프레임 내부에는 로컬 변수 스택이 있는데, 기본 타입 변수와 참조 타입 변수가 추가되거나 제거된다.
스택 영역에 변수가 생성되는 시점은 초기화가 될 때, 즉 다시 말하면 변서에 값이 저장될 때 이다. 변수는 선언된 블럭 안에서만 스텍에 존재하고 블럭 범위를 벗어나게 된다면 해당 변수는 스택에서 제거가 된다.
char c = 'A'; // ①
if(c == 'A') { // ②
int i = 100;
double d = 1.23;
}
// ③
boolean b = false; // ④
위 변수 선언 순서를 통해서 스택 영역의 변화를 확인하자.
| 1 | 2 | 3 | 4 |
|---|---|---|---|
| c - A | c - A i - 100 d - 1.23 | c - A | c - A b - false |
①에서는 스택 영역에서 변수 c가 저장이 되고, 다음으로 조건문 블럭 내부에서 변수 i와 d가 생성이 되고 조건문 블럭이 끝나는 순간 스택 영역에는 i와 d가 제거가 된다. 여기서 c가 선언된 블럭은 아직 끝나지 않았기 때문에 남겨져 있다. 마지막으로 b가 스택 영역에 생성된다.
기본 타입 변수는 스택 영역에서 직접 값을 갖고 있는 형태이다. 하지만 참조 타입 변수는 스택 영역에 힙 영역의 객체 주소를 갖는다고 말을 했다.
int [] arr = {1, 2, 3, 4}을 위 코드에 작성하면 스택 영역에서 어떻게 관리하는지 확인하자.

해당 이미지를 보면 변수 c와 b를 보면 직접 값을 갖고 있는 것을 확인할 수 있다. 하지만 배열 arr을 보면 @774라는 것이 보인다. @774는 배열 arr이 힙에 생성된 배열 객체를 참조하고 있다는 사실을 보여주는 식별 정보이다.
기본 타입 변수에서 해당 연산을 하면 동일한 값인지 아님 다른 값인지를 검증을 하여 논리 타입의 값을 반환하는 동작을 한다. 하지만 이를 참조 타입 변수에서 사용을 한다면 어떤 값을 기준으로 동일한지 비교를 할까?
힌트를 준다면 JVM 스택 영역에서 변수를 어떻게 관리하는지 생각하면 답은 쉽게 얻을 수 있다.
참조 타입의 변수들은 동일한 객체를 참조하는지, 다른 객체를 참조하는지를 기준으로 검증을 한다. 즉, 다시 말해 힙 영역의 객체 주소가 동일한지 검증을 한다.
int[] arr = {1, 2, 3, 4};
int[] arr2 = arr;
int[] arr3 = new int[] {1, 2, 3, 4};
System.out.println(arr == arr2); // 출력 결과 : true
System.out.println(arr == arr3); // 출력 결과 : false
System.out.println(arr2 == arr3); // 출력 결과 : false
각 출력문 결과를 보면 배열 arr, arr2를 비교 했을 때 true가 출력이 되고, arr, arr2 와 arr3을 비교 했을 때 전부 false가 출력 된다.

그 이유는 위 이미지를 보면 배열 arr3만 다른 주소를 참조하고 있기 때문이다.
참조 타입 변수는 힙 영역의 객체를 참조하지 않는다는 뜻으로 Null값을 가질 수 있다. 해당 값은 초기값으로 사용할 수 있기 때문에 JVM스택 영역에서 Null값으로 초기화 된 변수를 생성할 수 있다.
참조 변수가 Null을 갖고 있을 경우에는, 참조 객체가 없으므로 변수를 통해 객체를 사용할 수 없다. 그런데 만약 해당 상태에서 있지도 않은 객체의 데이터나 메서드를 사용하는 코드를 실행하게 된다면 NullPointerException이 발생하게 된다.
기본 타입에 대해서 설명을 할 때 String에 대해서 자세한 설명을 하지 않았다. 그 이유는 초반에 바로 참조 타입에 대하여 설명을 한다면 혼란이 생길 수 있다고 생각 했기 때문이다.
문자열은 String객체로 생성되고, 변수는 String객체를 참조한다. 그렇기 때문에 String 변수는 스택 영역에 변수가 생성되고, 문자열 리터럴은 힙 영역에서 String객체로 생성되어 변수에 String객체의 번지를 저장하게 된다.
new 연산자는 힙 영역에 새로운 객체를 생성할 때 사용되는 연산자로 객체 생성 연산자라고 부른다.
String name1 = new String("짱구");
String name2 = new String("짱구");
위 처럼 코드를 작성하게 된다면 힙 영역에서 “짱구”라는 문자열 객체가 2개 생성이 되고 name1과 2는 동일한 문자이지만 참조 번지가 다른 “짱구”라는 객체 주소를 저장하게 된다.
그러면 String타입의 변수의 문자열만 동일한지 비교하고 싶은데 방법이 없는가? 라는 의문점이 생길 수 있다.
순수하게 문자열이 동일 여부를 확인할 때 String객체에서 지원하는 .equals()메서드를 사용하면 참조 번지를 비교하는 것이 아닌 순수 문자열이 동일한지 비교하게 된다.
System.out.println(name1.equals(name2)); // true
배열이란 같은 타입의 데이터를 연속된 공간에 나열하고, 각 인덱스를 부여하여 데이터를 관리하기 위한 자료 구조이다.
배열은 다음과 같은 특징이 존재 한다는 것을 명심해야 한다.
배열 선언은 아래와 같다.
int[] arr1; // 타입[] 변수;
String arr2[]; // 타입 변수[];
배열 선언에 있어서 대괄호의 위치는 타입 뒤나, 변수명 뒤에 붙여서 선언할 수 있다.
배열 변수는 참조 변서에 속하기 때문에 선언할 때 참조할 배열 객체가 없다면 배열 변수는 Null값으로 초기화 할 수 있다.
배열 객체 생성할 때 값을 목록을 사용하거나 new연산자를 사용하는 방법이 있다.
타입[ ] 변수 = { 값1, 값2, 값3, …};배열 변수를 이미 선언한 후에는 다른 실행문에서 중괄호를 사용한 배열 생성은 허용되지 않는다
String[] names;
names = {"짱구", "철수", "훈이", "맹구"}; // 컴파일 에러
배열 변수를 이미 선언한 후 값 목록들이 나중에 결정되는 상황이라면 new연산자를 사용하여 값 목록을 지정해줘야 한다.
names = new String[] {"짱구", "철수", "훈이", "맹구"};
추가적으로 메서드의 매개값으로 배열이 있을 경우, 배열을 생성과 동시에 매개값으로 사용하기 위해서도 new연산자를 사용 해야 한다.
void method(int[] nums) {...} // 정수 배열을 매개값으로 사용하는 메서드
method({1, 2, 3, 4}); // 컴파일 에러
method(new int[]{1, 2, 3, 4});
new연산자로 배열 객체를 생성할 수 있다.타입[ ] 변수 = new 타입[ 길이 ];new연산자로 배열을 생성할 수 있다.double[] muns = null;
muns = new boolean[10];
배열 생성 시 길이를 명시했을 때 자동적으로 기본값으로 초기화된다.
예를 들어서 new int[3]로 배열을 생성하면 배열 내부는 기본값으로 초기화가 되어 배열의 값들은 0으로 채워진다.
| 분류 | 타입 | 초기값 |
|---|---|---|
| 기본 타입 (정수) | byte [ ] | 0 |
| char [ ] | “\u0000” | |
| short [ ] | 0 | |
| int [ ] | 0 | |
| long [ ] | 0L | |
| 기본 타입 (실수) | float [ ] | 0.0F |
| double [ ] | 0.0 | |
| 가보 타입 (논리) | boolean [ ] | false |
| 참조 타입 | 클래스 [ ] | null |
| 인터페이스 [ ] | null |
지금까지 알아본 배열은 1차원 배열이다. 이제는 다차원 배열에 대해서 알아보자.
기존 1차원 배열과 달리 값들을 행과 열로 구성된 2차원 배열에 저장할 수 있다. 여기서 값들은 가로와 세로 인덱스를 가지고 관리를 하게 된다.
int[][] muns = new int[2][3];
| index | 0 | 1 | 2 |
|---|---|---|---|
| 0 | 0 | 0 | 0 |
| 1 | 0 | 0 | 0 |