순간을 기록으로

[Java] 다형성(polymorphism) 본문

Development/JAVA

[Java] 다형성(polymorphism)

luminous13 2022. 5. 16. 15:27

다형성

자바 OOP는 캡슐화, 상속, 다형성, 추상화라는 핵심적인 4가지 특징을 가지고 있습니다. 오늘은 그 중에서 다형성에 대해 알아 보겠습니다.
다형성 영어로는 polymorphism입니다. poly라는 다양,많음의 의미와 morphism 형태라는 뜻이 합쳐 단어 뜻을 유추하자면 '여러가지 형태를 가질 수 있는 성질(능력)'이라고 해석할 수 있습니다.

간단히 일상에서 다형성을 찾아보겠습니다.

 

일상에서 다형성

위의 이미지를 보면 한 사람이 있습니다. 이 사람은 엄마가 될 수 있고, 직원이 될 수 있으며, 발표자가 될 수 있는 등 필요한 상황에 따라 다양한 역할(형태)를 바꿔 끼울 수 있습니다. 이것을 일상에서의 다형성이라고 볼 수 있습니다.

 

 

자바에서 다형성

자바에서 다형성은 다음과 같습니다. 

  • 한 타입의 참조변수로 여러 타입의 객체를 참조 할 수 있는 능력.
  • 즉. 다형성이란 상위클래스 타입의 참조변수로 하위클래스의 인스턴스를 참조할 수 있는 성질을 말합니다.

추가로, 위의 문장을 보면 상위클래스와 하위클래스가 등장합니다. 즉 다형성은 상속을 기반으로하는 성질입니다.

 

class Tv {
    boolean power;	// 전원
    int channel;	// 채널
    
    void power() { power = !power;}
    void channelUp() { ++channel;}
    void channelDown() { --channel;}
}
class CaptionTv extends Tv {
    String test;	// 자막
    void caption() {}	// 자막 달기
}

Tv 클래스와 CaptionTv 클래스는 상속관계다.

 

지금까지 객체를 생성하면 아래와 같이 참조변수 타입과 인스턴스 타입이 일치했습니다.

 

Tv t = new Tv();
CaptionTv c = new CaptionTv();

 

그러면 참조변수 타입과 인스턴스 타입이 다르면 어떤 차이가 있는지 일치하는 경우와 불일치하는 경우를 비교해보겠습니다.

CaptionTv c = new CaptionTv();	// 참조변수 타입 = 인스턴스 타입
Tv t = new CatptionTv();	// 참조변수 타입(상위클래스) != 인스턴스 타입(하위클래스)

 

결론적으로 참조변수 타입에 따라 참조할 수 있는(=가리킬 수 있는) 인스턴스 멤버의 갯수가 달라집니다.

접근할 수 있는 멤버에 회색을 칠할 수 있다고 가정하겠습니다.

참조변수 c의 경우 두 타입이 모두 CaptionTv입니다. 따라서 CaptionTv의 모든 멤버를 사용할 수 있습니다.

반면 참조변수 t의 경우 참조변수 타입이 상위클래스이므로 인스턴스 멤버 중 상위클래스에서 선언한 멤버만 접근할 수 있습니다. 

그리고 다음과 같은 사실을 알 수 있습니다.

  • 참조변수가 사용할 수 있는 멤버 갯수 <= 인스턴스 멤버 갯수

 

그러면 반대로 하위클래스 타입의 참조변수로 조상 타입의 인스턴스를 참조하는 것은 왜 안될까요?

하위클래스 타입의 참조변수로 조상 타입의 인스턴스를 참조하면 위 식이 반대가 됩니다.

참조변수가 사용할 수 있는 멤버 갯수 > 인스턴스 멤버 갯수 

이를 리모컨에 빗대 표현하자면 버튼은 있지만 버튼 기능이 제대로 동작하지 않는 상황이라고 말할 수 있습니다. 이럴 경우 에러의 위험성이 있기 때문에 자바에서는 금지하고 있습니다.

 

참조변수의 형변환

기본형 변수를 바꿀 수 있는 것처럼 참조변수도 형변환이 가능합니다. 참조변수 형변환을 캐스팅이라하고 다음과 같이 2가지 경우가 있습니다.

  • 업캐스팅(Up-casting): 하위타입 참조변수 --> 상위타입 참조변수 [생략 O]
  • 다운캐스팅(Down-casting): 상위타입 참조변수 --> 하위타입 참조변수 [생략 X]

참조변수의 형변환은 아래와 같은 조건을 전제합니다.

  • 두 클래스는 상속관계다.

클래스 사이에 상속관계여야 '하위 타입 참조변수 <----> 상위 타입 참조변수'와 같이 형 양 방향으로 형변환이 가능합니다.

여기서 중요한 점은 바로 위 상위클래스, 바로 아래 하위클래스만 적용되는 게 아니라 더 먼 상위 클래스와 하위 클래스로도 형변환이 가능하다는 점입니다. 그래서 Object를 상속받는 모든 클래스는 모두 Object형으로 형변환이 가능합니다.

