본문 바로가기
IT/김영한의실전JAVA-기본_inFlearn

10. 다형성1 - 시작 / 다형성과 캐스팅 / 캐스팅의 종류 / 다운캐스팅과 주의점 / instanceof / 다형성과 메서드 오버라이딩

by for-learn 2025. 4. 6.

10-1. 다형성 시작

객체지향 프로그래밍의 대표적 특징: 캡슐화, 상속, 다형성

 

다형성(Polymorphism)

  • '다양한 형태', '여러 형태'
  • 한 객체가 여러 타입의 객체로 취급될 수 있는 능력
  • 하나의 객체는 하나의 타입으로 고정되어 있는데,
    다형성을 사용하면 하나의 객체가 다른 타입으로 사용될 수 있다는 뜻

2가지 핵심이론

  • 다형적 참조 (여러 형태로 참조)
  • 메서드 오버라이딩

poly.basic.Parent.java (다형적 참조)

package poly.basic;

public class Parent {
    public void parentMethod() {
        System.out.println("Parent.parentMethod");
    }
}

poly.basic.Child.java

package poly.basic;

public class Child extends Parent {
    public void childMethod() {
        System.out.println("Child.childMethod");
    }
}

poly.basic.PolyMain.java (~ 변수가 ~ 인스턴스를 참조)

package poly.basic;

public class PolyMain {
    public static void main(String[] args) {
        // 부모 변수가 부모 인스턴스 참조
        System.out.println("Parent -> Parent");
        Parent parent = new Parent();
        parent.parentMethod();

        // 자식 변수가 자식 인스턴스 참조
        System.out.println("Child -> Child");
        Child child = new Child();
        child.parentMethod();
        child.childMethod();

        // 부모 변수가 자식 인스턴스 참조(다형적 참조)
        System.out.println("Parent -> Child");
        Parent poly = new Child();
        poly.parentMethod();

        // 자식은 부모를 담을 수 없다.
        //Child child1 = new Parent();

        // 자식의 기능은 호출할 수 없다. 컴파일 오류 발생
        // 부모 타입 변수는 부모만 알고 있기 때문
        //poly.childMethod();
    }
}

  • 부모 타입의 변수가 부모 인스턴스 참조 => 메모리상 Parent만 생성 (자식 생성 X)
  • 자식 타입의 변수가 자식 인스턴스 참조 => 메모리상 Parent, Child 모두 생성

 

부모 타입의 변수가 자식 인스턴스 참조

  • => 자식 타입인 Child를 생성 => 메모리상 Parent, Child 모두 생성
  • 부모 타입은 자식 타입을 담을 수 있다.
    Parent poly = new Child(); : 성공
  • 자식 타입은 부모 타입을 담을 수 없다. 
    Child child1 = new Parent(); : 컴파일 오류

다형적 참조

  • 지금까지 항상 같은 타입에 참조를 대입
    Parent parent = new Parent()
    Child child = new Child()
  • Parent 타입의 변수는 자신은 물론, 자식 타입과 그 하위 타입도 참조 할 수 있다. 
    Parent poly = new Parent()
    Parent poly = new Child()
    Parent poly = new Grandson() : Child 하위에 손자가 있다면 가능

다형적 참조의 한계

  • poly.childMethod() 호출 불가
  • 호출자인 polyParent 타입이다. 따라서 Parent 클래스부터 시작해서 필요한 기능을 찾는다.
  • 상속 관계는 부모 방향으로 찾아 올라갈 수는 있지만 자식 방향으로 찾아 내려갈 수는 없다.
  • Parent부모 타입이고 상위에 부모가 없다. childMethod()를 찾을 수 없으므로 컴파일 오류가 발생한다.

childMethod()를 호출하고 싶으면 캐스팅이 필요

 

10-2. 다형성과 캐스팅

poly.basic.CastingMain1.java (다운캐스팅)

package poly.basic;

public class CastingMain1 {
    public static void main(String[] args) {
        // 부모 변수가 자식 인스턴스 참조(다형적 참조)
        Parent poly = new Child(); // x001
        // 단, 자식의 기능은 호출할 수 없다. 컴파일 오류 발생
        //poly.childMethod();

        // 다운캐스팅(부모 타입 -> 자식 타입)
        //Child child = poly;
        Child child = (Child) poly; // x001
        child.childMethod();
    }
}

  • Parent poly = new Child()
    부모 타입의 변수를 사용하게 되면, 자식 타입에 있는 기능을 호출 할 수 없다. 
    1. poly.childMethod()를 호출하면, 먼저 참조값(x001)을 사용해 인스턴스를 찾는다.
    2. polyParent 타입인데, Parent는 최상위 부모이고 상속 관계는 부모로만 찾아서 올라갈 수 있다. 
    3. childMethod()는 자식 타입의 기능이므로 호출 불가 -> 컴파일 오류

  • 다운캐스팅 Child child = (Child) poly //Parent poly
    호출하는 타입을 Child 타입으로 변경하면, 인스턴스의 Child에 있는 childMEthod() 호출 가능
    1. 부모는 자식을 담을 수 있지만 자식은 부모를 담을 수 없다. 
    2. poly의 참조값(x001)을 읽고 다운캐스팅을 통해 자식 타입으로 변환 후
    3. Child child에 대입
    4. poly의 타입은 기존과 같이 Parent로 유지됨
  • 캐스팅
    • 업캐스팅(upcasting): 부모 타입으로 변경
    • 다운캐스팅(downcasting): 자식 타입으로 변경

