Java 클래스 · 상속 · 다형성 E05

메서드는 객체를 따르고 필드는 참조를 따른다. 생성자는 부모부터. 오버로딩은 컴파일 때, 오버라이딩은 실행 때. 네 문장이 17문항이다

왜 이 장을 하나
Java 문제의 함정은 언제나 지금 실행되는 것이 부모 코드인가 자식 코드인가다. 오버라이딩된 메서드는 객체의 실제 타입(new 뒤)을 따르고, 필드와 static 메서드와 오버로딩은 참조 변수의 타입(선언 쪽)을 따른다. 이 갈림 하나에 17문항이 걸려 있다.
다루는 것상속 · 오버라이딩 · 오버로딩 · 생성자 순서 · super · this · 인터페이스 · 추상 클래스
문항기출 17 · 예상 2 · 훈련 10
끝나면호출마다 객체 타입인지 참조 타입인지 가른다

01왜 이 장인가

왜 이걸 하나 — Java 34문항 중 17문항이 이 장이고 11회 중 10회에 나왔다. 2026년 2회에는 2문항이었다.

11회분 220문항 중 이 장의 기출은 17문항이다. 2023년 1회부터 2026년 2회까지 2024년 2회만 빼고 매 회 나왔고, 2023년 1회 · 3회 · 2024년 3회 · 2025년 3회 · 2026년 2회에는 2문항씩이었다. 훈련 Java 10문항(1~3 · 5 · 16~19 · 27 · 30)이 붙는다.

몇 문항무엇을 적나보기
오버라이딩 출력7부모 참조로 자식 메서드 호출2023-3회 1번 · 14번 · 2024-1회 16번 · 2024-3회 17번 · 2025-1회 13번 · 2026-2회 9번 · 16번
오버로딩 · 정적 바인딩4매개변수 타입으로 고르기, static 메서드2024-3회 18번 · 2025-1회 20번 · 2025-2회 10번 · 2026-1회 1번
생성자 순서3부모 생성자 먼저, this() · super()2023-1회 17번 · 20번 · 2024-1회 10번
인터페이스 · 키워드3implements 구현, super, 인터페이스 타입 호출2024-2회 12번 · 2025-3회 13번 · 14번

→ 17문항 중 11문항이 참조 타입 vs 객체 타입 하나로 갈린다. 2024년 3회 17번(a.x 는 부모 필드, a.getX() 는 자식 메서드)과 2025년 2회 10번(static 메서드는 참조 타입)이 이 규칙의 양쪽 끝이다.

E01 진단의 Java 3문항 중 9번(super.print → 10a20b)과 16번(재정의된 func → 509)이 이 장이다. Java 를 4년 써 온 개발자에게도 시험의 함정은 실무와 다른 자리에 있다 — 필드는 오버라이딩되지 않는다는 것과 오버로딩은 컴파일 때 정해진다는 것이다.

문항 수는 이 노트의 문제 데이터에서 ch=E05 인 것을 센 값이다(2026-09-14). 기출 코드는 grandlife.co.kr 복원에서 옮겼고, 17문항 전부 JDK 17 로 실행하거나(키워드 빈칸 2개는 채워서) 컴파일해 정답을 확인했다. 실행 순서 문제(2024-1회 10번)는 코드 추적으로 확인했다.

02한눈에

왜 이걸 하나 — 시험 전날 규칙 8개를 다시 본다. 전부 「누구를 따르나」다.

오버라이딩 — 객체를 따른다Parent p = new Child(); 에서 p.method() 는 Child 의 것. 부모 코드 안에서 자기를 부르는 draw() 도 자식 것이 실행된다(동적 바인딩). 재귀 호출도 마찬가지.
필드 — 참조를 따른다필드는 오버라이딩되지 않는다. p.x 는 Parent 의 x. 자식에 같은 이름의 x 가 있어도 참조 타입이 Parent 면 Parent 것. 메서드 안의 x 는 그 메서드가 속한 클래스의 x.
static 메서드 — 참조를 따른다static 메서드는 오버라이딩이 아니라 숨김(hiding). ref.tag() 는 참조 타입의 것. 인스턴스 메서드만 객체를 따른다.
오버로딩 — 컴파일 때 정해진다같은 이름 다른 매개변수. 어느 것을 부를지는 인자의 선언 타입으로 컴파일 때 결정. 정확히 일치 → 자동 형변환 → 박싱 → Object 순. 부모 코드 안의 f("a") 는 부모가 아는 시그니처 f(Object) 로 묶인다.
생성자 순서자식을 만들면 부모 생성자가 먼저(암묵적 super()). this(…) 는 같은 클래스의 다른 생성자를 먼저 부른다. 생성자 안에서 오버라이딩된 메서드를 부르면 자식 것이 실행된다.
super · thissuper.method() 는 부모 것을 명시적으로. super(a) 는 부모 생성자, 생성자의 첫 줄에만. this.x 는 내 필드, this(…) 는 내 다른 생성자.
추상 클래스 · 인터페이스abstract 메서드는 몸통 없음, 자식이 구현. 인터페이스는 implements 로 구현하고 모든 추상 메서드를 구현해야 한다. 인터페이스 타입 변수로 불러도 구현체의 코드가 돈다. 상속은 extends.
접근 제한 · 형변환private 는 자식도 못 본다(상속은 되지만 접근 불가). protected 는 자식 가능. 부모 참조 → 자식 참조는 (Child) p 캐스팅, instanceof 로 확인.

03밑바닥

왜 이걸 하나 — 규칙 8개는 「언제 정해지나」 하나로 묶인다. 컴파일 때 정해지면 참조, 실행 때 정해지면 객체.

3-1. 메서드는 실행 때, 필드는 컴파일 때

Java 는 인스턴스 메서드 호출을 실행 때 객체의 실제 클래스에서 찾는다(동적 바인딩). 그래서 NumberBox box = new SquareBox(); box.calculate(3) 은 SquareBox 의 것이 돌아 9 다(2024년 1회 16번). 부모 코드 안에서 부른 것도 같다 — 2023년 3회 1번의 Base.paint 가 부르는 draw() 는 Sub 의 draw 라 D 가 찍힌다.

필드는 다르다. 필드 접근은 컴파일 때 참조 변수의 타입으로 정해지므로 Base a = new Derived(); a.x 는 Base 의 3 이다. 2024년 3회 17번은 이 둘을 한 식에 넣었다 — a.getX() 는 Derived 것(7 × 3 = 21), a.x 는 Base 것(3), b.getX() 21, b.x 7, 합 52. 메서드는 객체 · 필드는 참조라고 두 번 갈라 적으면 된다.

2023년 3회 1번을 끝까지 따라가면 규칙이 다 보인다. value.paint() 는 Sub 의 paint → super.draw() 는 명시적이라 Base 의 draw 로 B, 그 안의 draw() 는 다시 동적 바인딩이라 Sub 의 D, 돌아와 C, this.draw() 로 D, 마지막 value.draw() 로 D → BDCDD. super 가 붙은 호출만 부모로 고정된다.

3-2. 오버로딩은 컴파일 때 시그니처로 정해진다

오버로딩은 같은 이름의 메서드를 매개변수 타입으로 고르는 것이고, 고르는 시점이 컴파일 때다. 2026년 1회 1번에서 A.g() 안의 f("a") 는 A 가 아는 메서드 f(Object) 로 묶이고, 실행 때 B 가 그 f(Object) 를 오버라이딩했으니 "2" 다. B 의 f(String) 은 A 가 모르는 시그니처라 후보에 없다.

2024년 3회 18번은 제네릭이 끼었다. Collection<T> 의 value 는 컴파일 때 Object 로 지워지므로 print(value)print(Object) 가 선택돼 B0 다. 실제 값이 Integer 라도 선택은 선언 타입으로 한다. 2025년 1회 20번은 calc("5") 가 String 판을 고르고 그 안에서 int 판을 부르는 꼴이라 두 정의를 오가며 4 가 된다.

static 메서드도 참조를 따른다. 2025년 2회 10번의 ref.tag() 는 Parent 참조라 "P", 인스턴스 메서드 ref.value(2) 는 Child 것이라 5 → 5P. static 은 오버라이딩이 아니라 숨김이라는 말이 이 뜻이다.

