상속

류한선·2025년 6월 22일

실기연습-2

목록 보기
25/95

좋아요! 🔥 이번에는 훨씬 복잡하고 헷갈리는 상속 트릭 문제를 드릴게요.
이 문제를 정신 바짝 차리고 디버깅해야 합니다.
난이도 확!! 🚀


📝 문제 (더 어려운 상속 트릭 문제)

코드

class A {
    String value = "A";

    A() {
        System.out.println("[A 생성자 시작]");
        init();
        System.out.println("[A 생성자 끝]");
    }

    void init() {
        System.out.println("A.init() 호출됨 value=" + value);
    }

    void print() {
        System.out.println("A.print() 호출됨 value=" + value);
    }
}

class B extends A {
    String value = "B";

    B() {
        System.out.println("[B 생성자 시작]");
        value = "B 생성자에서 바뀜"; 
        System.out.println("[B 생성자 끝]");
    }

    @Override
    void init() {
        System.out.println("B.init() 호출됨 value=" + value);
    }

    @Override
    void print() {
        System.out.println("B.print() 호출됨 value=" + value);
    }
}

class C extends B {
    String value = "C";

    C() {
        System.out.println("[C 생성자 시작]");
        value = "C 생성자에서 바뀜";
        System.out.println("[C 생성자 끝]");
    }

    @Override
    void init() {
        System.out.println("C.init() 호출됨 value=" + value);
    }

    @Override
    void print() {
        System.out.println("C.print() 호출됨 value=" + value);
    }
}

public class Test {
    public static void main(String[] args) {
        A obj = new C();
        System.out.println("-- 이제 print 호출 --");
        obj.print();
    }
}

🎯 문제

  1. 프로그램의 출력값을 한 줄 한 줄 정확히 써보세요.
  2. 그런 출력값이 나오는 이유를 생성자 호출 순서 + 필드 초기화 순서 + 동적 바인딩까지 자세히 설명해 보세요.

💡 이 문제의 포인트

init()이 생성자 안에서 호출될 때 실제 오버라이딩된 메서드가 호출된다는 점
필드 초기화 타이밍 (String value = ...) vs 생성자 호출 타이밍
super() 호출 구조와 부모 클래스 생성자 호출 시점



🎯 정답 풀이 및 디버깅 스타일 해설

1️⃣ new C() 호출

→ 생성자 호출 체인: A()B()C()
→ 각 클래스의 필드 초기화 & 생성자 바디 실행 순서:

  1. 가장 먼저 A의 필드 value="A" 초기화

  2. A 생성자 호출 → [A 생성자 시작] 출력

  3. A 생성자 안에서 init() 호출됨

    • 하지만 오버라이딩 → C의 init() 호출 (가장 하위 클래스 메서드 호출)
    • 그러나 아직 C의 필드 초기화 전임!!!
      ⚠️ C의 필드 초기화 전 → C의 value 필드는 기본값 null 상태
      C.init() 호출됨 value=null 출력
  4. A 생성자 끝 → [A 생성자 끝] 출력

  5. B 필드 초기화(value="B")

  6. B 생성자 호출 → [B 생성자 시작] 출력
    → B 생성자 바디에서 value = "B 생성자에서 바뀜"; 대입
    [B 생성자 끝] 출력

  7. C 필드 초기화(value="C")

  8. C 생성자 호출 → [C 생성자 시작] 출력
    → C 생성자 바디에서 value = "C 생성자에서 바뀜";
    [C 생성자 끝] 출력


🔍 정리된 출력 순서

  1. [A 생성자 시작]
  2. C.init() 호출됨 value=null (init 호출되는 시점엔 C의 value 초기화 안됨)
  3. [A 생성자 끝]
  4. [B 생성자 시작]
  5. [B 생성자 끝]
  6. [C 생성자 시작]
  7. [C 생성자 끝]

2️⃣ obj.print();

obj의 실제 타입 C → C의 print 호출
→ 현재 C의 value = "C 생성자에서 바뀜"
→ 출력: C.print() 호출됨 value=C 생성자에서 바뀜


🎯 프로그램의 최종 출력

[A 생성자 시작]
C.init() 호출됨 value=null
[A 생성자 끝]
[B 생성자 시작]
[B 생성자 끝]
[C 생성자 시작]
[C 생성자 끝]
-- 이제 print 호출 --
C.print() 호출됨 value=C 생성자에서 바뀜


🎯 정리해 드릴 "왜 이렇게 동작?" 포인트

