객체 배열과 참조 경로
객체 배열의 칸에 저장되는 참조와 반복 변수가 복사하는 값을 단계별로 추적합니다.
회원 정보가 여러 개가 되면 entry1, entry2, entry3처럼 변수 이름에 번호를 붙이는 방식도 곧 한계에 닿습니다.
반복문을 쓰려면 회원들을 하나의 순서로 보관해야 합니다.
이때 Member[]를 사용하지만, 배열 안에 객체가 통째로 들어간다고 생각하면 이후의 null과 공유 참조를 설명하기 어렵습니다.
객체 배열은 두 종류의 객체를 함께 사용합니다.
배열 객체가 먼저 있고, 각 배열 칸에는 Member 인스턴스에 도달하는 참조값이 저장됩니다.
배열 생성과 기록 생성을 따로 해야 하는 이유가 여기에 있습니다.
객체 배열의 초기 상태
다음 코드는 길이가 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 = "kim@example.com";
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 = "kim@example.com";
firstMember.age = 35;
firstMember.active = true;
Member secondMember = new Member();
secondMember.email = "lee@example.com";
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. kim@example.com / 35세 / true
2. lee@example.com / 25세 / false배열 리터럴 {firstMember, secondMember}에 들어가는 것도 두 변수의 참조값입니다.
따라서 배열 칸을 통해 첫 회원을 바꾸면 firstMember로 읽어도 바뀐 값이 보입니다.
members[0].age = 50;
System.out.println(firstMember.age); // 50참조 접근식 해석
members[i].email에는 접근이 두 번 들어 있습니다.
members[i]가 배열의 i번째 칸에서Member참조를 읽습니다..email이 그 참조를 따라 인스턴스의 이메일 필드를 읽습니다.
오류를 찾을 때 이 표현을 한 덩어리로 보지 마세요.
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 = "replacement@example.com";
}
System.out.println(members[0].email); // 기존 이메일 그대로member = new Member()는 지역 변수에 새 참조를 넣었을 뿐입니다.
배열의 members[0]이나 members[1]에 대입하지 않았습니다.
배열 칸을 실제로 교체하려면 인덱스 반복문에서 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 = "member@example.com";
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가 내부에서 용량과 크기를 구분하는 이유를 이해할 준비가 됩니다.
지금은 고정 배열의 한계를 분명히 관찰하는 데 집중합니다.
연습 문제
다음 조건의 Member[]가 있습니다.
Member[] members = new Member[4];
int size = 3;0~2번 칸에는 객체가 있고 3번 칸은 null입니다.
다음을 구현하세요.
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("kim@example.com", 20, true);
members[size++] = create("lee@example.com", 35, false);
members[size++] = create("park@example.com", 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;
}
}lee@example.com / 35세
park@example.com / 25세
비활성 합계=60세여기서는 다음 절에서 자세히 다룰 생성 메서드를 미리 사용했습니다.
create가 돌려준 참조값이 배열 칸에 저장됩니다.
members.length 전체를 순회하면서 곧바로 member.active를 읽으면 마지막 null 슬롯에서 NullPointerException이 납니다.
for-each가 필요하다면 if (member == null) continue;를 둘 수 있지만, 실제 데이터 수를 알고 있다면 size 범위만 순회하는 쪽이 상태를 더 정확하게 표현합니다.
객체 배열은 객체를 통째로 담지 않고 참조값을 담습니다.
배열 칸과 반복 변수에 같은 참조가 복사될 수 있다는 사실을 확인했습니다.
다음 절에서는 숫자 복사와 참조값 복사를 같은 대입 규칙으로 설명하고, 왜 공유된 필드 변경이 여러 경로에서 보이는지 파고듭니다.