3-3. 생성자는 부모부터, 그 안의 오버라이딩은 자식 것

자식 객체를 만들면 부모 생성자가 먼저 돈다. 명시하지 않아도 super() 가 첫 줄에 있는 셈이고, 2024년 1회 10번의 순서(main → new Child → Child 생성자 → Parent 생성자 → p.getT() → Parent 의 getT)가 그것이다 — Child 의 getT(int) 는 매개변수가 달라 오버로딩이라 Parent 의 getT() 가 실행된다.

2025년 1회 13번은 생성자 안에서 오버라이딩된 메서드를 부른다. Base() 가 update() 를 부르면 객체는 이미 Derived 라 Derived.update 가 돌고, 그 시점의 static total 에 곱이 쌓인다. total 을 줄마다 표로 적어야 54 가 나온다. this(500) 은 같은 클래스의 다른 생성자를 먼저 부르는 것이라 2023년 1회 20번의 x 가 500 이 된다.

2026년 2회 9번의 super.print() 는 부모 print 를 먼저 돌려 10a 를 찍고 이어서 20b 를 찍는다. 2026년 2회 16번은 오버라이딩된 func 이 a × c 라 500, 부모 func 은 1 + 5 + 3 = 9, 합 509. 두 문항이 이 장의 규칙 둘(super 명시 · 오버라이딩)을 한 회차에 담았다.

3-4. 인터페이스는 계약이고 키워드가 답이다

인터페이스는 메서드 이름만 정한 계약이라 구현 클래스가 implements 로 받아 전부 구현해야 한다(2025년 3회 13번). 인터페이스 타입 변수로 불러도 구현체의 코드가 도는 것은 오버라이딩과 같은 원리다 — 2024년 2회 12번의 Odd · Even 구현체가 각각 25 · 20 을 돌려준다. 부모 생성자를 부르는 예약어는 super 다(2025년 3회 14번).

통과 기준은 넷이다. 첫째, 호출 하나하나에 「메서드 → 객체 타입, 필드 · static · 오버로딩 → 참조 타입」을 적용한다. 둘째, super 가 붙은 호출만 부모로 고정된다는 것을 안다. 셋째, 생성자 순서(부모 먼저 · this() 먼저)와 생성자 안의 오버라이딩 호출을 표로 따라간다. 넷째, 제네릭 · String · int 오버로딩의 선택 규칙을 말한다. 기출 17문항이 이 넷 안에 있다.
자식에 같은 이름의 필드가 있으면 부모 참조로도 자식 필드가 보인다.필드는 오버라이딩되지 않는다. Base a = new Derived(); a.x 는 Base 의 x 다. 2024년 3회 17번의 52 와 2025년 1회 13번의 54 가 이 규칙 위에 있다. 메서드만 객체를 따른다.
부모 코드가 f("a") 를 부르면 자식의 f(String) 이 선택된다.오버로딩 선택은 컴파일 때 부모가 아는 시그니처로 한다. 부모에 f(Object) 만 있으면 그것으로 묶이고, 실행 때 자식이 f(Object) 를 오버라이딩했으면 그것이 돈다. 2026년 1회 1번의 답이 3 이 아니라 2 인 이유다.

04기출

왜 이걸 하나 — 호출마다 객체 · 참조를 표시하며 푸는 연습이다.

코드의 호출 줄마다 옆에 「객」 또는 「참」을 적고, 생성자가 있으면 부모 → 자식 순서를 번호로 적는다. 정답을 연 뒤 어느 호출에서 갈렸는지 보면 규칙 8개 중 무엇이 빠졌는지 나온다.

2023-1회 · 17번Java

다음 Java 프로그램의 실행 결과를 쓰시오.

abstract class Vehicle { String name; abstract public String getName(String value); public String getName() { return "Vehicle name: " + name; } } class Car extends Vehicle { public Car(String value) { name = value; } public String getName(String value) { return "Car name: " + value; } } public class Main { public static void main(String[] args) { Vehicle vehicle = new Car("Spark"); System.out.println(vehicle.getName()); } }
정답Vehicle name: Spark

Vehicle name: Spark. 인자 없는 getName() 은 부모에만 있으므로 부모 것이 돌고, name 에는 생성자가 넣은 Spark 가 들어 있다.

원리 — 이름이 같은 메서드가 둘 있어도 매개변수의 꼴이 다르면 서로 다른 메서드다(오버로딩). Car 의 getName(String value) 는 인자를 하나 받는 메서드이고, Vehicle 의 getName() 은 인자가 없는 메서드라 둘은 남남이다. 자식이 부모 메서드를 갈아끼우는 것(오버라이딩)은 이름과 매개변수 꼴이 완전히 같을 때만 일어난다. vehicle.getName() 은 인자가 없으니 갈아끼워진 적 없는 부모의 것이 그대로 돈다. 추상 클래스는 직접 만들 수 없지만 자식 객체를 부모 이름으로 담는 것은 된다.

따라가기

  1. new Car("Spark") → Car 생성자에서 부모 필드 name = "Spark"
  2. vehicle.getName() — 인자 없음 → Vehicle.getName()
  3. "Vehicle name: " + name → Vehicle name: Spark

헷갈리는 자리 — Car 에 getName 이 있으니 "Car name: Spark" 라고 적는 것. 그것은 인자를 받는 다른 메서드라 이 호출로는 불리지 않는다. 오버로딩(같은 이름 · 다른 매개변수)과 오버라이딩(같은 이름 · 같은 매개변수)을 나누는 문제이고, 2024년 1회 10번의 getT() 도 같은 함정이다.

복원 코드를 JDK 17 로 컴파일 · 실행해 같은 출력을 확인했다. 복원 출처 grandlife.co.kr(2026-09-14) — 공식 원문이 아니라 문장 · 변수명이 실제와 다를 수 있다.

2023-1회 · 20번Java

다음 Java 프로그램의 실행 결과를 쓰시오.

class Parent { int x = 100; Parent() { this(500); } Parent(int x) { this.x = x; } int getX() { return x; } } class Child extends Parent { int x = 4000; Child() { this(5000); } Child(int x) { this.x = x; } } public class Main { public static void main(String[] args) { Child item = new Child(); System.out.println(item.getX()); } }
정답500

— 500. Child 를 만들 때 부모 생성자 사슬이 Parent 의 x 를 500 으로 만들고, getX() 는 부모 메서드라 부모의 x 를 돌려준다.

원리this(500) 은 같은 클래스의 다른 생성자를 부르는 것이고, 자식 생성자는 첫 줄에 super() 를 안 적어도 부모의 인자 없는 생성자가 몰래 먼저 불린다. 그래서 new Child() 는 Child() → Child(5000) → (암묵) Parent() → Parent(500) 순으로 이어진다. 부모와 자식이 같은 이름 x 를 각각 가지면 두 칸이 따로 있다(필드는 갈아끼워지지 않는다). getX() 는 Parent 에만 있으므로 그 안의 x 는 Parent 의 x 다.

따라가기

  1. Child() → this(5000) → Child(int x) 로 들어가기 전에 부모 생성자 Parent() 호출
  2. Parent() → this(500) → Parent(int x): Parent.x = 500
  3. 돌아와서 Child(int x): Child.x = 5000
  4. item.getX() → Parent 의 getX → Parent.x → 500

헷갈리는 자리 — 5000(자식 x)이나 4000(자식 초기값)을 적는 것. getX 는 자식 칸을 볼 수 없다. 100 을 적는 것은 this(500) 을 못 본 것이다. 필드는 참조형 · 소속 클래스에 따라, 메서드는 실제 객체에 따라 정해진다는 규칙을 이 문제로 외운다.

복원 코드를 JDK 17 로 컴파일 · 실행해 같은 출력을 확인했다. 복원 출처 grandlife.co.kr(2026-09-14) — 공식 원문이 아니라 문장 · 변수명이 실제와 다를 수 있다.

2023-3회 · 1번Java

다음 Java 프로그램의 실행 결과를 쓰시오.

class Base { public void paint() { System.out.print("A"); draw(); } public void draw() { System.out.print("B"); draw(); } } class Sub extends Base { public void paint() { super.draw(); System.out.print("C"); this.draw(); } public void draw() { System.out.print("D"); } } public class Main { public static void main(String[] args) { Base value = new Sub(); value.paint(); value.draw(); } }
정답BDCDD

