본문 바로가기

객체 배열과 참조 경로

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

회원 정보가 여러 개가 되면 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 인스턴스 세 개를 자동으로 만들지는 않습니다.

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 = "[email protected]";
first.age = 35;

members[0] = first;

members[0] = first도 대입입니다.

first 변수에 들어 있는 참조값이 배열의 0번 칸으로 복사됩니다.

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

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


객체 배열 초기화

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

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

  1. members[i]가 배열의 i번째 칸에서 Member 참조를 읽습니다.
  2. .email이 그 참조를 따라 인스턴스의 이메일 필드를 읽습니다.
배열 슬롯에서 Member 필드까지의 접근과 실패 경계

MemberArrayMain의 초기화 직후 members 배열의 두 칸은 각각 별도 Member 객체를 참조합니다. 배열 참조, 인덱스, 선택한 칸의 참조를 구분하면 필드 접근의 세 실패 경계를 찾을 수 있습니다.

배열 칸의 참조를 읽은 뒤 객체 필드에 접근합니다

members → 배열 칸 → Member 객체의 경로를 따라갑니다. 배열 참조, 인덱스, 칸의 참조는 서로 다른 실패 경계입니다.

MemberArrayMain의 배열 초기화 직후 · members[0].email 읽기

members 배열의 두 슬롯에서 별도 Member 객체로 가는 참조 members는 길이 2의 배열을 참조합니다. 0번 칸은 김 회원 객체를, 1번 칸은 이 회원 객체를 참조하며 email 필드는 배열 칸이 아닌 각 객체 안에 있습니다. members 배열의 참조값 Member[] · length=2 [0] 첫 객체 참조 [1] 둘째 객체 참조 첫 Member email: [email protected] age: 35 둘째 Member email: [email protected] age: 25
배열 칸의 참조를 읽은 뒤 객체 필드에 접근합니다 members→길이2배열, [0]→Kim35객체와[1]→Lee25객체. 배열칸은참조값이며 객체필드와분리. members 배열의 참조값 Member[] · length=2 [0] · 첫 객체 참조 [1] · 둘째 객체 참조 첫 Member [email protected] age = 35 둘째 Member [email protected] age = 25 실선: 변수·칸의 참조 배열 칸에는 객체 참조값 저장
  1. members는 길이 2의 배열을 참조합니다.
  2. members[0]은 첫 Member의 참조값입니다. 그 객체의 email은 [email protected], age는 35입니다.
  3. members[1]은 둘째 Member의 참조값입니다. 그 객체의 email은 [email protected], age는 25입니다.

members[0]과 firstMember, members[1]과 secondMember는 각각 같은 객체에 도달합니다. 배열 칸에는 객체 자체가 아니라 참조값이 들어 있습니다.

new Member[3]만 실행한 초기 상태는 [null, null, null]입니다. 위 그림은 그 초기 상태가 아니라, 두 객체를 만든 뒤 배열 리터럴로 참조를 넣은 별도 예제입니다.

members[i].email에서 구분할 세 실패 경계
먼저 확인할 값실패 조건발생하는 예외
members배열 참조가 nullNullPointerException
i배열이 있을 때 i < 0 또는 i >= members.lengthArrayIndexOutOfBoundsException
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의 필드 변경과 반복 변수 재대입

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의 참조값을 저장
필드와 반복 변수와 배열 칸은 다른 저장 위치입니다 세 독립 비교: 필드 쓰기는 member와배열칸이공유한 객체 active=true;지역member재대입은새객체만가리키고배열은원래객체;members[i]대입은칸에replacement참조저장. 실선은대입후참조이며실행순서아님. 1 · 필드 쓰기 2 · 지역 재대입 3 · 배열 칸 대입 members[i] 저장된 참조값 member 저장된 참조값 members[i] 저장된 참조값 member 저장된 참조값 members[i] 저장된 참조값 같은 Member active = true 공유 필드 변경 기존 Member 이전 email 그대로 새 Member 새 email을 설정 배열 칸 미저장 replacement 객체 배열의 같은 칸이 새 참조를 보관 세 사례는 독립 실행 · 실선은 현재 저장한 참조 경로
필드와 반복 변수와 배열 칸은 다른 저장 위치입니다 세 독립 비교: 필드 쓰기는 member와배열칸이공유한 객체 active=true;지역member재대입은새객체만가리키고배열은원래객체;members[i]대입은칸에replacement참조저장. 실선은대입후참조이며실행순서아님. 1 · 필드 쓰기 members[i] 기존 객체 참조 member 복사한 같은 참조값 같은 Member active = true 필드가 바뀜 2 · 지역 재대입 members[i] 기존 참조 그대로 기존 Member 이전 email 그대로 member new의 새 참조값 새 Member 새 email을 설정 배열 칸에는 미저장 3 · 배열 칸 대입 members[i] replacement 참조 저장 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를 씁니다.

  • 배열 칸: 원 참조 유지
  • 객체 필드: true로 변경
  • 반복 변수로도 공유 객체 필드는 바꿀 수 있습니다.
지역 재대입