✅ 생성자 호출 순서:

  1. 상위 클래스 필드 초기화 → 상위 클래스 생성자 호출
  2. 하위 클래스 필드 초기화 → 하위 클래스 생성자 호출
    ✅ 생성자 안에서 호출된 오버라이딩 메서드는 가장 하위 클래스 메서드가 호출되므로 아직 필드 초기화가 안 된 상태일 수 있다 (null 나올 수 있음)
    ✅ 필드 value가 부모와 자식에 각각 존재 → this.value / super.value 구분 필수
    ✅ 업캐스팅해도 동적 바인딩 때문에 오버라이딩된 메서드 호출

좋아요! 🔥 이번에는 인터페이스 + 다형성 + 업캐스팅 트릭 문제static 트릭 문제를 한 문제씩 내드릴게요.
그리고 자세히 디버깅하면서 풀이해 드릴게요.


📝 문제 1: 인터페이스 + 다형성 + 업캐스팅 트릭 문제

코드

interface Animal {
    void sound();
    static void info() {
        System.out.println("Animal interface static info");
    }
}

class Dog implements Animal {
    String name = "Dog";

    @Override
    public void sound() {
        System.out.println(name + " barks");
    }

    static void info() {
        System.out.println("Dog class static info");
    }
}

class Cat implements Animal {
    String name = "Cat";

    @Override
    public void sound() {
        System.out.println(name + " meows");
    }

    static void info() {
        System.out.println("Cat class static info");
    }
}

public class Test {
    public static void main(String[] args) {
        Animal a1 = new Dog();
        Animal a2 = new Cat();

        a1.sound();
        a2.sound();

        a1.info();
        a2.info();

        Dog.info();
        Cat.info();
        Animal.info();
    }
}

🎯 문제

이 프로그램의 출력값을 예측하고, 왜 그런 출력이 나오는지 하나씩 디버깅해 보세요.


🎯 풀이 및 디버깅

한 줄 한 줄 따라가며!

  1. Animal a1 = new Dog(); → 업캐스팅. 실제 객체는 Dog.
  2. Animal a2 = new Cat(); → 업캐스팅. 실제 객체는 Cat.

a1.sound();

  • 동적 바인딩 → 실제 Dog의 sound() 호출 → "Dog barks"

a2.sound();

  • 동적 바인딩 → 실제 Cat의 sound() 호출 → "Cat meows"

a1.info(); a2.info();

  • ⚠️ info()는 인터페이스의 static 메서드이므로 오버라이딩 X.
  • static 메서드는 동적 바인딩 없음 → 참조 타입 기준
  • a1, a2 타입은 Animal이므로 Animal.info() 호출됨
    → 출력 2번: "Animal interface static info"

Dog.info(); Cat.info(); Animal.info();

  • 각각의 클래스에서 정의한 static 메서드를 호출:
    "Dog class static info"
    "Cat class static info"
    "Animal interface static info"

🎯 정답 출력

Dog barks
Cat meows
Animal interface static info
Animal interface static info
Dog class static info
Cat class static info
Animal interface static info

🎯 포인트

✅ 인터페이스의 static 메서드는 구현 클래스의 static 메서드와 전혀 다른 것
✅ 호출할 때 참조변수의 타입 기준 → 업캐스팅된 경우 인터페이스 쪽 호출
✅ 정적 메서드는 오버라이딩 없음!



📝 문제 2: static 트릭 문제

코드

class X {
    static int num = 0;

    X() {
        num++;
        System.out.println("X 생성자 호출 num=" + num);
    }

    static {
        num += 10;
        System.out.println("static 블록 호출 num=" + num);
    }
}

class Y extends X {
    static int num = 100;

    Y() {
        num++;
        System.out.println("Y 생성자 호출 num=" + num);
    }

    static {
        num += 50;
        System.out.println("Y static 블록 호출 num=" + num);
    }
}

public class Test2 {
    public static void main(String[] args) {
        new Y();
        new Y();
    }
}

🎯 문제

이 프로그램의 출력값을 정확히 쓰고 왜 그렇게 출력되는지 디버깅해 보세요.


🎯 풀이 및 디버깅

📍 클래스 초기화 순서

  1. new Y() 호출되면 → Y 클래스 로딩
  2. Y가 X 상속 → X 먼저 로딩
  3. X의 static 블록 호출 → X.num += 10 → 출력
    static 블록 호출 num=10
  4. Y의 static 블록 호출 → Y.num += 50 → 초기 Y.num=100 → 100+50 = 150 출력
    Y static 블록 호출 num=150