— BDCDD. super.draw() 만 부모 것이고, 그 안에서 부르는 draw()this.draw() · value.draw() 는 전부 자식 것이다.

원리 — 객체가 Sub 이면 draw() 를 어디서 부르든 Sub 의 draw 가 돈다(오버라이딩, 실제 객체 기준). 딱 하나 예외가 super.draw() 로, "부모 판을 직접 불러라"는 지시다. 그런데 그 부모 판 안에서 다시 draw() 를 부르면 그것은 super 가 아니라 보통 호출이라 다시 자식 것으로 간다. 즉 super 는 그 한 줄에만 효력이 있고 안쪽까지 번지지 않는다. value.paint() 도 Sub 의 paint 다.

따라가기

  1. value.paint() → Sub.paint
  2. super.draw() → Base.draw: B 를 찍고 draw() 호출 → Sub.draw: D
  3. print("C") → C
  4. this.draw() → Sub.draw: D
  5. value.draw() → Sub.draw: D. 합쳐서 BDCDD

헷갈리는 자리 — Base.draw 안의 draw() 를 부모 것으로 보고 B 가 계속 찍힌다고(무한 반복) 여기는 것. 자식이 draw 를 덮어썼으니 D 한 번으로 끝난다. Base.paint 의 A 는 한 번도 안 찍힌다 — paint 도 갈아끼워졌다.

복원 코드를 JDK 17 로 컴파일 · 실행해 같은 출력을 확인했다. 복원 출처 grandlife.co.kr(2026-09-14) — 공식 원문이 아니라 문장 · 변수명이 실제와 다를 수 있다.

2023-3회 · 14번Java

다음 Java 프로그램의 실행 결과를 쓰시오.

class Parent { int compute(int number) { if (number <= 1) { return number; } return compute(number - 1) + compute(number - 2); } } class Child extends Parent { int compute(int number) { if (number <= 1) { return number; } return compute(number - 1) + compute(number - 3); } } public class Main { public static void main(String[] args) { Parent value = new Child(); System.out.print(value.compute(7)); } }
정답2

— 2. 부모 타입 변수에 자식 객체가 들어 있으므로 Child 의 compute(n − 1 과 n − 3 규칙)가 돌고, 그 재귀 안의 호출도 전부 Child 것이다.

원리Parent value = new Child() 는 겉은 Parent, 속은 Child 다. 메서드는 속(실제 객체)을 따르므로 value.compute(7) 은 Child.compute 다. 그 안에서 자기를 다시 부르는 compute(number - 1) 도 같은 객체의 메서드라 계속 Child 규칙이다. 멈춤 조건 number <= 1 은 그 수를 그대로 돌려주므로 0 과 음수도 값으로 나온다 — 여기서 −1 이 나오는 것이 계산의 핵심이다. 작은 수부터 표를 만들어 올라간다.

따라가기 — c(n) = c(n − 1) + c(n − 3), n ≤ 1 이면 n.

  1. c(1) = 1, c(0) = 0, c(−1) = −1
  2. c(2) = c(1) + c(−1) = 1 − 1 = 0
  3. c(3) = c(2) + c(0) = 0, c(4) = c(3) + c(1) = 1
  4. c(5) = c(4) + c(2) = 1, c(6) = c(5) + c(3) = 1
  5. c(7) = c(6) + c(4) = 1 + 1 = 2

헷갈리는 자리 — Parent 규칙(n − 1, n − 2)으로 풀어 피보나치 13 을 적는 것. 그리고 c(−1) 을 0 으로 두는 것 — 조건이 <= 1 이라 −1 을 그대로 돌려준다. 이 두 자리에서 답이 갈린다.

복원 코드를 JDK 17 로 컴파일 · 실행해 같은 출력을 확인했다. 복원 출처 grandlife.co.kr(2026-09-14) — 공식 원문이 아니라 문장 · 변수명이 실제와 다를 수 있다.

2024-1회 · 10번Java

다음 Java 코드에서 번호가 붙은 부분의 실행 순서를 중복 번호 없이 쓰시오.

class Parent { Parent(int a, int b) { } // ① void getT() { } // ② } class Child extends Parent { Child(int x) { // ③ super(x + 1, x); } void getT(int value) { } // ④ } public class Main { public static void main(String[] args) { // ⑤ Parent p = new Child(3); // ⑥ p.getT(); // ⑦ } }
정답⑤ → ⑥ → ③ → ① → ⑦ → ②

— ⑤ → ⑥ → ③ → ① → ⑦ → ②. main 이 시작되고, 자식 생성자가 부모 생성자를 부른 뒤, getT() 는 부모의 것이 불린다.

원리 — 객체를 만들 때는 자식 생성자에 먼저 들어가지만, 그 첫 줄의 super(…) 가 부모 생성자를 부르므로 실제로 몸통이 먼저 끝나는 것은 부모 쪽이다. 그래서 순서는 "자식 진입 → 부모 완료 → 자식 완료"다. Child 의 getT(int value) 는 인자를 받으니 Parent 의 getT() 와 다른 메서드(오버로딩)이고, p.getT() 는 인자가 없으니 부모 것이다. 번호를 "실행이 그 줄에 도달한 순서"로 적는다.

따라가기

  1. ⑤ main 시작
  2. ⑥ new Child(3) 평가 시작 → ③ Child 생성자 진입 → super(4, 3) → ① Parent 생성자
  3. ⑦ p.getT() 호출 → ② Parent.getT 실행
  4. ④ 는 인자 있는 다른 메서드라 한 번도 안 불린다

헷갈리는 자리 — ① 을 ③ 보다 앞에 두는 것. 부모 생성자가 먼저 "끝나지만" 진입은 자식이 먼저라, 문제가 "실행 순서"를 물으면 ③ → ① 이다. ④ 를 넣어 ⑦ → ④ 로 적는 것 — getT()getT(int) 는 다른 메서드다. 2023년 1회 17번과 같은 함정이다.

각 자리에 출력문을 넣어 JDK 17 로 실행해 순서를 확인했다. 복원 출처 grandlife.co.kr(2026-09-14) — 공식 원문이 아니라 문장 · 변수명이 실제와 다를 수 있다.

2024-1회 · 16번Java

다음 Java 프로그램의 실행 결과를 쓰시오.

class NumberBox { int calculate(int n) { return n + 1; } } class SquareBox extends NumberBox { @Override int calculate(int n) { return n * n; } } public class Main { public static void main(String[] args) { NumberBox box = new SquareBox(); System.out.print(box.calculate(3)); } }
정답9

— 9. 부모 타입으로 담았어도 실제 객체가 SquareBox 이므로 갈아끼운 calculate(n × n)가 돌아 3 × 3 이다.

원리 — 자식이 부모와 이름 · 매개변수가 같은 메서드를 다시 정의하면(오버라이딩) 그 객체에서는 언제나 자식 판이 돈다. 변수의 겉모양(NumberBox box)은 "무엇을 부를 수 있는가"만 정하고, "어느 판이 도는가"는 new SquareBox() 가 정한다. @Override 는 컴파일러에게 "이건 갈아끼우기다, 부모에 같은 메서드가 없으면 오류를 내라"고 알리는 표시일 뿐 동작을 바꾸지는 않는다.

따라가기