참조변수 형변환 예시

다음과 같이 3개의 클래스  Car, FireEngine, Abulance 사용해서 설명하겠습니다. 관계과 코드는 아래와 같습니다.

상속관계

public class Car {
	String color;
	int door;
	void drive() {
		System.out.println("운전하기");
	}
	void stop() {
		System.out.println("멈추기");
	}
}
public class FireEngine extends Car {
	void water() {
		System.out.println("물뿌리기");
	}
}
public class Ambulance extends Car {
	void siren() {
		System.out.println("사이렌 울리기");
	}
}

첫 번째 예시 - 상속 관계가 아닌 경우

FireEngine f;
Abulance a;
a = (Ambulance) f;	// 상속관계가 아니므로 형변환 불가
f = (FireEngine) a;	// 상속관계가 아니므로 형변환 불가

 

두 번째 예시 - 상속관계인 경우

Car car = null;
FireEngine fe = new FireEngine();
FireEngine fe2 = null;

car = fe;	         // 참조변수가 상위클래스 타입이므로 생략가능 - 업캐스팅
fe2 = (FireEngine)car;	// 참조변수가 하위클래스 타입이므로 명시적으로 작성해야됨 - 다운캐스팅

 

세 번째 예시 -  메모리로 형변환 보기

 

Car car = null;
FireEngine fe = new FireEngine();
FireEngine fe2 = null;

fe.water();
car = fe;	// 업캐스팅
// car.water();	// 호출불가
fe2 = (FireEngine)car;	// 다운캐스팅
fe2.water();

 

 

1. Car car = null;

- Car 타입의 참조변수 car를 선언하고 null로 초기화한 상태.

2. FireEngine fe = new FireEngine();

- FireEngine 인스턴스를 생성하고 FireEngine 타입 참조변수가 가리키고(참조하는) 있는 상태.

3.car = fe; // 업캐스팅 

  • 참조변수 fe가 참조하고 있는 인스턴스를 참조변수 car도 참조할 수 있도록 참조값을 넘겨주고 있다. 이 때 두 참조변수의 타입이 다르므로 참조변수 fe를 Car로 형변환 하였다. 업캐스팅이라 생략되었다.
  • 이제 참조변수  car를 통해서도 FireEngine 인스턴스에 접근할 수 있다. 하지만 참조변수 car은 Car 타입이므로 Car 멤버에만 접근할 수 있다. 따라서 water()에는 접근할 수 없다.

4.fe2 = (FireEngine)car; // 다운캐스팅

  • 참조변수 car가 참조하고 있는 인스턴스를 참조변수 fe2도 참조할 수 있도록 참조값을 할당한다. 이 때 두 참조변수의 타입이 다르므로 참조변수 car을 다운캐스팅하였다. 다운캐스팅의 경우 명시적으로 작성해야한다. 
  • 이제 참조변수 fe2를 통해서도 FireEngine 인스턴스에 접근할 수 있다. car와 달리 fe2는 FireEngine타입이므로 모든 멤버에 접근할 수 있다.

네 번째 예시 - 하위타입 참조변수는 상위 타입 인스턴스를 참조할 수 없다.

Car car = new Car();
Car car2 = null;
FireEngine fe = null;

car.drive();
fe = (FireEngine)car;	// 컴파일 OK, 실행 시 에러
fe.drive();
car2 = fe;
car2.drive();

위 코드를 실행해보면 ClassCastException 에러가 발생합니다. 언듯보기에는 큰 문제는 없어보입니다. 

첫 번째로 두 참조변수의 타입은 상속관계이고, 명시적으로 형변환 연산자를 작성해서 다운캐스팅을 하고 있습니다.

문제는 참조변수 car가 참조하고 있는 인스터스가  Car타입의 인스턴스라는 것입니다.

앞서 배운 것처럼 상위타입의 인스턴스를 하위타입의 참조변수로 참조하는 것은 허용되지 않습니다.

만약 첫 번쨰 줄을 Car car = new FireEngine()이였다면 에러가 발생하지 않았을 겁니다.결론은 다음과 같습니다.

 

  • 형 변환 하기전에, 참조변수가 가리키는 인스턴스의 타입이 무엇인지 확인하는 것이 중요하다.

그리고 이것을 확인하기 위해 instanceof 연산자가 필요합니다.

instanceof 연산자

참조변수가 참조하고 있는 인스턴스의 실제 타입을 확인하기 위해 사용합니다.

연산 결과로 true를 얻었다는 것은 참조변수가 검사한 타입으로 형변환이 가능하다는 것을 의미합니다. 

왼쪽에는 참조변수 오른쪽에는 타입이 옵니다.

 

조상타입의 인스턴스는 모든 멤버에 접근할 수 없는 단점이 있습니다. 그래서 실제 인스턴스와 같은 타입으로 형변환을 해야지 인스턴스의 모든 멤버를 사용할 수 있습니다.

