한 타입의 참조 변수를 통해 여러 타입의 객체를 참조할 수 있도록 하는 것
객체지향 프로그래밍의 대표적인 특징으로 캡슐화, 상속, 다형성이 있고
다형성은 객체지향 프로그래밍의 꽃이다.
프로그래밍에서 다형성은 한 객체가 여러 타입의 객체로 취급될 수 있는 능력을 뜻한다.
다형성을 이해하기 위해 2가지 핵심 이론을 알아보자.
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 parent = new Parent();
parent.parentMethod(); // 호출 가능
Child child = new Child();
child.childMethod(); // 호출가능
Parent poly = new Child();
poly.parentMethod(); // 호출가능
poly.childMethod(); // 호출 불가능 -> 컴파일 오류 발생
그림을 통해서 확인해보면, 일단 상속관계에서 자식 클래스 타입의 인스턴스를 생성하게 된다면 메모리 주소 안에 자식 클래스 뿐만 아니라 부모 클래스 정보까지 담을 수 있는 공간이 생긴다고 말했다.
new Child()를 통해서 Child 인스턴스를 생성하면 메모리 주소 안에 부모 타입 Parent와 Child타입이 두가지 생성이 되며, 이 인스턴스의 주소를 담는 child변수를 통해 메모리에 접근하여 메서드를 호출한다.
그렇다면 new Parent()를 생성하면 어떻게 될까? (메모리 구조 안에는 무슨 일이 일어날까?)
부모 클래스의 인스턴스를 생성하면 메모리 주소 안에 부모클래스 타입만 생성된다.
부모는 자식에 대한 정보를 모르기 때문에 자식클래스 타입이 생성되지 않는다!
Parent parent = new Child();
parent.parentMethod();
이 코드를 실행하면 어떻게 될까?
Parent 타입의 참조변수는 생성된 Child()의 메모리 주소값을 저장한다.
이 메모리에 접근하여 해당 부모클래스의 메서드를 호출한다.
결국에 참조변수 parent가 저장한 메모리 주소값 안에는 부모클래스와 자식 클래스에 대한 정보가 들어있다.
그렇다면 왜
Parent poly = new Child();
poly.childMethod();
이거는 호출이 불가능인지?
간단하게 말하면 호출자인 poly의 타입이 Parent 타입이다.
따라서 Parent 클래스부터 시작하여 필요한 기능을 찾는데, 만약 해당 기능이 없다면 부모 방향으로 올라갈 순 있지만 자식 방향으로 내려갈 수 없기 때문이다.
Parent는 부모 타입이고 상위에 부모가 없고 (Object 클래스가 있긴한데).. childMethod()를 찾을 수 없으므로 컴파일 오류가 발생한다.
다형적 참조 : 부모는 자식을 품을 수 있다.
Parent poly는 부모 타입이고 new Child()를 통해 생성된 결과는 Child 타입이다.
따라서 부모 타입은 자식 타입을 담을 수 있으므로 문제가 발생하지 않는다.
다만
Child child = new Parent(); // 컴파일 오류 -> 자식은 부모를 담을 수 없다.
즉, 본인을 기준으로 본인의 자식 클래스 타입들은 다 담을 수 있다.
다양한 형태를 참조할 수 있다고 하여 다형적 참조라고 부른다.
위의 문제점을 해결하기 위해서 캐스팅을 이용하면 된다.
캐스팅의 종류에는 2가지가 있다.
업캐스팅(Up-Casting)
다운캐스팅(Down-Casting)
Parent poly = new Child();
Child child = (Child) poly;
child.childMethod();
부모 클래스 -> 자식 클래스 로 변경하는 것을 다운캐스팅이라고한다.
반대로 자식 클래스 -> 부모 클래스 로 변경하는 것을 업캐스팅이라고 한다.
그림으로 쉽게 이야기를 하면 이렇게 된다.
Child child = (Child) poly // 다운캐스팅을 통해 부모타입을 자식 타입으로 변환한 다음 대입 시도
Child child = (Child)x001 // 참조값을 읽은 다음 자식 타입으로 지정
Child child = x001 // 최종 결과
(타입)처럼 괄호와 그 사이에 타입을 지정하면 참조 대상을 특정 타입으로 변경할 수 있다.
캐스팅을 한다고 해서 Parent poly의 타입이 변하는 것은 아니고 참조값을 꺼내고 꺼낸 참조값이 Child 타입이 되는 것이다.
캐스팅의 2가지 유형에서
Child child = new Child();
Parent parent1 = (Parent) child; // 업캐스팅 생략, 생략 권장
Parent parent2 = child; // 업캐스팅 생략
다운 캐스팅 -> 이 경우에는 생략할 수 없고 개발자가 직접 명시적으로 캐스팅을 해야한다.
Parent parent1 = new Child();
Child child1 = (Child) parent1;
child1.childMethod(); // 문제가 없다
결국 new 연산자를 통해 생성된 인스턴스는 Child 타입이므로 해당 메모리 주소 안에 Child와 Parent 둘다 생성이 되기 때문이다.
처음에 Parent 참조변수에 주소값을 저장했다가 Child 타입으로 주소를 다시 참조하여도 해당 메모리 주소 안에 Child 타입이 있기 때문에 childMethod() 메서드를 호출하여도 문제가 발생하지 않는다.
다만!
Parent parent1 = new Parent();
Child child1 = (Child)parent1;
child1.childMethod(); // 호출 문제 발생
new 연산자를 통해 생성된 인스턴스는 Parent 타입이기에 Child 클래스에 대한 정보가 저장되지 않는다.
따라서 다운캐스팅을 하는데에는 문제가 없지만, 자식 클래스 호출자를 통해 해당 메서드를 불러올 떄, Child 인스턴스가 없으므로 런타임 오류가 발생한다.
자바는 이렇게 사용할 수 없는 타입으로 다운캐스팅하는 경우에 ClassCastException 이라는 예외를 발생시킨다.
컴파일 오류는 변수명 오타, 잘못된 클래스 이름 사용 등 자바 프로그램을 실행하기 전에 발생하는 오류로 IDE 에서 즉시 확인할 수 있끼에 좋은 오류이다.
런타임 오류는 이름 그대로 프로그램이 실행되고 있는 시점에서 발생하는 오류로 프로그램 실행 도중에 발생하기에 안좋은 오류이다.
참조하는 대상이 다양하기 때문에 인스턴스 타입을 확인하고 싶을 때 사용하는 키워드
Parent parent = new Child();
if(parent instanceof Child) {
System.out.println("true");
}
parent 변수가 참조하는 인스턴스 타입이 Child라면 "true"를 출력하는 코드이다.
이럴 경우 true가 출력된다.
Child child = new Child();
child instanceof Child // -> true
child instanceof Parent // -> true
자식 클래스 같은 경우에는 부모 클래스까지 포함하고 있기 때문에 true이다.
오버라이딩 된 메서드가 항상 우선권을 갖는다.
다형성을 이루는 또 하나의 중요한 핵심이론이 바로 메서드 오버라이딩
기존 기능을 덮어 새로운 기능을 재정의한다는 것이다.
public class Parent{
public String value = "parent";
public void method() {
System.out.println("Parent.method");
}
}
public class Child{
public String value = "child";
@Override
public void method() {
System.out.println("Child.method");
}
}
이럴 경우에
Parent poly = new Child();
System.out.println("value= " + poly.value); // 변수는 오버라이딩 X
poly.method(); // 메서드 오버라이딩
출력이
value= parent
Child.method
이렇게 출력이 나온다.
poly 변수는 Parent 타입이다.
따라서 poly.value, poly.method()를 호출하면 인스턴스의 Parent 타입에서 기능을 찾아서 확인한다.
변수는 기능 그대로 Parent 타입에 있는 value를 찾는다.
하지만!! 메서드는 위에서 말했듯 오버라이딩 된 메서드가 항상 우선권을 갖는다 이므로 Child.method() 가 오버라이딩 되어 있기 때문에 Child.method()가 호출된다.
다형성은 객체지향에서 꽃이기 때문에 더 활용을 하면 어려워질 수 있으므로 꼭 잘 이해하자.