[JAVA] 다형성_1
다형성이란?
- 다형성(Polymorphism)은 다양한 형태를 의미.
- 프로그래밍에서 다형성이란, 하나의 객체가 여러 타입으로 참조될 수 있는 능력을 말한다.
보통 하나의 객체는 하나의 타입으로만 사용된다.
하지만 다형성을 사용하면 하나의 객체가 여러 타입으로 사용될 수 있다.
다형성을 이해하려면 두 가지 개념을 알아야 한다:
- 다형적 참조 (Polymorphic Reference)
- 메서드 오버라이딩 (Method Overriding)
다형적 참조 (Polymorphic Reference)
다형성을 이해하려면 먼저 다형적 참조 개념을 알아야 한다. 다형적 참조는 부모 타입의 변수로 자식 인스턴스를 참조하는 것 을 의미한다.
1
2
3
4
5
6
7
8
// Parent.java
package poly.basic;
public class Parent {
public void parentMethod() {
System.out.println("Parent.parentMethod");
}
}
1
2
3
4
5
6
7
8
// Child.java
package poly.basic;
public class Child extends Parent {
public void childMethod() {
System.out.println("Child.childMethod");
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// 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(); // 부모 타입에서 선언된 메서드만 호출 가능
// poly.childMethod(); // 컴파일 오류 - Parent 타입에는 존재하지 않는 메서드
}
}
// 실행결
Parent -> Parent
Parent.parentMethod
Child -> Child
Parent.parentMethod
Child.childMethod
Parent -> Child
Parent.parentMethod
부모 타입 변수 → 부모 타입 인스턴스
1
Parent parent = new Parent();
Parent
클래스만 메모리에 생성된다.parent.parentMethod()
호출 시Parent
클래스의 메서드가 실행된다.
자식 타입 변수 → 자식 타입 인스턴스
1
Child child = new Child();
Child
와Parent
클래스 모두 메모리에 생성된다.- 자식 변수는 부모 메서드와 자식 메서드 모두 호출할 수 있다.
부모 타입 변수 → 자식 타입 인스턴스 (다형적 참조)
1
Parent poly = new Child(); //업캐스팅
Child
와Parent
클래스 모두 메모리에 생성된다.- 변수의 타입이
Parent
이므로,Parent
에 정의된 메서드만 호출할 수 있다. - 자식 고유 메서드인
childMethod()
는 호출할 수 없다. (컴파일 오류)
1
poly.childMethod(); // 컴파일 오류 발생
poly
는Parent
타입이므로Parent
에 정의되지 않은 메서드는 호출할 수 없다.- 부모 타입은 자식의 기능을 알지 못하므로 접근할 수 없다.
- 이럴 경우 형변환(캐스팅)을 통해 접근해야 한다.
캐스팅
1
2
Child casted = (Child) poly; // 형변환 캐스팅
casted.childMethod(); // 가능
형변환을 사용하면 자식 타입에만 있는 기능도 사용할 수 있다.
이처럼 부모 타입의 참조 변수를 자식 타입으로 변환하는 것을 다운캐스팅(downcasting) 이라고 한다.
Child child = (Child) poly;
이 코드는 poly
가 가리키는 인스턴스(실제 객체)가 Child
타입이라는 것을 가정하고, 그 참조값을 Child
타입 변수에 대입하는 것이다.
참고: poly의 참조값만 복사되는 것이고,
poly
자체의 타입은 여전히Parent
이다. 변하는 것은 아니다.
일시적 다운캐스팅
다운캐스팅 결과를 변수에 담지 않고, 필요한 순간에만 일시적으로 타입을 변환해서 메서드를 호출할 수도 있다.
1
((Child) poly).childMethod();
poly
는Parent
타입이지만, 괄호를 두 번 감싸(Child)
로 일시적으로 타입을 변환한 뒤, 곧바로childMethod()
를 호출한다.- 이 방식은 변수를 따로 만들 필요가 없어서 간편하다.
- 물론 실제 인스턴스가 자식 타입이어야만 가능하다.
이 캐스팅은 단지 poly가 가리키는 참조값을 Child로 해석해서 호출하는 것 뿐이다. 실제로 poly의 타입 자체가 바뀌는 것은 아니다. 즉, poly는 여전히 Parent 타입 이다.
업캐스팅
다운캐스팅과 반대로, 자식 타입을 부모 타입으로 변환하는 것을 업캐스팅 이라고 한다.
1
2
3
4
5
6
7
Child child = new Child();
Parent parent1 = (Parent) child; // 명시적 업캐스팅 (사실 생략 가능)
Parent parent2 = child; // 암묵적 업캐스팅 (추천 방식)
// 실행 결과
Parent.parentMethod
Parent.parentMethod
Child
인스턴스를Parent
타입으로 참조하는 것은 업캐스팅이다.- 이 경우 형변환 코드를 생략해도 자동으로 변환된다.
- 자바에서는 업캐스팅을 매우 자주 사용하므로 생략하는 것이 일반적이다.
1
Parent parent = new Child(); // 업캐스팅 생략한 형태
업캐스팅은 자바 컴파일러가 자동으로 처리해준다. 반면, 다운캐스팅은 명시적인 형변환이 없으면 컴파일 오류가 발생 한다.
다운캐스팅과 주의점
다운캐스팅은 실제 인스턴스가 자식 타입일 때만 안전 하게 사용할 수 있다. 잘못된 다운캐스팅은 ClassCastException
이라는 런타임 오류를 발생시킨다.
1
2
3
4
5
6
7
Parent parent1 = new Child();
Child child1 = (Child) parent1;
child1.childMethod(); // OK
Parent parent2 = new Parent();
Child child2 = (Child) parent2; // 런타임 오류 발생
child2.childMethod(); // 실행되지 않음
왜 오류가 생길까
1
2
Parent parent2 = new Parent(); // 실제 인스턴스는 Parent
Child child2 = (Child) parent2; // 존재하지 않는 기능에 접근하려고 함
parent2
는Parent
타입 인스턴스이기 때문에, 메모리에Child
관련 정보가 없음- 그런데
Child
로 변환하려 하니까 자바가 런타임 예외를 발생시킴 - 이 예외가 바로
ClassCastException
1
2
Child.childMethod
Exception in thread "main" java.lang.ClassCastException: class poly.basic.Parent cannot be cast to class poly.basic.Child
안전하게 캐스팅하려면?
자바에서는 다운캐스팅 전에 instanceof
키워드를 사용해서 실제 인스턴스 타입을 검사할 수 있다.
1
2
3
4
5
6
if (parent2 instanceof Child) {
Child safeChild = (Child) parent2;
safeChild.childMethod();
} else {
System.out.println("Child 타입이 아닙니다.");
}
→ 이렇게 검사하면 런타임 오류 없이 안전하게 다운캐스팅할 수 있다.
업캐스팅은 안전하고, 다운캐스팅은 왜 위험할까?
업캐스팅은 항상 안전하다
1
A a = new C(); // 업캐스팅
- 클래스 A → B → C 순으로 상속 관계가 있다고 가정하자.
new C()
를 하면 메모리에는C
,B
,A
모두 함께 생성된다.- 따라서 상위 타입인 A, B, C 어느 타입으로 참조해도 문제 없다.
- 이처럼 위로 올라가는 업캐스팅은 인스턴스 내부에 모든 타입 정보가 포함되어 있어 안전하다.
그래서 업캐스팅은 형변환 생략도 가능하고, 자바에서 매우 자주 쓰인다
다운캐스팅은 조심해야 한다
1
C c = (C) new B(); // 런타임 오류 (ClassCastException)
new B()
로 생성하면 인스턴스에는B
,A
만 존재한다.- 그런데
C
는 존재하지 않기 때문에C
로 캐스팅하면 없는 기능에 접근하려는 시도다. - 자바는 이 상황에서
ClassCastException
이라는 런타임 오류를 발생시킨다.
하위 타입은 객체 생성 시 포함되지 않기 때문에, 존재하지 않는 부분을 참조하게 될 위험이 있다.
컴파일 오류 vs 런타임 오류
구분 | 설명 | 예시 | 확인 시점 |
---|---|---|---|
컴파일 오류 | 문법적 오류, 즉시 확인 가능 | 변수 오타, 클래스 없음 | 코드 작성 시 |
런타임 오류 | 실행 중 발생하는 오류 | 잘못된 다운캐스팅 | 프로그램 실행 중 |
- 컴파일 오류는 IDE가 바로 알려주기 때문에 안전하다.
- 런타임 오류는 실제 사용자 앞에서 발생할 수 있기 때문에 위험하다.
- 그래서 다운캐스팅은 항상 조심해서 사용해야 한다.
instanceof
다형성에서는 부모 타입 변수가 다양한 자식 인스턴스를 참조할 수 있기 때문에,
현재 참조하고 있는 실제 인스턴스의 타입을 확인할 필요가 있다.
이때 사용하는 것이 바로 instanceof
연산자이다.
1
2
Parent parent1 = new Parent();
Parent parent2 = new Child();
parent1
은Parent
인스턴스를 참조한다.parent2
는Child
인스턴스를 참조한다.
이처럼 부모 타입 변수는 다양한 자식 인스턴스를 참조할 수 있으므로,
다운캐스팅 전에 instanceof로 검사하는 것이 안전한 코드 작성에 도움이 된다.
instanceof 결과 예시
1
2
3
4
5
6
7
8
9
10
11
12
13
14
private static void call(Parent parent) {
parent.parentMethod();
if (parent instanceof Child) {
System.out.println("Child 인스턴스 맞음");
Child child = (Child) parent; // 안전한 다운캐스팅
child.childMethod();
}
}
new Parent() instanceof Parent // true
new Child() instanceof Parent // true (부모 타입도 포함됨)
new Parent() instanceof Child // false
new Child() instanceof Child // true
- 왼쪽 인스턴스를 오른쪽 타입에 대입할 수 있는지 판단하는 구조
- 대입 가능하면
true
, 불가능하면false
를 반환한다
Java 16부터: 패턴 매칭 기반 instanceof
Java 16부터는 instanceof
를 사용할 때 타입 검사 + 캐스팅 + 변수 선언을 한 번에 처리할 수 있다.
1
2
3
if (parent instanceof Child child) {
child.childMethod();
}
- 위 코드는
parent
가Child
타입인 경우만 실행된다. - 동시에
Child child = (Child) parent
를 자동으로 수행한 효과를 가진다. - 불필요한 캐스팅 없이 더 간결한 코드 작성이 가능하다.
정리
instanceof
는 다운캐스팅의 안전성을 확보하기 위한 필수 도구이다.- 실제 인스턴스 타입을 확신할 수 없을 때는
instanceof
로 검사한 뒤 캐스팅해야 한다. - Java 16 이상에서는 패턴 매칭을 활용해 더 깔끔하게 작성할 수 있다.
다형성과 메서드 오버라이딩
다형성을 이루는 또 하나의 핵심 이론은 바로 메서드 오버라이딩(Overriding) 이다. 오버라이딩은 부모 클래스에 정의된 메서드를 자식 클래스에서 재정의하는 것이다.이때 중요한 점은, 오버라이딩 된 메서드는 항상 우선권을 가진다.
다형성과 함께 사용할 때, 메서드 오버라이딩의 진짜 힘이 발휘된다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// Parent.java
public class Parent {
public String value = "parent";
public void method() {
System.out.println("Parent.method");
}
}
// Child.java
public class Child extends Parent {
public String value = "child";
@Override
public void method() {
System.out.println("Child.method");
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
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(); // 메서드는 오버라이딩됨
}
}
//실행결과
Child -> Child
value = child
Child.method
Parent -> Parent
value = parent
Parent.method
Parent -> Child
value = parent
Child.method
- 변수는 타입에 따라 결정된다 → 정적 바인딩
- 메서드는 실제 인스턴스에 따라 결정된다 → 동적 바인딩
오버라이딩의 우선순위
오버라이딩된 메서드는 항상 하위 클래스 쪽 메서드가 우선순위를 가진다.
예를 들어,
- 부모 → 자식 → 손자 클래스가 있고,
- 셋 다
method()
를 오버라이딩했다면, - 손자의 메서드가 실행된다. 즉, 더 하위 클래스일수록 우선순위가 높다.
정리
- 다형적 참조: 부모 타입 변수로 자식 인스턴스를 참조할 수 있다.
- 메서드 오버라이딩: 자식이 부모의 메서드를 재정의하여 새로운 동작을 제공한다.
-
다형성과 오버라이딩을 함께 사용하면,
부모 타입 변수를 통해 자식의 오버라이딩된 메서드를 실행할 수 있다.
이 조합이 바로 다형성의 진짜 위력이며, 유연하고 확장 가능한 객체지향 설계의 핵심이다.
출처: 김영한의 실전 자바 - 기본편