  1. box 의 겉 NumberBox, 속 SquareBox
  2. box.calculate(3) → 속의 판 → 3 × 3 = 9

헷갈리는 자리 — 겉이 NumberBox 니까 n + 1 = 4 라고 적는 것. 필드라면 겉을 따르지만 메서드는 속을 따른다. 이 규칙 하나가 자바 문제 절반의 뼈대라 "필드는 겉, 메서드는 속"으로 외운다. 만약 SquareBox 가 calculate 를 안 만들었다면 부모 것이 돌아 4 가 됐을 것이다.

복원 코드를 JDK 17 로 컴파일 · 실행해 같은 출력을 확인했다. 복원 출처 grandlife.co.kr(2026-09-14) — 공식 원문이 아니라 문장 · 변수명이 실제와 다를 수 있다.

2024-2회 · 12번Java

다음 Java 프로그램의 실행 결과를 쓰시오.

public class Main { public static void main(String[] args) { int odd = 0; int even = 0; for (int i = 1; i <= 9; i++) { if (i % 2 == 0) even += i; else odd += i; } System.out.printf("%d, %d", odd, even); } }
정답25, 20

25, 20. 1 부터 9 까지 홀수의 합이 25, 짝수의 합이 20 이고 printf 의 서식대로 쉼표와 띄어쓰기가 붙는다.

원리i % 2 == 0 은 2 로 나눠 나머지가 0, 즉 짝수라는 뜻이다. 반복이 1 부터 9 까지 돌며 짝수는 even 에, 나머지(홀수)는 odd 에 더한다. printf("%d, %d", odd, even) 은 서식 문자열의 %d 자리에 차례로 값을 끼우고 나머지 글자(쉼표 · 공백)는 그대로 찍는다. 자바의 printf 는 C 와 같은 꼴이며 줄바꿈은 안 넣는다.

따라가기

  1. 홀수 1 + 3 + 5 + 7 + 9 = 25
  2. 짝수 2 + 4 + 6 + 8 = 20
  3. 서식 "%d, %d" → 25, 20

헷갈리는 자리 — 순서를 바꿔 20, 25 로 적는 것(첫 %d 가 odd). 쉼표 뒤 공백을 빼먹는 것 — 서식 문자열의 글자가 답의 일부다. 9 를 짝수에 넣거나 0 을 세는 것도 흔한 실수인데, 반복은 1 부터라 0 은 없다. 조건이 i < 9 였다면 9 가 빠져 16, 20 이 된다 — 등호를 본다.

복원 코드를 JDK 17 로 컴파일 · 실행해 같은 출력을 확인했다. 복원 출처 grandlife.co.kr(2026-09-14) — 공식 원문이 아니라 문장 · 변수명이 실제와 다를 수 있다.

2024-3회 · 17번Java

다음 Java 프로그램의 실행 결과를 쓰시오.

public class Main { public static void main(String[] args) { Base a = new Derived(); Derived b = new Derived(); System.out.print(a.getX() + a.x + b.getX() + b.x); } } class Base { int x = 3; int getX() { return x * 2; } } class Derived extends Base { int x = 7; int getX() { return x * 3; } }
정답52

— 52. 메서드는 실제 객체(Derived)를 따라 둘 다 21 이고, 필드 x 는 변수의 겉모양을 따라 a.x 는 3, b.x 는 7 이다.

원리 — 자바에서 메서드는 갈아끼워지지만(오버라이딩) 필드는 갈아끼워지지 않는다. 부모와 자식이 같은 이름의 필드를 가지면 객체 안에 두 칸이 나란히 있고, 어느 칸을 읽을지는 변수의 선언 타입이 정한다. Base a = new Derived() 에서 a.x 는 Base 의 칸(3), a.getX() 는 Derived 의 메서드(그 안의 x 는 Derived 칸 7 → 21). Derived b 는 겉도 Derived 라 b.x 가 7 이다. 한 문제 안에 겉 · 속 규칙을 두 번 넣은 것이다.

따라가기

  1. a.getX() → Derived.getX → 7 × 3 = 21
  2. a.x → 겉이 Base → 3
  3. b.getX() → 21, b.x → 겉이 Derived → 7
  4. 21 + 3 + 21 + 7 = 52

헷갈리는 자리 — a.x 를 7 로 읽어 56 을 적는 것. 필드는 겉을 따른다. a.getX() 를 Base 판(3 × 2 = 6)으로 읽어 37 로 적는 것 — 메서드는 속을 따른다. 두 규칙을 반대로 적용하면 정확히 틀린다.

복원 코드를 JDK 17 로 컴파일 · 실행해 같은 출력을 확인했다. 복원 출처 grandlife.co.kr(2026-09-14) — 공식 원문이 아니라 문장 · 변수명이 실제와 다를 수 있다.

2024-3회 · 18번Java

다음 Java 프로그램의 실행 결과를 쓰시오.

class Printer { void print(Integer value) { System.out.print("A" + value); } void print(Object value) { System.out.print("B" + value); } void print(Number value) { System.out.print("C" + value); } } public class Main { static class Collection<T> { T value; Collection(T value) { this.value = value; } void print() { new Printer().print(value); } } public static void main(String[] args) { new Collection<>(0).print(); } }
정답B0

— B0. 제네릭 T 는 컴파일 때 Object 로 지워지므로 print(value)print(Object) 로 묶이고, 값 0 이 뒤에 붙는다.

원리 — 오버로딩(같은 이름 · 다른 매개변수 타입) 중 어느 것을 부를지는 실행 때가 아니라 컴파일 때 "변수의 선언 타입"으로 정해진다. Collection<T> 안의 value 는 타입이 T 인데, 자바의 제네릭은 컴파일이 끝나면 T 를 그 상한인 Object 로 바꿔 버린다(타입 소거). 그래서 new Printer().print(value) 는 실제로 Integer 0 이 들어 있어도 print(Object) 로 묶인다. 실행 때 "더 잘 맞는 Integer 판"으로 바꿔 타는 일은 없다 — 오버로딩은 정적이다.

따라가기

  1. new Collection<>(0) → T 는 Integer 로 추론되지만 코드 안에서는 Object 취급
  2. print(value) → 컴파일 때 print(Object) 선택
  3. "B" + 0 → B0

헷갈리는 자리 — 값이 정수니 print(Integer) 의 A0, 혹은 Number 라서 C0 을 적는 것. 오버로딩 선택은 값이 아니라 선언 타입으로, 그것도 컴파일 때 끝난다. 2026년 1회 1번(f(Object) 와 f(String))이 같은 규칙의 다른 얼굴이다.

복원 코드를 JDK 17 로 컴파일 · 실행해 같은 출력을 확인했다. 복원 출처 grandlife.co.kr(2026-09-14) — 공식 원문이 아니라 문장 · 변수명이 실제와 다를 수 있다.

2025-1회 · 13번Java

다음 Java 프로그램의 실행 결과를 쓰시오.

public class Main { public static void main(String[] args) { new Derived(); System.out.println(Base.total); } } class Base { static int total = 0; int value = 1; Base() { total += ++value; update(); } void update() { total += total; } } class Derived extends Base { int value = 10; Derived() { value += 2; total += value++; update(); } @Override void update() { total += total * 2; } }
정답54

— 54. 부모 생성자 안에서 부른 update() 도 자식 판이 돌아 total 이 2 → 6 이 되고, 자식 생성자에서 12 를 더해 18, 다시 자식 update 로 54 가 된다.

원리new Derived() 는 부모 생성자 Base() 를 먼저 돌린다. 그 안에서 update() 를 부르면 만들어지는 객체가 Derived 이므로 갈아끼운 Derived.update 가 돈다 — 생성자 안이라도 오버라이딩은 그대로다. ++value 는 먼저 1 을 올린 값을 쓰고, value++ 는 지금 값을 쓴 뒤 올린다. Base 의 value 와 Derived 의 value 는 이름만 같은 다른 칸이다. static 인 total 은 클래스에 하나뿐이라 부모 · 자식이 같은 것을 고친다.

따라가기 — total = 0.