📍 new Y() 생성자 호출

  • X 생성자 호출 → X.num++ (10 → 11) → 출력 "X 생성자 호출 num=11"
  • Y 생성자 호출 → Y.num++ (150 → 151) → 출력 "Y 생성자 호출 num=151"

📍 두 번째 new Y() 생성자 호출

  • 이미 클래스 초기화 끝났으므로 static 블록 호출 안 함
  • X 생성자 호출 → X.num++ (11 → 12) → 출력 "X 생성자 호출 num=12"
  • Y 생성자 호출 → Y.num++ (151 → 152) → 출력 "Y 생성자 호출 num=152"

🎯 정리된 출력값

static 블록 호출 num=10
Y static 블록 호출 num=150
X 생성자 호출 num=11
Y 생성자 호출 num=151
X 생성자 호출 num=12
Y 생성자 호출 num=152

🎯 포인트

✅ static 블록은 클래스 최초 로딩 때 1번만 호출
✅ 부모 클래스 static → 자식 클래스 static → 부모 생성자 → 자식 생성자 호출 순서
✅ 부모의 static 필드와 자식의 static 필드가 이름 겹치지 않고 별도로 존재 (X.num, Y.num 별개)


좋아요! 🎯 이번에는 **증감 연산자(++, --)**를 교묘히 섞어서 실수를 유도하는 트릭 문제를 내고, 풀이까지 자세히 해드릴게요.


📝 문제: 상속 + static + 증감연산자 트릭

이번에는 상속 계층static 필드를 이용하고, 거기에 증감 연산자를 섞습니다.


📜 코드

class A {
    static int num = 0;

    A() {
        num += 5;
        System.out.println("A 생성자 호출 num=" + num++);
    }

    static {
        num = 10;
        System.out.println("A static 블록 num=" + num++);
    }
}

class B extends A {
    static int num = 100;

    B() {
        num -= 3;
        System.out.println("B 생성자 호출 num=" + num--);
    }

    static {
        num += 10;
        System.out.println("B static 블록 num=" + num--);
    }
}

public class Test {
    public static void main(String[] args) {
        new B();
        new B();
    }
}

🎯 문제

이 프로그램의 출력값을 한 줄 한 줄 정확히 써 보세요.
그리고 왜 그런 출력값이 나오는지 철저히 디버깅해 보세요!



🎯 풀이 및 디버깅 스타일 해설

🔍 클래스 초기화 + static 블록 순서

new B() 호출 → B 클래스 초기화

  1. B 상속 부모 → A 클래스 초기화 시작

    • A.static { num = 10; } → num=10 출력 "A static 블록 num=10"
    • 이후 num++ → num=11
  2. B 클래스 초기화

    • B.static { num = 100; }(이미 num 선언됨) → num += 10 → num = 110
    • 출력 "B static 블록 num=110"
    • num-- → num = 109

🔍 이제 new B() 생성자 호출

1번째 new B()

  1. 부모 A 생성자 호출

    • 현재 A.num = 11 (A와 B의 num은 다른 정적필드!)
    • num += 5 → A.num = 11+5 = 16
    • 출력 "A 생성자 호출 num=16"
    • num++ → A.num = 17
  2. 자식 B 생성자 호출

    • 현재 B.num = 109
    • num -= 3 → B.num = 106
    • 출력 "B 생성자 호출 num=106"
    • num-- → B.num = 105

2번째 new B()

  1. 부모 A 생성자 호출

    • 현재 A.num = 17
    • num += 5 → A.num = 22
    • 출력 "A 생성자 호출 num=22"
    • num++ → A.num = 23
  2. 자식 B 생성자 호출

    • 현재 B.num = 105
    • num -= 3 → B.num = 102
    • 출력 "B 생성자 호출 num=102"
    • num-- → B.num = 101


🎯 정리된 출력값

A static 블록 num=10
B static 블록 num=110
A 생성자 호출 num=16
B 생성자 호출 num=106
A 생성자 호출 num=22
B 생성자 호출 num=102


💡 트릭 포인트 정리

✅ 부모(A)와 자식(B) 클래스의 num 필드는 이름은 같지만 완전히 별개
✅ static 블록은 클래스 최초 로딩할 때만 호출
num++ vs ++num, num-- 트릭 → 후위 연산자는 출력 후 증가/감소하기 때문에 출력값을 헷갈리기 쉬움
✅ 반드시 호출 순서와 현재 필드 상태를 하나씩 추적하기


0개의 댓글