member가 새 Member를 가리키도록 바뀝니다.

  • member: 새 email 객체
  • members[i]: 기존 객체
  • 지역 변수 대입이 배열 칸을 자동으로 갱신하지 않습니다.
배열 칸 대입

members[i]에 replacement 참조를 저장합니다.

  • 배열 칸: 새 참조
  • 쓰기 대상: 배열 저장 위치
  • 배열 칸을 바꾸려면 그 칸에 직접 대입해야 합니다.

for-each의 필드 변경과 반복 변수 재대입

세 독립 사례에서 공유 필드 쓰기, foreach 지역 변수 재대입, 배열 칸 대입은 변경 대상이 다릅니다. 같은 참조 역할을 고정해 각 대입 전후를 비교하면 배열 교체 여부가 드러납니다.

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의 참조값을 저장
필드와 반복 변수와 배열 칸은 다른 저장 위치입니다 세 독립 비교: 필드 쓰기는 member와배열칸이공유한 객체 active=true;지역member재대입은새객체만가리키고배열은원래객체;members[i]대입은칸에replacement참조저장. 실선은대입후참조이며실행순서아님. 1 · 필드 쓰기 2 · 지역 재대입 3 · 배열 칸 대입 members[i] 저장된 참조값 member 저장된 참조값 members[i] 저장된 참조값 member 저장된 참조값 members[i] 저장된 참조값 같은 Member active = true 공유 필드 변경 기존 Member 이전 email 그대로 새 Member 새 email을 설정 배열 칸 미저장 replacement 객체 배열의 같은 칸이 새 참조를 보관 세 사례는 독립 실행 · 실선은 현재 저장한 참조 경로
필드와 반복 변수와 배열 칸은 다른 저장 위치입니다 세 독립 비교: 필드 쓰기는 member와배열칸이공유한 객체 active=true;지역member재대입은새객체만가리키고배열은원래객체;members[i]대입은칸에replacement참조저장. 실선은대입후참조이며실행순서아님. 1 · 필드 쓰기 members[i] 기존 객체 참조 member 복사한 같은 참조값 같은 Member active = true 필드가 바뀜 2 · 지역 재대입 members[i] 기존 참조 그대로 기존 Member 이전 email 그대로 member new의 새 참조값 새 Member 새 email을 설정 배열 칸에는 미저장 3 · 배열 칸 대입 members[i] replacement 참조 저장 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를 씁니다.

  • 배열 칸: 원 참조 유지
  • 객체 필드: true로 변경
  • 반복 변수로도 공유 객체 필드는 바꿀 수 있습니다.
지역 재대입

member가 새 Member를 가리키도록 바뀝니다.

  • member: 새 email 객체
  • members[i]: 기존 객체
  • 지역 변수 대입이 배열 칸을 자동으로 갱신하지 않습니다.
배열 칸 대입

members[i]에 replacement 참조를 저장합니다.

  • 배열 칸: 새 참조
  • 쓰기 대상: 배열 저장 위치
  • 배열 칸을 바꾸려면 그 칸에 직접 대입해야 합니다.

용량과 회원 수

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

길이 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 = "[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가 내부에서 용량과 크기를 구분하는 이유를 이해할 준비가 됩니다.

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

고정 배열의 용량 5와 사용한 회원 수 1

FixedCapacityRegistry에서 첫 회원을 저장하고 size를 증가시킨 직후 배열 길이는 5이고 size는 1입니다. 사용 구간은 0번 칸뿐이며 1부터 4번 칸은 null입니다.

용량 다섯 칸 중 회원이 있는 칸은 하나입니다

첫 회원 저장 후 length는 5, size는 1입니다. 회원 순회는 size 앞까지만 읽고, 다음 추가는 size 위치에 저장합니다.

FixedCapacityRegistry의 size++ 직후 · members.length = 5, size = 1

배열 다섯 칸 중 0번만 사용한 상태 0번 칸에는 첫 회원의 참조가 있고 1, 2, 3, 4번 칸은 null입니다. size가 1이므로 회원 순회는 0 이상 1 미만이고, 배열의 유효 인덱스 범위는 0 이상 5 미만입니다. members length = 5 [0] member 참조 [1] null [2] null [3] null [4] null 사용 중 1칸 미사용 4칸 · 객체 참조 없음
용량 다섯 칸 중 회원이 있는 칸은 하나입니다 length5/size1의 실제 [0]참조와[1..4]null구간,첫객체 [email protected]/30. 실선은참조,범위박스는사용·미사용소속이며순차객체생성흐름아님. members · length=5 사용 구간 [0,1) 미사용 구간 [1,5) [0] · member 참조 [1] · null [2] · null [3] · null [4] · null Member 한 개 [email protected] age = 30 size=1 · 다음 저장은 [1] 사용 칸만 순회 · 5번 칸 없음
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세·비활성입니다. 실행 전에 어떤 두 회원이 출력되고 합계가 얼마일지 적어 보세요.

다음을 구현하세요.

  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("[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 범위만 순회하는 쪽이 상태를 더 정확하게 표현합니다.


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

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

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