  1. Base(): total += ++value → Base.value 1 → 2, total = 2
  2. Base() 안 update() → Derived.update: total += total × 2 → 2 + 4 = 6
  3. Derived(): value(10) += 2 → 12; total += value++ → 6 + 12 = 18 (value 는 13)
  4. update() → 18 + 36 = 54

헷갈리는 자리 — 부모 생성자 안의 update 를 부모 판(total += total → 4)으로 계산해 다른 값을 얻는 것. 그리고 ++value 를 1 로 쓰는 것 — 앞에 붙은 ++ 는 올린 뒤의 값 2 다. 6 → 18 → 54 의 세 배 규칙이 보이면 맞게 간 것이다.

복원 코드를 JDK 17 로 컴파일 · 실행해 같은 출력을 확인했다. 복원 출처 grandlife.co.kr(2026-09-14) — 공식 원문이 아니라 문장 · 변수명이 실제와 다를 수 있다.

2025-1회 · 20번Java

다음 Java 프로그램의 실행 결과를 쓰시오.

public class Main { static int calc(int value) { if (value <= 1) return value; return calc(value - 1) + calc(value - 2); } static int calc(String text) { int value = Integer.parseInt(text); if (value <= 1) return value; return calc(value - 1) + calc(value - 3); } public static void main(String[] args) { System.out.println(calc("5")); } }
정답4

— 4. calc("5") 는 String 판에서 시작해 calc(4)calc(2) 를 부르는데, 인자가 int 라 그 뒤로는 전부 int 판(n − 1, n − 2)이다.

원리 — 같은 이름 calc 가 둘인데 하나는 String 을, 하나는 int 를 받는다(오버로딩). 어느 것을 부를지는 넘기는 값의 타입으로 정해진다. calc("5") 는 String 판이고, 그 안의 calc(value - 1) 은 int 를 넘기니 int 판이다. String 판은 딱 한 번만 돌고 규칙(n − 1, n − 3)도 그 한 번에만 쓰인다. 나머지는 int 판의 피보나치 규칙이다. 두 규칙을 언제 바꿔 타는지가 전부다.

따라가기 — int 판: c(0) = 0, c(1) = 1, c(2) = 1, c(3) = 2, c(4) = 3.

  1. calc("5") → value 5 → calc(4) + calc(2) (둘 다 int 판)
  2. calc(4) = c(3) + c(2) = 2 + 1 = 3
  3. calc(2) = c(1) + c(0) = 1
  4. 3 + 1 = 4

헷갈리는 자리 — String 판 규칙(n − 1, n − 3)을 끝까지 쓰는 것. 첫 호출 뒤에는 인자가 int 라 String 판으로 돌아오지 않는다. 반대로 처음부터 int 판으로 보고 c(5) = 5 를 적는 것 — 첫 갈림이 (4, 2) 다.

복원 코드를 JDK 17 로 컴파일 · 실행해 같은 출력을 확인했다. 복원 출처 grandlife.co.kr(2026-09-14) — 공식 원문이 아니라 문장 · 변수명이 실제와 다를 수 있다.

2025-2회 · 10번Java

다음 Java 프로그램의 실행 결과를 쓰시오.

public class Main { static class Parent { int value(int n) { return n + 2; } static String tag() { return "P"; } } static class Child extends Parent { int value(int n) { return n + 3; } static String tag() { return "C"; } } public static void main(String[] args) { Parent ref = new Child(); System.out.print(ref.value(2) + ref.tag()); } }
정답5P

— 5P. 인스턴스 메서드 value 는 자식 판(n + 3)이 돌아 5 이고, static 메서드 tag 는 변수의 겉모양 Parent 를 따라 P 다.

원리 — 오버라이딩은 인스턴스 메서드에만 일어난다. static 메서드는 객체가 아니라 클래스에 붙어 있어서 ref.tag() 라고 써도 실제로는 Parent.tag() 로 컴파일된다(변수의 선언 타입). 자식이 같은 이름의 static 메서드를 만들어도 그것은 "숨기기"일 뿐 갈아끼우기가 아니다. 그래서 같은 객체에서 value 는 속(Child)을, tag 는 겉(Parent)을 따른다. 필드가 겉을 따르는 것과 같은 편이다.

따라가기

  1. ref.value(2) → Child.value → 2 + 3 = 5
  2. ref.tag() → static → Parent.tag → "P"
  3. 5 + "P" → 문자열 결합 → 5P

헷갈리는 자리 — 5C 로 적는 것. static 은 오버라이딩되지 않는다. 겉을 따르는 것 두 가지(필드 · static 메서드)와 속을 따르는 것 한 가지(인스턴스 메서드)로 정리해 둔다. 숫자와 문자열의 + 는 결합이라 5P 이지 덧셈이 아니다.

복원 코드를 JDK 17 로 컴파일 · 실행해 같은 출력을 확인했다. 복원 출처 grandlife.co.kr(2026-09-14) — 공식 원문이 아니라 문장 · 변수명이 실제와 다를 수 있다.

2025-3회 · 13번Java

다음 Java 프로그램의 빈칸에 들어갈 키워드를 쓰시오.

interface Machine { void run(); } class Washer ① Machine { public void run() { System.out.print("RUN"); } }
정답implements

implements. 클래스가 인터페이스를 따를 때 쓰는 예약어다.

원리 — 인터페이스는 "이런 메서드를 반드시 갖춰라"는 약속 목록이다. 클래스가 그 약속을 지키겠다고 선언하는 말이 implements(구현하다)이고, 그 클래스는 목록의 메서드를 전부 몸통과 함께 만들어야 한다. 반면 클래스가 다른 클래스를 물려받을 때는 extends(확장하다)를 쓴다. 인터페이스가 다른 인터페이스를 물려받을 때도 extends 다. Machine 이 interface 로 선언됐으니 Washer 쪽은 implements 다.

따라가기

  1. Machine 은 interface → 약속 run() 하나
  2. Washer 는 class → 약속을 지키는 쪽 → implements Machine
  3. run() 을 public 으로 구현했으므로 컴파일된다

헷갈리는 자리 — extends 를 적는 것. 클래스 → 인터페이스는 implements, 클래스 → 클래스와 인터페이스 → 인터페이스는 extends 로 짝을 외운다. 인터페이스 메서드는 기본이 public 이라 구현할 때 public 을 빼면 오류가 나는 것도 같은 묶음의 함정이다.

빈칸에 답을 넣어 JDK 17 로 컴파일해 확인했다. 복원 출처 grandlife.co.kr(2026-09-14) — 공식 원문이 아니라 문장 · 변수명이 실제와 다를 수 있다.

2025-3회 · 14번Java

다음 Java 코드의 ①에 들어갈 예약어를 쓰시오.

class Rectangle { int width; int height; Rectangle(int width, int height) { this.width = width; this.height = height; } } class Square extends Rectangle { Square(int side) { ①(side, side); } int area() { return width * height; } }
정답super

super. 자식 생성자에서 부모 생성자를 부를 때 쓰는 예약어다.

원리 — Rectangle 에는 인자 없는 생성자가 없고 (width, height) 를 받는 생성자만 있다. 자식 Square 를 만들려면 부모 부분도 만들어져야 하는데, 자바는 부모의 인자 없는 생성자를 몰래 부르므로 그런 생성자가 없으면 컴파일 오류가 난다. 그래서 자식 생성자 첫 줄에 super(side, side) 로 "부모의 저 생성자를 이 값으로 불러라"고 직접 적어야 한다. 정사각형은 가로와 세로가 같으니 side 를 두 번 넘긴다. super(...) 는 반드시 첫 줄이어야 한다.

따라가기

  1. new Square(5) → Square(int side) → super(5, 5) → Rectangle(5, 5)
  2. width = 5, height = 5 → area() = 25

헷갈리는 자리this(side, side) 를 적는 것. this(…) 는 같은 클래스의 다른 생성자를 부르는 것인데 Square 에는 두 인자 생성자가 없다. Rectangle(side, side) 처럼 클래스 이름을 적는 것도 오류다 — 생성자 호출은 super 나 this 뿐이다.

빈칸에 답을 넣어 JDK 17 로 컴파일해 확인했다. 복원 출처 grandlife.co.kr(2026-09-14) — 공식 원문이 아니라 문장 · 변수명이 실제와 다를 수 있다.

2026-1회 · 1번Java

다음 Java 프로그램을 실행했을 때 출력되는 값을 쓰시오.

class A { String f(Object x) { return "1"; } String g() { return f("a"); } } class B extends A { String f(Object x) { return "2"; } String f(String x) { return "3"; } } public class Main { public static void main(String[] args) { A a = new B(); System.out.println(a.g()); } }
정답2

— 2. a.g() 는 A 에 적힌 g() 를 돌리고, 그 안의 f("a") 는 컴파일 때 f(Object) 로 정해진 뒤 실행 때 B 가 덮어쓴 f(Object) 로 간다.

원리 — 자바는 메서드를 두 번 고른다. 첫째, 컴파일 때 "어느 이름 · 어느 매개변수 꼴인가"를 고른다(오버로딩 선택). 둘째, 실행 때 "그 꼴을 가진 메서드 중 실제 객체의 것"을 고른다(오버라이딩). 편지에 비유하면 봉투의 주소(시그니처)는 부칠 때 정해지고, 그 집에 누가 사는지(실제 객체)는 배달할 때 본다. 봉투를 쓰는 곳은 A 안이라 A 가 아는 f(Object) 라고만 적힌다. B 의 f(String) 은 이름만 같을 뿐 다른 봉투라 후보에도 못 든다.

따라가기

