[Java 기본]Section 10 - 다형성

우영·2024년 4월 3일

JAVA 기본편

목록 보기
10/13

다형성 시작

객체 지향 프로그래밍의 대표적인 특징 - 캡슐화, 상속, 다형성
다형성은 직관적이지 않아 이해하기도 어렵고, 잘 활용하기는 더 어렵다.

다형성 2가지 핵심 이론

  • 다형적 참조
  • 메서드 오버라이딩

다형적 참조

  • 부모 클래스, 자식 클래스를 만들고 부모 타입의 변수가 부모 인스턴스 참조하고 자식 타입의 변수가 자식 인스턴스 참조, 부모 변수가 자식 인스턴스 참조가 가능하다는 것을 확인
package poly.basic;

public class Child extends Parent{

    public void childMethod() {
        System.out.println("Child.childMethod");
    }
}
package poly.basic;

public class Parent {

    public void parentMethod() {
        System.out.println("Parent.parentMethod");
    }
}
package poly.basic;

import com.sun.source.util.ParameterNameProvider;

public class ParentMain {

    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 parent = new Parent()
      • Parent 인스턴스를 만들었다. 부모 타입인 Parent를 생성했기 때문에 메모리 상에 Paren만 생성된다.(자식은 생성되지 않는다.)
  • 자식 타입의 변수가 자식 인스턴스 참조 Child child = new Child()

    • Child 인스턴스를 만듦. 자식 타입인 child를 생성했기 때문에 Child와 Parent가 모두 생성된다.

(중요)다형적 참조: 부모 타입의 변수가 자식 인스턴스 참조
Parent Child: poly.parentMethod()

  • 부모 타입은 자식을 담을 수 있다.
  • Parent poly = new Child()
    Child 인스턴스를 만들었다. 이 경우 자식 타입인 Child를 생성했기 때문에 메모리 상에 child와 parent가 모두 생성된다.



다형적 참조

지금까지는 항상 같은 타입에 참조를 대입했다. 그래서 보통 한 가지 형태만 참조할 수 있다.

  • Parent parent = new Parent()
  • Child child = new Child()

그런데 parent 타입의 변수는 다음과 같이 자신인 parent는 물론이고, 자식 타입까지 참조할 수 있다. 만약 손자가 있다면 손자도 그 하위 타입도 참조 가능

  • Parent poly = new Parent()
  • Parent poly = new Child()
  • Parent poly = new Grandson():child 하위에 손자가 있다면 가능

자바에서 부모타입은 자신은 물론이고, 자신을 기준으로 모든 자식 타입을 참조할 수 있다. 이것이 다양한 형태를 참조할 수 있다고 해서 다형적 참조라 한다.

다형적 참조의 한계

Parent poly = new Child()

  • 이렇게 자식을 참조한 상황에서 poly가 자식 타입인 child에 있는 childMethod()를 호출하면 컴파일 오류가 발생한다.
    • why? 부모 방향으로 찾아 올라갈 수는 있지만 자식 방향으로 찾아 내려갈 수는 없기 때문에

다형적 참조의 핵심은 부모는 자식을 품을 수 있다는 것이다.

  • childMethod()를 쓰면 편한데 왜 다형적 참조가 필요하지? 란 생각이 들 수 있는데 이건 다른 이론도 같이 알아야 하기 때문에 지금은 다형성의 문법과 이론을 익히는데 집중!!

다형성과 캐스팅

Parent poly = new Child()와 같이 부모타입의 변수를 사용하게 되면 poly.childMethod()와 같이 자식 타입에 있는 기능은 호출할 수 없다.

package poly.basic;

public class CastingMain1 {

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

        Child child = (Child) poly;
        child.childMethod();
    }
}

  • poly.childMethod()를 호출하면 먼저 참조값을 사용해서 인스턴스를 찾는다.
  • 인스턴스 안에서 사용할 타입을 찾아야 한다. poly는 Parent타입이다.
  • Parent는 최상위 부모이다. 상속 관계는 부모로만 찾아서 올라갈 수 있다. childMethod()는 자식 타입에 있으므로 호출할 수 없다. 따라서 컴파일 오류가 발생한다.

캐스팅 용어

cast는 금속이나 다른 물질을 녹여서 특정한 형태나 모양으로 만드는 과정을 의미한다.

  • Child child = (Child) poly;
    • Parent poly라는 부모 타입을 child라는 자식 타입으로 변경하는 것을 다운캐스팅
    • 반대로 부모타입으로 변경하는 것은 업캐스팅이라 한다.

다운캐스팅


캐스팅의 종류

일시적 다운 캐스팅

: 해당 메서드를 호출하는 순간만 다운캐스팅하는 것

package poly.basic;

public class CastingMain2 {

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

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

        //일시적 다운캐스팅 - 해당 메서드를 호출하는 순간만 다운캐스팅
        ((Child) poly).childMethod();
    }
}

