[Java] 객체 지향 4대 특징-2 (다형성)
다형성이란 한 객체가 여러 타입의 객체로 취급될 수 있는 능력을 말한다. 보통의 객체는 하나의 타입으로 고정되어 있지만 다형성을 사용하면 하나의 객체가 다른 타입으로 사용될 수 있다. 다형성은 상속과 추상화와 연계되어 사용하므로 일반적인 객체에 경우에는 다형성을 적용할 수 없음을 기억하자.
다형성의 특징을 알아보기 전에 상속을 받은 클래스들의 인스턴스를 생성했을 때, 해당 인스턴스의 상태를 짚어봐야 한다.
// 부모 클래스
public class Parent {
public void parentMethod() {
System.out.println("parent method");
}
}
// 자식 클래스
public class Child extends Parent {
public void childMethod(){
System.out.println("child method");
}
}
위와 같이 상속 관계가 형성되어 있다고 가정할 때, 각각의 타입별로 인스턴스를 생성하면 아래의 그림과 같이 생성된다.
여기서 기억해야 할 것은 부모 타입은 자식 타입의 정보를 전혀 모른다는 점이다.
다형성의 특징
1. 다형적 참조
다형적 참조란 상속 관계에서 부모 타입은 자식 타입의 인스턴스를 담을 수 있음을 의미한다.
// 부모 클래스
public class Parent {
public void parentMethod() {
System.out.println("parent method");
}
}
// 자식 클래스
public class Child extends Parent {
public void childMethod(){
System.out.println("child method");
}
}
// 부모 타입은 자식 타입의 인스턴스를 담을 수 있다.
Parent parent = new Child();
이 일이 가능한 이유는 자식 타입의 인스턴스를 생성할 때 아래의 그림처럼 부모 타입의 인스턴스도 한 공간에 같이 생성되기 때문이다.
반대로 자식 타입은 부모 타입의 인스턴스를 담을 수 없다.
Child child = new Parent(); // 불가능.
그림에서 확인할 수 있듯이 부모 타입은 자식 타입의 정보를 하나도 가지고 있지 않다. 따라서 이는 타입 불일치로 컴파일 오류가 발생한다.
💡 자식 타입은 부모 타입의 정보를 가지고 있으나, 부모 타입은 자식 타입의 정보를 전혀 모른다.
그렇다면 아래와 같은 상황은 가능할까?
Parent parent = new Child();
parent.childMethod(); // 실행이 될까?
부모 타입으로 선언했으나, 실질적으로 생성된 것은 자식 타입의 인스턴스이기 때문에 실행에 문제가 없을 것이라고 판단하기 쉽다. 여기서 다시 기억해야 할 것은 부모 타입은 자식 타입의 정보를 전혀 가지고 있지 않다. 따라서 부모 타입으로 자식 인스턴스를 정의했다 하더라도 자식 타입의 메서드를 호출할 정보를 부모 타입은 전혀 알지 못하기 때문에 호출이 불가능하다.
또한, 상속에서 인스턴스 타입을 우선 탐색한 후, 없을 경우 상위 클래스를 탐색한다고 하였으며 최상위 클래스에도 존재하지 않는다면 컴파일 오류가 발생한다고 하였다. 이러한 탐색은 상위에서 하위로 내려가는 하향식 탐색은 존재하지 않는다.
만약 그래도 자식 타입의 메서드를 호출해야 한다면 캐스팅을 통해 가능하게 할 수 있다.
캐스팅
캐스팅은 객체의 타입을 상위, 하위 클래스 타입으로 변경하는 것을 의미한다. 여기서 기억해야 할 자바의 대원칙이 있다. 바로 값을 대입하는 것이 아니라 복사한다는 것이다. 따라서 캐스팅을 통해 타입을 변경했다 하더라도 기존의 객체에는 영향을 주지 않는 것이 좋다.
- 업 캐스팅 : 자식 -> 부모타입으로 변경
- 다운 캐스팅 : 부모 -> 자식타입으로 변경
업 캐스팅
// 업캐스팅
Child child = new Child();
Parent p1 = (Parent)child; // 업캐스팅 생략 안한 경우
Parent p2 = child; // 업캐스팅 생략한 경우
업 캐스팅은 타입을 변경하는 (Parent)를 생략할 수 있으며 생략을 권장한다.
다운 캐스팅
Parents parent = new Child(); // 가능
parent.parentMethod(); // 실행 가능
parent.childMethod(); // 오류 발생. Child의 메서드를 사용하는 것은 불가능
위와 같은 상황에서 child의 메서드를 사용하기 위해서는 다운 캐스팅이 필요하다.
// 일시적 다운 캐스팅
Child poly = (Child)parent;
poly.childMethod(); // 실행 가능
일시적으로 다운 캐스팅할 객체를 poly 객체에 담아 사용하므로서 기존의 객체에는 영향을 주지 않도록 한다.
다운 캐스팅은 업 캐스팅과 다르게 생략을 절대절대절대 해서는 안되는데 다운 캐스팅을 잘못할 경우에는 심각한 런타임 오류(ClassCastException)가 발생할 수 있기 때문이다.
Parent parent = new Parent();
Child child = (Child)parent; // 다운캐스팅 오류 발생
Parent 타입의 인스턴스는 자식 타입의 정보를 가지고 있지 않기 때문에 이런 식의 다운 캐스팅으로 인한 오류가 발생할 수 있다. 따라서 빠르게 오류를 찾을 수 있도록 캐스팅 표시를 하는 것이 바람직하다.
2. 메서드 오버라이딩
다형적 참조의 한계로 인해 캐스팅을 사용하였으나 다운 캐스팅은 심각한 오류를 초래할 수 있다. 그렇기 때문에 불가피한 이유가 아니라면 메서드 오버라이딩을 통해 자식 타입의 메서드를 호출할 수 있도록 한다.
// 부모 클래스
public class Parent {
String value = "Parent.value";
public void method() {
System.out.println("Parent.method");
}
}
// 자식 클래스는 부모 클래스의 메서드를 오버라이딩함.
public class Child extends Parent {
String value = "Child.value";
@Override
public void method() {
System.out.println("Child.method");
}
}
// 부모 타입으로 자식 타입의 인스턴스를 생성해 변수와 메서드를 호출해보자.
Parent parent = new Child();
System.out.println(parent.value); // 결과값 : Parent.value
parent.method(); // 결과값 : Child.value
위와 같이 부모 타입으로 자식 타입의 인스턴스를 생성해 변수를 호출한 경우와 메서드를 호출한 경우 서로 상반되는 결과값을 가진 것을 알 수 있다. 변수의 경우에는 Parent 타입을 우선으로 탐색하여 해당 결과값을 반환하였으나 메서드의 경우에는 Parent의 메서드가 아닌 Child의 메서드를 사용하였다.
상속의 특징때문에 변수이든, 메서드이든 정의한 타입에 맞는 인스턴스에 접근해 정보를 찾는 것이 맞지만 부모 타입의 메서드를 자식 타입에서 오버라이딩을 통해 재정의했을 경우에는 오버라이딩된 메서드를 우선시하기 때문에 자식 타입의 메서드를 호출한 것이다.