공부/Java

자바 다형성 1편 (Polymorphism)

Stair 2024. 9. 4. 11:21
반응형

객체지향 프로그래밍의 대표적인 특징으로는 캡슐화, 상속, 다형성이 있다.

캡슐화와 상속은 이전 내용을 참고하자.

https://surrealcode.tistory.com/48

 

자바 접근 제어자

자바는 public, private 같은 접근 제어자(access modifier)를 제공한다. 접근 제어자를 사용하면 해당 클래스 외부에서 특정 필드나 메서드에 접근하는 것을 허용하거나 제한할 수 있다. 스피커의 음량

surrealcode.tistory.com

https://surrealcode.tistory.com/51

 

자바 상속이란

다음과 같은 전기, 가스 자동차 클래스와 메인 클래스가 있다고 가정하자public class ElectricCar { public void move(){ System.out.println("차를 이동합니다."); } public void charge(){ System.out.println("충전합니다."); }

surrealcode.tistory.com

 

다형성(Polymorphism)은 이름 그대로 "다양한 형태", "여러 형태"를 뜻한다.

프로그래밍에서 다형성은 한 객체가 여러 타입의 객체로 취급될 수 있는 능력을 뜻한다. 보통 하나의 객체는 하나의 타입으로 고정되어 있다. 그러나 다형성을 사용하면 하나의 객체가 다른 타입으로 사용될 수 있다는 뜻이다.

 

다형성을 이해하기 위해서는 크게 2가지 핵심 이론을 알아야한다.

1. 다형적 참조 : 부모는 자식을 담을 수 있다.

2. 메서드 오버라이딩 : 기존 기능을 하위 타입에서 새로운 기능으로 재정의한다.

 

 

 

1.다형적 참조 : 부모 타입의 변수가 자식 인스턴스를 참조한다.

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

    public void childMethod(){
        System.out.println("Child.childMethod");
    }
}
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();

}

 

부모타입의 변수가 자식 인스턴스를 참조한다.

 

 

 

 

 

 

Parent poly = new Child() 를 보자 Child() 인스턴스를 만들었기에 이 경우 메모리상에 Child와 상속하는 Parent가 모두 생성된다.

 

이렇게 생성된 참조값을 Parent 타입 변수인 poly에 담아둔다.

 

* 부모 타입은 자식을 담을 수 있다.

 

* Parent poly 타입은 부모 타입이다 new Child()를 통해 생성된 결과는 Child 타입이다. 자바에서 부모타입은 자식타입을 담을 수 있다.

 

* 반대로 자식타입은 부모 타입을 담을 수 없다. Child child1 = new Parent() -> X(불가능)

 

* 자식의 기능은 호출할 수 없다. poly.childMethod() -> X(불가능하다. Parent에는 parentMethod()밖에 없기 때문이다.)

 

 

 

 

 

 

이전까지 배웠을때는 항상 같은 타입에 참조를 대입하였다.

ex) Parent parent = new Parent();             Child child = new Child();

 

그런데 Parent 타입의 변수는 자기 자신인 Parent 는 물론이고 본인을 상속받은 모든 하위타입도 참조할 수 있다.

poly는 Parent타입 변수이기에 Child를 먼저 찾아보는 것이 아닌 Parent에서부터 메서드를 찾는다.

 

poly.childMethod()는 불가능하다. 왜일까?

호출자인 poly는 Parent 타입이다. 따라서 parent 클래스부터 시작해서 필요한 기능을 찾는다. 그런데 상속 관계는 부모 방향으로 찾아 올라갈 순 있지만 내려갈 순 없다.

 

하지만 인스턴스에는 childMethod()는 존재한다. 이런 경우에 호출을 하고 싶다면 캐스팅을 통해 호출할 수 있다.

 

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

 


        System.out.println("Parent -> Child");
        Parent poly = new Child();
        poly.parentMethod();
//        poly.childMethod();

 

Parent poly = new Child()와 같이 부모 타입의 변수를 사용하게 되면 poly.childMethod()를 사용할 수 없다.

이럴때 다운캐스팅을 통해 부모타입을 자식 타입으로 바꿀 수 있다.

    public static void main(String[] args) {
        Parent poly = new Child();
//        poly.childMethod();

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

    }

 

 

자식은 부모를 담을 수 없기 때문이다 따라서 (Child)를 통해 다운캐스팅을 해주어 이 문제를 해결한다.

실행 순서

Child child = (Child) poly

Child child = (Child) x001

Child child = x001

 

->*다운캐스팅을 한다고 해서 poly의 타입이 변하는 것은 아니다.*

다운캐스팅 덕분에 child.childMethod()를 호출할 수 있게 되었다. childMethod()를 호출하기 위해 해당 인스턴스를 찾아간 다음 Child 타입을 찾는다. Child 타입에는 childMethod()가 있으므로 해당 기능을 호출할 수 있다.

 

 

자식 타입의 기능을 사용하려면 위처럼 다운 캐스팅 결과를 변수에 담아두고 이후에 기능을 사용하면 됐었다.

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

 

 

