다형성3

류한선·2025년 6월 21일

실기연습-2

목록 보기
21/95

좋아요! 🎯 이번에도 한층 업그레이드된 트릭 문제를 준비해 드릴게요.
이번 문제에서는:

abstract classinterface의 조합
✅ 정적 필드 초기화 트릭
✅ 생성자 안에서 추상 메서드 호출하기
✅ 오버로딩 vs 오버라이딩 헷갈리기
instanceof 검사와 캐스팅


🧠 트릭 문제

📜 코드

interface Speaker {
    default void say() {
        System.out.println("Speaker says: Hello!");
    }
    void speak();
}

abstract class Animal implements Speaker {
    static int count = 0;

    Animal() {
        count++;
        System.out.println("Animal constructor, count = " + count);
        speak();
    }

    abstract void eat();

    @Override
    public void speak() {
        System.out.println("Animal speaks");
    }
}

class Dog extends Animal {
    String name = "Barky";

    Dog() {
        System.out.println("Dog constructor");
    }

    @Override
    void eat() {
        System.out.println(name + " is eating");
    }

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

class Cat extends Animal {
    String name = "Kitty";

    Cat() {
        System.out.println("Cat constructor");
    }

    @Override
    void eat() {
        System.out.println(name + " is eating");
    }

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

public class AbstractInterfaceTest {
    public static void main(String[] args) {
        Animal a1 = new Dog();       // (1)
        a1.eat();                    // (2)
        a1.say();                    // (3)

        Animal a2 = new Cat();       // (4)
        a2.eat();                    // (5)
        a2.say();                    // (6)

        Speaker s = new Dog();       // (7)
        s.speak();                   // (8)

        if (s instanceof Animal) {   // (9)
            ((Animal)s).eat();
        }
    }
}

🎯 트릭 포인트 정리

Animal 생성자 안에서 speak() 호출 → 오버라이딩된 메서드 호출됨, 그런데 필드 초기화 전 상태
✅ 정적 필드 count 증가 확인
say() 디폴트 메서드 호출 → 인터페이스의 메서드 호출
instanceof 검사 후 캐스팅


이 문제를 출력값을 한 줄 한 줄 디버깅하면서 풀이해 드릴까요?
원하시죠?
지금 바로 풀이 갑니다! 🎯


🧠 풀이하기


(1) Animal a1 = new Dog();

정적 초기화

  • 정적 필드 count = 0 초기화됨
  • new Dog() 호출 → 부모(Animal) 생성자 호출

Animal() 생성자

  • count++count = 1
  • 출력: Animal constructor, count = 1
  • speak() 호출 → 동적 바인딩 → Dog.speak() 호출
  • 하지만 아직 Dog 생성자 안에서 name 초기화하기 전 → null barks

Dog() 생성자

  • 출력: Dog constructor

(2) a1.eat();

  • 오버라이딩된 Dog.eat() 호출
  • 이제 Dog 생성자 끝나고 name = "Barky"
  • 출력: Barky is eating

(3) a1.say();

  • Animal에는 say()가 없음 → Speaker.say() 디폴트 메서드 호출
  • 출력: Speaker says: Hello!

(4) Animal a2 = new Cat();

Animal() 생성자

  • count++count = 2
  • 출력: Animal constructor, count = 2
  • speak() 호출 → 동적 바인딩 → Cat.speak()
  • 아직 Cat 생성자 안에서 name 초기화 전 → null meows

Cat() 생성자

  • 출력: Cat constructor

(5) a2.eat();

  • 오버라이딩된 Cat.eat() 호출
  • 이제 Cat 생성자 끝나고 name = "Kitty"
  • 출력: Kitty is eating

(6) a2.say();

  • Speaker.say() 디폴트 메서드 호출
  • 출력: Speaker says: Hello!

(7) Speaker s = new Dog();

Animal() 생성자

  • count++count = 3
  • 출력: Animal constructor, count = 3
  • speak() 호출 → Dog.speak() 호출 → 아직 Dog.name 초기화 전 → null barks

Dog() 생성자

  • 출력: Dog constructor

(8) s.speak();

  • s 참조 타입은 Speaker, 실제 객체 Dog
  • 오버라이딩된 Dog.speak() 호출 → 이제 name = "Barky"
  • 출력: Barky barks

(9) if (s instanceof Animal)

  • 참 → 캐스팅
  • ((Animal)s).eat() → 오버라이딩된 Dog.eat() 호출
  • 출력: Barky is eating

🎯 최종 출력 정리

Animal constructor, count = 1
null barks
Dog constructor
Barky is eating
Speaker says: Hello!
Animal constructor, count = 2
null meows
Cat constructor
Kitty is eating
Speaker says: Hello!
Animal constructor, count = 3
null barks
Dog constructor
Barky barks
Barky is eating

✅ 정리 포인트

