다형성1

2024. 2. 9. 18:13Java

◎ 다형성

-> 한 객체가 여러 타입의 객체로 취급될 수 있는 성질을 의미한다.

-> 보통 하나의 객체는 하나의 타입으로 고정되어 있지만 다형성을 사용하면 하나의 객체가 다른 타입으로도 사용할 수 있다.

-> 예제

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

-> Parent 클래스와 이 클래스를 상속받는 Child 클래스를 이용한 코드다.

-> 먼저 Parent 타입으로 인스턴스를 생성하면 메모리에는 Parent 인스턴스만 생성되기 떄문에 Parent의 기능만 사용할 수 있다.

-> Parent클래스로부터 상속받아 생성된 Child의 인스턴스를 생성하면 메모리에는 Parent와 Child가 모두 생성된다.

-> child를 통해 Child의 기능과 Parent의 기능을 모두 사용할 수 있다.

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.parentMethod();
        ;
        // 자식의 기능을 호출할 수 없다. -> Parent에는 childMethod()가 없기 때문
//        poly.childMethod();
    }
}

-> Parent poly = new Child()는 부모 타입의 변수가 자식 인스턴스를 참조하도록 작성된 코드다.

-> new 키워드로 Child 인스턴스를 만들면 자식 타입인 Child를 생성한 것이기 때문에 메모리에는 Child와 Parent가 모두 생성된다.

-> 생성된 참조값은 Parent 타입의 변수 poly에 담겨있다.

-> 또한 poly는 자식 클래스의 메서드인 childMethod()에는 접근할 수 없다.

-> 부모 타입은 자식 타입을 담을 수 있다는 것을 알 수 있다.

-> 부모 타입이 자신을 포함하여 자신을 기준으로 모든 자식 타입을 참조할 수 있다. 이것을 다형적 참조라고 한다.

-> 그런데 반대로 자식타입에 부모 타입을 담을 수 없다. Child child1 = new Parent()는 컴파일 오류가 발생한다.

 

-> 결과

-> 그런데 코드를 보면 Parent 타입의 변수 poly로 Child의 메서드인 childMethod()에는 접근할 수 없다는 것을 확인했다.

-> poly.childMethod()를 호출하면 poly는 Parent타입이기 떄문에 Parent 클래스부터 시작해서 childMethod()를 찾는다. 

-> 상속 관계에서는 자식 클래스에서 없으면 부모 클래스로 가서 찾을 수 있지만 반대로 부모 클래스에서 자식 클래스 방향으로 찾아갈 수 없다.

-> 자식 클래스인 Child는 Parent의 정보를 받았기 때문에 Parent에 접근이 가능하지만 부모 클래스인 Parent에는 Child에 대한 정보가 없기 때문에 자식 클래스의 속성과 기능에는 접근할 수 없다.

-> 이 문제는 캐스팅을 통해 해결할 수 있다.

 

◎ 다형성과 캐스팅

-> 이전 코드에서 부모 타입에서 자식 타입에 있는 기능을 사용할 수 없다는 것을 알았다.

-> 이 경우 Parent타입인 poly에 담긴 참조값을 자식 클래스인 Child로 형 변환을 하면 Child의 기능에 접근할 수 있다. 이를 다운 캐스팅이라 한다.

