객체 배열과 참조 경로
객체 배열의 칸에 저장되는 참조와 반복 변수가 복사하는 값을 단계별로 추적합니다.
회원 정보가 여러 개가 되면 entry1, entry2, entry3처럼 변수 이름에 번호를 붙이는 방식도 곧 한계에 닿습니다.
반복문을 쓰려면 회원들을 하나의 순서로 보관해야 합니다.
이때 Member[]를 사용하지만, 배열 안에 객체가 통째로 들어간다고 생각하면 이후의 null과 공유 참조를 설명하기 어렵습니다.
객체 배열은 두 종류의 객체를 함께 사용합니다.
배열 객체가 먼저 있고, 각 배열 칸에는 Member 인스턴스에 도달하는 참조값이 저장됩니다.
배열 생성과 회원 객체 생성을 따로 해야 하는 이유가 여기에 있습니다.
이번 절의 실행 클래스는 모두 앞 절과 같은 Member.java를 사용합니다. 예를 들어 EmptyObjectArray를 실행하려면 src/members/ch04/v5에 두 파일을 두고 프로젝트 작업 폴더에서 다음처럼 실행합니다. 다른 예제에서는 EmptyObjectArray 부분을 해당 클래스 이름으로 바꿉니다.
javac -d out src/members/ch04/v5/Member.java src/members/ch04/v5/EmptyObjectArray.java
java -cp out EmptyObjectArray객체 배열의 초기 상태
다음 코드는 길이가 3인 Member 배열을 만듭니다.
Member[] members = new Member[3];이 한 줄이 만든 것은 참조를 담을 칸 세 개뿐입니다.
각 칸은 참조형의 기본값인 null로 시작합니다.
Member 인스턴스 세 개를 자동으로 만들지는 않습니다.
import java.util.Arrays;
public class EmptyObjectArray {
public static void main(String[] args) {
Member[] members = new Member[3];
System.out.println(Arrays.toString(members));
}
}[null, null, null]배열 칸에 실제 회원을 넣으려면 각 객체를 만들고 그 참조를 대입합니다.
Member first = new Member();
first.email = "[email protected]";
first.age = 35;
members[0] = first;members[0] = first도 대입입니다.
first 변수에 들어 있는 참조값이 배열의 0번 칸으로 복사됩니다.
객체가 배열 안으로 이동하거나 복제되는 것이 아닙니다.
대입 뒤에도 first와 members[0]은 같은 인스턴스에 도달합니다.
객체 배열 초기화
인스턴스를 먼저 만든 뒤 배열을 초기화할 수도 있습니다.
public class MemberArrayMain {
public static void main(String[] args) {
Member firstMember = new Member();
firstMember.email = "[email protected]";
firstMember.age = 35;
firstMember.active = true;
Member secondMember = new Member();
secondMember.email = "[email protected]";
secondMember.age = 25;
secondMember.active = false;
Member[] members = {firstMember, secondMember};
for (int i = 0; i < members.length; i++) {
Member member = members[i];
System.out.printf(
"%d. %s / %d세 / %s%n",
i + 1, member.email, member.age, member.active
);
}
}
}1. [email protected] / 35세 / true
2. [email protected] / 25세 / false배열 리터럴 {firstMember, secondMember}에 들어가는 것도 두 변수의 참조값입니다.
따라서 배열 칸을 통해 첫 회원을 바꾸면 firstMember로 읽어도 바뀐 값이 보입니다.
members[0].age = 50;
System.out.println(firstMember.age); // 50참조 접근식 해석
members[i].email에는 접근이 두 번 들어 있습니다.
members[i]가 배열의 i번째 칸에서Member참조를 읽습니다..email이 그 참조를 따라 인스턴스의 이메일 필드를 읽습니다.
MemberArrayMain의 초기화 직후 members 배열의 두 칸은 각각 별도 Member 객체를 참조합니다. 배열 참조, 인덱스, 선택한 칸의 참조를 구분하면 필드 접근의 세 실패 경계를 찾을 수 있습니다.
배열 칸의 참조를 읽은 뒤 객체 필드에 접근합니다
members → 배열 칸 → Member 객체의 경로를 따라갑니다. 배열 참조, 인덱스, 칸의 참조는 서로 다른 실패 경계입니다.
MemberArrayMain의 배열 초기화 직후 · members[0].email 읽기
members는 길이 2의 배열을 참조합니다.members[0]은 첫 Member의 참조값입니다. 그 객체의email은[email protected],age는 35입니다.members[1]은 둘째 Member의 참조값입니다. 그 객체의email은[email protected],age는 25입니다.
members[0]과 firstMember, members[1]과 secondMember는 각각 같은 객체에 도달합니다. 배열 칸에는 객체 자체가 아니라 참조값이 들어 있습니다.
new Member[3]만 실행한 초기 상태는 [null, null, null]입니다. 위 그림은 그 초기 상태가 아니라, 두 객체를 만든 뒤 배열 리터럴로 참조를 넣은 별도 예제입니다.
| 먼저 확인할 값 | 실패 조건 | 발생하는 예외 |
|---|---|---|
members | 배열 참조가 null | NullPointerException |
i | 배열이 있을 때 i < 0 또는 i >= members.length | ArrayIndexOutOfBoundsException |
members[i] | 인덱스가 유효해도 선택한 칸이 null | .email에서 NullPointerException |
접근을 막는 세 실패 경계
- 1 · 배열 참조
members == null- 배열 참조가 없어
NullPointerException이 발생합니다. - 2 · 인덱스 범위
i < 0또는i >= members.length- 배열이 있어도 범위를 벗어나면
ArrayIndexOutOfBoundsException이 발생합니다. - 3 · 선택한 칸의 참조
members[i] == null- 인덱스가 유효해도 칸이 null이면
.email에서NullPointerException이 발생합니다.
오류를 찾을 때 이 표현을 한 덩어리로 보지 마세요.
members 자체가 null일 수 있고, 인덱스가 범위를 벗어날 수 있고, members[i]가 null일 수도 있습니다.
각각 실패 종류가 다릅니다.
Member[] members = new Member[2];
// members = null;
// System.out.println(members[0]); // 배열 참조가 null: NullPointerException
// System.out.println(members[2]); // 인덱스 범위 초과: ArrayIndexOutOfBoundsException
System.out.println(members[0].email); // 칸이 null: NullPointerException스택 트레이스가 같은 줄을 가리키더라도 어느 중간값이 잘못됐는지 분해해 확인해야 합니다.
디버거에서는 members, i, members[i]를 차례로 관찰하면 됩니다.
for-each의 참조 복사
인덱스가 필요 없다면 향상된 for문으로 읽기 쉽도록 바꿀 수 있습니다.
for (Member member : members) {
System.out.println(member.email + " / " + member.age);
}매 반복마다 배열 칸의 참조값이 지역 변수 member에 복사됩니다.
그래서 다음 필드 변경은 원본 배열이 가리키는 객체에도 보입니다.
for (Member member : members) {
member.active = true;
}반면 반복 변수 자체를 새 객체로 바꿔도 배열 칸은 바뀌지 않습니다.
for (Member member : members) {
member = new Member();
member.email = "[email protected]";
}
System.out.println(members[0].email); // 기존 이메일 그대로member = new Member()는 지역 변수에 새 참조를 넣었을 뿐입니다.
배열의 members[0]이나 members[1]에 대입하지 않았습니다.
배열 칸을 실제로 교체하려면 인덱스 반복문에서 members[i] = replacement처럼 써야 합니다.
for-each의 필드 변경과 반복 변수 재대입
세 독립 사례에서 공유 필드 쓰기, foreach 지역 변수 재대입, 배열 칸 대입은 변경 대상이 다릅니다. 같은 참조 역할을 고정해 각 대입 전후를 비교하면 배열 교체 여부가 드러납니다.
for-each의 member에는 현재 배열 칸의 참조값이 복사됩니다. 객체 필드 쓰기, 지역 변수 재대입, 배열 칸 대입은 서로 다른 저장 위치를 바꿉니다.
필드와 반복 변수와 배열 칸은 다른 저장 위치입니다
for-each는 배열 칸의 참조값을 지역 변수로 복사합니다. 무엇을 대입 대상으로 삼는지에 따라 배열에서 관찰되는 결과가 달라집니다.
현재 반복의 member에는 members[i]의 참조값이 복사되어 있고, 해당 칸은 null이 아닙니다.
| 실행하는 문장 | 바꾸는 저장 위치 | 배열에서 다시 읽으면 |
|---|---|---|
member.active = true; | 같이 참조하는 Member 객체의 active 필드 | members[i].active도 true |
| 지역 변수 member의 참조값, 그리고 새 객체의 필드 | 배열 칸의 참조와 기존 이메일은 그대로 |
members[i] = replacement; | 배열의 i번째 칸에 저장된 참조값 | 해당 칸에 replacement의 참조값을 저장 |
- 1 · 공유 객체의 필드 변경
member.active = true;- 같이 참조하는 객체의 active 필드를 씁니다.
members[i].active로 읽어도 true입니다. - 2 · 지역 변수의 참조 재대입
member = new Member();member.email = "[email protected]";- 지역 변수와 새 객체의 필드만 바뀝니다. 배열 칸의 참조와 기존 이메일은 그대로입니다.
- 3 · 배열 칸의 참조 교체
members[i] = replacement;- 배열의 i번째 칸에 replacement의 참조값을 저장합니다.
마지막 문장은 인덱스 반복문에서 배열 칸을 실제로 교체하는 방법입니다. 반복 변수 member에 대입하는 것과 구분합니다.
이 예의 조건
- 세 비교는 독립 실행입니다.
관계도의 보충 설명
| 구분 | 내용 |
|---|---|
| 2 | 세 독립 비교: 필드 쓰기는 member와 배열 칸이 공유한 객체의 active=true를 저장한다. 지역 member 재대입은 새 객체만 가리키고 배열은 원래 객체를 가리킨다. members[i] 대입은 해당 칸에 replacement 참조를 저장한다. 실선은 대입 후의 참조이며 실행 순서가 아니다. |
| 구분 | 내용 |
|---|---|
| 설명 | 세 독립 비교: 필드 쓰기는 member와 배열 칸이 공유한 객체의 active=true를 저장한다. 지역 member 재대입은 새 객체만 가리키고 배열은 원래 객체를 가리킨다. members[i] 대입은 해당 칸에 replacement 참조를 저장한다. 실선은 대입 후의 참조이며 실행 순서가 아니다. |
| 전체 읽기 정보 | for-each의 필드 변경과 반복 변수 재대입 for-each의 필드 변경과 반복 변수 재대입 for-each의 member에는 현재 배열 칸의 참조값이 복사됩니다. 객체 필드 쓰기, 지역 변수 재대입, 배열 칸 대입은 서로 다른 저장 위치를 바꿉니다. 필드와 반복 변수와 배열 칸은 다른 저장 위치입니다 for-each는 배열 칸의 참조값을 지역 변수로 복사합니다. 무엇을 대입 대상으로 삼는지에 따라 배열에서 관찰되는 결과가 달라집니다. 현재 반복의 member 에는 members[i] 의 참조값이 복사되어 있고, 해당 칸은 null 이 아닙니다. 서로 다른 대입 대상과 배열에서 관찰되는 결과 실행하는 문장 바꾸는 저장 위치 배열에서 다시 읽으면 member.active = true; 같이 참조하는 Member 객체의 active 필드 members[i].active 도 true member = new Member(); member.email = "[email protected]"; 지역 변수 member 의 참조값, 그리고 새 객체의 필드 배열 칸의 참조와 기존 이메일은 그대로 members[i] = replacement; 배열의 i번째 칸에 저장된 참조값 해당 칸에 replacement 의 참조값을 저장 1 · 공유 객체의 필드 변경 member.active = true; 같이 참조하는 객체의 active 필드를 씁니다. members[i].active 로 읽어도 true입니다. 2 · 지역 변수의 참조 재대입 member = new Member(); member.email = "[email protected]"; 지역 변수와 새 객체의 필드만 바뀝니다. 배열 칸의 참조와 기존 이메일은 그대로입니다. 3 · 배열 칸의 참조 교체 members[i] = replacement; 배열의 i번째 칸에 replacement의 참조값을 저장합니다. 마지막 문장은 인덱스 반복문에서 배열 칸을 실제로 교체하는 방법입니다. 반복 변수 member 에 대입하는 것과 구분합니다. |
| 관찰·상황 | 값·보충 정보 | 변화·이유 |
|---|---|---|
| 필드 쓰기 member와 members[i]의 같은 객체에 active=true를 씁니다. |
|
|
| 지역 재대입 member가 새 Member를 가리키도록 바뀝니다. |
|
|
| 배열 칸 대입 members[i]에 replacement 참조를 저장합니다. |
|
|
for-each의 필드 변경과 반복 변수 재대입
세 독립 사례에서 공유 필드 쓰기, foreach 지역 변수 재대입, 배열 칸 대입은 변경 대상이 다릅니다. 같은 참조 역할을 고정해 각 대입 전후를 비교하면 배열 교체 여부가 드러납니다.
for-each의 member에는 현재 배열 칸의 참조값이 복사됩니다. 객체 필드 쓰기, 지역 변수 재대입, 배열 칸 대입은 서로 다른 저장 위치를 바꿉니다.
필드와 반복 변수와 배열 칸은 다른 저장 위치입니다
for-each는 배열 칸의 참조값을 지역 변수로 복사합니다. 무엇을 대입 대상으로 삼는지에 따라 배열에서 관찰되는 결과가 달라집니다.
현재 반복의 member에는 members[i]의 참조값이 복사되어 있고, 해당 칸은 null이 아닙니다.
| 실행하는 문장 | 바꾸는 저장 위치 | 배열에서 다시 읽으면 |
|---|---|---|
member.active = true; | 같이 참조하는 Member 객체의 active 필드 | members[i].active도 true |
| 지역 변수 member의 참조값, 그리고 새 객체의 필드 | 배열 칸의 참조와 기존 이메일은 그대로 |
members[i] = replacement; | 배열의 i번째 칸에 저장된 참조값 | 해당 칸에 replacement의 참조값을 저장 |
- 1 · 공유 객체의 필드 변경
member.active = true;- 같이 참조하는 객체의 active 필드를 씁니다.
members[i].active로 읽어도 true입니다. - 2 · 지역 변수의 참조 재대입
member = new Member();member.email = "[email protected]";- 지역 변수와 새 객체의 필드만 바뀝니다. 배열 칸의 참조와 기존 이메일은 그대로입니다.
- 3 · 배열 칸의 참조 교체
members[i] = replacement;- 배열의 i번째 칸에 replacement의 참조값을 저장합니다.
마지막 문장은 인덱스 반복문에서 배열 칸을 실제로 교체하는 방법입니다. 반복 변수 member에 대입하는 것과 구분합니다.
이 예의 조건
- 세 비교는 독립 실행입니다.
관계도의 보충 설명
| 구분 | 내용 |
|---|---|
| 2 | 세 독립 비교: 필드 쓰기는 member와 배열 칸이 공유한 객체의 active=true를 저장한다. 지역 member 재대입은 새 객체만 가리키고 배열은 원래 객체를 가리킨다. members[i] 대입은 해당 칸에 replacement 참조를 저장한다. 실선은 대입 후의 참조이며 실행 순서가 아니다. |
| 구분 | 내용 |
|---|---|
| 설명 | 세 독립 비교: 필드 쓰기는 member와 배열 칸이 공유한 객체의 active=true를 저장한다. 지역 member 재대입은 새 객체만 가리키고 배열은 원래 객체를 가리킨다. members[i] 대입은 해당 칸에 replacement 참조를 저장한다. 실선은 대입 후의 참조이며 실행 순서가 아니다. |
| 전체 읽기 정보 | for-each의 필드 변경과 반복 변수 재대입 for-each의 필드 변경과 반복 변수 재대입 for-each의 member에는 현재 배열 칸의 참조값이 복사됩니다. 객체 필드 쓰기, 지역 변수 재대입, 배열 칸 대입은 서로 다른 저장 위치를 바꿉니다. 필드와 반복 변수와 배열 칸은 다른 저장 위치입니다 for-each는 배열 칸의 참조값을 지역 변수로 복사합니다. 무엇을 대입 대상으로 삼는지에 따라 배열에서 관찰되는 결과가 달라집니다. 현재 반복의 member 에는 members[i] 의 참조값이 복사되어 있고, 해당 칸은 null 이 아닙니다. 서로 다른 대입 대상과 배열에서 관찰되는 결과 실행하는 문장 바꾸는 저장 위치 배열에서 다시 읽으면 member.active = true; 같이 참조하는 Member 객체의 active 필드 members[i].active 도 true member = new Member(); member.email = "[email protected]"; 지역 변수 member 의 참조값, 그리고 새 객체의 필드 배열 칸의 참조와 기존 이메일은 그대로 members[i] = replacement; 배열의 i번째 칸에 저장된 참조값 해당 칸에 replacement 의 참조값을 저장 1 · 공유 객체의 필드 변경 member.active = true; 같이 참조하는 객체의 active 필드를 씁니다. members[i].active 로 읽어도 true입니다. 2 · 지역 변수의 참조 재대입 member = new Member(); member.email = "[email protected]"; 지역 변수와 새 객체의 필드만 바뀝니다. 배열 칸의 참조와 기존 이메일은 그대로입니다. 3 · 배열 칸의 참조 교체 members[i] = replacement; 배열의 i번째 칸에 replacement의 참조값을 저장합니다. 마지막 문장은 인덱스 반복문에서 배열 칸을 실제로 교체하는 방법입니다. 반복 변수 member 에 대입하는 것과 구분합니다. |
| 관찰·상황 | 값·보충 정보 | 변화·이유 |
|---|---|---|
| 필드 쓰기 member와 members[i]의 같은 객체에 active=true를 씁니다. |
|
|
| 지역 재대입 member가 새 Member를 가리키도록 바뀝니다. |
|
|
| 배열 칸 대입 members[i]에 replacement 참조를 저장합니다. |
|
|
용량과 회원 수
회원 저장소가 최대 다섯 개까지 입력을 받는다고 해서 처음부터 다섯 회원을 만들 필요는 없습니다.
길이 5인 배열을 만들고 현재 사용한 칸의 수를 별도 변수로 관리할 수 있습니다.
public class FixedCapacityRegistry {
public static void main(String[] args) {
Member[] members = new Member[5];
int size = 0;
Member member = new Member();
member.email = "[email protected]";
member.age = 30;
members[size] = member;
size++;
for (int i = 0; i < size; i++) {
System.out.println(members[i].email);
}
}
}members.length는 저장할 수 있는 최대 칸 수이고 size는 실제로 채운 회원 수입니다.
둘을 같은 뜻으로 사용하면 남은 null 슬롯을 역참조하거나, 배열이 찼는데 새 값을 넣어 범위 오류가 납니다.
if (size >= members.length) {
System.out.println("더 이상 회원을 추가할 수 없습니다.");
} else {
members[size] = member;
size++;
}이 구조는 나중에 ArrayList가 내부에서 용량과 크기를 구분하는 이유를 이해할 준비가 됩니다.
지금은 고정 배열의 한계를 분명히 관찰하는 데 집중합니다.
FixedCapacityRegistry에서 첫 회원을 저장하고 size를 증가시킨 직후 배열 길이는 5이고 size는 1입니다. 사용 구간은 0번 칸뿐이며 1부터 4번 칸은 null입니다.
용량 다섯 칸 중 회원이 있는 칸은 하나입니다
첫 회원 저장 후 length는 5, size는 1입니다. 회원 순회는 size 앞까지만 읽고, 다음 추가는 size 위치에 저장합니다.
FixedCapacityRegistry의 size++ 직후 · members.length = 5, size = 1
- 0번 칸 · 사용 중
member의 참조값이 저장되어 있습니다.- 1, 2, 3, 4번 칸 · 미사용
- 네 칸 모두
null이며 연결된 Member 객체가 없습니다.
- 회원 순회는
0 <= i && i < size, 즉 0번 칸만 읽습니다. 그 회원의 이메일은[email protected]입니다. - 5번 칸은 존재하지 않습니다. 추가 전
size >= members.length이면 중단하고, 여유가 있을 때만members[size] = member;뒤size++;를 실행합니다.
연습 문제
다음 조건의 Member[]가 있습니다.
Member[] members = new Member[4];
int size = 3;0~2번 칸에는 객체가 있고 3번 칸은 null입니다. 직접 만들 데이터는 순서대로 [email protected]·20세·활성, [email protected]·35세·비활성, [email protected]·25세·비활성입니다. 실행 전에 어떤 두 회원이 출력되고 합계가 얼마일지 적어 보세요.
다음을 구현하세요.
size까지만 순회합니다.active == false인 회원의 이메일과 나이를 출력합니다.- 비활성 회원의 나이 합계를 구합니다.
members.length까지 무조건 순회했을 때 생길 수 있는 실패를 설명합니다.for-each를 사용한다면null슬롯을 어떻게 처리할지 적습니다.
해설 보기
public class PendingMembers {
public static void main(String[] args) {
Member[] members = new Member[4];
int size = 0;
members[size++] = create("[email protected]", 20, true);
members[size++] = create("[email protected]", 35, false);
members[size++] = create("[email protected]", 25, false);
int pendingAge = 0;
for (int i = 0; i < size; i++) {
Member member = members[i];
if (!member.active) {
System.out.println(member.email + " / " + member.age + "세");
pendingAge += member.age;
}
}
System.out.println("비활성 합계=" + pendingAge + "세");
}
static Member create(String email, int age, boolean active) {
Member member = new Member();
member.email = email;
member.age = age;
member.active = active;
return member;
}
}[email protected] / 35세
[email protected] / 25세
비활성 합계=60세여기서는 다음 절에서 자세히 다룰 생성 메서드를 미리 사용했습니다.
create가 돌려준 참조값이 배열 칸에 저장됩니다.
members.length 전체를 순회하면서 곧바로 member.active를 읽으면 마지막 null 슬롯에서 NullPointerException이 납니다.
for-each가 필요하다면 if (member == null) continue;를 둘 수 있지만, 실제 데이터 수를 알고 있다면 size 범위만 순회하는 쪽이 상태를 더 정확하게 표현합니다.
객체 배열은 객체를 통째로 담지 않고 참조값을 담습니다.
배열 칸과 반복 변수에 같은 참조가 복사될 수 있다는 사실을 확인했습니다.
다음 절에서는 숫자 복사와 참조값 복사를 같은 대입 규칙으로 설명하고, 왜 공유된 필드 변경이 여러 경로에서 보이는지 파고듭니다.