[다형성(Polymorphism)]
객체지향 프로그래밍의 대표적인 특징으로는 캡슐화, 상속, 다형성이 있다. 그 중에 다형성은 객체지향 프로그래밍의 꽃
다형성: 다양한 형태, 여러 형태
* 프로그래밍에서 다형성은 한 객체가 여러 타입의 객체로 취급될 수 있는 능력을 뜻한다. 보통 하나의 객체는 하나의 타입으로 고정되어 있다. 그런데 다형성을 사용하면 하나의 객체가 다른 타입으로 사용될 수 있다는 뜻.
<핵심 이론>
1. 다형성 참조
2. 메서드 오버라이딩
[다형성 참조]
Child.java
package Polymorphism.basic;
public class Child extends Parent{
public void childMethod(){
System.out.println("Child.childMethod");
}
}
Parent.java
package Polymorphism.basic;
public class Parent {
public void parentMethod(){
System.out.println("Parent.parentMethod");
}
}
PolyMain.java
package Polymorphism.basic;
public class PolyMain {
public static void main(String[] args) {
// 부모 변수가 부모 인스턴스 참조
System.out.println("Parent -> Parent");
Parent parent=new Parent();
parent.parentMethod();
}
}
<설명>
부모 타입의 변수가 부모 인스턴스를 참조한다.
Parent parent=new Parent()
Parent인스턴스를 만들었다. 이 경우 부모 타입인 Parent를 생성했기 때문에 메모리 상에 Parent만 생성된다.(자식은 생성x)
생성된 참조값은 Parent타입의 변수인 parent에 담아둔다.
parent.parentMehtod()를 호출하면 인스턴스의 Parent클래스에 있는 parentMethod()가 호출된다.
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();
}
}
<설명>
자식 타입의 변수가 자식 인스턴스를 참조한다.
Child child=new Child()
Child인스턴스를 만들었다. 이 경우 Child를 생성했기 때문에 메모리 상에 Child와 Parent가 모두 생성된다.
생성된 참조값을 Child타입의 변수인 child에 담아둔다.
child.childMethod()를 호출하면 인스턴스의 Child클래스에 있는 childMehtod()가 호출된다.
System.out.println("Parent -> Child");
Parent poly=new Child(); // 부모 타입인데, 자식 인스턴스를 넣음(부모 타입은 자식을 참조할 수 있다.)
poly.parentMethod();
* 부모님은 마음이 넓어서 자식을 품을 수 있다. 라고 생각하면 쉬움
<설명>
1. 부모 타입의 변수가 자식 인스턴스를 참조한다.
2. Parent poly = new Child()
3. Child인스턴스를 만들었다. 이 경우, 자식 타입인 child를 생성했기 때문에 메모리 상에 Child와 Parent가 모두 생성된다.
4. 생성된 참조값을 Parent타입의 변수인 poly에 담아둔다.
"부모는 자식을 담을 수 있다"
부모 타입은 자식 타입을 담을 수 있다.
Parent poly는 보시다시피 부모타입이다. new child()를 통해 생성된 결과는 Child타입이다. 자바에서 부모타입은 자식타입을 담을 수 있다.
* 단, 자식 타입은 부모 타입을 담을 수 없다.
Child child1=new Parent(): 컴파일 오류 발생
System.out.println("Parent -> Child");
Parent poly=new Child(); // 부모 타입인데, 자식 인스턴스를 넣음(부모 타입은 자식을 참조할 수 있다.)
poly.parentMethod(); // 부모 메서드 가능
poly.childMethod(); // 자식 메서드는 불가능(컴파일 오류 발생)
단, 자식 메서드는 불가능하다. parent타입은 본인만 알고 있어서 childmethod호출 불가능
(이유는 상속 관계는 부모 방향으로 찾아 올라갈 수 있지만, 자식 방향으로는 찾아 내려갈 수는 없다. Parent는 부모 타입이고 상위에 부모가 없다. 따라서 childMethod()를 찾을 수 없으므로 컴파일 오류가 발생-> childmethod를 호출하고 싶으면 ? 캐스팅 사용)
+ parent타입 변수는 부모는 물론이고 자식 타입까지 참조할 수 있다. (손자가 있다면 손자도 담을 수 있듯이 하위 타입도 참조할 수 있다.)
Parent poly=new Parent()
Parent poly=new Child()
Parent poly=new Grandson() : child하위에 손자가 있다면 가능
[다형성과 캐스팅]
Parent poly=new Child()와 같이 부모 타입의 변수를 사용하게되면, 자식 메서드는 호출할 수 없다.
//다운캐스팅(부모 타입-> 자식타입)
// Child child = poly; //x001
// child.childMethod();
// 이거 오류! 자식은 속이 좁아서 부모를 담을 수 없다.
// 개발자가 임의로 다운캐스팅
Child child=(Child)poly; // poly를 child로 작위적으로함
child.childMethod();
<설명>
1. poly.childMethod()를 호출하면 먼저 참조값을 사용해서 인스턴스를 찾는다.
2. 인스턴스 안에 사용할 타입을 찾는다. poly는 parent타입이다.
3. parent는 최상위 부모이다. 상속 관계는 부모로만 찾아서 올라갈 수 있다. childMethod()는 자식 타입에 있으므로 호출할 수 없다. 따라서 컴파일 오류가 발생
다운캐스팅
하지만, 이렇게 하면 다음과 같은 문제에 봉착한다.
"부모는 자식을 담을 수 있지만, 자식은 부모를 담을 수 없다."
Child child=poly // parent poly변수
-> 부모 타입을 사용하는 변수를 자식 타입에 대입하려고 하면 컴파일 오류가 발생한다. 자식은 부모를 담을 수 없다.
이때는 다운 캐스팅이라는 기능을 사용해서 부모 타입을 잠깐 자식 타입으로 변경하면 된다.
Child child=(Child)poly
-> (타입)처럼 괄호와 그 사이에 타입을 지정하면 참조 대상을 특정 타입으로 변경할 수 있다. 이렇게 특정 타입으로 변경하는 것을 캐스팅이라 한다.
오른쪽에 있는 (Child) polu코드를 먼저 보자. poly는 Parent타입이다. 이 타입을 (Child)를 사용해서 일시적으로 자식타입인 Child타입으로 변경한다. 그리고 나서 왼쪽에 있는 Child child에 대입한다.
Child child=(Child) poly; // 다운캐스팅을 통해 부모 타입을 자식 ㅏ입으로 변환한 다음 대입 시도
Child child=(Child)x001; // 참조값을 읽은 다음 자식 타입으로 지정
Child child=X001 // 최종 결과
* 캐스팅을 한다고 해서 Parent poly의 타입이 변하는 것은 아니다. 해당 찹조값을 꺼내고 꺼낸 참조값이 child타입이 되는 것이다. 따라서 poly의 타입은 Parent로 기존과 가이 유지된다.
[캐스팅]
업캐스팅 : 부모 타입으로 변경
다운캐스팅 : 자식 타입으로 변경
캐스팅: 금속이나 다른 물질을 녹여서 특정한 형태나 모양으로 만드는 과정을 의미
Child child=(Child)poly 경우, Parent poly라는 부모 타입을 Child라는 자식 타입으로 변경했다. 부모 타입을 자식 타입으로 변경하는 것을 다운캐스팅이라 한다. 반대로 부모 타입으로 변경하는 것은 업 캐스팅이다.
// 다운캐스팅(부모 타입-> 자식타입)
Child child=(Child)poly;
child.childMethod();
다운캐스팅 덕분에 child.childMethod()를 호출할 수 있게 되었다. childMethod()를 호출하기 위해 해당 인스턴스를 찾아간 다음 Child타입을 찾는다. Child타입에는 childMethod()가 있으므로, 해당 기능을 호출할 수 있다.
[캐스팅의 종류]
다운캐스팅 결과를 변수에 담아두는 과정이 번거롭다. 이런 과정 없이 일시적으로 다운캐스팅을 해서 인스턴스에 있는 하위 클래스의 기능을 바로 호출할 수 있다.
일시적 다운캐스팅
// 일시적 다운캐스팅 - 해당 메서드를 호출하는 순간만 다운캐스팅
((Child)poly).childMethod();
// poly.childMethod() 이 메서드 우선순위가 더 높아서 poly에 괄호를 덮어준다.
poly는 parent타입이다. 그런데 이 코드를 실행하면 parent타입을 임시로 Child로 변경한다. 그리고 메서드를 호출할 때 Child타입에서 찾아서 실행한다.
정확히는 poly가 Child타입으로 바뀌는 것은 아니다.
((Child)poly).childMethod(); // 다운캐스팅을 통해 부모 타입을 자식 타입으로 변호나 후 기능 호출
((Child)x001).childMethod(); // 참조값을 읽은 다음 자식 타입으로 다운캐스팅
* 캐스팅을 한다고 해서 parent poly의 타입이 변하는 것은 아니다. 해당 참조값을 꺼내고 꺼낸 참조값이 Child타입이 되는 것이다. 따라서 poly의 타입은 Parent로 그대로 유지된다.
이렇게 일시적 다운캐스팅을 사용하면 별도의 변수 없이 인스턴스의 자식 타입의 기능을 사용할 수 있다.
업캐스팅
다운 캐스팅과 반대로 현재 타입을 부모 타입으로 변경하는 것을 업캐스팅이라고 한다.
package Polymorphism.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();
}
}
업캐스팅은 생략할 수 있다. 다운캐스팅은 생략할 수 없다. 참고로 업캐스팅은 매우 자주 사용하기 때문에 생략을 권장한다.
자바에서 부모는 자식을 담을 수 있다. 하지만 그 반대는 안된다.(꼭 필요하다면 다운 캐스팅을 해야한다.)
업캐스팅은 생략해도 되고, 다운 캐스팅은 왜 개발자가 직접 명시적으로 캐스팅을 해야할까?
[다운캐스팅과 주의점]
다운캐스팅을 잘못하면 심각한 런타임 오류가 발생할 수 있다.
public class CastingMain4 {
public static void main(String[] args) {
Parent parent1=new Child();
//Child child1=parent1; // 자식은 부모를 못담음
Child child1=(Child) parent1;
child1.childMethod();// 문제 없음
}
}
Parent parent2=new Parent();
Child child2=(Child)parent2;
child2.childMethod();
// 다운 캐스팅을 자동으로 하지 않는 이유
public class CastingMain4 {
public static void main(String[] args) {
Parent parent1=new Child();
//Child child1=parent1; // 자식은 부모를 못담음
Child child1=(Child) parent1;
child1.childMethod();// 문제 없음
Parent parent2=new Parent();
Child child2=(Child)parent2; // 런타임 오류-ClassCastExcept
child2.childMethod(); // 실행 불가
}
}
예제의 parent2를 다운캐스팅하면 ClassCastException이라는 심각한 런타임 오류가 발생한다.
이 코드를 자세히 알아보자
Parent parent2=new Parent();
먼저 new Parent()로 부모 타입으로 객체를 생성한다. 따라서 메모리 상에 자식 타입은 전혀 존재하지 않는다. 생성 결과를 parent2에 담아둔다. 이 경우 같은 타입이므로 여기서는 문제가 발생하지 않는다.
Child child2=(Child)parent2;
다음으로 parent2를 Child타입으로 다운캐스팅한다. 그런데 parent2는 Parent로 생성되었다. 따라서 메모리 상에 Child자체가 존재하지 않는다. Child자체를 사용할 수 없는 것이다.
자바에서는 이렇게 사용할 수 없는 타입으로 다운캐스팅하는 경우에 ClassCastException이라는 예외를 발생시킨다. 예외가 발생하면 다음 동작이실행되지 않고, 프로그램이 종료된다. 따라서 child2.childMethod()코드 자체가 실행되지 않는다.
업캐스팅이 안전하고 다운캐스팅이 위험한 이유
업캐스팅의 경우, 이런 문제가 발생 x -> 왜냐하면 객체를 생성하면 해당 타입의 상위 부모 타입은 모두 함께 생성된다.
따라서 위로만 타입을 변경하는 업 캐스팅은 메모리 상에 인스턴스가 모두 존재하기 때문에 항상 안전하다. 따라서 캐스팅을 생략할 수 있다.
반면에, 다운캐스팅의 경우 인스턴스에 존재하지 않는 하위 타입으로 캐스팅하는 문제가 발생할 수 있다. 왜냐하면 객체를 생성하면 부모 타입은 모두 함께 생성되지만, 자식 타입은 생성되지 않는다. 따라서 개발자가 이런 묹를 인지하고 사용해야 한다는 의미로 명시적으로 캐스팅을 해주어야 한다.
클래스 a, b, c는 상속관계이다.
new C()로 인스턴스를 생성하면 인스턴스 내부에 자신과 부모인 A,B,C가 모두 생성된다. 따라서 C의 부모 타입인 A,B,C모두 C인스턴스를 참조할 수 있다. 상위로 올라가는 업캐스팅은 인스턴스 내부에 부모가 모두 생성되기 때문에 문제가 발생하지 않는다.
new B()로 인스턴스를 생성하면 인스턴스 내부에 자신과 부모인 A,B가 생성된다. 따라서 B의 부모 타입인 A,B모두 B인스턴스를 참조할 수 있다. 상위로 올라가는 업캐스팅은 인스턴스 내부에 부모가 모두 생성되기 때문에 문제가 발생하지 않는다. 하지만, 객체를 생성할 때 하위 자식은 생성되지 않기 때문에 하위로 내려가는 다운캐스팅은 인스턴스 내부에 없는 부분을 선택하는 문제가 발생할 수 있다.
컴파일 오류 VS 런타임 오류
컴파일 오류는 변수명 오타, 잘못된 클래스 이름 사용 등 자바 프로그램을 실행하기 전에 발생하는 오류이다.
이런 오류는 IDE에서 즉시 확인할 수 있어서 안전하고 좋은 오류이다.
반면에 런타임 오류는 이름 그대로 프로그램이 실행되고 있는 시점에 발생하는 오류이다. 런타임 오류는 매우 안좋은 오류이다. 왜냐하면 보통 고객이 해당 프로그램을 실행하는 도중에 발생하기 때문이다.
[instanceof]
다형성에서 참조형 변수는 이름 그대로 다양한 자식을 대상으로 참조할 수 있다. 그런데 참조하는 대상이 다양하기 때문에 어떤 인스턴스를 참조하고 있는지 확인하려면??
Parent parent1=new Parent();
Parent parent2=new Child();
여기서 Parent는 자신과 같은 Parent의 인스턴스도 참조할 수 있고, 자식 타입인 Child의 인스턴스도 참조할 수 있다. 이떄 parent1, parent2 변수가 참조하는 인스턴스의 타입을 확인하고 싶다면? instanceof키워드를 사용하면 된다.
package Polymorphism.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) { // parent가 Child로 부터 생성된 인스턴스면?
System.out.println("Child인스턴스 맞음\n");
Child child=(Child) parent;
child.childMethod();
}else{
System.out.println("Child 인스턴스 아님\n");
}
}
}
call(Parent parent) 메서드를 보자.
이 메서드는 매개변수로 넘어온 parent가 참조하는 타입에 따라서 다른 명령을 수행한다.
참고로 지금처럼 다운캐스팅을 수행하기 전에는 먼저 instanceof를 사용해서 원하는 타입으로 변경이 가능한지 확인한 다음에 다운 캐스팅을 수행하는것이 안전하다.
+ 참고로 instanceof키워드는 오른쪽 대상의 자식 타입을 왼쪽에서 참조하는 경우에도 true를 반환한다.
parent instanceof Parent // parent는 Child의 인스턴스
new Parent() instanceof Parent; // parent가 Parent의 인스턴스를 참조하는 경우: true
new Child() instanceof Parent; // parent가 Child의 인스턴스를 참조하는 경우: true
// -> 마지막은 부모는 자식을 담을 수 있기 때문에 true
쉽게 이야기해서 오른쪽에 있는 타입에 왼쪽에 있는 인스턴스의 타입이 들어갈 수 있는지 대입해보면 된다. 대입이 가능한 true, 불가능하면 false가 된다.
new Parent() instanceof Parent;
Parent p=new Parent(); // 같은 타입이라서 true
new Child() instanceof Parent;
Parent p=new Child(); // 부모는 자식을 담을 수 있어서: true
new Parent() instanceof Child;
Child c=new Parent(); // 자식은 부모를 담을 수 없다. false
new Child() instanceof Child;
Child c=new Child(); // 같은 타임이라서 true
자바16부터는 instanceof를 사용하면서 동시에 변수를 선언할 수 있다.
private static void call(Parent parent) {
parent.parentMethod();
//Child인스턴스인 경우 childMethod() 실행
if (parent instanceof Child child) { // 변수선언도 동시에 가능함
System.out.println("Child인스턴스 맞음\n");
child.childMethod();
}else{
System.out.println("Child 인스턴스 아님\n");
}
}
[다형성과 메서드 오버라이딩]
메서드 오버라이딩에서 꼭 기억해야할 점은 오버라이딩 된 메서드가 항상 우선권을 가진다는 점이다.
그래서 이름도 기존 기능을 덮어 새로운 기능을 재정의한다는 뜻의 오버라이딩이다.
앞서 메서드 오버라이딩을 학습했지만, 지금까지 학습한 메서드 오버라이딩은 반쪽짜리다. 메서드 오버라이딩의 진짜 힘은 다형성과 함꼐 사용할 때 나타난다.
parent, child모두 value라는 멤버 변수를 가지고 있다.
- 멤버 변수는 오버라이딩 되지 않는다.
Parent, Child모두 method()라는 같은 메서드를 가지고 있다. Child에서 메서드를 오버라이딩 했다.
- 메서드는 오버라이딩 된다.
child.java
package Polymorphism.overiding;
public class Child extends Parent{
public String value="child";
@Override
public void method() {
System.out.println("Child.method");
}
}
parent.java
package Polymorphism.overiding;
public class Parent {
public String value="parent";
public void method(){
System.out.println("Parent.method");
}
}
OveridingMain.java
package Polymorphism.overiding;
public class Parent {
public String value="parent";
public void method(){
System.out.println("Parent.method");
}
}
child변수는 Child타입이다. 따라서 child.value, child.method()를 호출하면 인스턴스의 Child타입에서 기능을 찾아서 실행한다.
// 부모 변수가 부모 인스턴스 참조
Parent parent=new Parent();
System.out.println("Parent -> Parent");
System.out.println("value = "+ parent.value);
parent.method();
parent변수는 parent타입이다. 따라서 parent.value, parent.method()를 호출하면 인스턴스의 parent타입에서 기능을 찾아서 실행한다.
Parent poly=new Child();
System.out.println("Parent -> Child");
System.out.println("value = "+ poly.value); // 변수는 오버라이딩x
poly.method(); // 메서드는 오버라이딩o
변수는 오버라이딩이 안되고, 메서드는 오버라이딩이 된다.
poly변수는 Parent타입이다. 따라서 poly.value, poly.method()를 호출하면 인스턴스의 Parent타입에서 기능을 찾아서 실행한다.
- poly.value: parent타입에 있는 value값을 찾아서 읽는다.
- poly.method(): parent타입에 있는 method()를 실행하려고 한다. 그런데 하위 타입인 child.method()가 오버라이딩되어 있다. 오버라이딩 된 메서드는 항상 우선권을 가진다. 따라서 Parent.method()가 아니라 Child.method()가 실행된다.
오버라이딩된 메서드는 항상 우선권을 가진다. 오버라이딩은 부모 타입에서 정의한 기능을 자식 타입엣 재정의하는 것이다. 만약 자식에서도 오버라이딩하고 손자에서도 같은 메서드를 오버라이딩을 하면 손자의 오버라이딩 메서드가 우선권을 가진다. 더 하위 자식의 오버라이딩된 메서드가 우선권을 가지는 것이다.
다형적 참조: 하나의 변수 타입으로 다양한 자식 인스턴스를 참조할 수 있는 기능
메서드 오버라이딩: 기존 기능을 하위 타입에서 새로운 기능으로 재정의