배열과 인덱스
같은 타입의 여러 값을 배열에 저장하고 길이와 마지막 인덱스를 구분해 회원가입의 고정 용량 저장소를 만듭니다.
회원이 늘 때마다 email1, email2, email3 변수를 추가하면 같은 합계·출력 코드도 함께 늘어납니다.
배열은 같은 타입의 원소를 하나의 참조와 인덱스로 관리합니다.
길이는 원소 수이고 마지막 인덱스는 길이보다 하나 작습니다.
배열 경계 예외
public final class LengthAsIndex {
public static void main(String[] args) {
int[] age = {20, 30, 40};
System.out.println("length=" + age.length);
System.out.println(age[age.length]);
}
}length=3
Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: Index 3 out of bounds for length 3정상 인덱스는 0, 1, 2입니다.
length는 원소 수 3이지 마지막 위치가 아닙니다.
반복 조건은 index < age.length로 둡니다.
length는 개수이고 읽기는 index가 length보다 작을 때만 한다
인덱스 0·1·2를 읽고 3에서 index<length 검사로 종료하는 흐름과 검사 없는 실패를 대비합니다. 조건이 잘못된 원소 접근을 막는 전환을 드러낼 수 있습니다.
age의 길이 3은 원소 수이며 정상 순회는 index 3에서 읽기를 멈춘다. age[length]를 직접 읽으면 범위 예외가 발생한다.
ARRAY · BOUNDARIES
length는 개수, 마지막 인덱스는 length−1
길이3의 배열은 인덱스0·1·2에서 읽습니다. 0부터 시작하는 정상 순회는3에서 멈추지만, age[3]을 직접 읽으면 범위 예외가 납니다.
- 실제 배열
age={20,30,40},length=3입니다.index 0→20,1→30,2→40이며 마지막 유효 인덱스는 2입니다.- 정상 순회
index < age.length를 먼저 검사합니다. 0·1·2에서만 읽고,index=3이면 false여서 읽지 않고 끝냅니다.- 검사 없는 접근
age[age.length]는age[3]이므로ArrayIndexOutOfBoundsException입니다.
new int[0]에는 유효 인덱스가 없습니다. index < empty.length 반복은 0회지만 empty[0]을 읽으면 예외입니다.
이 예의 조건
- age={20,30,40}, length=3이며 유효 인덱스는 0, 1, 2입니다.
- 정상 순회의 조건 검사와 원 실패 예제의 무조건 age[age.length] 접근을 같은 index=3 경계에서 비교합니다.
관계도의 보충 설명
| 구분 | 내용 |
|---|---|
| 22 | index = 0 |
| 구분 | 내용 |
|---|---|
| 전체 읽기 정보 | length는 개수이고 읽기는 index가 length보다 작을 때만 한다 길이 3 배열의 유효 인덱스와 검사 없는 범위 밖 접근 age의 길이 3은 원소 수이며 정상 순회는 index 3에서 읽기를 멈춘다. age[length]를 직접 읽으면 범위 예외가 발생한다. ARRAY · BOUNDARIES length는 개수, 마지막 인덱스는 length−1 길이 3의 배열은 인덱스 0·1·2에서 읽습니다. 0부터 시작하는 정상 순회는 3에서 멈추지만, age[3]을 직접 읽으면 범위 예외가 납니다. 실제 배열 age={20,30,40} , length=3 입니다. index 0→20 , 1→30 , 2→40 이며 마지막 유효 인덱스는 2입니다. 정상 순회 index < age.length 를 먼저 검사합니다. 0·1·2에서만 읽고, index=3 이면 false여서 읽지 않고 끝냅니다. 검사 없는 접근 age[age.length] 는 age[3] 이므로 ArrayIndexOutOfBoundsException 입니다. new int[0] 에는 유효 인덱스가 없습니다. index < empty.length 반복은 0회지만 empty[0] 을 읽으면 예외입니다. |
| 관찰·상황 | 값·보충 정보 | 변화·이유 |
|---|---|---|
| 다음 인덱스 후보 정상 순회는 유효 위치를 읽은 뒤 3을 검사하고 실패 예제는 length=3을 직접 인덱스로 씁니다. |
|
|
| 읽기 허용 여부 순회는 3<3을 검사하지만 실패 예제는 사전 조건 없이 3번 위치를 읽으려 합니다. |
|
|
| 각 경로의 종료 조건 있는 순회는 정상 종료하고 무조건 접근은 예외로 끝납니다. |
|
|
length는 개수이고 읽기는 index가 length보다 작을 때만 한다
인덱스 0·1·2를 읽고 3에서 index<length 검사로 종료하는 흐름과 검사 없는 실패를 대비합니다. 조건이 잘못된 원소 접근을 막는 전환을 드러낼 수 있습니다.
age의 길이 3은 원소 수이며 정상 순회는 index 3에서 읽기를 멈춘다. age[length]를 직접 읽으면 범위 예외가 발생한다.
ARRAY · BOUNDARIES
length는 개수, 마지막 인덱스는 length−1
길이3의 배열은 인덱스0·1·2에서 읽습니다. 0부터 시작하는 정상 순회는3에서 멈추지만, age[3]을 직접 읽으면 범위 예외가 납니다.
- 실제 배열
age={20,30,40},length=3입니다.index 0→20,1→30,2→40이며 마지막 유효 인덱스는 2입니다.- 정상 순회
index < age.length를 먼저 검사합니다. 0·1·2에서만 읽고,index=3이면 false여서 읽지 않고 끝냅니다.- 검사 없는 접근
age[age.length]는age[3]이므로ArrayIndexOutOfBoundsException입니다.
new int[0]에는 유효 인덱스가 없습니다. index < empty.length 반복은 0회지만 empty[0]을 읽으면 예외입니다.
이 예의 조건
- age={20,30,40}, length=3이며 유효 인덱스는 0, 1, 2입니다.
- 정상 순회의 조건 검사와 원 실패 예제의 무조건 age[age.length] 접근을 같은 index=3 경계에서 비교합니다.
관계도의 보충 설명
| 구분 | 내용 |
|---|---|
| 22 | index = 0 |
| 구분 | 내용 |
|---|---|
| 전체 읽기 정보 | length는 개수이고 읽기는 index가 length보다 작을 때만 한다 길이 3 배열의 유효 인덱스와 검사 없는 범위 밖 접근 age의 길이 3은 원소 수이며 정상 순회는 index 3에서 읽기를 멈춘다. age[length]를 직접 읽으면 범위 예외가 발생한다. ARRAY · BOUNDARIES length는 개수, 마지막 인덱스는 length−1 길이 3의 배열은 인덱스 0·1·2에서 읽습니다. 0부터 시작하는 정상 순회는 3에서 멈추지만, age[3]을 직접 읽으면 범위 예외가 납니다. 실제 배열 age={20,30,40} , length=3 입니다. index 0→20 , 1→30 , 2→40 이며 마지막 유효 인덱스는 2입니다. 정상 순회 index < age.length 를 먼저 검사합니다. 0·1·2에서만 읽고, index=3 이면 false여서 읽지 않고 끝냅니다. 검사 없는 접근 age[age.length] 는 age[3] 이므로 ArrayIndexOutOfBoundsException 입니다. new int[0] 에는 유효 인덱스가 없습니다. index < empty.length 반복은 0회지만 empty[0] 을 읽으면 예외입니다. |
| 관찰·상황 | 값·보충 정보 | 변화·이유 |
|---|---|---|
| 다음 인덱스 후보 정상 순회는 유효 위치를 읽은 뒤 3을 검사하고 실패 예제는 length=3을 직접 인덱스로 씁니다. |
|
|
| 읽기 허용 여부 순회는 3<3을 검사하지만 실패 예제는 사전 조건 없이 3번 위치를 읽으려 합니다. |
|
|
| 각 경로의 종료 조건 있는 순회는 정상 종료하고 무조건 접근은 예외로 끝납니다. |
|
|
배열의 선언·생성·초기화
public final class ArrayCreation {
public static void main(String[] args) {
int[] age;
age = new int[3];
System.out.println(age[0]);
System.out.println(age[1]);
System.out.println(age[2]);
}
}0
0
0int[] age는 int 배열을 가리킬 변수 선언이고 new int[3]은 길이 3의 배열을 생성합니다.
숫자 원소는 0, boolean은 false, 참조 원소는 null 기본값을 받습니다.
기본값이 저장된 회원을 뜻하지는 않으므로 실제 데이터 수를 별도로 관리해야 합니다.
배열 리터럴로 값과 길이를 함께 정할 수도 있습니다.
int[] age = {20, 30, 40};
String[] emails = {"variables", "conditions", "loops"};생성 뒤 길이는 바뀌지 않습니다.
더 큰 용량이 필요하면 새 배열을 만들고 값을 옮기거나 뒤에서 컬렉션을 사용합니다.
인덱스 기반 반복
public final class ArrayTraversal {
public static void main(String[] args) {
int[] age = {20, 30, 40};
int total = 0;
for (int index = 0; index < age.length; index++) {
total += age[index];
System.out.println(index + "=" + age[index]);
}
System.out.println("total=" + total);
}
}0=20
1=30
2=40
total=90인덱스는 원소를 읽고 바꾸는 위치입니다.
age[1] = 35는 둘째 원소만 변경합니다.
배열 변수에 새 배열을 대입하지 않는 한 같은 배열의 나머지 원소는 유지됩니다.
빈 배열도 유효합니다.
int[] empty = new int[0];길이는 0이고 정상 인덱스가 하나도 없습니다.
index < empty.length 반복은 0회 실행하지만 empty[0]은 예외입니다.
배열 참조 대입
배열 변수에는 배열을 가리키는 참조 값이 들어 있습니다.
다른 배열 변수에 대입하면 원소 전체가 복제되는 것이 아니라 같은 배열을 가리키는 참조가 복사됩니다.
import java.util.Arrays;
public final class ArrayAliasing {
public static void main(String[] args) {
int[] original = {20, 30, 40};
int[] alias = original;
alias[1] = 99;
System.out.println("original=" + Arrays.toString(original));
System.out.println("alias=" + Arrays.toString(alias));
System.out.println("same=" + (original == alias));
}
}original=[20, 99, 40]
alias=[20, 99, 40]
same=true두 변수는 서로 다른 이름이지만 같은 배열 객체를 가리킵니다.
따라서 alias를 통해 둘째 원소를 바꾸면 original로 읽어도 99입니다.
기본형 int 변수의 값 복사와 배열 참조 복사를 같은 방식으로 생각하면 이 동작을 놓치기 쉽습니다.
독립된 배열이 필요하면 새 배열을 만들고 원소를 복사해야 합니다.
import java.util.Arrays;
public final class IndependentArrayCopy {
public static void main(String[] args) {
int[] original = {20, 30, 40};
int[] copied = Arrays.copyOf(original, original.length);
copied[1] = 99;
System.out.println("original=" + Arrays.toString(original));
System.out.println("copied=" + Arrays.toString(copied));
System.out.println("same=" + (original == copied));
}
}original=[20, 30, 40]
copied=[20, 99, 40]
same=falseArrays.copyOf는 지정한 새 길이의 배열을 만들고 기존 원소를 옮깁니다.
참조 대입은 같은 배열을 공유하고 복사는 새 배열을 만든다
alias는 같은 배열을 가리키지만 copyOf는 새 배열을 만든 뒤 각 예제에서 index 1을 바꿉니다. 두 독립 실행을 대응 단계로 유지하여 참조와 변경 범위를 보여 줄 수 있습니다.
참조 대입 뒤에는 두 변수가 하나의 배열을 보지만 Arrays.copyOf를 사용한 별도 실행에서는 서로 다른 배열을 본다.
ARRAY · BOUNDARIES
참조 대입은 공유, 배열 복사는 독립
별도 실행 A와 B를 비교합니다. alias는 원본 배열을 함께 가리키고, Arrays.copyOf는 새 배열을 만들어 복사본의 변경을 분리합니다.
- 실행 A · ArrayAliasing
alias=original이므로 두 변수가 같은 배열을 참조합니다.alias[1]=99뒤 둘 다[20,99,40]을 읽고same=true입니다.- 실행 B · IndependentArrayCopy
- 다시
original=[20,30,40]으로 시작합니다.Arrays.copyOf(original,original.length)는 새 배열을 만듭니다.copied[1]=99뒤 원본은[20,30,40], 복사는[20,99,40],same=false입니다.
화살표는 변수의 참조입니다. 두 패널은 독립 실행이며 B는 A의 변경된 원본을 이어받지 않습니다. 이 예제는 int[] 원소 복사입니다.
이 예의 조건
- ArrayAliasing과 IndependentArrayCopy는 독립 실행입니다.
관계도의 보충 설명
| 구분 | 내용 |
|---|---|
| 전체 읽기 정보 | 참조 대입은 같은 배열을 공유하고 복사는 새 배열을 만든다 ArrayAliasing과 IndependentArrayCopy의 서로 독립된 실행에서의 참조 관계 참조 대입 뒤에는 두 변수가 하나의 배열을 보지만 Arrays.copyOf를 사용한 별도 실행에서는 서로 다른 배열을 본다. ARRAY · BOUNDARIES 참조 대입은 공유, 배열 복사는 독립 별도 실행 A와 B를 비교합니다. alias는 원본 배열을 함께 가리키고, Arrays.copyOf는 새 배열을 만들어 복사본의 변경을 분리합니다. 실행 A · ArrayAliasing alias=original 이므로 두 변수가 같은 배열을 참조합니다. alias[1]=99 뒤 둘 다 [20,99,40] 을 읽고 same=true 입니다. 실행 B · IndependentArrayCopy 다시 original=[20,30,40] 으로 시작합니다. Arrays.copyOf(original,original.length) 는 새 배열을 만듭니다. copied[1]=99 뒤 원본은 [20,30,40] , 복사는 [20,99,40] , same=false 입니다. 화살표는 변수의 참조입니다. 두 패널은 독립 실행이며 B는 A의 변경된 원본을 이어받지 않습니다. 이 예제는 int[] 원소 복사입니다. |
| 관찰·상황 | 값·보충 정보 | 변화·이유 |
|---|---|---|
| 초기 배열 두 실행의 원본은 20,30,40입니다. |
|
|
| 인덱스 1 변경 alias 또는 copied의 1번을 99로 바꿉니다. |
|
|
| 원본 비교 A 원본은 바뀌고 B 원본은 유지됩니다. |
|
|
참조 대입은 같은 배열을 공유하고 복사는 새 배열을 만든다
alias는 같은 배열을 가리키지만 copyOf는 새 배열을 만든 뒤 각 예제에서 index 1을 바꿉니다. 두 독립 실행을 대응 단계로 유지하여 참조와 변경 범위를 보여 줄 수 있습니다.
참조 대입 뒤에는 두 변수가 하나의 배열을 보지만 Arrays.copyOf를 사용한 별도 실행에서는 서로 다른 배열을 본다.
ARRAY · BOUNDARIES
참조 대입은 공유, 배열 복사는 독립
별도 실행 A와 B를 비교합니다. alias는 원본 배열을 함께 가리키고, Arrays.copyOf는 새 배열을 만들어 복사본의 변경을 분리합니다.
- 실행 A · ArrayAliasing
alias=original이므로 두 변수가 같은 배열을 참조합니다.alias[1]=99뒤 둘 다[20,99,40]을 읽고same=true입니다.- 실행 B · IndependentArrayCopy
- 다시
original=[20,30,40]으로 시작합니다.Arrays.copyOf(original,original.length)는 새 배열을 만듭니다.copied[1]=99뒤 원본은[20,30,40], 복사는[20,99,40],same=false입니다.
화살표는 변수의 참조입니다. 두 패널은 독립 실행이며 B는 A의 변경된 원본을 이어받지 않습니다. 이 예제는 int[] 원소 복사입니다.
이 예의 조건
- ArrayAliasing과 IndependentArrayCopy는 독립 실행입니다.
관계도의 보충 설명
| 구분 | 내용 |
|---|---|
| 전체 읽기 정보 | 참조 대입은 같은 배열을 공유하고 복사는 새 배열을 만든다 ArrayAliasing과 IndependentArrayCopy의 서로 독립된 실행에서의 참조 관계 참조 대입 뒤에는 두 변수가 하나의 배열을 보지만 Arrays.copyOf를 사용한 별도 실행에서는 서로 다른 배열을 본다. ARRAY · BOUNDARIES 참조 대입은 공유, 배열 복사는 독립 별도 실행 A와 B를 비교합니다. alias는 원본 배열을 함께 가리키고, Arrays.copyOf는 새 배열을 만들어 복사본의 변경을 분리합니다. 실행 A · ArrayAliasing alias=original 이므로 두 변수가 같은 배열을 참조합니다. alias[1]=99 뒤 둘 다 [20,99,40] 을 읽고 same=true 입니다. 실행 B · IndependentArrayCopy 다시 original=[20,30,40] 으로 시작합니다. Arrays.copyOf(original,original.length) 는 새 배열을 만듭니다. copied[1]=99 뒤 원본은 [20,30,40] , 복사는 [20,99,40] , same=false 입니다. 화살표는 변수의 참조입니다. 두 패널은 독립 실행이며 B는 A의 변경된 원본을 이어받지 않습니다. 이 예제는 int[] 원소 복사입니다. |
| 관찰·상황 | 값·보충 정보 | 변화·이유 |
|---|---|---|
| 초기 배열 두 실행의 원본은 20,30,40입니다. |
|
|
| 인덱스 1 변경 alias 또는 copied의 1번을 99로 바꿉니다. |
|
|
| 원본 비교 A 원본은 바뀌고 B 원본은 유지됩니다. |
|
|
새 길이가 더 크면 남은 칸은 기본값 0으로 채워지고, 더 작으면 뒤 원소가 잘립니다.
배열 자체의 length가 바뀐 것이 아니라 다른 길이의 새 배열이 생긴 것입니다.
int[] expanded = Arrays.copyOf(original, 5);
// [20, 30, 40, 0, 0]회원가입의 용량을 늘릴 때 emails와 age를 각각 새 배열로 복사할 수 있지만, 두 배열의 새 길이를 같게 유지해야 합니다.
또한 size는 실제 회원 수 그대로 두어 새로 생긴 기본값 칸을 회원 정보로 취급하지 않습니다.
복사 전후에 다음 불변식을 확인합니다.
0 <= size && size <= array.lengthindex0부터size- 1까지는 실제 회원입니다.- 복사 뒤 같은
index의 기존 값은 그대로입니다. size부터 새length- 1까지는 아직 사용하지 않는 공간입니다.
병렬 배열의 관계
클래스를 배우기 전에는 이메일과 나이를 같은 길이의 두 배열에 저장할 수 있습니다.
public final class ParallelArrays {
public static void main(String[] args) {
String[] emails = {"[email protected]", "[email protected]"};
int[] age = {40, 50};
if (emails.length != age.length) {
throw new IllegalStateException("parallel lengths differ");
}
for (int index = 0; index < emails.length; index++) {
System.out.println(emails[index] + "=" + age[index]);
}
}
}인덱스 0의 두 값이 첫 회원이고 인덱스 1의 두 값이 둘째 회원입니다.
throw new IllegalStateException(...)은 두 배열의 길이가 다를 때 현재 실행을 예외로 중단하는 문장입니다. 한 배열에만 있는 인덱스를 같은 회원으로 취급하는 대신, 관계가 깨졌음을 즉시 드러냅니다. 위의 두 배열은 길이가 모두 2이므로 이 분기는 실행되지 않습니다. 직접 확인하려면 나이 배열의 50을 빼고 실행해 정상 출력 대신 예외가 발생하는지 보세요.
정렬·삭제·삽입 때 한 배열만 움직이면 관계가 깨지는 약점이 있습니다.
ch4에서 하나의 객체로 묶기 전까지 모든 변경이 같은 인덱스를 유지해야 합니다.
용량과 저장 개수
public final class FixedMemberRegistry {
public static void main(String[] args) {
String[] emails = new String[3];
int[] age = new int[3];
int size = 0;
emails[size] = "[email protected]";
age[size] = 40;
size++;
emails[size] = "[email protected]";
age[size] = 50;
size++;
for (int index = 0; index < size; index++) {
System.out.println(emails[index] + "=" + age[index]);
}
System.out.println("size=" + size + ", capacity=" + emails.length);
}
}[email protected]=40
[email protected]=50
size=2, capacity=3emails.length는 세 칸의 용량이고 size는 의미 있는 두 회원의 수입니다.
length까지 순회하면 마지막 빈 칸이 null=0으로 출력됩니다.
추가할 때는 먼저 size < emails.length를 확인하고 두 배열을 같은 size 위치에 쓴 뒤 마지막에 size를 증가시킵니다.
이메일과 나이를 같은 인덱스로 맞춘 두 배열에서 size 2는 사용 영역이고 length 3은 전체 용량이다.
ARRAY · BOUNDARIES
같은 인덱스가 한 회원, size 뒤는 빈 용량
두 배열의 길이는3이지만 저장된 회원은2명입니다. 조회는 size 앞까지 하고, 추가는 같은 위치에 두 값을 쓴 뒤 개수를 늘립니다.
size=2, capacity=3
- index 0 · 회원
emails[0][email protected]·age[0]=40- index 1 · 회원
emails[1][email protected]·age[1]=50- index 2 · 미사용
emails[2]=null·age[2]=0. 기본값은 세 번째 회원이 아닙니다.
불변식: 두 배열의 길이는 같고 0 <= size && size <= length입니다. 조회는 index < size까지입니다. length까지 출력하면 null=0도 나옵니다.
추가 전에 용량 검사
size < emails.length인지 먼저 확인해야 합니다. 현재 예제의 두 번 쓰기에 이 검사 코드가 이미 있다는 뜻은 아닙니다.같은 위치에 두 값 쓰기
emails[size]와age[size]에 한 회원의 값을 씁니다.마지막에 개수 증가
두 값을 쓴 뒤
size++합니다. 새로 생긴 기본값 칸은 회원 수에 넣지 않습니다.
연습 문제
{25, 55, 35, 40} 배열에서 최댓값과 인덱스를 찾으세요.
첫 원소를 최댓값의 초깃값으로 사용해 실제 배열에 없는 값을 초기화에 쓰지 않습니다.
해설 보기
public final class OldestMember {
public static void main(String[] args) {
int[] age = {25, 55, 35, 40};
int maxIndex = 0;
for (int index = 1; index < age.length; index++) {
if (age[index] > age[maxIndex]) {
maxIndex = index;
}
}
System.out.println("index=" + maxIndex);
System.out.println("age=" + age[maxIndex]);
}
}index=1
age=55빈 배열에서는 index 0도 없으므로 이 알고리즘 전에 length가 0인지 검사해야 합니다.