public class CastingMain1 {
    public static void main(String[] args) {
        // 부모 변수가 자식 인스턴스 참조(다형적 참조)
        Parent poly = new Child();

        // 자식의 기능은 호출 불가
//        poly.childMethod();

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

-> 결과

-> Parent 타입 poly에 (Child)를 붙여 형변환을 했다. 이제 poly는 Child타입이 된다.

-> 이제 poly에 담겨있던 참조값은 Child 타입의 child 변수에 담겨졌고, child 변수는 Child의 기능에 접근할 수 있으며 부모 타입인 Parent의 기능에도 접근할 수 있다.

Child child = (Child) poly; // 다운 캐스팅으로 부모 타입을 자식 타입으로 변환
Child child = (Child) x001; // 해당 참조 값이 자식 타입의 변수로 대입
Child child = x001; // 변환

-> 이렇게 부모 타입을 자식 타입으로 변경하는 것을 다운 캐스팅이라고 한다. 반대로 자식 타입을 부모 타입으로 변경하는 것은 업 캐스팅이라 한다.

 

◎ 캐스팅 종류

-> 이전에 다운 캐스팅을 할 때 Child 타입의 child 변수를 선언하여 다운 캐스팅을 진행했다. 하지만 이런 과정 없이 일시적으로 다운 캐스팅을 해서 필요할 때만 잠깐 다운 캐스팅을 하여 하위 클래스의 기능을 사용하도록 할 수 있다.

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

-> poly는 Parent타입으로 Child의 기능을 사용하지 못한다. 그래서 기존에는 Child 타입의 변수를 생성해서 여기에 다운 캐스팅은 poly의 참조값을 child에 복사 후 저장하여 사용했다.

-> ((Child) poly).childMethod(); 이 코드를 실행하면 Child 타입의 변수 선언을 할 필요 없이 이 코드에서만 Child의 기능에 접근할 수 있다. 이것을 일시적 다운 캐스팅이라 한다.

-> 일시적 다운 캐스팅 코드 이후 poly로 childMethod()에 접근하면 컴파일 오류가 발생한다.

 

-> 업캐스팅 : 현재 타입을 부모 타입으로 변환하는 것이다.

public class CastingMain3 {
    public static void main(String[] args) {
        Child child = new Child();
        Parent parent1 = (Parent) child; // 업 캐스팅은 명시적 캐스팅 생략 가능(생략 권장)
        Parent parent2 = child; // 업 캐스팅 생략

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

-> Child 타입의 child 변수를 부모 클래스인 Parent 타입으로 변경했다. 업캐스팅은 생략 가능하다.

 

◎ 다운 캐스팅 - 주의점

-> 다운 캐스팅 시 발생할 수 있는 문제와 해결방법에 대해 알아본다.

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(); // 실행 불가
    }
}

-> 결과

-> parent1을 Child 타입으로 다운 캐스팅하여 childMethod()를 호출하는 것은 정상적으로 동작하지만 parent2를 Child 타입으로 다운 캐스팅하여 childMethod()를 호출하면 ClassCastException이라는 런타임 오류가 발생한다.

-> 먼저 parent1은 Parent 타입이지만 new 키워드를 통해 Child 인스턴스를 생성했기 때문에 메모리에는 Parent와 Child가 둘 다 있어 해당 참조값을 Child 타입으로 다운 캐스팅한 변수로 접근을 하면 Child의 기능을 사용할 수 있다.

-> parent2는 new 키워드로 Parent인스턴스를 생성했다. 따라서 메모리에는 Parent만 있을 뿐 Child는 없다.

-> 메모리 상에 Child가 없기 때문에 Child 타입으로 다운 캐스팅해서 접근을 해도 메모리에 존재하지 않기 때문에Child 자체를 사용할 수 없다.

-> 자바에서는 사용할 수 없는 타입으로 다운 캐스팅을 할 경우 ClassCastException예외를 발생시킨다. 해당 예외가 발생함녀 다음 동작을 수행하지 않고, 프로그램이 종료된다. 위의 코드에서도 Child child2 = (Child) parent2; 코드에서 오류가 발생하여 이후 코드인 child2.childMethod()를 수행하지 않았다.

 

※ 업캐스팅은 캐스팅 코드를 생략을 해도 될 정도로 안전하지만, 다운 캐스팅은 왜 주의해야 할까?

- 업캐스팅은 자식이 부모 타입으로 변환하는 것이다. 캐스팅을 하게 되면 자식 클래스의 인스턴스를 생성하고, 반환되는 참조값을 변수에 담아 해당 변수를 통해 업 캐스팅을 진행하는데, 이 떄 인스턴스를 생성하면서 메모리에는 자식 인스턴스와 부모 인스턴스가 모두 존재하기 때문에 메모리 상에 없는 인스턴스의 기능을 참조하는 일이 없다.

-> 반면 다운 캐스팅 시에는 부모 클래스의 인스턴스를 생성한 상태기 때문에 메모리에는 부모 인스턴만 존재한다. 따라서 자식 클래스로 다운 캐스팅을 해도 참조할 수 있는 자식 인스턴스가 존재하지 않는다.

 

※ 런타임 오류 vs 컴파일 오류

- 런타임 오류는 프로그램이 실행되고 있는 시점에 발생하는 오류다. 

- 컴파일 오류는 변수명 오타나 잘못된 클래스 이름 사용 등 자바 프로그램을 실행하기 전에 발생하는 오류다. 컴파일 오류는 IDE에서 바로 확인할 수 있어 프로그램 실행 전 사전에 오류를 방지할 수 있다.

 

◎ instanceOf

-> instanceOf를 사용하면 참조형 변수가 어떤 인스턴스를 참조하고 있는지 체크할 수 있다.

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

-> 결과

-> instanceOf를 통해 참조형 변수가 어떤 인스턴스를 참조하는지 알 수 있다.

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가 Child를 참조하는지 확인한다.

-> parent1에 있는 참조값의 메모리에는 Parent 인스턴스만 존재하기 때문에 call()메서드에서 parent instanceOf Child는 false다. 따라서 다운 캐스팅을 진행하지 않는다.

-> parent2에 있는 참조값의 메모리에는 Parent, Child 인스턴스가 모두 존재하기 때문에 parent instanceOf Child는 true다, 따라서 Child 타입으로 다운 캐스팅하여 childMethod()에 접근한다.

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

new Parent() instanceOf Parent // true
new Child() instanceOf Parent // true

-> instanceOf를 통해 참조형 변수가 어떤 인스턴스를 참조하는지 체크하여 다운 캐스팅 시 발생할 수 있는 런타임에러를 사전에 방지해준다.

 

※ 자바 16 - Pattern Matching for instanceOf

- 자바16부터는 instanceOf를 사용함과 동시에 변수를 선언할 수 있다.

    private static void call(Parent parent) {
        parent.parentMethod();

        // Child 인스턴스인 경우 childMethod() 실행
        if (parent instanceof Child child) {
            System.out.println("Child 인스턴스");
//            child = (Child) parent;
            child.childMethod();
        }
    }
}

- parent instanceOf Child child로 instanceOf를 사용함과 동시에 child 변수를 선언하여 해당 조건이 true인 경우 child 변수로 바로 다운 캐스팅한다.

 

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

-> 메서드 오버라이딩을 한 경우 오버라이딩 된 메서드가 항상 우선권을 가진다.

-> 다음은 Parent 클래스를 Child클래스가 상속받고, Parent 클래스에 있는 method()를 Child에서 재정의한 코드다.

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 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();
        System.out.println();

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

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

-> 결과

-> OverridingMain에서 child변수와 parent 변수는 각각 new Child(), new Parent()를 통해 인스턴스가 생성되었다. child는 Child의 value와 method에 접근하고, parent는 Parent의 value와 method에 접근한다.

-> 변수 poly는 Parent 타입이고, new 키워드를 통해 Child인스턴스를 생성한다. 그러면 아래와 같이 메모리 구조가 생성된다.

-> 메모리에는 Parent와 Child가 모두 있고, poly.value를 호출하면 poly는 Parent 타입이기 떄문에 Parent의 value를 읽는다.

-> poly.method()를 호출하면 method()를 찾아가는데 method()는 Child 클래스에서 재정의되어 있다. 메서드가 오버라이딩 된 경우에는 오버라이딩 된 메서드가 우선권을 가지므로 Child의 method()를 호출하게된다.

 

※ 다형성의 핵심이론

- 다형적 참조 : 하나의 변수 타입으로 다양한 자식 인스턴스를 참조하는 기능

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

 

 

 

 

★ 참고

김영한의 실전 자바 - 기본편

'Java' 카테고리의 다른 글

Object 클래스  (0) 2024.10.29
다형성2  (0) 2024.02.11
상속  (0) 2024.02.08
final  (0) 2024.02.07
자바 메모리 구조 & static  (0) 2024.02.06