클래스와 인스턴스
new가 Member 객체를 만들고 참조값을 반환한 뒤 필드에 접근하는 순서를 메모리 흐름과 함께 추적합니다.
앞 절에서 Member라는 타입을 선언했지만 아직 회원 정보는 하나도 존재하지 않습니다.
클래스는 필드의 구성만 설명합니다.
실제 이메일과 나이가 들어갈 저장 공간은 new Member()를 실행해야 생깁니다.
이 차이를 놓치면 “클래스를 만들었는데 왜 값이 저장되지 않지?” 또는 “변수에 객체 전체가 들어 있나?” 같은 혼란이 이어집니다.
이번 절의 핵심은 문법 한 줄을 네 단계로 풀어 읽는 것입니다.
Member member = new Member();오른쪽의 new가 인스턴스를 만들고, JVM이 그 인스턴스에 도달할 수 있는 참조값을 돌려주며, 왼쪽 변수는 그 참조값을 보관합니다.
클래스는 공통 구조를 선언하고 인스턴스는 그 구조대로 실제 상태를 보관합니다.
- Class · 구조
Member의 필드와 메서드 정의 · 그 자체로 기록 상태 없음
- Instance · 상태
x101: kim@example.com/25 · x202: lee@example.com/40처럼 객체마다 별도 값
클래스와 인스턴스
먼저 클래스 파일과 실행 파일을 분리합니다.
public class Member {
String email;
int age;
boolean active;
}public class MemberMain {
public static void main(String[] args) {
Member member = new Member();
member.email = "member@example.com";
member.age = 45;
member.active = false;
System.out.printf(
"%s / %d세 / %s%n",
member.email, member.age, member.active
);
}
}두 파일을 함께 컴파일하고 main이 있는 클래스를 실행합니다.
javac -d out src/members/ch04/v4/Member.java src/members/ch04/v4/MemberMain.java
java -cp out MemberMain
member@example.com / 45세 / falseMember.java만 컴파일해도 클래스 파일은 만들 수 있지만 실행 시작점인 main이 없습니다.
반대로 MemberMain은 Member 타입을 사용하므로 두 소스가 모두 컴파일 대상에 있어야 합니다.
클래스 선언과 프로그램 실행 역할을 분리해 두면 이후 여러 프로그램이 같은 타입을 재사용할 수 있습니다.
new의 실행 순서
Member member = new Member();를 한 덩어리로 외우지 말고 다음 순서로 읽어 봅시다.
- JVM이
Member인스턴스를 저장할 공간을 힙에 확보합니다. - 각 필드를 타입의 기본값으로 초기화합니다.
email은null,age는0,active는false입니다. - 새 인스턴스에 도달할 수 있는 참조값을 결과로 돌려줍니다.
- 지역 변수
member에 그 참조값을 복사합니다.
실제 참조값의 숫자나 주소 표현은 JVM 구현에 맡겨집니다.
교재에서는 관계를 설명하려고 x101 같은 기호를 쓰지만 애플리케이션이 그 값을 계산하거나 바꾸지는 않습니다.
new는 객체 공간을 만들고 초기화한 뒤 그 객체에 도달할 참조값을 결과로 냅니다.
- 표현식
new Member()
- 할당
힙에 x101 공간
- 초기화
필드 기본값·생성자
- 결과
참조 x101
- 대입
member = x101
다음 프로그램으로 필드를 대입하기 전의 기본값을 확인할 수 있습니다.
public class DefaultFieldValues {
public static void main(String[] args) {
Member member = new Member();
System.out.println("email=" + member.email);
System.out.println("age=" + member.age);
System.out.println("active=" + member.active);
}
}email=null
age=0
active=false기본값이 있다는 사실과 그 값이 업무적으로 올바르다는 사실은 다릅니다.
이메일 없는 회원 정보를 허용하지 않을 예정이라면 객체를 만든 직후 유효한 값을 넣어야 합니다.
생성 시점에 유효성을 강제하는 방법은 생성자를 다루는 장에서 개선합니다.
점 연산자와 참조
member.email을 “변수 안에 있는 email”이라고만 이해하면 참조형이 어려워집니다.
실제로는 다음 두 동작이 결합됩니다.
member변수에서 참조값을 읽어 인스턴스에 도달합니다.- 그 인스턴스의
email필드를 선택합니다.
member.email = "member@example.com";
int copiedAge = member.age;첫 줄은 참조를 따라가 email 필드에 문자열 참조를 저장합니다.
둘째 줄은 같은 방식으로 age 필드에서 숫자 45를 읽어 copiedAge에 복사합니다.
점 왼쪽에 객체가 아니라 null이 있으면 첫 단계에서 경로가 끊깁니다.
그 실패는 뒤의 null 절에서 자세히 다룹니다.
member.email은 참조를 해석한 뒤 도착한 객체에서 email 슬롯을 선택하는 경로입니다.
- 변수 읽기
member
- 참조 해석
x101
- 객체 도착
Member@x101
- 필드 선택
email
- 값 읽기
"member@example.com"
참조 변수와 객체 생성
다음 코드는 컴파일되지 않습니다.
public class UninitializedReference {
public static void main(String[] args) {
Member member;
System.out.println(member.email);
}
}error: variable member might not have been initializedMember member;는 참조값을 담을 지역 변수만 선언합니다.
객체도 만들지 않았고 변수에 null조차 대입하지 않았습니다.
자바는 초기화되지 않은 지역 변수를 읽는 것을 컴파일 단계에서 막습니다.
아래 코드는 컴파일되지만 실행 중 실패합니다.
Member member = null;
System.out.println(member.email);이번에는 변수에 null이라는 값이 분명히 들어 있으므로 컴파일러 규칙은 통과합니다.
그러나 실행할 때 따라갈 인스턴스가 없어 NullPointerException이 발생합니다.
“변수 선언”, “참조값 대입”, “객체 생성”을 같은 동작으로 보지 않아야 두 오류의 시점을 구분할 수 있습니다.
다중 인스턴스
클래스는 한 번 선언하고 new는 필요한 만큼 호출할 수 있습니다.
public class TwoMembers {
public static void main(String[] args) {
Member firstMember = new Member();
firstMember.email = "kim@example.com";
firstMember.age = 25;
Member secondMember = new Member();
secondMember.email = "lee@example.com";
secondMember.age = 40;
System.out.println(firstMember.email + " / " + firstMember.age);
System.out.println(secondMember.email + " / " + secondMember.age);
}
}두 변수의 타입과 두 인스턴스의 필드 구조는 같지만 저장된 상태는 독립적입니다.
firstMember.age를 30으로 바꿔도 secondMember.age는 40으로 남습니다.
각각의 new가 다른 인스턴스를 만들고 다른 참조값을 반환했기 때문입니다.
하나의 클래스 정의가 여러 인스턴스의 공통 구조가 되지만 각 객체의 필드 값은 독립적입니다.
- x101
email=kim@example.com · age=25
- x202
email=lee@example.com · age=40
- x303
email=park@example.com · age=50
디버깅할 때는 클래스 이름보다 참조 관계를 먼저 봅니다.
같은 Member 타입이라고 같은 객체는 아닙니다.
반대로 변수 이름이 다르더라도 같은 참조값이 복사되면 같은 객체를 바라볼 수 있습니다.
그 공유 문제는 기본형과 참조형을 비교할 때 직접 재현합니다.
연습 문제
다음 조건을 만족하는 프로그램을 작성하세요.
Member인스턴스를 두 개 만듭니다.- 첫 회원은 이메일
"kim@example.com", 나이 25세, 활성 상태true로 설정합니다. - 둘째 회원은 이메일
"lee@example.com", 나이 40세, 활성 상태false로 설정합니다. - 두 인스턴스의 필드가 서로 영향을 주지 않는지 확인하기 위해 첫 회원의 나이를 30세로 바꾸고 둘 다 다시 출력합니다.
Member shared = first;를 추가했을 때shared.age = 99가 어느 인스턴스를 바꾸는지 실행 전에 예측합니다.
해설 보기
public class TwoMemberExercise {
public static void main(String[] args) {
Member first = new Member();
first.email = "kim@example.com";
first.age = 25;
first.active = true;
Member second = new Member();
second.email = "lee@example.com";
second.age = 40;
second.active = false;
first.age = 30;
System.out.println(first.email + "=" + first.age);
System.out.println(second.email + "=" + second.age);
Member shared = first;
shared.age = 99;
System.out.println("first=" + first.age);
System.out.println("second=" + second.age);
}
}kim@example.com=30
lee@example.com=40
first=99
second=40첫 번째와 두 번째 new는 서로 다른 객체를 만들었습니다.
shared = first에서는 새 객체를 만들지 않고 첫 객체의 참조값을 복사했으므로 shared와 first가 같은 필드를 읽고 씁니다.
이 결과를 아직 완전히 설명하기 어렵다면 정상입니다.
다음 두 절에서 객체 배열과 참조값 복사를 차례로 분해합니다.
클래스는 필드 구조이고 인스턴스는 new가 만든 실제 상태입니다.
변수에는 인스턴스 전체가 아니라 그 상태에 도달하는 참조값이 들어갑니다.
다음 절에서는 여러 참조를 배열에 넣을 때 메모리 구조가 어떻게 한 층 더 생기는지 살펴봅니다.