업캐스팅

: 다운캐스팅과 반대로 현재 타입을 부모 타입을 변경하는 것

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();
    }

}
  • 업캐스팅은 생략할 수 있다. 다운캐스팅은 생략할 수 없다. 참고로 업캐스팅은 매우 자주 사용하기 때문에 생략을 권장한다.
  • 자바에서 부모는 자식을 담을 수 있다. 하지만 그 반대는 안된다. (꼭 필요하다면 다운캐스팅을 해야 한다.)

다운캐스팅은 직접 명시적으로 캐스팅 하는 이유?

다운 캐스팅과 주의점

  • 다운캐스팅은 잘못하면 심각한 런타임 오류가 발생할 수 있다.
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; //런타임 오류
        child2.childMethod(); //실행 불가
    }
}
  • Parent parent2 = new Parent(); -> new Parent()로 부모 타입으로 객체를 생성한다. 따라서 자식 타입은 생성되지 않는다. 같은 타입이므로 여기서는 문제 발생 x

  • Parent parent1 = new Child(); -> parent2를 Child 타입으로 다운캐스팅한다. 그런데 parent2는 Parent로 생성이 되었다. 따라서 메모리 상에 Child 자체가 존재 x -> 이렇게 사용할 수 없는 타입으로 다운캐스팅하는 경우에 ClassCastException이라는 예외를 발생 -> 예외가 발생하면 다음 동작이 실행되지 않고, 프로그램 종료 -> child2.childMethod() 코드 자체가 실행x

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

  • 업스캐팅의 경우 : 객체를 생성하면 해당 타입의 상위 부모 타입은 모두 함께 생성됨. 따러서 위로만 타입을 변경하는 업스캐팅은 메모리 상에 인스턴스가 모두 존재하기 때문에 항상 안전하다. 따라서 캐스팅을 생략 가능

  • 다운캐스팅의 경우 : 인스턴스에 존재하지 않는 하위 타입으로 캐스팅하는 문제가 발생할 수 있다. 왜냐하면 객체를 생성하면 부모 타입은 모두 함께 생성되지만 자식 타입은 생성되지 않는다. 따라서 개발자가 이런 문제를 인지하고 사용해야 한다는 의미로 명시적으로 캐스팅을 해주어야 한다.

컴파일 오류 vs 런타임 오류

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


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("parent2 호출");
        call(parent2);
    }
    private static void call(Parent parent) {
        parent.parentMethod();
        if (parent instanceof Child) {
            System.out.println("Child 인스턴스 맞음");
            Child child = (Child) parent;
            child.childMethod();
        }
    }
}

  • Call(Parent parent) 메서드를 보면 매개변수로 넘어온 parent가 참조하는 타입에 따라서 다른 명령을 수행한다. 참고로 지금처럼 다운캐스팅을 수행하기 전에는 먼저 instanceof를 사용해서 원하는 타입으로 변경이 가능한지 확인한 다음에 다운캐스팅을 수행하는 것이 안전하다.

  • 해당 메서드를 처음 호출할 때 parent는 Parent의 인스턴스를 참조한다.

  • 해당 메서드를 다음으로 호출할 때 parent는 child의 인스턴스를 참조한다.

자바 16 - Pattern Matching for instanceof
자바 16부터는 instanceof를 사용하면서 동시에 변수를 선언할 수 있다.

package poly.basic;

public class CastingMain6 {

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

        Parent parent2 = new Child();
        System.out.println("parent2 호출");
        call(parent2);
    }
    private static void call(Parent parent) {
        parent.parentMethod();
        if (parent instanceof Child child) {
            System.out.println("Child 인스턴스 맞음");
            child.childMethod();
            
        }
    }
}

  • 인스턴스가 맞는 경우 직접 다운캐스팅 하는 코드를 생략할 수 있다.

다형성과 메서드 오버라이딩

다형성을 이루는 또 하나의 중요한 이론은 메서드 오버라이딩이다.
오버아이딩에서 꼭! 기억해야 할 점은 오버라이딩 된 메서드가 항상 우선권을 가진다는 점!! (그래서 이름도 기존 기능을 덮어 새로운 기능을 재정의 한다는 뜻의 오버라이딩)

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); // 변수는 오버라이딩X
        poly.method();//메서드 오버라이딩!
    }
}
package poly.overriding;

public class Parent {

    public String value = "parent";

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

public class Child extends Parent {

    public String value = "child";

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

실행결과를 보면 맨 마지막에 value = parent지만 child.method로 된것을 알 수 있다. 그 이유는 바로 오버라이딩 된 메서드는 항상 우선권을 가지기 때문이다. 만약 자식에서도 오버라이딩 하고 손자에서도 같은 메서드를 오버라이딩을 하면 더 하위 자식의 오버라이딩 된 메서드가 우선권을 가진다.


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

0개의 댓글