[Java] 클래스, 객체, 참조변수
클래스
- 결국 하나의 데이터 타입 (int 나 double 처럼)
- 사용자 정의 타입이라고 부르기도 함
- 배열 변수를 선언할 때도 배열 원소 각각의 타입을 클래스로 할 수도 있음
// 클래스는 결국 하나의 "타입"
Person1 [] members = new Person1 [10]; // members 배열의 각각의 타입이 Person1 클래스 (타입)
members[0] = first; // 참조변수 first
members[1] = new Person1();
members[1].name = "David";
members[1].number = "1234567890";
System.out.println("Name : " + members[1].name + ", Number : " + members[1].name);
클래스 vs. 객체 vs. 참조변수
- 클래스는 데이터 타입이므로 값을 저장하기 위해선 참조 변수와 함께 new 연산자를 사용하여 객체를 만들어 줘야 한다.
- 클래스의 변수는 primitive type의 변수와는 다르게 참조 변수이다.
- 클래스의 참조 변수에는 객체의 주소가 저장되고, new 연산자로 생성된 객체(이름이 없는 객체)에 실제 데이터가 저장된다.
- 그러나 primitive type의 변수에는 변수 자체에 값이 저장된다.
참조 변수에 값을 대입하려면 객체를 생성해서 주소를 받아야 한다
// 클래스는 결국 하나의 "타입"
Person1 [] members = new Person1 [10]; // members 배열의 각각의 타입이 Person1 클래스 (타입)
members[0] = first;
members[1] = new Person1();
members[1].name = "David";
members[1].number = "1234567890";
members[2].name = "David"; // NullPointerException 발생
members[2].number = "23456789";
System.out.println("Name : " + members[1].name + ", Number : " + members[1].name);
왜 NullPointerException이 발생할까?
그 이유는 Person1이란 데이터 타입을 갖는 members 배열의 각 원소들 ex. members[2]는 참조 변수인데, 참조 변수에 값을 저장하려면 반드시 new 연산자를 통해 가리킬 수 있는 객체의 주소를 넣어줘야만 가능하기 때문이다.
그러나 members[2]의 경우 new 연산자를 사용하지 않아 아직 가리키는 객체의 주소값이 없다.
즉, null을 가지고 있는 상태이다.
그러나 여기에 객체의 필드인 name과 number를 가지고 오려고 했으니, 에러가 나는 것이다.
members[2] = new Person1();
members[2].name = "David";
members[2].number = "23456789";
이렇게 하면 에러가 나지 않는다.
primitive type이 아닌 모든 변수들은 참조 변수이다
int는 primitive type이지만 int []는 참조 변수이다.
int [] num = new int[100];
num은 100개의 원소를 가진 배열을 가리키는 주소를 가진 참조 변수이며 메모리의 스택 영역에 생성된다.
실제 배열 객체는 힙 영역에 생성된다.
JVM의 메모리 사용 영역
JVM은 운영체제에서 할당받은 메모리 영역을 메소드 영역, 힙 영역, 스택 영역으로 구분해서 사용함
메소드 영역에는 정적 필드, 상수, 메소드 코드, 생성자 코드가 위치
힙 영역에는 객체가 생성됨 (ex. new 연산자로 생성된 클래스 인스턴스, 배열 객체 등등...)
스택 영역에는 (참조 + primitive)변수가 생성됨
ref. 혼자 공부하는 자바 p.166
어떻게 보면 스택 영역에는 변수가 생성되는 건 맞지만, 변수의 종류에 따라 참조 변수이면 주소가 저장되고, primitive 변수이면 값이 저장된다고 볼 수 있는 것.
객체는 new 연산자에 의해서만 생성된다
하단의 코드에서 second 변수는 똑같이 first가 가리키는 객체를 가리킨다.
여기서 객체는 딱 한 개이다.
new 연산자에 의해서만 생성되기 때문.
Person1 first;
first = new Person1(); // object
first.name = "John";
first.number = "01234567890";
System.out.println("Name : " + first.name + ", Number : " + first.name);
Person1 second = first; // first라는 참조변수가 가지는 주소를 second가 복사함 -> second도 객체를 참조
값에 의한 호출 (Call By Value) vs. 참조에 의한 호출 (Call By Reference)
Call By Value
- 전달받은 값을 복사하여 값을 호출하는 것을 의미
- 자바는 항상 Call By Value 방식을 이용함
- 인자값을 전달해줄 때 값을 복사하여 전달해줌
- 자바에선 인자가 Primitive type일 경우 값이 독립적인 객체에 복사되어 전달되므로 (깊은 복사) 복사된 객체는 원본 객체와 완전히 분리되어 있어 함수 내에서 인자의 값이 변경되어도 호출한 곳에서 인자로 넣어준 변수를 프린트해봤을 때 변화가 없다.
- 아래 예시는 인자 a, b와 swap 매서드 내에서의 x, y가 서로 다르므로 값이 그대로이다.
- 이는 각각의 변수가 독립적인 값을 가진 객체이기 때문이다.
Class CallByValue{
public static void swap(int x, int y) {
int temp = x;
x = y;
y = temp;
}
public static void main(String[] args) {
int a = 10;
int b = 20;
System.out.println("swap() 호출 전 : a = " + a + ", b = " + b); // swap() 호출 전 : a = 10, b = 20
swap(a, b);
System.out.println("swap() 호출 후 : a = " + a + ", b = " + b); // swap() 호출 후 : a = 10, b = 20
}
}
- 한편 자바에서 인자가 primitive type이 아닌 참조 변수일 경우엔, 참조 변수 안에 담겨 있는 값이 바로 객체의 주소이다.
- 따라서, 메서드 호출 시의 인자와 메서드의 파라미터 인자가 모두 동일한 객체를 가리키는 참조 변수가 되므로 함수 내에서 값이 변경되면 메서드 호출 시의 인자도 같은 객체를 가리키고 있으므로 값이 변하게 된다.
Class CallByReference{
int value;
CallByReference(int value) {
this.value = value;
}
public static void swap(CallByReference x, CallByReference y) {
int temp = x.value;
x.value = y.value;
y.value = temp;
}
public static void main(String[] args) {
CallByReference a = new CallByReference(10);
CallByReference b = new CallByReference(20);
System.out.println("swap() 호출 전 : a = " + a.value + ", b = " + b.value); // swap() 호출 전 : a = 10, b = 20
swap(a, b);
System.out.println("swap() 호출 전 : a = " + a.value + ", b = " + b.value); // swap() 호출 후 : a = 20, b = 10
}
}
Call By Reference
- 인자로 값이 아닌 메모리주소를 넘겨줌으로써 해당 매게변수는 주소를 참조함으로써 객체에 대한 직접적인 핸들링 권한을 가진다.
cf. 얕은 복사 vs. 깊은 복사
얕은 복사(Shallow Copy)
객체를 복사할 때 원본 객체의 참조를 공유하는 방식.
다시 말해, 복사된 객체는 원본 객체의 주소를 참조하게 됨.
따라서 하나의 객체를 수정하면 다른 객체도 영향을 받을 수 있음.
깊은 복사(Deep Copy)
객체를 복사할 때 원본 객체와 독립적인 별개의 객체를 생성하는 방식.
복사된 객체는 원본 객체와 완전히 분리되어 있으므로 하나의 객체를 수정해도 다른 객체에는 영향을 주지 않음.