  1. A a = new B(); — 겉은 A, 속은 B
  2. a.g() — g 는 A 에만 있으니 A.g 가 돈다
  3. A.g 안의 f("a") — A 클래스 안에서 컴파일됐고 A 가 아는 f 는 f(Object) 하나. 봉투 = f(Object)
  4. 실행 때 속 객체 B 에 f(Object) 가 덮어써져 있으므로 B.f(Object) → 2

헷갈리는 자리 — "a" 는 String 이니 B.f(String) 이 더 잘 맞지 않나? 아니다. 오버로딩 선택은 A 안에서 끝났고 그때 f(String) 은 없었다. 답을 3 으로 적는 것이 이 문제의 함정이다. 2024년 3회 18번(제네릭 T 가 Object 로 지워져 print(Object) 가 골라짐)과 같은 규칙이다.

복원 코드를 JDK 17 로 컴파일 · 실행해 같은 출력을 확인했다. 복원 출처 grandlife.co.kr(2026-09-14) — 공식 원문이 아니라 문장 · 변수명이 실제와 다를 수 있다.

2026-2회 · 9번Java

다음 Java 프로그램의 실행 결과를 쓰시오.

class A { int a; public A(int a) { this.a = a; } void print() { System.out.print(a + "a"); } } class B extends A { int b; public B(int a, int b) { super(a); this.b = b; } @Override void print() { super.print(); System.out.print(b + "b"); } } public class Main { public static void main(String[] args) { B obj = new B(10, 20); obj.print(); } }
정답10a20b

— 10a20b. 자식의 print 가 먼저 부모 print 를 부르고(10a), 이어서 자기 것을 찍는다(20b).

원리super(a) 는 부모 생성자에 값을 넘겨 부모 필드 a 를 채우는 것이고, super.print() 는 갈아끼우기 전의 부모 판 print 를 그 자리에서 한 번 부르는 것이다. 자식이 print 를 덮어썼어도 super. 을 붙이면 부모 판이 돈다. 문자열과 숫자를 + 로 이으면 덧셈이 아니라 붙이기라 a + "a" 는 "10a" 다. 두 print 는 print(줄바꿈 없음)라 한 줄에 이어진다.

따라가기

  1. new B(10, 20) → super(10) → A.a = 10, 이어서 B.b = 20
  2. obj.print() → B.print → super.print() → A.print → 10a
  3. 이어서 b + "b" → 20b. 합쳐서 10a20b

헷갈리는 자리 — 20b 만 적는 것(super.print 를 빼먹음)이나 순서를 바꿔 20b10a 로 적는 것. super.print() 가 먼저 오는 줄이라 10a 가 앞이다. 10 + "a" 를 계산하려 드는 것 — 한쪽이 문자열이면 붙이기다.

복원 코드를 JDK 17 로 컴파일 · 실행해 같은 출력을 확인했다. 복원 출처 grandlife.co.kr(2026-09-14) — 공식 원문이 아니라 문장 · 변수명이 실제와 다를 수 있다.

2026-2회 · 16번Java

다음 Java 프로그램의 실행 결과를 쓰시오.

class A { int a; private int b; protected int c; A(int a, int b, int c) { this.a = a; this.b = b; this.c = c; } int func() { return a + b + c; } } class B extends A { B(int a, int b, int c) { super(a, b, c); } @Override public int func() { return a * c; } } public class Main { public static void main(String[] args) { A a = new A(1, 5, 3); B b = new B(10, 30, 50); System.out.print(a.func() + b.func()); } }
정답509

— 509. a.func() 는 부모 판이라 1 + 5 + 3 = 9, b.func() 는 갈아끼운 판이라 10 × 50 = 500 이다.

원리 — a 는 A 로 만든 객체이므로 A.func 가 그대로 돌고, b 는 B 로 만들었으니 B 가 덮어쓴 func 가 돈다. B.func 안에서 ac 를 쓸 수 있는 이유는 접근 범위 때문이다 — int a(기본, 같은 패키지)와 protected int c 는 자식이 볼 수 있지만 private int b 는 A 안에서만 보인다. 그래서 B.func 가 b 를 안 쓴 것이고, 만약 썼다면 컴파일 오류다. super(a, b, c) 로 값은 전부 부모 칸에 저장돼 있다.

따라가기

  1. a = new A(1, 5, 3) → a.func() = 1 + 5 + 3 = 9
  2. b = new B(10, 30, 50) → super 로 a 10, b 30, c 50
  3. b.func() → B.func → 10 × 50 = 500
  4. 9 + 500 = 509

헷갈리는 자리 — b.func() 도 부모 판으로 계산해 90 을 더하는 것(99). 오버라이딩된 객체는 자기 판이다. 답이 정수 덧셈인 것도 확인한다 — 둘 다 int 라 붙이기가 아니라 더하기다(9509 가 아니다).

복원 코드를 JDK 17 로 컴파일 · 실행해 같은 출력을 확인했다. 복원 출처 grandlife.co.kr(2026-09-14) — 공식 원문이 아니라 문장 · 변수명이 실제와 다를 수 있다.

예상 1회 · 1번Java빈출 원리: 오버라이딩 vs 필드

다음 코드의 출력을 쓰시오.

class Animal { String name = "Animal"; void cry() { System.out.print("A"); } void sound() { cry(); System.out.print(name); } } class Dog extends Animal { String name = "Dog"; void cry() { System.out.print("D"); } } public class Main { public static void main(String[] args) { Animal a = new Dog(); a.sound(); System.out.print(a.name); } }
정답DAnimalAnimal

— DAnimalAnimal. 메서드는 자식 것이 불리지만(D), 필드는 참조 타입인 Animal 것이 읽힌다(Animal · Animal).

원리 — 자바에서 메서드 호출은 "실제 객체"를 보고 결정된다(동적 바인딩). a 는 Animal 타입 변수지만 안에 든 것은 Dog 이므로 cry() 는 Dog 의 것이 불려 D 가 찍힌다. 그런데 필드는 다르다 — 필드는 오버라이딩되지 않고 "가려질(hiding)" 뿐이며, 어느 필드를 읽을지는 컴파일 시점의 타입으로 정해진다. sound() 는 Animal 클래스 안에 적힌 코드라 거기서 name 은 Animal 의 name 이고, a.name 은 a 의 타입이 Animal 이라 역시 Animal 의 name 이다. Dog 의 name = "Dog" 는 Dog 타입 변수로 봐야만 보인다.

따라가기

  1. a.sound() → Animal 의 sound 실행 → 안의 cry() 는 Dog 것 → D
  2. sound 안의 name → Animal 코드 안 → Animal
  3. a.name → a 의 타입 Animal → Animal
  4. 합쳐 DAnimalAnimal

헷갈리는 자리 — 필드도 오버라이딩된다고 생각해 DDogDog 로 적는 것. "메서드는 객체를 보고, 필드는 타입을 본다" 한 줄로 잡는다. 필드에 자식 값을 쓰게 하려면 getName() 같은 메서드로 감싸야 한다 — 그것이 캡슐화가 권장되는 이유 중 하나다.

이전 자료의 예상 문제다. 코드를 손으로 따라가고 JDK 17 실행 결과와 대조했다.

예상 2회 · 8번Java빈출 원리: super + 동적 바인딩

다음 코드의 출력을 쓰시오.

class A { int f() { return 3; } int g() { return f() + 1; } } class B extends A { int f() { return 5; } int g() { return super.g() + f(); } } public class Main { public static void main(String[] args) { System.out.print(new B().g()); } }
정답11

— 11. super.g() 안에서 불리는 f() 도 B 의 f 라서 5 + 1 = 6 이고, 거기에 B 의 f() 5 를 더해 11 이다.

원리 — super.g() 는 "부모 클래스의 g 코드를 실행하라"이지 "부모 객체로 바꿔라"가 아니다. 실행되는 객체는 여전히 B 인스턴스다. 그래서 A 의 g 안에 있는 f() 호출은 동적 바인딩으로 B 의 f 를 부른다 — A 의 f 가 아니다. 결과 super.g() = 5 + 1 = 6. B 의 g 는 거기에 자기 f() = 5 를 더해 11 을 돌려준다. super 는 "코드 선택"만 바꾸고 "객체"는 못 바꾼다는 것이 핵심이다.

따라가기

