1. 다형성(Polymorphism)
- 사용 방법은 동일하지만 실행 결과가 다양하게 나오는 성질.
- 한 객체가 여러 타입의 객체로 취급될 수 있는 능력.
- 보통 하나의 객체는 하나의 타입으로 고정되어 있지만, 다형성을 사용하면 하나의 객체가 다른 타입으로 사용될 수 있음.
▶ 핵심이론
① 다형적 참조
② 메서드 오버라이딩
package poly.basic;
public class Parent {
public void parentMethod() {
System.out.println("Parent.parentMethod");
}
}
package poly.basic;
public class Child extends Parent {
public void childMethod() {
System.out.println("Child.childMethod");
}
}
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 타입은 Parent만 알고 있기 때문에 Child의 메서드 호출 불가능.
}
}
실행결과
Parent → Parent
Parent.parentMethod
Child → Child
Parent.parentMethod
Child.childMethod
Parent → Child
Parent.parentMethod
[Parent, Child, PolyMain]
① Parent → Parent
- 부모 타입의 변수가 부모 인스턴스 참조.
- Parent 인스턴스 생성하면 부모 타입인 Parent를 생성했기 때문에 메모리 상에 Parent만 생성됨. (자식 생성되지 않음)
- 생성된 참조값을 Parent 타입의 변수인 parent에 담아둠.
- 따라서 parent.parentMethod() 호출하면 인스턴스의 Parent 클래스에 있는 parentMethod()가 호출됨.
② Child → Child
- 자식 타입의 변수가 자식 인스턴스 참조.
- Child 인스턴스 생성하면 자식 타입인 Child를 생성했기 때문에 메모리 상에 Child와 Parent가 모두 생성.
- 생성된 참조값을 Child 타입의 변수인 child에 담아둠.
- 따라서 child.childMethod()를 호출하면 인스턴스의 Child 클래스에 있는 childMethod()가 호출됨.
③ Parent → Child
- 부모 타입의 변수가 자식 인스턴스 참조.
- Child 인스턴스 생성하면 자식 타입인 Child를 생성했기 때문에 메모리 상에 Child와 Parent가 모두 생성.
- 생성된 참조값을 Parent 타입의 변수인 poly에 담아둠.
! 알아두기 !
부모는 자식을 담을 수 있음.
- Parent poly는 부모 타입이고, new Child()를 통해 생성된 결과는 Child 타입임.
- 반대로 자식 타입은 부모 타입을 담을 수 없음. → Child child = new Parent();
즉 다형적 참조는
부모타입인 자신은 물론, 자신을 기준으로 모든 자식 타입을 참조할 수 있는데 이러한 다양한 형태를 참조하는 것을 의미.
그리고 상속관계에서 부모 방향으로 찾아 올라갈 수 있지만 자식 방향으로 찾아 내려갈 수는 없음.
▶ 다형성과 캐스팅
Parent poly = new Child()와 같이 부모 타입의 변수를 사용하게 되면 poly.childMethod()와 같이 자식 타입에 있는 기능은 호출 불가능.
package poly.basic;
public class CastingMain1 {
public static void main(String[] args) {
// 부모 변수가 자식 인스턴스 참조(다형적 참조)
Parent poly = new Child();
// 단 자식의 기능은 호출 불가능.
// poly.childMethod();
// 다운캐스팅(부모타입 → 자식타입)
Child child = (Child) poly;
child.childMethod();
}
}
실행결과
Child.childMethod
[CastingMain1]
poly.childMethod()를 호출하면 먼저 참조값을 사용해 인스턴스를 찾고, 인스턴스 안에서 사용할 타입을 찾음.
poly는 Parent 타입이므로 Parent는 최상위 부모이기 때문에 상속 관계에서 부모로만 찾아 올라갈 수 있음.
childMethod()는 자식 타입에 있으므로 호출 불가능하여 컴파일 오류가 발생함.
호출하는 타입을 자식인 Child 타입으로 변경하면 인스턴스의 Child에 있는 childMethod()를 호출할 수 있음.
하지만 부모는 자식을 담을 수 있는데, 자식은 부모를 담을 수 없는데 이때 사용하는 것이 다운캐스팅임.
(타입)처럼 괄호와 그 사이에 타입을 지정하면 참조 대상을 특정 타입으로 변경 가능. 이렇게 특정 타입으로 변경하는 것을 캐스팅이라고 함.
Child child = (Child) poly;에서 poly는 Parent 타입이고, 이 타입을 (Child)를 사용해서 일시적으로 자식 타입인 Child 타입으로 변경.
그러고 나서 왼쪽에 있는 Child child에 대입하는 것.
참고로 캐스팅한다고 해서 Parent poly의 타입이 변경하는 것이 아닌 해당 참조값을 꺼내고 그 참조값이 Child 타입이 되는 것임.
▷ 캐스팅
- 업 캐스팅 : 부모타입으로 변경.
- 다운 캐스팅 : 자식타입으로 변경.
▶ 일시적 다운캐스팅
package poly.basic;
public class CastingMain2 {
public static void main(String[] args) {
// 부모 변수가 자식 인스턴스 참조(다형적 참조)
Parent poly = new Child();
// 단 자식의 기능은 호출 불가능.
// poly.childMethod();
// 다운캐스팅(부모타입 → 자식타입)
Child child = (Child) poly;
child.childMethod();
// 일시적 다운캐스팅 - 해당 메서드를 호출하는 순간만 다운캐스팅
((Child) poly).childMethod();
}
}
실행결과
Child.childMethod
[CastingMain2]
Poly는 Parent 타입이고, 이 코드는 Parent 타입을 임시로 Child로 변경함.
그리고 메서드 호출 시 Child 타입에서 찾아서 실행하는데 정확히는 poly가 Child 타입으로 바뀌는 것은 아님.
해당 참조값을 꺼내고 꺼낸 참조값이 Child 타입이 되는 것이므로 poly의 타입은 Parent 그대로 유지되는 것.
이렇게 일시적 다운캐스팅을 사용하면 별도의 변수 없이 인스턴스의 자식 타입의 기능을 사용할 수 있음.
▶ 업캐스팅
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();
}
}
실행결과
Parent.parentMethod
Parent.parentMethod
[CastingMain3]
Child 타입을 Parent 타입에 대입해야 함. 따라서 타입을 변환하는 캐스팅이 필요.
부모타입으로 변환하는 경우에는 다음과 같이 캐스팅 코드인 (타입) 생략 가능.
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(); // 실행불가
}
}
[CastingMain4]
Parent parent2 = new Parent();
new Parent()로 부모타입으로 객체 생성하면 메모리 상에 자식 타입은 전혀 존재하지 않음.
생성 결과를 parent2에 담아두고, 이 경우 같은 타입이므로 여기서는 문제 발생하지 않음.
Child child2 = (Child) parent2;
parent2를 Child 타입으로 다운캐스팅하면 parent2는 Parent로 생성됐기 때문에 메모리 상에 Child 자체가 존재하지 않으므로 Child 자체를 사용하는 것이 불가능.
그러므로 예외가 발생하여 프로그램이 실행되지 않고 종료됨.
▶ 업캐스팅이 안전하고, 다운캐스팅이 위험한 이유
업캐스팅의 경우 이런 문제가 절대 발생하지 않음. 그 이유는 객체 생성 시 해당 타입의 상위 부모 타입은 모두 함께 생성됨.
따라서 위로만 타입을 변경하는 업캐스팅은 메모리 상에 인스턴스가 모두 존재하기 때문에 항상 안전하므로 캐스팅을 생략할 수 있음.
반면에 다운캐스팅의 경우 인스턴스에 존재하지 않은 하위타입으로 캐스팅하는 문제가 발생할 수 있음.
그 이유는 객체 생성 시 부모 타입은 모두 함께 생성되지만 자식 타입은 생성되지 않으므로 개발자는 이러한 문제를 인지하고 사용하여 명시적으로 캐스팅을 해줘야 함.
▶ instanceof
다형성에서 참조형 변수는 이름 그대로 다양한 자식을 대상으로 참조할 수 있음.
그런데 참조하는 대상이 다양하기 때문에 어떤 인스턴스를 참조하고 있는지 확인하기 위해 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("parent2 호출");
call(parent2);
}
private static void call(Parent parent) {
parent.parentMethod();
// Child 인스턴스인 경우 childMethod() 실행
if(parent instanceof Child) {
System.out.println("Child 인스턴스 맞음");
Child child = (Child) parent;
child.childMethod();
}
/*
자바16부터 가능
if(parent instanceof Child child) {
System.out.println("Child 인스턴스 맞음");
child.childMethod();
}
*/
}
}
실행결과
parent1 호출
Parent.parentMethod
parent2 호출
Parent.parentMethod
Child 인스턴스 맞음
Child.childMethod
▶ 다형성과 메서드 오버라이딩
메서드 오버라이딩에서 꼭 기억해야 할 것은 오버라이딩 된 메서드가 항상 우선권을 가진다는 점.
package poly.overriding;
public class Parent {
public String value = "parent";
public void method() {
System.out.println("Parent.method");
}
}
package poly.overriding;
public class Child extends Parent {
public String value = "child";
@Override
public void method() {
System.out.println("Child.method");
}
}
package poly.overriding;
public class OverringMain {
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); // 변수는 오버라이딩x
poly.method(); // 메서드는 오버라이딩o
}
}
실행결과
Child → Child
value = child
Child.method
Parent → Parent
value = parent
Parent.method
Parent → Child
value = parent
Child.method
Parent, Child는 모두 value라는 같은 멤버 변수를 가지고 있음. → 멤버변수는 오버라이딩 되지 않음.
Parent, Child는 모두 method()라는 같은 메서드를 가지고 있음. → Child에서 메서드 오버라이딩함. 메서드는 오버라이딩 됨.
① Child → Child
Child 변수 타입은 Child이므로 child.value, child.method() 호출하면 인스턴스의 Child 타입에서 기능을 찾아 실행함.
② Parent → Parent
Parent 변수 타입은 Parent이므로 parent.value, parent.method() 호출하면 인스턴스의 Parent 타입에서 기능을 찾아 실행함.
③ Parent → Child

poly 변수는 Parent 타입이므로 poly.value, poly.method()를 호출하면 인스턴스의 Parent 타입에서 기능을 찾아 실행함.
하지만 poly.method()에서 오버라이딩 된 메서드는 항상 우선권을 가지므로 자식의 오버라이딩된 메서드가 우선권을 가지기 때문에 자식의 메서드를 호출하는 것.
▶ 다형적 참조 : 하나의 변수 타입으로 다양한 자식 인스턴스를 참조할 수 있는 기능.
▶ 메서드 오버라이딩 : 기존 기능을 하위 타입에서 새로운 기능으로 재정의.
'Studying > JAVA' 카테고리의 다른 글
[JAVA]_다형성과 설계 (0) | 2025.03.20 |
---|---|
[JAVA]_Polymorphism2(다형성2) (0) | 2025.03.19 |
[JAVA]_Inheritance(상속) (0) | 2025.02.24 |
[JAVA]_final (0) | 2025.02.18 |
[JAVA]_자바 메모리 구조와 static (1) | 2025.02.11 |