클래스와 인스턴스
new가 Member 객체를 만들고 그 참조값을 변수에 넘기는 과정을 단계별로 따라가고, 점 연산자로 필드를 읽고 쓸 때 실제로 일어나는 일을 이해합니다.
앞 글에서 Member 클래스를 선언했지만, 아직 회원 정보는 하나도 존재하지 않습니다. 클래스는 회원 정보가 어떤 필드로 이루어지는지만 정해 둔 것이고, 실제 이메일과 나이를 담을 공간은 new Member()를 실행해야 생깁니다. 이 차이를 놓치면 '클래스를 만들었는데 값은 어디에 저장되지?'라든가 '변수 안에 객체가 통째로 들어 있나?' 같은 혼란이 생깁니다.
이 글에서는 다음 한 줄을 여러 단계로 나눠 읽는 연습을 합니다. 오른쪽의 new가 객체를 만들고, 그 객체를 찾아갈 수 있는 참조값을 돌려주면, 왼쪽 변수가 그 참조값을 보관합니다. 이 순서를 정확히 알면 이 장의 나머지 내용인 객체 배열, 대입, 메서드 호출, null이 모두 같은 원리로 설명됩니다.
Member member = new Member();클래스는 틀이고 인스턴스는 실제 값입니다
클래스와 객체의 관계는 회원가입 신청서 양식과 작성된 신청서에 빗대어 볼 수 있습니다. 양식에는 '이메일', '나이' 같은 칸만 있고 내용은 비어 있습니다. 양식 하나로 신청서를 몇 장이든 작성할 수 있고, 각 신청서에 적힌 내용은 서로 독립적입니다. Member 클래스가 양식이라면, new Member()로 만든 객체 하나하나가 작성된 신청서입니다.
이렇게 어떤 클래스로 만든 객체를 그 클래스의 인스턴스(instance)라고 부릅니다. '객체'와 '인스턴스'는 거의 같은 뜻으로 쓰이지만, 'Member의 인스턴스'처럼 말하면 어느 클래스로 만든 객체인지를 강조하게 됩니다. 다만 이 비유는 한 군데에서 맞지 않습니다. 신청서는 손에 직접 들 수 있지만, 자바의 변수는 객체를 직접 담지 않고 객체가 있는 곳을 가리키는 참조값만 담습니다. 이 차이가 이 장의 핵심이므로 아래에서 차근차근 확인하겠습니다.
첫 Member 객체 만들기
먼저 클래스 선언과 실행용 클래스를 두 파일로 나눕니다. Member.java는 앞 글에서 선언한 세 필드짜리 클래스이고, MemberMain.java에는 실행이 시작되는 main 메서드가 있습니다.
public class Member {
String email;
int age;
boolean active;
}public class MemberMain {
public static void main(String[] args) {
Member member = new Member();
member.email = "[email protected]";
member.age = 45;
member.active = false;
System.out.printf(
"%s / %d세 / %s%n",
member.email, member.age, member.active
);
}
}main은 먼저 Member 객체를 하나 만들고, 그 객체의 세 필드에 차례로 값을 넣은 다음, 필드 값을 읽어 한 줄로 출력합니다. 두 파일을 함께 컴파일하고 main이 있는 클래스를 실행합니다.
javac -d out src/members/ch04/v4/Member.java src/members/ch04/v4/MemberMain.java
java -cp out MemberMain[email protected] / 45세 / false출력의 세 값은 main에서 대입한 필드 값이 그대로 나온 것입니다. printf의 첫 번째 %s에 member.email이, %d에 member.age가, 두 번째 %s에 member.active가 들어갔습니다.
두 파일을 함께 지정하는 데는 이유가 있습니다. MemberMain은 Member라는 타입을 사용하므로, 컴파일러는 MemberMain을 컴파일하면서 Member의 정의도 알아야 합니다. 그런데 작업 폴더에서 MemberMain.java만 지정하면 컴파일러는 src/members/ch04/v4 안의 Member.java를 찾지 못하고 cannot find symbol 오류와 함께 symbol: class Member라고 알려 줍니다. 반대로 Member.java만 컴파일하면 클래스 파일은 생기지만, main 메서드가 없으므로 java -cp out Member로 실행할 수는 없습니다. 이미 컴파일해 둔 Member.class가 있는 폴더를 클래스 경로로 알려 주는 방법도 있지만, 이 장에서는 필요한 소스 파일을 함께 지정하는 방식으로 통일합니다.
이 장의 뒤 예제들도 같은 세 필드의 Member를 사용합니다. 코드 제목에 적힌 예제 폴더마다 Member.java를 하나 두고, 실행할 클래스와 함께 컴파일하세요. 이때 다른 폴더의 Member.java까지 한꺼번에 지정하면 안 됩니다. 이름이 같은 클래스가 두 번 선언된 셈이 되어 duplicate class: Member 오류가 납니다.
new Member()가 하는 일
이제 Member member = new Member(); 한 줄이 실행될 때 무슨 일이 일어나는지 순서대로 보겠습니다. 표의 마지막 열은 그 단계가 끝난 직후의 상태입니다.
| 단계 | 하는 일 | 직후의 상태 |
|---|---|---|
| 1 | 힙(heap)에 Member 객체 하나가 들어갈 공간을 확보합니다 | 이름 없는 새 객체가 생깁니다 |
| 2 | 모든 필드를 타입별 기본값으로 채웁니다 | email은 null, age는 0, active는 false |
| 3 | 생성자를 실행합니다 | Member에는 생성자를 선언하지 않았으므로 바뀌는 것이 없습니다 |
| 4 | 새 객체를 가리키는 참조값을 new 식의 결과로 돌려줍니다 | 이 교재에서는 이 값을 x101처럼 적습니다 |
| 5 | =가 그 참조값을 지역 변수 member에 저장합니다 | member에 x101이 들어 있습니다 |
1단계의 힙은 자바 프로그램이 객체를 두는 메모리 영역입니다. 메모리 영역들의 역할은 6장에서 자세히 다루므로, 지금은 '객체는 변수와 다른 곳에 따로 만들어진다'는 점만 기억하면 됩니다. 3단계의 생성자(constructor)는 객체를 만들 때 실행되는 초기화 코드로 5장에서 배웁니다. 지금의 Member처럼 생성자를 하나도 선언하지 않으면 컴파일러가 내용이 없는 기본 생성자를 자동으로 넣어 주기 때문에, 이 단계에서 필드 값은 그대로입니다. 필드 선언에 int age = 14;처럼 초깃값을 적었다면 바로 이 단계에서 그 값이 대입됩니다.
표를 보면 일이 둘로 나뉜다는 것을 알 수 있습니다. 객체를 만들고 참조값을 내놓는 것(1단계부터 4단계까지)은 new가 하고, 그 참조값을 변수에 넣는 것(5단계)은 대입이 합니다. 실제로 new Member();라고만 써도 컴파일되고 객체도 만들어집니다. 하지만 돌려받은 참조값을 아무 변수에도 저장하지 않았으므로, 그 객체를 다시 찾아갈 방법은 없습니다.
x101은 설명을 위해 붙인 이름일 뿐이고, 실제 참조값이 어떤 모양인지는 JVM 구현이 정합니다. 자바 프로그램은 참조값에 숫자를 더하는 식의 계산을 할 수 없고, 원하는 참조값을 직접 만들어 넣을 수도 없습니다. 참조값으로 하는 일은 주로 다른 변수에 복사하고, 두 참조가 같은지 비교하고, 그것을 따라가 객체를 쓰는 것입니다.
필드의 기본값 확인하기
2단계에서 필드가 기본값을 받는다는 것을 직접 확인해 보겠습니다. 다음 프로그램은 객체를 만든 직후, 아무 필드에도 값을 넣지 않은 상태로 세 필드를 출력합니다.
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=falseemail은 String 타입, 즉 참조형이므로 기본값이 null입니다. null은 '아무 객체도 가리키지 않는다'는 뜻의 특별한 참조값입니다. int인 age는 0, boolean인 active는 false로 시작합니다. 3장에서 new int[3]의 칸이 0으로, new String[3]의 칸이 null로 채워졌던 것과 같은 규칙입니다.
기본값이 있다는 것과 그 값이 올바른 회원 정보라는 것은 다른 문제입니다. 이메일이 null이고 나이가 0인 회원은 실제로는 의미가 없지만, 지금의 Member는 이런 객체가 만들어지는 것을 막지 못합니다. 그래서 객체를 만든 직후에 필드마다 올바른 값을 넣어야 하고, 하나라도 빠뜨리면 기본값이 그대로 남습니다. 객체를 만들 때 필요한 값을 반드시 받도록 강제하는 방법은 5장의 생성자에서 다룹니다.
점 연산자는 참조를 따라갑니다
member.email을 '변수 member 안에 있는 email'이라고 읽으면, 변수 안에 객체가 통째로 들어 있다고 착각하기 쉽습니다. 실제로는 두 동작이 이어서 일어납니다. 먼저 변수 member에서 참조값을 읽어 그 참조가 가리키는 객체를 찾아가고, 그다음 그 객체의 email 필드를 고릅니다.
member.email = "[email protected]";
int copiedAge = member.age;첫 줄은 member의 참조값 x101을 따라가 그 객체의 email 필드에 문자열을 저장합니다. String도 참조형이므로, 정확히 말하면 필드에 저장되는 것은 문자열 객체를 가리키는 참조값입니다. 둘째 줄도 같은 방법으로 객체를 찾아가되, 이번에는 age 필드의 값 45를 읽어 지역 변수 copiedAge에 복사합니다. 복사가 끝난 뒤에는 두 값이 따로 존재하므로, 나중에 member.age를 바꿔도 copiedAge는 45 그대로입니다.
아래 그림은 같은 Member 선언을 쓰는 두 프로그램의 상태를 나란히 보여 줍니다. 왼쪽은 DefaultFieldValues에서 객체를 만든 직후의 기본값이고, 오른쪽은 MemberMain처럼 필드에 값을 넣은 뒤 int copiedAge = member.age;로 45를 복사하는 모습입니다.
생성 기본값과 필드 대입을 구분하고 참조를 따라 읽는다
new Member의 기본값과 명시 필드 대입 후 copiedAge 읽기를 두 독립 프로그램으로 비교합니다. 참조를 따라 필드를 쓰고 숫자만 복사하는 전후를 단계로 설명할 수 있습니다.
new Member()의 인스턴스 생성과 참조 대입을 구분한다. DefaultFieldValues의 null, 0, false와 별도 MemberMain의 [email protected], 45, false를 비교하고 member.age 읽기의 결과 45를 설명한다.
객체 생성과 필드 대입은 다른 단계입니다
new 직후의 기본값과 사용자가 필드를 대입한 상태를 비교합니다. 변수에는 객체의 참조값이 들어 있습니다.
Member member = new Member();
new Member()가 새 인스턴스를 만들고 필드를 기본값으로 초기화합니다.- 그 인스턴스의 참조값을 돌려주면 지역 변수
member가 참조값을 보관합니다.
| 인스턴스 필드 | DefaultFieldValues · new 직후 | MemberMain · 필드 대입 이후 |
|---|---|---|
email | null | "[email protected]" |
age | 0 | 45 |
active | false · 기본값 | false · 명시적 대입값 |
같은 Member 선언을 사용하는 서로 다른 두 프로그램의 상태 비교입니다.
- DefaultFieldValues · new 직후
email = nullage = 0active = false· 기본값
MemberMain의 대입 이후 int copiedAge = member.age;를 실행하면, member의 참조를 따라 인스턴스의 age 필드에서 읽은 45가 copiedAge에 복사됩니다.
이 예의 조건
- DefaultFieldValues와 MemberMain은 독립 프로그램입니다.
관계도의 보충 설명
| 구분 | 내용 |
|---|---|
| 2 | 독립 DefaultFieldValues의 null/0/false와 MemberMain의 [email protected]/45/false. 각 member→객체 참조, 뒤 프로그램만 age 45를 copiedAge에 값으로 복사한다. |
| 구분 | 내용 |
|---|---|
| 설명 | 독립 DefaultFieldValues의 null/0/false와 MemberMain의 [email protected]/45/false. 각 member→객체 참조, 뒤 프로그램만 age 45를 copiedAge에 값으로 복사한다. |
| 전체 읽기 정보 | 생성 기본값과 필드 대입을 구분하고 참조를 따라 읽는다 생성 기본값과 필드 대입을 구분하고 참조를 따라 읽는다 new Member()의 인스턴스 생성과 참조 대입을 구분한다. DefaultFieldValues의 null, 0, false와 별도 MemberMain의 [email protected], 45, false를 비교하고 member.age 읽기의 결과 45를 설명한다. 객체 생성과 필드 대입은 다른 단계입니다 new 직후의 기본값과 사용자가 필드를 대입한 상태를 비교합니다. 변수에는 객체의 참조값이 들어 있습니다. Member member = new Member(); new Member() 가 새 인스턴스를 만들고 필드를 기본값으로 초기화합니다. 그 인스턴스의 참조값을 돌려주면 지역 변수 member 가 참조값을 보관합니다. 같은 Member 선언을 사용하는 서로 다른 두 프로그램의 상태 비교 인스턴스 필드 DefaultFieldValues · new 직후 MemberMain · 필드 대입 이후 email null "[email protected]" age 0 45 active false · 기본값 false · 명시적 대입값 같은 Member 선언을 사용하는 서로 다른 두 프로그램의 상태 비교입니다. DefaultFieldValues · new 직후 email = null age = 0 active = false · 기본값 MemberMain · 필드 대입 이후 email = "[email protected]" age = 45 active = false · 명시적 대입값 MemberMain의 대입 이후 int copiedAge = member.age; 를 실행하면, member 의 참조를 따라 인스턴스의 age 필드에서 읽은 45 가 copiedAge 에 복사됩니다. |
| 관찰·상황 | 값·보충 정보 | 변화·이유 |
|---|---|---|
| 객체 생성 member 지역 변수가 new Member의 참조를 받습니다. |
|
|
| 명시적 필드 대입 MemberMain에서 email과 age를 설정합니다. |
|
|
| 숫자 복사 MemberMain이 age를 copiedAge로 읽습니다. |
|
|
생성 기본값과 필드 대입을 구분하고 참조를 따라 읽는다
new Member의 기본값과 명시 필드 대입 후 copiedAge 읽기를 두 독립 프로그램으로 비교합니다. 참조를 따라 필드를 쓰고 숫자만 복사하는 전후를 단계로 설명할 수 있습니다.
new Member()의 인스턴스 생성과 참조 대입을 구분한다. DefaultFieldValues의 null, 0, false와 별도 MemberMain의 [email protected], 45, false를 비교하고 member.age 읽기의 결과 45를 설명한다.
객체 생성과 필드 대입은 다른 단계입니다
new 직후의 기본값과 사용자가 필드를 대입한 상태를 비교합니다. 변수에는 객체의 참조값이 들어 있습니다.
Member member = new Member();
new Member()가 새 인스턴스를 만들고 필드를 기본값으로 초기화합니다.- 그 인스턴스의 참조값을 돌려주면 지역 변수
member가 참조값을 보관합니다.
| 인스턴스 필드 | DefaultFieldValues · new 직후 | MemberMain · 필드 대입 이후 |
|---|---|---|
email | null | "[email protected]" |
age | 0 | 45 |
active | false · 기본값 | false · 명시적 대입값 |
같은 Member 선언을 사용하는 서로 다른 두 프로그램의 상태 비교입니다.
- DefaultFieldValues · new 직후
email = nullage = 0active = false· 기본값
MemberMain의 대입 이후 int copiedAge = member.age;를 실행하면, member의 참조를 따라 인스턴스의 age 필드에서 읽은 45가 copiedAge에 복사됩니다.
이 예의 조건
- DefaultFieldValues와 MemberMain은 독립 프로그램입니다.
관계도의 보충 설명
| 구분 | 내용 |
|---|---|
| 2 | 독립 DefaultFieldValues의 null/0/false와 MemberMain의 [email protected]/45/false. 각 member→객체 참조, 뒤 프로그램만 age 45를 copiedAge에 값으로 복사한다. |
| 구분 | 내용 |
|---|---|
| 설명 | 독립 DefaultFieldValues의 null/0/false와 MemberMain의 [email protected]/45/false. 각 member→객체 참조, 뒤 프로그램만 age 45를 copiedAge에 값으로 복사한다. |
| 전체 읽기 정보 | 생성 기본값과 필드 대입을 구분하고 참조를 따라 읽는다 생성 기본값과 필드 대입을 구분하고 참조를 따라 읽는다 new Member()의 인스턴스 생성과 참조 대입을 구분한다. DefaultFieldValues의 null, 0, false와 별도 MemberMain의 [email protected], 45, false를 비교하고 member.age 읽기의 결과 45를 설명한다. 객체 생성과 필드 대입은 다른 단계입니다 new 직후의 기본값과 사용자가 필드를 대입한 상태를 비교합니다. 변수에는 객체의 참조값이 들어 있습니다. Member member = new Member(); new Member() 가 새 인스턴스를 만들고 필드를 기본값으로 초기화합니다. 그 인스턴스의 참조값을 돌려주면 지역 변수 member 가 참조값을 보관합니다. 같은 Member 선언을 사용하는 서로 다른 두 프로그램의 상태 비교 인스턴스 필드 DefaultFieldValues · new 직후 MemberMain · 필드 대입 이후 email null "[email protected]" age 0 45 active false · 기본값 false · 명시적 대입값 같은 Member 선언을 사용하는 서로 다른 두 프로그램의 상태 비교입니다. DefaultFieldValues · new 직후 email = null age = 0 active = false · 기본값 MemberMain · 필드 대입 이후 email = "[email protected]" age = 45 active = false · 명시적 대입값 MemberMain의 대입 이후 int copiedAge = member.age; 를 실행하면, member 의 참조를 따라 인스턴스의 age 필드에서 읽은 45 가 copiedAge 에 복사됩니다. |
| 관찰·상황 | 값·보충 정보 | 변화·이유 |
|---|---|---|
| 객체 생성 member 지역 변수가 new Member의 참조를 받습니다. |
|
|
| 명시적 필드 대입 MemberMain에서 email과 age를 설정합니다. |
|
|
| 숫자 복사 MemberMain이 age를 copiedAge로 읽습니다. |
|
|
그렇다면 점 왼쪽의 변수에 객체 대신 null이 들어 있으면 어떻게 될까요? 첫 번째 동작인 '참조를 따라가기'에서 찾아갈 객체가 없으므로 실행이 멈춥니다. 바로 다음 절에서 이 경우를 직접 확인합니다.
선언, null 대입, 객체 생성은 서로 다른 일입니다
다음 코드는 컴파일되지 않습니다. 실행 중에 NullPointerException이 나는 것이 아니라, 그보다 앞서 javac가 오류를 내고 멈춘다는 점을 보세요.
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조차 넣지 않았습니다. 필드와 달리 지역 변수는 기본값을 받지 않기 때문에, 자바 컴파일러는 값을 넣기 전에 읽으려는 코드를 컴파일 단계에서 막습니다.
다음 코드는 컴파일은 되지만 실행하다가 멈춥니다.
public class NullReference {
public static void main(String[] args) {
Member member = null;
System.out.println(member.email);
}
}Exception in thread "main" java.lang.NullPointerException: Cannot read field "email" because "<local1>" is null
at NullReference.main(NullReference.java:4)이번에는 변수에 null이라는 값이 분명히 들어 있으므로 컴파일러는 통과시킵니다. 하지만 실행할 때 member.email이 참조를 따라가려고 보니 가리키는 객체가 없어서 NullPointerException이 발생합니다. 메시지의 <local1>은 '1번 지역 변수'라는 뜻입니다. 0번은 args가 차지하고 member가 1번입니다. javac는 기본 설정에서 지역 변수의 이름 정보를 클래스 파일에 남기지 않기 때문에 이름 대신 번호가 나왔습니다. javac -g로 컴파일하면 "member" is null처럼 이름이 보이고, IDE는 대개 이 정보를 남기도록 설정되어 있어서 IDE에서 실행해도 이름이 보입니다. null과 이 예외는 같은 장의 null과 NullPointerException에서 더 자세히 다룹니다.
두 오류는 시점이 다릅니다. 앞의 것은 컴파일할 때, 뒤의 것은 실행할 때 일어납니다. 변수를 선언하는 일, 변수에 참조값(또는 null)을 넣는 일, new로 객체를 만드는 일을 서로 다른 동작으로 구분해 두면 어떤 오류가 왜 그 시점에 나는지 설명할 수 있습니다.
같은 클래스로 인스턴스를 여러 개 만들기
클래스는 한 번만 선언하고, new는 필요한 만큼 호출할 수 있습니다. 다음 프로그램은 Member 인스턴스 두 개를 만들어 각각 다른 값을 넣습니다.
public class TwoMembers {
public static void main(String[] args) {
Member firstMember = new Member();
firstMember.email = "[email protected]";
firstMember.age = 25;
Member secondMember = new Member();
secondMember.email = "[email protected]";
secondMember.age = 40;
System.out.println(firstMember.email + " / " + firstMember.age);
System.out.println(secondMember.email + " / " + secondMember.age);
}
}[email protected] / 25
[email protected] / 40두 변수의 타입도 같고 두 객체의 필드 구성도 같지만, 저장된 값은 서로 독립적입니다. new를 실행할 때마다 새 객체가 만들어지고, 각 new가 서로 다른 참조값을 돌려주었기 때문입니다. 첫 번째 new가 x101을, 두 번째 new가 x202를 돌려주었다고 생각하면 됩니다. 그래서 firstMember.age를 30으로 바꾸어도 secondMember.age는 40으로 남습니다.
디버깅할 때는 변수의 타입보다 그 변수가 어느 객체를 가리키는지를 먼저 확인해야 합니다. 같은 Member 타입이라고 해서 같은 객체인 것은 아닙니다. 반대로 이름이 다른 두 변수가 같은 참조값을 가지면 같은 객체를 가리키게 되는데, 이 경우는 연습 문제에서 먼저 맛보고 기본형과 참조형 복사에서 자세히 다룹니다.
더 깊이: 참조값은 메모리 주소일까
자바 명세는 참조값을 '객체를 가리키는 포인터'라고 설명하지만, 그것이 실제 메모리 주소인지는 정하지 않습니다. HotSpot 같은 JVM은 내부적으로 주소에 가까운 값을 쓰기도 하지만, 가비지 컬렉터가 객체를 다른 위치로 옮기면 그 값도 바뀝니다. 자바 언어가 참조값의 정체를 숨기고 복사·비교·따라가기만 허용하는 덕분에, JVM은 프로그램을 깨뜨리지 않고 객체를 옮길 수 있습니다.
참조 변수를 그대로 출력하면 Member@2a139a55 같은 문자열이 나와서 주소처럼 보일 수 있습니다. 이것은 Object의 기본 toString이 만든 '클래스 이름 + @ + 해시 코드의 16진수 표기'입니다. 해시 코드는 한 번 정해지면 객체가 옮겨져도 바뀌지 않는 정수라서 객체의 현재 위치를 알려 주지 않으며, 실행 환경에 따라 다른 숫자가 나올 수 있습니다. toString과 해시 코드는 9장에서 다룹니다.
정리
- 클래스는 필드 구성을 정한 틀이고, 인스턴스는
new로 만든 실제 객체입니다. 같은 클래스로 서로 독립적인 인스턴스를 여러 개 만들 수 있습니다. new Member()는 공간 확보, 기본값 설정, 생성자 실행을 거쳐 새 객체의 참조값을 돌려줍니다. 그 참조값을 변수에 넣는 것은 대입입니다.- 필드는
null,0,false같은 기본값을 받지만 지역 변수는 받지 않습니다. member.email은 참조를 따라가 객체를 찾은 뒤 필드를 고르는 두 단계 동작입니다. 참조가null이면NullPointerException이 납니다.
연습 문제
다음 프로그램을 작성하세요.
Member인스턴스를 두 개 만듭니다. 첫 회원은 이메일"[email protected]", 나이 25세, 활성 상태true이고, 둘째 회원은 이메일"[email protected]", 나이 40세, 활성 상태false입니다.- 첫 회원의 나이를 30세로 바꾼 뒤 두 회원의 이메일과 나이를 출력해, 한 객체의 변경이 다른 객체에 영향을 주지 않는지 확인합니다.
- 이어서
Member shared = first;와shared.age = 99;를 추가하고 두 회원의 나이를 다시 출력합니다. 실행하기 전에 어느 회원의 나이가 99가 될지 먼저 예측해 보세요.
해설 보기
public class TwoMemberExercise {
public static void main(String[] args) {
Member first = new Member();
first.email = "[email protected]";
first.age = 25;
first.active = true;
Member second = new Member();
second.email = "[email protected]";
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);
}
}[email protected]=30
[email protected]=40
first=99
second=40처음 두 줄은 2번의 결과입니다. 두 new가 서로 다른 객체를 만들었으므로 first.age = 30은 첫 객체만 바꾸고, 둘째 회원은 40 그대로입니다.
3번의 Member shared = first;는 새 객체를 만들지 않습니다. new가 없으므로 객체는 여전히 두 개뿐이고, first에 들어 있던 참조값이 shared에 복사될 뿐입니다. 그래서 shared와 first는 같은 객체를 가리키고, shared.age = 99는 그 객체의 age 필드를 바꿉니다. 첫 객체의 나이는 25에서 30을 거쳐 99가 되었고, first.age로 읽어도 99가 나옵니다. 둘째 객체를 가리키는 변수는 second뿐이므로 40이 유지됩니다.
TwoMemberExercise: shared.age = 99 이후의 참조 관계
서로 다른 new 두 객체의 나이 30/40에서 shared=first 후 공유 객체만 99로 변경됩니다. 참조 변수 셋과 인스턴스 둘을 고정하여 공유 변경을 추적할 수 있습니다.
TwoMemberExercise의 shared.age=99 이후를 기준으로 first와 shared의 공유 객체와 second의 독립 객체를 구분한다. 첫 age는 25에서30을 거쳐99, 둘째는40을 유지한다.
세 참조 변수가 두 객체에 도달합니다
first와 shared는 같은 객체를, second는 별도 객체를 가리킵니다. shared.age = 99는 첫 객체만 바꿉니다.
TwoMemberExercise · shared.age = 99; 이후
- 첫 객체의 age: 25 → 30 → 99
first.age = 30과shared.age = 99는 같은 객체의 필드에 차례로 대입합니다.- 둘째 객체의 age: 40 유지
second는 다른 인스턴스를 가리키므로 위 두 대입의 영향을 받지 않습니다.
화살표는 참조가 가리키는 객체를 뜻합니다. Member shared = first;는 참조값만 복사하므로 변수는 3개, 인스턴스는 2개입니다.
이 예의 조건
- first·shared는 같은 객체, second는 다른 객체입니다.
관계도의 보충 설명
| 구분 | 내용 |
|---|---|
| 전체 읽기 정보 | TwoMemberExercise: shared.age = 99 이후의 참조 관계 TwoMemberExercise: shared.age = 99 이후의 참조 관계 TwoMemberExercise의 shared.age=99 이후를 기준으로 first와 shared의 공유 객체와 second의 독립 객체를 구분한다. 첫 age는 25에서 30을 거쳐 99, 둘째는 40을 유지한다. 세 참조 변수가 두 객체에 도달합니다 first와 shared는 같은 객체를, second는 별도 객체를 가리킵니다. shared.age = 99는 첫 객체만 바꿉니다. TwoMemberExercise · shared.age = 99; 이후 첫 번째 new의 객체 · first와 shared가 함께 참조 email = "[email protected]" age = 99 active = true 두 번째 new의 객체 · second만 참조 email = "[email protected]" age = 40 active = false 첫 객체의 age: 25 → 30 → 99 first.age = 30 과 shared.age = 99 는 같은 객체의 필드에 차례로 대입합니다. 둘째 객체의 age: 40 유지 second 는 다른 인스턴스를 가리키므로 위 두 대입의 영향을 받지 않습니다. 화살표는 참조가 가리키는 객체를 뜻합니다. Member shared = first; 는 참조값만 복사하므로 변수는 3개, 인스턴스는 2개입니다. |
| 관찰·상황 | 값·보충 정보 | 변화·이유 |
|---|---|---|
| 두 객체 생성 첫 new와 둘째 new가 독립 Member를 만듭니다. |
|
|
| 참조 복사 shared에 first의 참조값을 복사합니다. |
|
|
| 첫 객체 상태 shared.age=99를 실행해 첫 객체의 나이를 99로 바꾸고 공유한 두 참조로 읽습니다. |
|
|
TwoMemberExercise: shared.age = 99 이후의 참조 관계
서로 다른 new 두 객체의 나이 30/40에서 shared=first 후 공유 객체만 99로 변경됩니다. 참조 변수 셋과 인스턴스 둘을 고정하여 공유 변경을 추적할 수 있습니다.
TwoMemberExercise의 shared.age=99 이후를 기준으로 first와 shared의 공유 객체와 second의 독립 객체를 구분한다. 첫 age는 25에서30을 거쳐99, 둘째는40을 유지한다.
세 참조 변수가 두 객체에 도달합니다
first와 shared는 같은 객체를, second는 별도 객체를 가리킵니다. shared.age = 99는 첫 객체만 바꿉니다.
TwoMemberExercise · shared.age = 99; 이후
- 첫 객체의 age: 25 → 30 → 99
first.age = 30과shared.age = 99는 같은 객체의 필드에 차례로 대입합니다.- 둘째 객체의 age: 40 유지
second는 다른 인스턴스를 가리키므로 위 두 대입의 영향을 받지 않습니다.
화살표는 참조가 가리키는 객체를 뜻합니다. Member shared = first;는 참조값만 복사하므로 변수는 3개, 인스턴스는 2개입니다.
이 예의 조건
- first·shared는 같은 객체, second는 다른 객체입니다.
관계도의 보충 설명
| 구분 | 내용 |
|---|---|
| 전체 읽기 정보 | TwoMemberExercise: shared.age = 99 이후의 참조 관계 TwoMemberExercise: shared.age = 99 이후의 참조 관계 TwoMemberExercise의 shared.age=99 이후를 기준으로 first와 shared의 공유 객체와 second의 독립 객체를 구분한다. 첫 age는 25에서 30을 거쳐 99, 둘째는 40을 유지한다. 세 참조 변수가 두 객체에 도달합니다 first와 shared는 같은 객체를, second는 별도 객체를 가리킵니다. shared.age = 99는 첫 객체만 바꿉니다. TwoMemberExercise · shared.age = 99; 이후 첫 번째 new의 객체 · first와 shared가 함께 참조 email = "[email protected]" age = 99 active = true 두 번째 new의 객체 · second만 참조 email = "[email protected]" age = 40 active = false 첫 객체의 age: 25 → 30 → 99 first.age = 30 과 shared.age = 99 는 같은 객체의 필드에 차례로 대입합니다. 둘째 객체의 age: 40 유지 second 는 다른 인스턴스를 가리키므로 위 두 대입의 영향을 받지 않습니다. 화살표는 참조가 가리키는 객체를 뜻합니다. Member shared = first; 는 참조값만 복사하므로 변수는 3개, 인스턴스는 2개입니다. |
| 관찰·상황 | 값·보충 정보 | 변화·이유 |
|---|---|---|
| 두 객체 생성 첫 new와 둘째 new가 독립 Member를 만듭니다. |
|
|
| 참조 복사 shared에 first의 참조값을 복사합니다. |
|
|
| 첫 객체 상태 shared.age=99를 실행해 첫 객체의 나이를 99로 바꾸고 공유한 두 참조로 읽습니다. |
|
|
이 결과가 아직 낯설어도 괜찮습니다. 다음 두 글에서 참조값이 배열 칸과 다른 변수로 복사되는 과정을 차례로 살펴봅니다.
다음 글
객체 배열 만들고 사용하기에서는 여러 Member 객체를 배열에 담습니다. 배열 칸에도 객체가 아니라 참조값이 들어간다는 점과, 그 때문에 생기는 null 칸과 반복문의 동작을 살펴봅니다.