본문으로 건너뛰기

안동민 개발노트

본문 시작

객체 배열과 참조 경로

객체 배열의 칸에 저장되는 참조와 반복 변수가 복사하는 값을 단계별로 추적합니다.

회원 정보가 여러 개가 되면 entry1, entry2, entry3처럼 변수 이름에 번호를 붙이는 방식도 곧 한계에 닿습니다.

반복문을 쓰려면 회원들을 하나의 순서로 보관해야 합니다.

이때 Member[]를 사용하지만, 배열 안에 객체가 통째로 들어간다고 생각하면 이후의 null과 공유 참조를 설명하기 어렵습니다.

객체 배열은 두 종류의 객체를 함께 사용합니다.

배열 객체가 먼저 있고, 각 배열 칸에는 Member 인스턴스에 도달하는 참조값이 저장됩니다.

배열 생성과 기록 생성을 따로 해야 하는 이유가 여기에 있습니다.

HTML 다이어그램: /docs/java/ch4/ch4-3/1.html

객체 배열의 초기 상태

다음 코드는 길이가 3인 Member 배열을 만듭니다.

Member[] members = new Member[3];

이 한 줄이 만든 것은 참조를 담을 칸 세 개뿐입니다.

각 칸은 참조형의 기본값인 null로 시작합니다.

Member 인스턴스 세 개를 자동으로 만들지는 않습니다.

src/members/ch04/v5/EmptyObjectArray.java
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번 칸으로 복사됩니다.

객체가 배열 안으로 이동하거나 복제되는 것이 아닙니다.

대입 뒤에도 firstmembers[0]은 같은 인스턴스에 도달합니다.


객체 배열 초기화

인스턴스를 먼저 만든 뒤 배열을 초기화할 수도 있습니다.

src/members/ch04/v5/MemberArrayMain.java
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에는 접근이 두 번 들어 있습니다.

  1. members[i]가 배열의 i번째 칸에서 Member 참조를 읽습니다.
  2. .email이 그 참조를 따라 인스턴스의 이메일 필드를 읽습니다.
HTML 다이어그램: /docs/java/ch4/ch4-3/2.html

오류를 찾을 때 이 표현을 한 덩어리로 보지 마세요.

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처럼 써야 합니다.

HTML 다이어그램: /docs/java/ch4/ch4-3/3.html

용량과 회원 수

회원 저장소가 최대 다섯 개까지 입력을 받는다고 해서 처음부터 다섯 회원을 만들 필요는 없습니다.

길이 5인 배열을 만들고 현재 사용한 칸의 수를 별도 변수로 관리할 수 있습니다.

src/members/ch04/v5/FixedCapacityRegistry.java
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가 내부에서 용량과 크기를 구분하는 이유를 이해할 준비가 됩니다.

지금은 고정 배열의 한계를 분명히 관찰하는 데 집중합니다.

HTML 다이어그램: /docs/java/ch4/ch4-3/4.html

연습 문제

다음 조건의 Member[]가 있습니다.

Member[] members = new Member[4];
int size = 3;

0~2번 칸에는 객체가 있고 3번 칸은 null입니다.

다음을 구현하세요.

  1. size까지만 순회합니다.
  2. active == false인 회원의 이메일과 나이를 출력합니다.
  3. 총 비활성 회원 나이를 합산합니다.
  4. members.length까지 무조건 순회했을 때 생길 수 있는 실패를 설명합니다.
  5. for-each를 사용한다면 null 슬롯을 어떻게 처리할지 적습니다.
해설 보기
src/members/ch04/exercise/PendingMembers.java
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 범위만 순회하는 쪽이 상태를 더 정확하게 표현합니다.


객체 배열은 객체를 통째로 담지 않고 참조값을 담습니다.

배열 칸과 반복 변수에 같은 참조가 복사될 수 있다는 사실을 확인했습니다.

다음 절에서는 숫자 복사와 참조값 복사를 같은 대입 규칙으로 설명하고, 왜 공유된 필드 변경이 여러 경로에서 보이는지 파고듭니다.