10-1. 다형성 시작
객체지향 프로그래밍의 대표적 특징: 캡슐화, 상속, 다형성
다형성(Polymorphism)
- '다양한 형태', '여러 형태'
- 한 객체가 여러 타입의 객체로 취급될 수 있는 능력
- 하나의 객체는 하나의 타입으로 고정되어 있는데,
다형성을 사용하면 하나의 객체가 다른 타입으로 사용될 수 있다는 뜻
2가지 핵심이론
- 다형적 참조 (여러 형태로 참조)
- 메서드 오버라이딩
poly.basic.Parent.java (다형적 참조)
package poly.basic;
public class Parent {
public void parentMethod() {
System.out.println("Parent.parentMethod");
}
}
poly.basic.Child.java
package poly.basic;
public class Child extends Parent {
public void childMethod() {
System.out.println("Child.childMethod");
}
}
poly.basic.PolyMain.java (~ 변수가 ~ 인스턴스를 참조)
package poly.basic;
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.childMethod();
}
}
- 부모 타입의 변수가 부모 인스턴스 참조 => 메모리상 Parent만 생성 (자식 생성 X)
- 자식 타입의 변수가 자식 인스턴스 참조 => 메모리상 Parent, Child 모두 생성
부모 타입의 변수가 자식 인스턴스 참조
- => 자식 타입인 Child를 생성 => 메모리상 Parent, Child 모두 생성
- 부모 타입은 자식 타입을 담을 수 있다.
Parent poly = new Child(); : 성공 - 자식 타입은 부모 타입을 담을 수 없다.
Child child1 = new Parent(); : 컴파일 오류
다형적 참조
- 지금까지 항상 같은 타입에 참조를 대입
Parent parent = new Parent()
Child child = new Child() - Parent 타입의 변수는 자신은 물론, 자식 타입과 그 하위 타입도 참조 할 수 있다.
Parent poly = new Parent()
Parent poly = new Child()
Parent poly = new Grandson() : Child 하위에 손자가 있다면 가능
다형적 참조의 한계
- poly.childMethod() 호출 불가
- 호출자인 poly는 Parent 타입이다. 따라서 Parent 클래스부터 시작해서 필요한 기능을 찾는다.
- 상속 관계는 부모 방향으로 찾아 올라갈 수는 있지만 자식 방향으로 찾아 내려갈 수는 없다.
- Parent는 부모 타입이고 상위에 부모가 없다. childMethod()를 찾을 수 없으므로 컴파일 오류가 발생한다.
※ childMethod()를 호출하고 싶으면 캐스팅이 필요
10-2. 다형성과 캐스팅
poly.basic.CastingMain1.java (다운캐스팅)
package poly.basic;
public class CastingMain1 {
public static void main(String[] args) {
// 부모 변수가 자식 인스턴스 참조(다형적 참조)
Parent poly = new Child(); // x001
// 단, 자식의 기능은 호출할 수 없다. 컴파일 오류 발생
//poly.childMethod();
// 다운캐스팅(부모 타입 -> 자식 타입)
//Child child = poly;
Child child = (Child) poly; // x001
child.childMethod();
}
}
- Parent poly = new Child()
부모 타입의 변수를 사용하게 되면, 자식 타입에 있는 기능을 호출 할 수 없다.- poly.childMethod()를 호출하면, 먼저 참조값(x001)을 사용해 인스턴스를 찾는다.
- poly는 Parent 타입인데, Parent는 최상위 부모이고 상속 관계는 부모로만 찾아서 올라갈 수 있다.
- childMethod()는 자식 타입의 기능이므로 호출 불가 -> 컴파일 오류
- 다운캐스팅 Child child = (Child) poly //Parent poly
호출하는 타입을 Child 타입으로 변경하면, 인스턴스의 Child에 있는 childMEthod() 호출 가능- 부모는 자식을 담을 수 있지만 자식은 부모를 담을 수 없다.
- poly의 참조값(x001)을 읽고 다운캐스팅을 통해 자식 타입으로 변환 후
- Child child에 대입
- poly의 타입은 기존과 같이 Parent로 유지됨
- 캐스팅
- 업캐스팅(upcasting): 부모 타입으로 변경
- 다운캐스팅(downcasting): 자식 타입으로 변경
10-3. 캐스팅의 종류
다운캐스팅 결과를 변수에 담아두는 과정 없이,
일시적 다운 캐스팅으로 인스턴스에 있는 하위 클래스의 기능을 바로 호출
poly.basic.CastingMain2.java (일시적 다운캐스팅)
package poly.basic;
public class CastingMain2 {
public static void main(String[] args) {
// 부모 변수가 자식 인스턴스 참조(다형적 참조)
Parent poly = new Child(); // x001
// 단, 자식의 기능은 호출할 수 없다. 컴파일 오류 발생
//poly.childMethod();
...
// (추가)
// 일시적 다운캐스팅 - 해당 메서드를 호출하는 순간만 다운캐스팅
((Child) poly).childMethod();
}
}
- poly는 Parent 타입이고, ((Child)poly)를 통해 일시적으로 Child로 변경된다.
- poly의 참조값(x001)을 꺼내고(읽고), 꺼낸 참조값이 Child 타입이 됨.
- poly의 타입은 그대로 Parent로 유지
poly.basic.CastingMain3.java (업캐스팅)
package poly.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();
}
}
업캐스팅은 생략해도 되고, 다운캐스팅은 왜 개발자가 직접 명시적으로 캐스팅 해야 하는 이유
10-4. 다운캐스팅의 주의점
다운캐스팅을 잘못하면 심각한 런타임 오류 발생 가능
poly.basic.CastingMain4.java (다운캐스팅이 자동이 아닌 이유)
package poly.basic;
// 다운캐스팅을 자동으로 하지 않는 이유
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(); // 실행 불가
}
}
- Parent parent2 = new Parent()
Parent로 부모 타입 객체를 생성 => 메모리 상 자식 타입은 존재하지 않음 - Child child2 = (Child) parent2
메모리 상에 Child 자체가 존재하지 않음 - 사용할 수 없는 타입으로 다운캐스팅 => ClassCastException 예외 발생
- 예외가 발생되어 다음 동작 실행 안되고, 프로그램 종료
업캐스팅이 안전하고 다운캐스팅이 위험한 이유
1) 업캐스팅
- 상위로 올라가는 업케스팅은 인스턴스 내부에 부모가 모두 생성
- new C()로 인스턴스를 생성하면 인스턴스 내부에 자신과 부모인 A, B, C가 모두 생성
- C의 부모 타입인 A, B, C 모두 C 인스턴스를 참조 가능
A a = new C();
B b = new C();
C c = new C();
2) 다운캐스팅
- 객체를 생성할 때 하위 자식은 생성되지 않음
- 하위로 내려가는 다운케스팅은 인스턴스 내부에 없는 부분을 선택하는 문제가 발생
- new B()로 인스턴스를 생성하면 인스턴스 내부에 자신과 부모인 A, B가 생성
- B의 부모 타입인 A, B 모두 B 인스턴스를 참조
- C는 B 인스턴스를 참조 불가능
A a = new B();
B b = new B();
C c = new B(); // (컴파일 오류)
// 하위 타입에 대입할 수 없음
C c = (C) new B(); // (런타임 오류) => ClassCastException
// 하위 타입으로 강제 다운캐스팅, 하지만 B 인스턴스에 C와 관련된 부분이 없으므로 잘못된 캐스팅
컴파일 오류 vs 런타임 오류
- 컴파일 오류는
- 변수명 오타, 잘못된 클래스 이름 사용 등 자바 프로그램을 실행하기 전에 발생하는 오류
- 이런 오류는 IDE에서 즉시 확인할 수 있기 때문에 안전하고 좋은 오류
- 반면에 런타임 오류는
- 이름 그대로 프로그램이 실행되고 있는 시점에 발생하는 오류
- 런타임 오류는 매우 안좋은 오류
- 왜냐하면 보통 고객이 해당 프로그램을 실행하는 도중에 발생하기 때문
10-5. instanceof
다형성에서 참조형 변수는 다양한 자식을 대상으로 참조함.
이때, 변수가 참조하는 인스턴스의 타입을 instanceof로 확인 가능
poly.basic.CastingMain5.java ( instanceof )
package poly.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("paren2 호출");
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 인스턴스 아님");
}
}
}
- 다운캐스팅을 수행하기 전에는 먼저 instanceof 를 사용해서
원하는 타입으로 변경이 가능한지 확인한 다음에 다운캐스팅을 수행하는 것이 안전
instanceof 키워드는 오른쪽 대상의 자식 타입을 왼쪽에서 참조하는 경우에도 true를 반환
- 오른쪽에 있는 타입에 왼쪽에 있는 인스턴스의 타입이 들어갈 수 있는지 대입해보면 된다.
- 대입이 가능 하면 ` true ` , 불가능하면 ` false ` 가 된다.
// new ~~() 는 인스턴스를 표현
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
poly.basic.CastingMain6.java ( 자바 16 - Pattern Matching for instanceof )
package poly.basic;
public class CastingMain6 {
...
private static void call(Parent parent) {
parent.parentMethod();
// Child 인스턴스인 경우 childMethod() 실행
if (parent instanceof Child child) { // 변수선언
System.out.println("Child 인스턴스 맞음");
//Child child = (Child) parent; // 다운캐스팅
child.childMethod();
}
}
}
- instanceof를 사용하면서 동시에 변수를 선언
- 인스턴스가 맞는 경우 직접 다운캐스팅하는 코드를 생략
10-6. 다형성과 메서드 오버라이딩
- 다형성을 이루는 또 하나의 중요한 핵심 이론 => 메서드 오버라이딩
- "오버라이딩 된 메서드가 항상 우선권을 가진다"
- 기존 기능을 덮어 새로운 기능을 재정의 한다는 뜻 => 부모 타입에서 정의한 기능을 자식 타입에서 재정의
Parent, Child 모두 value라는 같은 멤버 변수 가짐 => 멤버 변수는 오버라이딩 되지 않음
Parent, Child 모두 method()라는 같은 메서드를 가짐 => Child에서 메서드를 오버라이딩 함
poly.overriding.Parent.java
package poly.overriding;
public class Parent {
public String value = "parent";
public void method() {
System.out.println("Parent.method");
}
}
poly.overriding.Child.java
package poly.overriding;
public class Child extends Parent {
public String value = "child";
@Override
public void method() {
System.out.println("Child.method");
}
}
poly.overriding.Overriding.java
package poly.overriding;
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();
// 부모 변수가 부모 인스턴스 참조
Parent parent = new Parent();
System.out.println("Parent -> Parent");
System.out.println("value = " + parent.value);
parent.method();
// 부모 변수가 자식 인스턴스 참조(다형적 참조)
Parent poly = new Child();
System.out.println("Parent -> Child");
System.out.println("value = " + poly.value); // 변수는 오버라이딩 안됨
poly.method(); // 메서드는 오버라이딩 됨
}
}
Parent => Child 이 부분이 중요
- 오더라이딩 된 메서드는 항상 우선권을 가진다.
- poly.method(): Parent 타입에 있는 method()를 실행하려고 할 때,
하위 타입인 Child.method()가 오버라이딩 되어 있어 Child.method()가 실행됨 - 만약 자식에서도 오버라이딩 하고 손자에서도 같은 메서드를 오버라이딩을 하면
손자의 오버라이딩 메서드가 우선권을 가진다.
다형성을 이루는 핵심 이론
- 다형적 참조 : 하나의 변수 타입으로 다양한 자식 인스턴스를 참조할 수 있는 기능
- 메서드 오버라이딩 : 기존 기능을 하위 타입에서 새로운 기능으로 재정의