10-3. 캐스팅의 종류

다운캐스팅 결과를 변수에 담아두는 과정 없이,

일시적 다운 캐스팅으로 인스턴스에 있는 하위 클래스의 기능을 바로 호출

poly.basic.CastingMain2.java (일시적 다운캐스팅)

package poly.basic;

public class CastingMain2 {
    public static void main(String[] args) {
        // 부모 변수가 자식 인스턴스 참조(다형적 참조)
        Parent poly = new Child(); // x001
        // 단, 자식의 기능은 호출할 수 없다. 컴파일 오류 발생
        //poly.childMethod();
        ...
        // (추가)
        // 일시적 다운캐스팅 - 해당 메서드를 호출하는 순간만 다운캐스팅
        ((Child) poly).childMethod();
    }
}

  • polyParent 타입이고, ((Child)poly)를 통해 일시적으로 Child로 변경된다.
  • poly의 참조값(x001)을 꺼내고(읽고), 꺼낸 참조값이 Child 타입이 됨.
  • poly의 타입은 그대로 Parent로 유지

poly.basic.CastingMain3.java (업캐스팅)

package poly.basic;
// upcasting vs downcasting
public class CastingMain3 {
    public static void main(String[] args) {

        Child child = new Child();
        Parent parent1 = (Parent) child; // 업캐스팅은 생략 가능, 생략 권장
        Parent parent2 = child; // 업캐스팅 생략

        parent1.parentMethod();
        parent2.parentMethod();
    }
}

 

업캐스팅은 생략해도 되고, 다운캐스팅은 왜 개발자가 직접 명시적으로 캐스팅 해야 하는 이유

 

10-4. 다운캐스팅의 주의점

다운캐스팅을 잘못하면 심각한 런타임 오류 발생 가능

poly.basic.CastingMain4.java (다운캐스팅이 자동이 아닌 이유)

package poly.basic;

// 다운캐스팅을 자동으로 하지 않는 이유
public class CastingMain4 {
    public static void main(String[] args) {
        Parent parent1 = new Child();
        Child child1 = (Child) parent1;
        child1.childMethod(); // 문제 없음

        Parent parent2 = new Parent();
        Child child2 = (Child) parent2; // 런타임 오류 - ClassCastException
        child2.childMethod(); // 실행 불가
    }
}

  • Parent parent2 = new Parent()
    Parent로 부모 타입 객체를 생성 => 메모리 상 자식 타입은 존재하지 않음
  • Child child2 = (Child) parent2
    메모리 상에 Child 자체가 존재하지 않음
  • 사용할 수 없는 타입으로 다운캐스팅 => ClassCastException 예외 발생
  • 예외가 발생되어 다음 동작 실행 안되고, 프로그램 종료

업캐스팅이 안전하고 다운캐스팅이 위험한 이유

1) 업캐스팅

 

  • 상위로 올라가는 업케스팅은 인스턴스 내부에 부모가 모두 생성
  • new C()로 인스턴스를 생성하면 인스턴스 내부에 자신과 부모인 A, B, C가 모두 생성
  • C의 부모 타입인 A, B, C 모두 C 인스턴스를 참조 가능
A a = new C();
B b = new C();
C c = new C();

 

2) 다운캐스팅

  • 객체를 생성할 때 하위 자식은 생성되지 않음
  • 하위로 내려가는 다운케스팅은 인스턴스 내부에 없는 부분을 선택하는 문제가 발생
  • new B()로 인스턴스를 생성하면 인스턴스 내부에 자신과 부모인 A, B가 생성
  • B의 부모 타입인 A, B 모두 B 인스턴스를 참조
  • C는 B 인스턴스를 참조 불가능
A a = new B();
B b = new B();
C c = new B(); // (컴파일 오류)
// 하위 타입에 대입할 수 없음 
C c = (C) new B(); // (런타임 오류) => ClassCastException
// 하위 타입으로 강제 다운캐스팅, 하지만 B 인스턴스에 C와 관련된 부분이 없으므로 잘못된 캐스팅

컴파일 오류 vs 런타임 오류

  • 컴파일 오류
    • 변수명 오타, 잘못된 클래스 이름 사용 등 자바 프로그램을 실행하기 전에 발생하는 오류
    • 이런 오류는 IDE에서 즉시 확인할 수 있기 때문에 안전하고 좋은 오류
  • 반면에 런타임 오류
    • 이름 그대로 프로그램이 실행되고 있는 시점에 발생하는 오류
    • 런타임 오류는 매우 안좋은 오류
    • 왜냐하면 보통 고객이 해당 프로그램을 실행하는 도중에 발생하기 때문