하지만 이 과정은 상당히 번거롭다. 사실 이러한 과정 없이 일시적으로 다운캐스팅을 해서 인스턴스에 있는 하위 클래스의 기능을 바로 호출할 수 있다.

((Child) poly).childMethod();

 

위처럼 poly를 Child타입으로 일시적으로 다운캐스팅을 하여 childMethod()를 호출한다.

*다시한번 말하지만 poly가 Child 타입으로 바뀌는 것이 아니다*

 

 

 

이 다운캐스팅과 반대되는것 또한 존재하는데 현재 타입을 부모 타입으로 변경하는 것을 업캐스팅이라고 한다.

public static void main(String[] args) {

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

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

}

원래는 Parent parent1 = (Parent) child; 처럼 업캐스팅이 되는 코드이지만 자바에서는 생략 가능하고 또한 생략을 권장하고 있다.

 

정리 : 업캐스팅은 생략 가능,다운캐스팅은 생략 불가.

          자바에서는 부모는 자식에 담을 수 있다. 하지만 그 반대는 안된다.(꼭 필요하다면 다운캐스팅을 해야한다.)

 

 

 

다운캐스팅의 주의점

다운캐스팅을 할때 왜 굳이 명시를 해줘야 하냐면, 다운 캐스팅은 잘못하면 심각한 런타임 오류가 발생할 수 있다.

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

 

위와 같은 코드는 child1.childMethod()는 정상 동작하나 child2.childMethod()는 동작하지 않는다

왜냐하면 Parent타입을 만들었는데 이 Parent타입은 메모리상에 자식 타입이 존재하지 않기에, 다운캐스팅을 하여도 Child 자체를 사용할 수 없어서이다.

 

 

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

업캐스팅의 경우 위와 같은 문제가 "절대로" 발생하지 않는다.

왜냐하면 객체 생성 시 해당 타입의 상위 부모 타입이 모두 함께 생성되기 때문이다. 업캐스팅은 메모리상에 인스턴스가 모두 존재하기 때문에 항상 안전하다.

 

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

 

 

 

 

 

 

여지껏 배운걸 정리하면 참조형 변수는 이름 그대로 다양한 자식을 대상으로 참조할 수 있었다. 그런데 참조하는 대상이 다양하기 때문에 어떤 인스턴스를 참조하고 있는지 헷갈리는 경우도 많을 것이다. 어떤 인스턴스를 참조하고 있는지 확인하려면 어떻게 해야할까?

 

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

    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();
    } else {
        System.out.println("Child 인스턴스 아님");
    }
}

 

 

call(Parent parent) 메서드는 매개변수로 넘어온 parent가 참조하는 타입에 따라서 다른 명령을 수행한다.

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

 

parent instanceof Child 는

new Parent() instanceof Child와 같은 뜻이다. false를 반환하게 된다.

 

다음 instanceof를 보면

parent instanceof Child 는

new Child() instanceof Child와 같은 뜻이다 해당 값이 참이므로 if문의 로직을 탄다.

 

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

 

 

 

 

다형성을 이루는 또 하나의 중요한 핵심 이론은 바로 메서드 오버라이딩이다.

오버라이딩 된 메서드는 항상 우선권을 가진다.

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

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

    public String value = "child";

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

    Parent parent = new Parent();
    System.out.println("Parent -> parent");
    System.out.println("parent.value = " + parent.value);
    parent.method();
    System.out.println();

    Parent poly = new Child();
    System.out.println("Parent -> Child");
    System.out.println("poly.value = " + poly.value);//변수는 오버라이딩 X
    poly.method(); //메서드는 오버라이딩 O


}

 

마지막이 중요하다.

poly는 Parent 타입이다. 따라서 poly.value, poly.method()를 호출하면 인스턴스의 Parent 타입에서 기능을 찾아서 실행한다. 하지만 Parent타입에 있는 method()는 child에서 오버라이딩 되어있다. 앞서 말했듯 메서드는 오버라이딩 된 메서드가 우선권을 가지기 때문에 Parent.method()가 아니라 Child.method()가 실행된다.

 

 

 

*오버라이딩 된 메서드는 절대적인 우선권을 가진다.

 

오버라이딩은 부모 타입에서 정의한 기능을 자식 타입에서 재정의하는 것이다. 만약 자식에서도 오버라이딩 하고 손자에서도 같은 메서드를 오버라이딩을 하면 손자의 오버라이딩 메서드가 우선권을 가진다. 더 하위 자식의 오버라이딩 된 메서드가 우선권을 가지는 것이다.

 

 

반응형

'공부 > Java' 카테고리의 다른 글

자바 좋은 객체 지향 프로그래밍이란?  (1) 2024.09.05
자바 다형성 2편 (Polymorphism)  (1) 2024.09.05
자바 상속이란  (1) 2024.09.03
자바 final  (1) 2024.09.02
자바 메모리 구조와 static  (2) 2024.09.02