예시

FireEngine fe = new FireEngine();

if (fe instanceof FireEngine) {
	System.out.println("fe가 가리키는 것은 FireEngine 인스턴스"); // True
}
if (fe instanceof Car) {
	System.out.println("fe가 가리키는 것은 Car 인스턴스");	// True
}
if (fe instanceof Object) {
	System.out.println("fe가 가리키는 것은 Object 인스턴스");     // True
}

fe가 가리키는 인스턴스의 타입은 FireEngine입니다. 첫 번째는 참인 것을 알겠는데, 두 번째와 세 번째는 왜 참일까요?

그 이유는 FireEngine 클래스는 Object 클래스와 Car 클래스의 자손클래스 입니다. 그러므로 상위의 멤버들을 상속받았기 때문에, FireEngine 인스턴스는 Object 인스턴스와 Car 인스턴스를 포함하고 있는 셈이기 때문입니다.

 

상위클래스와 하위클래스에 멤버변수가 중복 정의된 경우

  • 상위 타입의 참조변수를 사용했을 때 상위 클래스에 선언된 멤버변수에 접근한다.
  • 하위 타입의 참조변수를 사용했을 때 하위 클래스에 선언된 멤버변수에 접근한다.
  • 중복 정의가 되지 않았을 경우에는 참조변수 타입에 따른 변화는 없다. 선택의 여지가 없기 때문이다.
  • 메소드의 경우 참조변수 타입에 관계없이 항상 실제 인스턴스의 메소드(=오버라이딩된 메소드)가 호출된다.

예시

class BindingTest {
	public static void main(String[] args) {
    	Parent p = new Child();
        Child c = new Child();
    	
        System.out.println("p.x = " + p.x);
        p.method();
        
        System.out.println("c.x = " + c.x);
        c.method();
    }
}
class Parent {
	int x = 100;	// 멤버변수 중복 정의
    
	void method() {
    	System.out.println("부모 메소드 호출");
	}
}
class Child extends Parent {
	int x = 200;	// 멤버변수 중복 정의
    
	void method() {
    	System.out.println("자식 메소드 호출");
	}
}
p.x = 100
자식메소드 호출
pcx = 200
자식메소드 호출

매개변수에 다형성 적용하기

메소드의 매개변수에 다형성을 이용하면 메소드 추가를 줄일 수 있다.

 

예시

class Product {
    int price;		// 가격
    int bonusPont;	// 보너스포인트
}
class Tv extends Product {}	
class Computer extends Product {}
class Audio extends Product {}

class Buyer {			// 구매자
    int money = 1000;		// 보유금액
    int boinusPoint = 0;	// 보너스 점수
}

Buyer는 다음과 같이 메소들르 작성하여 제품을 구매할 수 있다.

다형성 이용 전

void buy(Tv tv) {
    money -= tv.price;
    bonusPoint -= tv.bonusPoint;
}
void buy(Computer computer) {
    money -= computer.price;
    bonusPoint -= computer.bonusPoint;
}
void buy(Audio audio) {
    money -= audio.price;
    bonusPoint -= audio.bonusPoint;
}
... // 가전제품이 추가될 때마다 메소드를 계속 추가해야되는 문제점 발생

 

다형성 이용 후

void buy(Product product) {	// 매개변수를 상위타입(Product)의 참조변수로 선언함
    money -= product.price;
    bonusPoint -= product.bonusPoint;
}
  • 매개변수를 상위타입의 참조변수로 선언했다.
  • 구매하는 기능은 어차피 상위클래스에 변수와 메소드 모두 정의되어 있기 때문에 각 가전제품마다 접근하여 호출할 수 있다.

 

배열 참조변수에 다형성 적용하기

기본적으로 배열은 같은 타입의 여러 변수를 묶는 자료구조입니다.

하지만

배열 참조변수로 상위클래스 타입의 참조변수를 선언하면 여러가지 타입이 모인 배열을 만들 수 있습니다. 위의 예를 다시 사용하겠습니다.

 

Product p[] = new Product[3];	// 상위클래스 타입의 참조변수로 배열 생성
p[0] = new Tv();		// Tv 타입 인스턴스를 첫 번째 요소에 할당
p[1] = new Computer();	// Computer 타입 인스턴스를 두 번쨰 요소에 할당
p[2] = new Audio();		// Audio 타입 인스턴스를 세 번째 요소에 할당
class Buyer {			
    int money = 1000;		
    int boinusPoint = 0;	
    Product[] items = new Product[10];	// 구입한 제품을 저장하기 위한 배열
    int i = 0;
    
    void buy(Product product) {
    	money -= product.price;
        bonusPoint += product.bonusPoint;
        items[i++] = p;		// 구입한 제품을 배열에 저장합니다.
    }
}

위의 예시처럼 하나의 배열을 사용하여 다양한 종류의 제품을 저장 할 수 있습니다.

 

감사합니다.

Comments