좋아요! 정보처리기사 실기 준비를 위해 “기본적이지만 함정이 숨어 있어 기본기가 확실하지 않으면 틀리는 문제”를 드리겠습니다.
이번 문제는 Java로 준비해보았고, 값의 복사와 참조, 배열 초기화, 연산자 우선순위를 잘 이해해야 합니다.
배열, int[] arr = {1,2,3};,변경 가능이다.)
다음 코드를 보고 실행 결과를 맞춰보세요.
public class TrickTest {
public static void main(String[] args) {
int[] arr1 = new int[3]; // (1)
int[] arr2 = arr1; // (2)
arr1[0] = 10; // (3)
arr2[1] = 20; // (4)
int x = arr1[2] + arr2[2]++; // (5)
System.out.println(arr1[0]); // (6)
System.out.println(arr1[1]); // (7)
System.out.println(arr1[2]); // (8)
System.out.println(arr2[2]); // (9)
System.out.println(x); // (10)
}
}
✅ arr1과 arr2가 어떻게 연결되어 있나? (깊은 복사 vs 얕은 복사?)
✅ arr1[2] + arr2[2]++에서 증감 연산자의 동작 시점
✅ Java의 배열 초기값
int[] arr1 = new int[3];arr1을 길이 3으로 생성.[0, 0, 0] (int 배열의 초기값은 0).arr1 = [0, 0, 0]
int[] arr2 = arr1;arr2도 arr1을 참조. 즉 같은 배열을 가리키므로 둘은 동일한 메모리 주소.arr2 -> arr1
arr1[0] = 10;arr1 = [10, 0, 0]
arr2 = arr1 (같은 것)
arr2[1] = 20;arr1 = [10, 20, 0]
arr2 = arr1
int x = arr1[2] + arr2[2]++;여기가 트릭 포인트!
arr1[2] + arr2[2]++ 에서 arr2[2]++의 의미:
arr2[2]++는 후위 증가 연산자이므로 값을 먼저 반환하고, 그 후에 증가.따라서:
arr1 = [10, 20, 1]
arr2 = arr1
x = 0
(6) arr1[0] → 10
(7) arr1[1] → 20
(8) arr1[2] → 1
(9) arr2[2] → 1
(10) x → 0
10
20
1
1
0
참조 vs 복사
arr2 = arr1;은 깊은 복사가 아닌 같은 참조를 넘기므로, arr1과 arr2는 같은 배열을 가리킨다.
후위 연산자 ++ 동작
arr2[2]++에서 반환값은 증가 전의 값(0)이다. 증가 연산은 반환 이후 일어난다.
배열 초기화 값
Java에서 new int[3];은 [0, 0, 0]으로 초기화됨을 알아야 한다.
좋아요! 이제 단계별 디버깅을 함께해요.
이 문제는 값과 참조를 섞어서 num과 numbers의 상태가 어떻게 변하는지를 한 줄 한 줄 디버깅해 보겠습니다.
중간중간 메모리 상태를 도식화하고, 최종 출력값까지 정리해 드릴게요.
public class ValueReferenceTest {
public static void main(String[] args) {
int num = 100;
int[] numbers = {1, 2, 3};
process(num, numbers); // (1)
System.out.println(num); // (A)
System.out.println(numbers[0]); // (B)
num = changeNum(num); // (2)
numbers = changeArray(numbers); // (3)
System.out.println(num); // (E)
System.out.println(numbers[0]); // (F)
}
static void process(int n, int[] arr) {
n += 50;
arr[0] += 50;
}
static int changeNum(int n) {
n = n * 2;
return n;
}
static int[] changeArray(int[] arr) {
int[] newArr = new int[arr.length];
for (int i = 0; i < arr.length; i++) {
newArr[i] = arr[i] + 100;
}
return newArr;
}
}
num = 100
numbers -> [1, 2, 3]
process(num, numbers); 호출이 호출에서 일어나는 일:
n = num의 값 복사 → n = 100arr = numbers의 참조 복사 → arr와 numbers가 같은 배열을 참조n += 50; // n = 100 + 50 = 150 (단, 호출자 num에 영향 없음)
arr[0] += 50; // arr[0] = 1 + 50 = 51 → numbers[0]도 51로 바뀜
num = 100 (n은 process 안에서만 변했으므로 num에 영향 없음)
numbers -> [51, 2, 3] (arr와 numbers가 같은 참조였으므로 numbers[0]만 바뀜)
System.out.println(num); // 100
System.out.println(numbers[0]); // 51
num = changeNum(num); 호출num = 200
numbers -> [51, 2, 3]
numbers = changeArray(numbers); 호출int[] newArr = new int[arr.length]; // newArr = [0,0,0]
i=0 → newArr[0] = arr[0]+100 = 51+100=151
i=1 → newArr[1] = arr[1]+100 = 2+100=102
i=2 → newArr[2] = arr[2]+100 = 3+100=103
[151,102,103]를 참조[51,2,3] 배열은 이제 다른 참조 없음(하지만 process 안에서 수정된 값 남아있었음).System.out.println(num); // 200
System.out.println(numbers[0]); // 151
| 시점 | num | numbers 참조 | 배열 상태 |
|---|---|---|---|
| 시작 | 100 | numbers → arr1 | arr1 = [1,2,3] |
| process 후 | 100 | numbers → arr1 | arr1 = [51,2,3] |
| changeNum 후 | 200 | numbers → arr1 | arr1 = [51,2,3] |
| changeArray 후 | 200 | numbers → arr2 | arr2 = [151,102,103] |
💡 이렇게 한 줄 한 줄 디버깅해 보시면:
return 해서 참조를 교체할 경우 호출자 참조도 바뀜.return으로 값을 넣어줘서 그런거임.
class Parent {
final int value = 10;
void print() {
System.out.println("Parent: " + value);
}
}
class Child extends Parent {
int value = 20;
@Override
void print() {
System.out.println("Child: " + value);
}
void printParent() {
System.out.println("Parent's value: " + super.value);
}
}
public class Test {
public static void main(String[] args) {
Parent p = new Child(); // (1)
Child c = new Child(); // (2)
p.print(); // (A)
c.print(); // (B)
c.printParent(); // (C)
Parent p2 = c; // (D)
p2.print(); // (E)
System.out.println(p2.value); // (F)
}
}
Parent p = new Child();Child 객체를 생성해서 Parent 타입의 참조 p에 대입.동적 바인딩이 작동하기 위해,
print()호출할 때 Child의 메서드가 호출될 수 있음.
Child c = new Child();Child 객체를 생성해 Child 타입의 참조 c에 대입.c의 참조 타입과 실제 객체 타입 모두 Child.p.print();p 참조 타입은 Parent지만, 실제 객체가 Child이므로 Child의 print() 오버라이딩된 메서드 호출.Child: 20
c.print();c의 타입과 객체 모두 Child이므로 Child의 print() 호출Child: 20
c.printParent();printParent() 호출super.value → Parent의 value(=10)를 출력Parent's value: 10
Parent p2 = c;c를 Parent 타입 참조 p2에 대입.p2.print();p2 참조 타입은 Parent이지만 실제 객체 타입은 Child이므로 Child의 print() 호출Child: 20
System.out.println(p2.value);여기가 트릭 포인트!
value는 정적 바인딩되므로 참조 타입 기준(Parent)으로 찾음.value 필드를 출력 → 1010
Child: 20
Child: 20
Parent's value: 10
Child: 20
10
| 참조 | 실제 객체 | 호출되는 메서드 | 출력 |
|---|---|---|---|
| p | Child | print() | Child: 20 |
| c | Child | print() | Child: 20 |
| c | Child | printParent() | Parent's value: 10 |
| p2 | Child | print() | Child: 20 |
| p2 | Child | value 필드 | 10 |
✅ 메서드 호출은 동적 바인딩 → 실제 객체의 오버라이딩 메서드 호출
✅ 필드 접근은 정적 바인딩 → 참조 타입의 필드를 참조
✅ super.value는 부모 클래스의 필드를 가리킴