  1. new B().g() → B 의 g 실행
  2. super.g() → A 의 g 코드: f() + 1 → f 는 B 것 → 5 + 1 = 6
  3. 6 + f() → 6 + 5 = 11

헷갈리는 자리 — super.g() 안의 f() 를 A 의 f 로 보고 3 + 1 + 5 = 9 로 적는 것. super 로 부른 메서드 안에서도 this 는 B 다. 부모의 f 를 명시적으로 부르려면 A 의 g 코드가 아니라 B 안에서 super.f() 라고 써야 한다.

이전 자료의 예상 문제다. 코드를 손으로 따라가고 JDK 17 실행 결과와 대조했다.

05틀리는 자리

왜 이걸 하나 — Java 는 원리를 알아도 한 호출에서 갈려 통째로 깎인다.

자리새는 꼴적는 법기출
필드a.x 를 자식 값으로 읽는다필드는 참조 타입. 메서드만 객체 타입2024-3회 17번 · 2025-1회 13번
staticref.tag() 를 자식 것으로static 메서드는 참조 타입(숨김)2025-2회 10번
오버로딩 후보부모가 모르는 f(String) 을 고른다부모 코드 안의 호출은 부모의 시그니처로 묶인다2026-1회 1번
제네릭T 를 Integer 로 보고 print(Integer)T 는 Object 로 지워진다2024-3회 18번
super 범위super.draw() 안의 draw() 도 부모로super 가 붙은 그 호출만 부모. 안에서 부르는 것은 다시 객체2023-3회 1번
생성자 순서자식 생성자 본문을 부모보다 먼저super() 가 첫 줄. this() 도 먼저2024-1회 10번 · 2023-1회 20번
오버로딩 · 오버라이딩getT(int) 를 getT() 의 오버라이딩으로매개변수가 다르면 오버로딩 — 부모 것이 그대로 남는다2024-1회 10번
표기10a20b 를 「10a 20b」로, 순서를 번호 없이print 는 줄바꿈 없음. 순서 문제는 문제의 번호 표기 그대로2026-2회 9번 · 2024-1회 10번

Java 출력 문제는 print 와 println 의 차이가 줄바꿈을 정한다. 코드에서 println 을 동그라미 치고, 답의 줄 수를 그 개수에 맞춘다.

06훈련

왜 이걸 하나 — 규칙 8개를 짧은 코드 10개로 돌린다. 매일 5문항씩 곁들이는 자리다.

훈련 10문항은 동적 바인딩, 필드 비오버라이딩, 생성자 순서와 super, 오버로딩 선택, instanceof 와 캐스팅, 인터페이스, 추상 클래스, static 숨김, 2026년 1회 1번 원리 재훈련, this() 체인이다. 정답은 전부 JDK 17 로 실행해 확인했다.

훈련 Java · 1번Java동적 바인딩 기본
class A { void show() { System.out.print("P"); } } class B extends A { void show() { System.out.print("C"); } } public class Main { public static void main(String[] args) { A obj = new B(); obj.show(); obj = new A(); obj.show(); } }
정답CP

— CP. 변수 타입은 둘 다 A 지만 실제 객체가 B 면 B 의 show, A 면 A 의 show 가 불린다.

원리 — 오버라이딩된 메서드는 "변수의 타입"이 아니라 "변수가 가리키는 실제 객체"의 것이 실행된다. A obj = new B() 에서 obj 의 타입은 A 지만 객체는 B 라 show() 는 C 를 찍는다. obj = new A() 로 갈아 끼우면 이제 객체가 A 라 P 를 찍는다. 같은 변수, 같은 호출문인데 결과가 다른 것이 다형성이다.

따라가기

  1. obj → B 객체 → show() 는 B 것 → C
  2. obj → A 객체 → show() 는 A 것 → P
  3. 합쳐 CP

이 노트에서 만든 변형 문제다. 코드를 손으로 따라가고 JDK 17 실행 결과와 대조했다.

훈련 Java · 2번Java필드는 오버라이딩되지 않는다
class A { int x = 10; int get() { return x; } } class B extends A { int x = 20; int get() { return x; } } public class Main { public static void main(String[] args) { A o = new B(); System.out.print(o.x + " " + o.get()); } }
정답10 20

— 10 20. 필드 o.x 는 타입(A)을 따라 10, 메서드 o.get() 은 객체(B)를 따라 20 이다.

원리 — 필드는 오버라이딩되지 않는다. 자식이 같은 이름의 필드를 선언하면 부모 필드를 "가릴" 뿐 둘 다 존재하고, 어느 것을 읽을지는 변수의 컴파일 타입으로 정해진다 — o 는 A 타입이라 A 의 x = 10. 반면 메서드 get() 은 오버라이딩되어 실제 객체 B 의 get 이 불리고, B 의 get 안에서 x 는 B 의 x = 20 이다. 한 줄에 "필드는 타입, 메서드는 객체"가 나란히 나오는 문제다.

따라가기

  1. o.x → A 타입 → A 의 x → 10
  2. o.get() → B 객체 → B 의 get → B 의 x → 20
  3. 출력 10 20

이 노트에서 만든 변형 문제다. 코드를 손으로 따라가고 JDK 17 실행 결과와 대조했다.

훈련 Java · 3번Java생성자 호출 순서 + super
class A { A() { System.out.print("A"); } A(int n) { System.out.print("A" + n); } } class B extends A { B() { System.out.print("B"); } B(int n) { super(n); System.out.print("B" + n); } } public class Main { public static void main(String[] args) { new B(); new B(5); } }
정답ABA5B5

— ABA5B5. 자식 생성자는 반드시 부모 생성자를 먼저 부른다 — 안 적으면 super() 가, 적으면 그 super(n) 이.

원리 — 자바는 자식 객체를 만들 때 부모 부분을 먼저 만든다. 그래서 자식 생성자의 첫 줄에는 항상 부모 생성자 호출이 있다. new B() 의 B() 에는 super 호출이 안 적혀 있지만 컴파일러가 super() 를 넣어 A() 가 먼저 돌아 A, 그다음 B. new B(5) 의 B(int) 는 super(n) 을 명시했으니 A(int) 가 돌아 A5, 그다음 B5 다. "부모 먼저, 그다음 자식"이 생성 순서의 철칙이다.

따라가기

  1. new B() → 숨은 super() → A() "A" → B() "B"
  2. new B(5) → super(5) → A(5) "A5" → "B5"
  3. 합쳐 ABA5B5

이 노트에서 만든 변형 문제다. 코드를 손으로 따라가고 JDK 17 실행 결과와 대조했다.

훈련 Java · 5번Java오버로딩 선택 (int vs double)
static String f(int n) { return "int" + n; } static String f(double n) { return "dbl" + n; } public static void main(String[] args) { System.out.print(f(3) + " " + f(3.0)); }
정답int3 dbl3.0

— int3 dbl3.0. 인자 타입에 따라 같은 이름의 다른 메서드가 골라진다(오버로딩).

원리 — 오버로딩은 이름이 같고 매개변수 타입 · 개수가 다른 메서드를 여러 개 두는 것이다. 컴파일러가 호출 인자의 타입을 보고 가장 잘 맞는 것을 고른다. f(3) 의 3 은 int 리터럴이라 f(int) 가, f(3.0) 의 3.0 은 double 리터럴이라 f(double) 이 불린다. "dbl" + n 에서 double 3.0 은 문자열로 바뀔 때 "3.0" 이 된다 — 소수점이 보존된다.

따라가기