  1. 생성자 안에서 오버라이딩된 메서드를 호출 → 초기화 안 된 필드(null)
  2. 인터페이스 디폴트 메서드 호출 → 참조 타입 기준 없음 → Speaker.say() 호출
  3. 캐스팅 후 오버라이딩된 메서드 호출
  4. 정적 필드(count) 증가 트래킹
  5. instanceof 검사 → 다운캐스팅

좋아요! 🎯
지금 중요한 부분을 잘 짚으셨어요.
여기서 헷갈리는 포인트를 딱 정리해 드릴게요.


🧠 필드 초기화 타이밍 정리

💡 중요한 사실 1:

자바에서 **인스턴스 필드 초기화(String name = "Barky";)**는 생성자 호출 전에 미리 끝납니다.


🧠 객체 생성 과정 (new Dog()의 내부 동작 순서)

예를 들어 new Dog()를 호출하면 이렇게 돌아갑니다:

  1. 메모리 확보 및 필드 기본값 초기화:
    Dogname 필드에 null 대입
    Animal.count = Animal.count + 1 (static 필드면 클래스 초기화 시 한번, 인스턴스 필드면 새로 0 대입)

  2. 부모 클래스(Animal) 생성자 호출 전에 Dog의 필드 초기화식 실행
    Dog.name = "Barky"가 이 시점에서 진행됨 ✅

  3. 부모(Animal()) 생성자 호출
    Animal() 안에서 speak() 호출
    Dog.speak() 오버라이드 호출 → 이때 name은 이미 "Barky"로 초기화 끝났음 🎯

  4. Animal() 생성자 종료 후 Dog() 생성자 본문 실행
    "Dog constructor" 출력


⚠️ 헷갈리기 쉬운 부분

많은 분들이 이렇게 오해합니다:

“생성자 안에서만 필드 초기화가 된다”

❌ 이건 틀렸습니다.

실제 동작은:

  • 필드 초기화식(String name = "Barky";)
  • 생성자 호출 전 미리 완료

✅ 결론

따라서 speak()가 호출되는 시점(Animal 생성자 안)에도 name 필드가 "Barky"로 이미 초기화되어 있으므로 Barky barks가 출력되는 겁니다.
Dog.eat() 호출할 때도 마찬가지. 이미 필드가 준비되어 있어서 Barky is eating이 출력되는 거예요.


좋은 질문이에요! 🎯
지금 헷갈리시는 부분을 깔끔히 정리해 드릴게요.
"필드가 초기화 안 된 상태에서 메서드를 호출한다" 라고 보셨던 경우가 있죠?
이건 보통 이런 상황들에서 나옵니다:


🔍 그런 경우란?

1️⃣ 정적 필드(static 필드)

  • 정적 필드의 초기화나 정적 블록의 실행 전에 정적 메서드를 호출하는 경우
  • 정적 초기화 순서에 의해 아직 null 상태일 수 있음
class StaticTest {
    static String text = init();

    static {
        doSomething();
    }

    static String init() {
        System.out.println("init called");
        return "hello";
    }

    static void doSomething() {
        System.out.println("doSomething: text = " + text); // null일 수 있음
    }
}

✅ 위 경우 text가 아직 초기화되지 않아서 null이 출력될 수 있죠.


2️⃣ 생성자 체인 중 부모 생성자 안에서 오버라이딩 메서드 호출하기

  • 방금 한 문제와 같은 상황.
  • 하지만 자식의 필드를 초기화식 대신 생성자 안에서 초기화한다면 문제가 발생.

예:

class Parent {
    Parent() {
        overriddenMethod();
    }

    void overriddenMethod() {
        System.out.println("Parent's method");
    }
}

class Child extends Parent {
    String name;

    Child() {
        name = "Barky"; // 생성자 안에서 초기화
    }

    @Override
    void overriddenMethod() {
        System.out.println(name + " barks"); // 여기서 name = null
    }
}

✅ 여기선 name 필드가 Child() 생성자 안에서 초기화되므로, Parent() 호출 시점에는 name == nullnull barks


3️⃣ 인스턴스 초기화 블록을 놓친 경우

  • 필드 대신 생성자 안에서만 초기화하거나
  • 혹은 정적 필드를 정적 블록 안에서 초기화하는 경우

🎯 한 줄 정리

✅ **필드 초기화식(String name = "Barky";)**은 생성자 호출 전에 무조건 미리 초기화됨.
✅ 그러나 생성자 안에서 필드를 초기화하거나 정적 블록에서 정적 필드를 초기화한다면, 부모 생성자 안에서 호출된 오버라이드 메서드에는 아직 값이 안 채워진 상태 → null 출력!


0개의 댓글