10-5. instanceof

다형성에서 참조형 변수는 다양한 자식을 대상으로 참조함.

이때, 변수가 참조하는 인스턴스의 타입instanceof로 확인 가능

poly.basic.CastingMain5.java ( instanceof )

package poly.basic;

public class CastingMain5 {
    public static void main(String[] args) {
        Parent parent1 = new Parent();
        System.out.println("parent1 호출");
        call(parent1);

        Parent parent2 = new Child();
        System.out.println("paren2 호출");
        call(parent2);
    }

    private static void call(Parent parent) {
        parent.parentMethod();
        if (parent instanceof Child) {
            System.out.println("Child 인스턴스 맞음");
            Child child = (Child) parent; // 다운캐스팅
            child.childMethod();
        } else {
            System.out.println("Child 인스턴스 아님");
        }
    }
}

  • 다운캐스팅을 수행하기 전에는 먼저 instanceof 를 사용해서
    원하는 타입으로 변경이 가능한지 확인한 다음에 다운캐스팅을 수행하는 것이 안전

instanceof 키워드는 오른쪽 대상의 자식 타입을 왼쪽에서 참조하는 경우에도 true를 반환

  • 오른쪽에 있는 타입에 왼쪽에 있는 인스턴스의 타입이 들어갈 수 있는지 대입해보면 된다.
  • 대입이 가능 하면 ` true ` , 불가능하면 ` false ` 가 된다.
// new ~~() 는 인스턴스를 표현
new Parent() instanceof Parent
Parent p = new Parent() // 같은 타입 true

new Child() instanceof Parent
Parent p = new Child() // 부모는 자식을 담을 수 있다. true

new Parent() instanceof Child
Child c = new Parent() // 자식은 부모를 담을 수 없다. false

new Child() instanceof Child
Child c = new Child() // 같은 타입 true

poly.basic.CastingMain6.java ( 자바 16 - Pattern Matching for instanceof )

package poly.basic;

public class CastingMain6 {

...

    private static void call(Parent parent) {
        parent.parentMethod();
        // Child 인스턴스인 경우 childMethod() 실행
        if (parent instanceof Child child) { // 변수선언
            System.out.println("Child 인스턴스 맞음");
            //Child child = (Child) parent; // 다운캐스팅
            child.childMethod();
        }
    }
}
  • instanceof를 사용하면서 동시에 변수를 선언
  • 인스턴스가 맞는 경우 직접 다운캐스팅하는 코드를 생략

10-6. 다형성과 메서드 오버라이딩

  • 다형성을 이루는 또 하나의 중요한 핵심 이론 => 메서드 오버라이딩
  • "오버라이딩 된 메서드가 항상 우선권을 가진다"
  • 기존 기능을 덮어 새로운 기능을 재정의 한다는 뜻 =>  부모 타입에서 정의한 기능을 자식 타입에서 재정의

Parent, Child 모두 value라는 같은 멤버 변수 가짐 => 멤버 변수는 오버라이딩 되지 않음

Parent, Child 모두 method()라는 같은 메서드를 가짐 => Child에서 메서드를 오버라이딩 함

poly.overriding.Parent.java

package poly.overriding;

public class Parent {
    public String value = "parent";

    public void method() {
        System.out.println("Parent.method");
    }
}

poly.overriding.Child.java

package poly.overriding;

public class Child extends Parent {
    public String value = "child";

    @Override
    public void method() {
        System.out.println("Child.method");
    }
}

poly.overriding.Overriding.java

package poly.overriding;

public class OverridingMain {
    public static void main(String[] args) {
        // 자식 변수가 자식 인스턴스 참조
        Child child = new Child();
        System.out.println("Child -> Child");
        System.out.println("value = " + child.value);
        child.method();

        // 부모 변수가 부모 인스턴스 참조
        Parent parent = new Parent();
        System.out.println("Parent -> Parent");
        System.out.println("value = " + parent.value);
        parent.method();

        // 부모 변수가 자식 인스턴스 참조(다형적 참조)
        Parent poly = new Child();
        System.out.println("Parent -> Child");
        System.out.println("value = " + poly.value); // 변수는 오버라이딩 안됨
        poly.method(); // 메서드는 오버라이딩 됨
    }
}

 

Parent => Child 이 부분이 중요

  • 오더라이딩 된 메서드는 항상 우선권을 가진다.
  • poly.method(): Parent 타입에 있는 method()를 실행하려고 할 때,
    하위 타입인 Child.method()가 오버라이딩 되어 있어 Child.method()가 실행
  • 만약 자식에서도 오버라이딩 하고 손자에서도 같은 메서드를 오버라이딩을 하면
    손자의 오버라이딩 메서드가 우선권을 가진다.

 

다형성을 이루는 핵심 이론

  1. 다형적 참조 : 하나의 변수 타입으로 다양한 자식 인스턴스를 참조할 수 있는 기능
  2. 메서드 오버라이딩 : 기존 기능을 하위 타입에서 새로운 기능으로 재정의