다형성(polymorphism)이란?
객체지향개념에서 다형성이란 '여러 가지 형태를 가질 수 있는 능력'을 의미한다. 자바에서는 한 타입의 참조변수로 여러 타입의 객체를 참조하는, 즉 상위 클래스 타입의 참조변수로 하위 클래스의 인스턴스를 참조할 수 있도록 다형성을 구현하였다. 다형성을 이해하기 위해선 상속의 개념에 대한 선 이해가 필수적이므로 부족하다면 상속을 먼저 공부한 뒤 이어서 진행하자.
아래의 분류도를 보고 간단한 질문의 답을 생각해보자. '정온동물에 포유류가 포함되는가?' 답은 '그렇다'이다. '정온동물에 조류가 포함되는가?' 답은 역시 '그렇다'이다. 이를 통해 하위의 존재는 상위의 존재에 포함돼있음을 알 수 있다. 따라서 정온동물이라고 했을 때 포유류가 될 수도 조류가 될 수도 있는 것이다.

이제 각각의 요소를 클래스로, 선을 상속으로 가정하고 생각해보자. 예를 들면 포유류 클래스는 정온동물 클래스를 상속받는다. 분류도의 개념을 클래스 사이에 적용시켜보면, 정온동물 타입은 포유류 타입을 포함하고 있다. 이를 코드 관점에서 생각하면 '정온동물 타입으로 선언한 참조 변수에는 포유류 클래스의 인스턴스를 저장할 수 있다.'이다. 조류 클래스의 경우도 동일하며, 이는 '정온동물 타입으로 선언한 참조 변수에는 포유류, 조류 클래스의 인스턴스를 저장할 수 있다.'를 의미한다. 또 다른 예시로 척추동물 타입에는 정온동물, 포유류, 조류 클래스의 인스턴스를 저장할 수 있다.
여기서 의문이 하나 생긴다. 생성한 인스턴스와 참조 변수의 타입이 서로 다른 경우 인스턴스 멤버의 접근은 어디까지 가능할까? 이해를 위해 카드 앞에는 정온동물 뒤에는 포유류라고 적혀있다고 하자. 우리에겐 카드의 앞면만이 보인다. 이 경우 우리는 정온동물이라는 것은 알지만 뒤에는 포유류인지 조류인지 알 수 없다. 따라서 정온동물이라는 것은 알기 때문에 정온동물의 특징은 생각할 수 있지만, 뒤에 적힌 포유류의 특징까지는 생각이 미치지 못 한다. 즉, 참조 변수의 타입이 정온동물인 경우 정온동물 클래스의 멤버에는 접근이 가능하지만 실제 생성한 인스턴스 고유의 멤버에는 접근이 불가능한 것이다.
아래 코드를 통해 좀 더 살펴보자.
class 정온동물 {
String 특징1;
void method() {
System.out.println(특징1);
}
}
class 포유류 extends 정온동물 {
String 특징2;
void method() {
System.out.println(특징2);
}
}
class 조류 extends 정온동물 {
String 특징3;
void method() {
System.out.println(특징3);
}
}
정온동물 동물1 = new 포유류();
정온동물 동물2 = new 조류();
System.out.println(동물1.특징1); //OK
System.out.println(동물1.특징2); //실패
System.out.println(동물2.특징1); //OK
System.out.println(동물2.특징3); //실패
그렇다면 포유류 타입의 참조 변수에 정온동물 클래스의 인스턴스를 저장할 수 있을까? 답은 '불가능하다'이다. 클래스는 위의 코드를 참조하자.
포유류 포유류1 = new 정온동물(); //실패
포유류1.특징2; //불가능
만약 생성이 가능하다고 해도 정온동물 클래스에는 존재하지 않는 멤버에 대해 접근하게 되면 문제가 발생하게 된다.
참조 변수의 형변환
서로 상속 관계에 있는 클래스 사이에서는 참조 변수의 형변환이 가능하다. 다형성의 개념에서 상위 클래스 타입의 참조 변수에 하위 클래스의 인스턴스를 참조하는 것이 가능하다고 하였다. 물론 자신의 클래스 타입의 참조 변수에 참조하는 것도 가능하다. 다음 코드를 확인해보자.
class 정온동물 {
String 특징1;
void method() {
System.out.println(특징1);
}
}
class 포유류 extends 정온동물 {
String 특징2;
void method() {
System.out.println(특징2);
}
}
class 조류 extends 정온동물 {
String 특징3;
void method() {
System.out.println(특징3);
}
}
정온동물 정온동물 = new 포유류();
포유류 포유류 = new 포유류();
정온동물 정온동물2 = (정온동물)포유류; //OK
//정온동물 정온동물2 = 포유류; 형변환 생략 가능
포유류 포유류2 = (포유류)정온동물; //OK, 형변환 생략 불가
조류 조류 = (조류)정온동물; //실패 : 정온동물의 실제 타입은 포유류이기 때문에 잘못된 형변환
자신보다 상위 클래스 타입으로 형변환하는 것을 '업캐스팅'이라 하고, 하위 클래스 타입으로 형변환하는 것을 '다운캐스팅'이라고 한다. 업캐스팅은 형변환 생략이 가능하지만 다운캐스팅은 형변환 생략이 불가능하다. 이런 차이는 사용할 수 있는 멤버의 범위 때문이다. 하위 타입에서 상위 타입으로의 형변환은 사용할 수 있는 멤버의 범위가 좁아지기 때문에 딱히 문제되지 않는다. 하지만 하위 타입으로의 형변환은 실제 생성한 인스턴스의 타입을 벗어나게 될 수 있다. 예제 코드를 통해 이해해보자.
Object 최상위 = new 정온동물();
최상위.특징1; //특징1의 경우 정온동물 인스턴스의 멤버이기 때문에 접근 불가능
정온동물 정온동물 = (정온동물)최상위; //정온동물 멤버 범위까지 사용가능
정온동물.특징2; //OK
포유류 포유류 = (포유류)정온동물;
포유류.특징2; //실제 생성한 인스턴스가 정온동물이기 때문에 접근 불가능
<알아두기>
- 실제 생성한 인스턴스 타입보다 하위 타입으로 형변환은 불가능하다.
- 형변환을 이용해 사용할 멤버의 범위를 정한다.
instanceof 연산자
참조 변수가 참조하고 있는 인스턴스의 실제 타입을 확인하기 위해 사용한다. 예제 코드는 다음과 같다.
void method(정온동물 정온동물) {
if (정온동물 instanceof 포유류) {
//인스턴스 타입이 포유류인 경우 수행
} else if (정온동물 instanceof 조류) {
//인스턴스 타입이 조류인 경우 수행
}
}
매개변수의 다형성
다형성을 이용하면 동일한 상위 타입을 상속받는 하위 타입들을 하나의 타입처럼 다룰 수 있다는 장점이 존재한다. 책의 예제 코드를 통해 알아보자.
class Product {
int price;
int bonusPoint;
}
class Tv extends Product {}
class Computer extends Product {}
class Audio extends Product {}
class Buyer {
int money = 1000;
int bonusPoint = 0;
void buy(Tv t) {
money -= t.price;
bonusPoint += t.bonusPoint;
}
void buy(Computer c) {
money -= c.price;
bonusPoint += c.bonusPoint;
}
void buy(Audio a) {
money -= a.price;
bonusPoint += a.bonusPoint;
}
}
Product 클래스를 상속받는 Tv, Computer, Audio 클래스가 존재하며, Buyer 클래스의 buy 메서드가 각각의 타입에 대해 정의되어 있다. 하지만 전부 같은 기능을 수행하기 때문에 비효율적이다. 이를 다형성을 이용해 하나의 메서드로 통일하면 다음과 같다.
class Buyer {
int money = 1000;
int bonusPoint = 0;
void buy(Product p) {
money -= p.price;
bonusPoint += p.bonusPoint;
}
}
코드가 매우 간결해졌다. 뿐만아니라 타입이 같다면 배열로 묶어 다룰 수도 있다.
Product[] products = new Product[3];
products[0] = new Tv();
products[1] = new Computer();
products[2] = new Audio();
※ 이 경우 공통된 부분 즉, 상위 클래스의 멤버에 대한 처리만 가능하다.
Q&A
Q. 다형성이란?
A. 상위 클래스 타입의 참조 변수에 하위 클래스의 인스턴스를 참조하는 것으로, 서로 다른 타입의 인스턴스를 하나의 타입으로 다룰 수 있다.
Q. 상위 클래스와 하위 클래스에 중복된 이름의 멤버 변수가 존재하는 경우 참조 타입에 따른 멤버 변수 호출은 어떻게 다른가?
A. 참조 타입이 상위 타입인 경우 상위 클래스에 정의된 멤버 변수가, 참조 타입이 하위 타입인 경우 하위 클래스에 정의된 멤버 변수가 호출된다.
참고자료
- Java의 정석
- 스프링 입문을 위한 자바 객체 지향의 원리와 이해
'개념서 > Java' 카테고리의 다른 글
| [Java] 내부 클래스 (0) | 2022.06.25 |
|---|---|
| [Java] 인터페이스 (0) | 2022.06.23 |
| [Java] 제어자 (0) | 2022.06.13 |
| [Java] package와 import (0) | 2022.06.10 |
| [Java] 상속 (0) | 2022.06.10 |
댓글