기본형 vs 참조형
시작
변수의 데이터 타입을 가장 크게 보면 기본형과 참조형으로 분류할 수 있다.
- 기본형(Primitive Type)
- `int`, `long`, `double`, `boolean`처럼 변수에 사용할 값을 직접 넣을 수 있는 데이터 타입 (소문자로 시작한다)
- 자바가 기본으로 제공하는 데이터 타입으로 개발자가 새로 정의할 수 없다.
- 해당 값을 바로 사용할 수 있다.
- 바로 연산 가능
- 참조형(Reference Type)
- `Student student1`, `int[] students`와 같이 데이터에 접근하기 위한 참조(주소)를 저장하는 데이터 타입
- 객체 또는 배열에 사용된다.
- 객체는 `.`(dot)을 통해서 메모리 상에 생성된 객체를 찾아가야 사용할 수 있다.
- 배열은 `[]`를 통해서 메모리 상에 생성된 객체를 찾아가야 사용할 수 있다.
- 바로 연산 불가능
쉽게 이야기해서 기본형 변수에는 직접 사용할 수 있는 값이 들어있지만 참조형 변수에는 위치(참조값)가 들어가 있다. 참조형 변수를 통해서 뭔가 하려면 결국 참조값을 통해 해당 위치로 이동해야 한다.
String
클래스로 참조형이다. 하지만 기본형처럼 문자 값을 바로 대입할 수 있다. 문자는 매우 자주 다루기 때문에 자바에서 특별하게 편의기능을 제공해주기 때문이다.
변수 대입
자바에서 변수에 값을 대입하는 것은 변수에 들어있는 값을 복사해서 대입하는 것이다.
기본형, 참조형 모두 항상 변수에 있는 값을 복사해서 대입한다. 기본형이면 변수에 들어있는 실제 사용하는 값을 복사해서 대입하고, 참조형이면 변수에 들어있는 참조값을 복사해서 대입한다.
기본형과 변수 대입
int a = 10;
int b = a;
변수에 값을 대입하더라도 실제 사용하는 값이 변수에 바로 들어있기 때문에 해당 값만 복사해서 대입한다고 생각하면 된다.
참조형과 변수 대입
Student s1 = new Student();
Student s2 = s1;
실제 사용하는 객체가 아니라 객체의 위치를 가리키는 참조값만 복사된다. 쉽게 이야기해서 실제 건물이 복사가 되는 것이 아니라 건물의 위치인 주소만 복사되는 것이다. 따라서 같은 건물을 찾아갈 수 있는 방법이 하나 늘어날 뿐이다.
참조형과 메서드 호출
public class ClassStart3 {
public static void main(String[] args) {
Student student1;
student1 = new Student();
student1.name = "학생1";
student1.age = 15;
student1.grade = 90;
Student student2 = new Student();
student2.name = "학생2";
student2.age = 16;
student2.grade = 80;
System.out.println("이름:" + student1.name + " 나이:" + student1.age + " 성적:" + student1.grade);
System.out.println("이름:" + student2.name + " 나이:" + student2.age + " 성적:" + student2.grade);
}
}
위 코드에는 중복되는 부분이 2가지 있다.
- `name`, `age`, `grade`에 값을 할당
- 학생 정보를 출력
이런 중복은 메서드를 통해 손쉽게 제거할 수 있다.
public class Method1 {
public static void main(String[] args) {
Student student1 = new Student(); //x001
initStudent(student1, "학생1", 15, 90);
Student student2 = new Student(); //x002
initStudent(student2, "학생2", 16, 80);
printStudent(student1);
printStudent(student2);
}
static void initStudent(Student student, String name, int age, int grade) {
student.name = name;
student.age = age;
student.grade = grade;
}
static void printStudent(Student student) {
System.out.println("이름:" + student.name + " 나이:" + student.age + " 성적:" + student.grade);
}
}
initStudent() 메서드 호출 분석
- 이 메서드를 호출하면 `student1`을 전달한다. 그러면 `student1`의 참조값이 매개변수 `student`에 전달된다. 이 참조값을 통해 `initStudent()` 메서드 안에서 `student1`이 참조하는 것과 동일한 `x001``Student` 인스턴스에 접근하고 값을 변경할 수 있다.
메서드에서 객체 반환
Student student1 = new Student();
initStudent(student1, "학생1", 15, 90);
Student student2 = new Student();
initStudent(student2, "학생2", 16, 80);
객체를 생성하고, 초기화를 설정하는 부분이다. 이렇게 2번 반복되는 부분을 하나로 합쳐보자.
public class Method2 {
public static void main(String[] args) {
Student student1 = createStudent("학생1", 15, 90);
System.out.println("student1=" + student1);
Student student2 = createStudent("학생2", 16, 80);
System.out.println("student2=" + student2);
printStudent(student1);
printStudent(student2);
}
static Student createStudent(String name, int age, int grade) {
Student student = new Student(); //x001
System.out.println("student=" + student);
student.name = name;
student.age = age;
student.grade = grade;
return student; //x001
}
static void printStudent(Student student) {
System.out.println("이름:" + student.name + " 나이:" + student.age + " 성적:" + student.grade);
}
}
createStudent() 메서드 호출 분석
메서드 내부에서 인스턴스를 생성한 후 참조값을 메서드 외부로 반환했다. 이 참조값만 있으면 해당 인스턴스에 접근할 수 있다. 여기서는 `student1`에 참조값을 보관하고 사용한다.
변수와 초기화
변수의 종류
- 멤버 변수(필드): 클래스에 선언
- 지역 변수: 메서드에 선언, 매개변수도 지역 변수의 한 종류이다.
변수의 값 초기화
- 멤버 변수: 자동 초기화
- 인스턴스의 멤버 변수는 인스턴스를 생성할 때 자동으로 초기화된다.
- 숫자(`int`)=`0`, `boolean`=`false`, 참조형 = `null` (`null`값은 참조할 대상이 없다는 뜻으로 사용된다.)
- 개발자가 초기값을 직접 지정할 수 있다.
- 지역 변수: 수동 초기화
- 지역 변수는 항상 직접 초기화해야 한다.
public class InitData {
int value1; //초기화 하지 않음
int value2 = 10; //10으로 초기화
}
`value1`은 초기값을 지정하지 않았고, `value2`는 초기값을 10으로 지정했다.
public class InitMain {
public static void main(String[] args) {
InitData data = new InitData();
System.out.println("value1 = " + data.value1); // 0
System.out.println("value2 = " + data.value2); // 10
}
}
- `value1`은 초기값을 지정하지 않았지만 멤버 변수는 자동으로 초기화 된다. 숫자는 `0`으로 초기화된다.
- `value2`는 `10`으로 초기값을 지정해두었기 때문에 객체를 생성할 때 `10`으로 초기화된다.
null
참조형 변수에서 아직 가리키는 대상이 없다면 `null`이라는 특별한 값을 넣어줄 수 있다. `null`은 값이 존재하지 않는, 없다는 뜻이다.
public class NullMain1 {
public static void main(String[] args) {
Data data = null;
System.out.println("1. data = " + data);
data = new Data();
System.out.println("2. data = " + data);
data = null;
System.out.println("3. data = " + data);
}
}
GC - 아무도 참조하지 않는 인스턴스의 최후
`data`에 `null`을 할당하면 앞서 생성한 `x001` `Data` 인스턴스를 더는 아무도 참조하지 않는다. 이렇게 아무도 참조하지 않게 되면 `x001`이라는 참조값을 다시 구할 방법이 없다. 따라서 해당 인스턴스에 다시 접근할 방법이 없다.
이렇게 아무도 참조하지 않는 인스턴스는 사용되지 않고 메모리 용량만 차지할 뿐이다.
C와 같은 과거 프로그래밍 언어는 개발자가 직접 명령어를 사용해서 인스턴스를 메모리에서 제거해야 했다. 만약 실수로 인스턴스 삭제를 누락하면 메모리에 사용하지 않는 객체가 가득해져서 메모리 부족 오류가 발생하게 된다.
자바는 이런 과정을 자동으로 처리해준다. 아무도 참조하지 않는 인스턴스가 있으면 JVM의 GC(가비지 컬렉션)가 더 이상 사용하지 않는 인스턴스라 판단하고 해당 인스턴스를 자동으로 메모리에서 제거해준다.
객체는 해당 객체를 참조하는 곳이 있으면, JVM이 종료할 때 까지 계속 생존한다. 그런데 중간에 해당 객체를 참조하는 곳이 모두 사라지면 그때 JVM은 필요 없는 객체로 판단다고 GC(가비지 컬렉션)를 사용해서 제거한다.
NullPointerException
참조값이 없이 객체를 찾아가면 `NullPointerException`이라는 예외가 발생한다. `NullPointerException`은 이름 그대로 `null`을 가리키다(Pointer)인데, 이때 발생하는 예외(Exception)다.
`null`은 없다는 뜻이므로 결국 주소가 없는 곳을 찾아갈 때 발생하는 예외이다.
public class NullMain2 {
public static void main(String[] args) {
Data data = null;
data.value = 10; //NullPointerException 예외 발생
System.out.println("data = " + data.value);
}
}
NullMain2 예제와 같이 지역 변수의 경우에는 `null` 문제를 파악하는 것이 어렵지 않다. 하지만 다음과 같이 멤버 변수가 `null`인 경우에는 주의가 필요하다.
public class Data {
int value;
}
public class BigData {
Data data;
int count;
}
`BigData` 클래스는 `Data data`, `int count` 두 변수를 가진다.
public class NullMain3 {
public static void main(String[] args) {
BigData bigData = new BigData();
System.out.println("bigData.count=" + bigData.count);
System.out.println("bigData.data=" + bigData.data);
//NullPointerException
System.out.println("bigData.data.value" + bigData.data.value);
}
}
실행 결과
bigData.count=0
bigData.data=null
Exception in thread "main" java.lang.NullPointerException: Cannot read field
"value" because "bigData.data" is null
at ref.NullMain3.main(NullMain3.java:10)
`BigData`를 생성하면 `BigData`의 인스턴스가 생성된다. 이때 `BigData` 인스턴스의 멤버 변수에 초기화가 일어나는데, `BigData`의 `data`멤버 변수는 참조형이므로 `null`로 초기화된다. `count` 멤버 변수는 숫자 이므로 `0`으로 초기화된다.
- `bigData.count`를 출력하면 `0`이 출력된다.
- `bigData.data`를 출력하면 참조값인 `null`이 출력된다. 이 변수는 아직 아무것도 참조하고 있지 않다.
- `bigData.data.value`를 출력하면 `data`의 값이 `null`이므로 `null`에 `.`(dot)을 찍게 되고, 따라서 참조할 곳이 없으므로 `NullPointerException` 예외가 발생한다.
이 문제를 해결하려면 `Data` 인스턴스를 만들고 `BigData.data` 멤버 변수에 참조값을 할당하면 된다.
public class NullMain4 {
public static void main(String[] args) {
BigData bigData = new BigData();
bigData.data = new Data();
System.out.println("bigData.count=" + bigData.count);
System.out.println("bigData.data=" + bigData.data);
System.out.println("bigData.data.value" + bigData.data.value);
}
}
실행 결과
bigData.count=0
bigData.data=ref.Data@x002
bigData.data.value=0
`NullPointerException`이 발생하면 `null`값에 `.`(dot)을 찍었다고 생각하면 문제를 쉽게 찾을 수 있다.
'Java' 카테고리의 다른 글
[Java/김영한] 생성자 (1) | 2024.09.08 |
---|---|
[Java/김영한] 객체 지향 프로그래밍 (0) | 2024.09.07 |
[Java/김영한] 클래스와 데이터 (0) | 2024.09.06 |
[Java/김영한] 메서드 (0) | 2024.08.15 |
[Java/김영한] 배열 (0) | 2024.08.14 |