  1. f(3) → int → "int" + 3 → int3
  2. f(3.0) → double → "dbl" + 3.0 → dbl3.0
  3. 출력 int3 dbl3.0

이 노트에서 만든 변형 문제다. 코드를 손으로 따라가고 JDK 17 실행 결과와 대조했다.

훈련 Java · 16번Javainstanceof + 캐스팅
Object o = "text"; if (o instanceof String) { String s = (String) o; System.out.print(s.length()); } else System.out.print("no");
정답4

— 4. o 가 실제로 String 이라 instanceof 가 참이고, 형 변환 뒤 "text" 의 길이 4 를 찍는다.

원리 — Object 타입 변수는 무엇이든 담을 수 있지만 그 타입으로는 String 메서드를 못 부른다. instanceof 는 "이 변수에 든 객체가 실제로 그 타입(또는 자식)인가"를 실행 시점에 검사한다. 참이면 (String) o 로 내려 형 변환(다운캐스팅)해 String 메서드를 쓴다. 검사 없이 캐스팅했다가 실제 타입이 다르면 ClassCastException 이 나므로 이 짝(instanceof → 캐스팅)이 관용구다.

따라가기

  1. o = "text" → 실제 String → instanceof String 참
  2. s = (String) o → s.length() = 4
  3. 출력 4

이 노트에서 만든 변형 문제다. 코드를 손으로 따라가고 JDK 17 실행 결과와 대조했다.

훈련 Java · 17번Java인터페이스 구현체 호출
interface Calc { int calc(int n); } class Odd implements Calc { public int calc(int n) { int s = 0; for (int i = 1; i <= n; i += 2) s += i; return s; } } // main: Calc c = new Odd(); System.out.print(c.calc(9));
정답25

— 25. 1 부터 9 까지 홀수 1 + 3 + 5 + 7 + 9 다.

원리 — 인터페이스 Calc 는 calc(int) 라는 약속만 있고 몸통이 없다. Odd 가 implements 로 그 약속을 구현했고, 구현 메서드는 반드시 public 이어야 한다(인터페이스 메서드가 public 이라 좁힐 수 없다). Calc c = new Odd() 로 인터페이스 타입 변수에 담아도 c.calc(9) 는 Odd 의 구현이 실행된다. 반복은 i = 1 에서 2 씩 올라 1 · 3 · 5 · 7 · 9 를 더한다 — 홀수 n 개의 합은 n² 이라 5² = 25 로 검산된다.

따라가기

  1. c.calc(9) → Odd 의 calc
  2. 1 + 3 + 5 + 7 + 9 = 25
  3. 출력 25

이 노트에서 만든 변형 문제다. 코드를 손으로 따라가고 JDK 17 실행 결과와 대조했다.

훈련 Java · 18번Java추상 클래스
abstract class Shape { abstract int area(); void print() { System.out.print("area:" + area()); } } class Rect extends Shape { int w = 3, h = 4; int area() { return w * h; } } // main: Shape s = new Rect(); s.print();
정답area:12

— area:12. 추상 클래스의 print 가 자식이 구현한 area() 를 불러 3 × 4 를 찍는다.

원리 — abstract 메서드 area() 는 몸통이 없어 Shape 자체로는 객체를 못 만들고, 자식 Rect 가 반드시 구현해야 한다. print() 는 Shape 에 몸통이 있고 그 안에서 area() 를 부른다 — 부모 코드가 "자식이 채울 빈칸"을 호출하는 구조다(템플릿 메서드 패턴의 축소판). Shape s = new Rect() 로 담아 s.print() 를 부르면 Shape 의 print 가 돌고, 그 안의 area() 는 실제 객체 Rect 의 것이라 12 다.

따라가기

  1. s.print() → Shape 의 print 실행
  2. "area:" + area() → Rect 의 area → 3 × 4 = 12
  3. 출력 area:12

이 노트에서 만든 변형 문제다. 코드를 손으로 따라가고 JDK 17 실행 결과와 대조했다.

훈련 Java · 19번Javastatic 메서드는 오버라이딩되지 않는다
class A { static void f() { System.out.print("SA"); } void g() { System.out.print("IA"); } } class B extends A { static void f() { System.out.print("SB"); } void g() { System.out.print("IB"); } } // main: A x = new B(); x.f(); x.g();
정답SAIB

— SAIB. static 메서드는 타입(A)을 따라 SA, 인스턴스 메서드는 객체(B)를 따라 IB 다.

원리 — static 메서드는 객체가 아니라 클래스에 붙어 있어 오버라이딩되지 않고 "가려질(hiding)" 뿐이다. 어느 것을 부를지는 변수의 컴파일 타입으로 정해진다 — x 는 A 타입이라 x.f() 는 A 의 f 다(원래 A.f() 라고 쓰는 것이 맞고 x.f() 는 경고 대상). 인스턴스 메서드 g() 는 오버라이딩되어 실제 객체 B 의 g 가 불린다. 필드와 static 메서드는 "타입", 인스턴스 메서드는 "객체" — 이 한 줄이 다형성 문제 전부를 가른다.

따라가기

  1. x.f() → static → A 타입 → SA
  2. x.g() → 인스턴스 → B 객체 → IB
  3. 합쳐 SAIB

이 노트에서 만든 변형 문제다. 코드를 손으로 따라가고 JDK 17 실행 결과와 대조했다.

훈련 Java · 27번Java2026년 1회 1번 원리 재훈련
class A { void g() { f("a"); } void f(Object o) { System.out.print("AO"); } } class B extends A { void f(Object o) { System.out.print("BO"); } void f(String s) { System.out.print("BS"); } } // main: A x = new B(); x.g();
정답BO

— BO. 어느 오버로드를 부를지는 A 의 코드에서 컴파일 시점에 f(Object) 로 정해지고, 그 f(Object) 의 실제 구현은 B 의 것이 실행된다.

원리 — 오버로딩(어느 시그니처인가)은 컴파일 시점에, 오버라이딩(누구의 구현인가)은 실행 시점에 결정된다. g() 는 A 안에 있고 A 에는 f(Object) 하나뿐이라 f("a") 는 f(Object) 로 묶인다 — B 에 f(String) 이 있어도 A 의 코드는 그것을 모른다. 실행 시점에 객체가 B 이므로 f(Object) 의 구현은 B 가 오버라이딩한 것이 불려 BO 다. f(String) 은 오버라이딩이 아니라 B 만의 새 오버로드라 여기서는 닿지 않는다.

따라가기

  1. x.g() → A 의 g → f("a") → A 에서 보이는 것은 f(Object)
  2. f(Object) 의 실제 구현 → B 의 f(Object) → BO

이 노트에서 만든 변형 문제다. 코드를 손으로 따라가고 JDK 17 실행 결과와 대조했다.

훈련 Java · 30번Javathis() 생성자 체인
class X { int x; X() { this(7); System.out.print("K"); } X(int n) { x = n; System.out.print("L"); } } // main: X o = new X(); System.out.print(o.x);
정답LK7

— LK7. this(7) 이 먼저 X(int) 를 실행해 L 을 찍고, 돌아와 K 를 찍은 뒤 x 는 7 이다.

원리 — this(...) 는 같은 클래스의 다른 생성자를 부르는 것이고 생성자의 첫 줄에만 올 수 있다. new X() 는 X() 로 들어가자마자 this(7) 로 X(int) 를 실행한다 — x = 7, "L" 출력. 그것이 끝나면 X() 의 나머지 줄 "K" 가 실행된다. 그래서 순서가 L 다음 K 다. o.x 는 7 이다. 생성자 체이닝은 초기화 코드를 한 곳에 모으는 관용구다.

따라가기

  1. new X() → X() 진입 → this(7) → X(int): x = 7, "L"
  2. X() 로 복귀 → "K"
  3. o.x → 7. 출력 LK7

이 노트에서 만든 변형 문제다. 코드를 손으로 따라가고 JDK 17 실행 결과와 대조했다.

직접 돌려 보는 법

JDK 17 이 깔려 있으니 코드를 Main.java 로 저장하고 파워셸에서 아래를 치면 된다. 클래스 이름이 파일 이름과 같아야 하고, public class 가 하나여야 한다.

javac -encoding UTF-8 Main.java; java Main

이 장의 기출 17개 · 훈련 10개는 위 명령(JDK 17.0.12)으로 실행해 답을 확인했다. 클래스 선언과 문장이 섞인 훈련 코드는 클래스는 밖으로, 문장은 main 안으로 옮겨 컴파일했다.

07백지 재현

왜 이걸 하나 — 아래를 안 보고 적을 수 있어야 이 장이 끝난다.