[Java/자바] 다형성(polymorphism)이란 ?

다형성

다형성은 객체지향 언어인 자바의 아주 중요한 특징 중 하나입니다.

다형성은 상속과 깊은 관계를 가지고 있으므로 상속에 대해 알고 싶다면 아래 포스팅을 참고하시면 됩니다.

 

[Java/자바] 클래스의 관계 - 상속(is-a)과 포함(has-a)

상속 상속이란, 기존의 클래스를 재사용해 새로운 클래스를 작성하는 것입니다. 상속을 통해서 클래스를 작성하면, 적은 양의 코드로 새로운 클래스를 작성할 수 있고 코드를 공통적으로 관리

hstory0208.tistory.com

 

다형성이란, "여러 가지 형태를 가질 수 있는 능력"을 의미하며

자바에서 한 타입의 참조변수로 여러 타입의 객체를 참조할 수 있도록 함으로써 다형성을 구현합니다.

쉽게 말하면, "조상클래스 타입의 참조변수로 자손클래스의 인스턴스를 참조할 수 있도록 하는 것."입니다.

 

좀 더 이해가 쉽게 코드예시를 통해 알아봅시다.

class Tv {
	boolean power; 	// 전원상태(on/off)
	int channel;	// 채널

	void power()        {   power = !power; }
	void channelUp()    { 	 ++channel;     }
	void channelDown()  {	 --channel;	    }
}

class CaptionTv extends Tv {
	boolean caption;		// 캡션상태(on/off)
	void displayCaption(String text) {
		if (caption == true) {	// 캡션 상태가 on(true)일 때만 text를 보여 준다.
			System.out.println(text);
		}
	}
}

 

Tv 클래스와 CationTv 클래스는 서로 상속관계에 있으며 그림으로 보면 다음과 같습니다.

이 두 클래스의 인스턴스를 생성하고 사용하기 위해서는 다음과 같이 할 수 있습니다.

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

 

우리가 클래스의 인스턴스를 생성할 때는 위처럼 인스턴스 타입과 일치하는 타입의 참조변수만을 사용했습니다.

이 처럼 인스턴스의 타입과 참조변수의 타입이 일치하는 것이 보통이지만

 

서로 상속관계에 있을 경우 아래 처럼 조상 클래스 타입의 참조변수로 자손 클래스의 인스턴스를 참조할 수도 있습니다.

하지만 반대로 자손타입의 참조변수로 조상 타입의 인스턴스를 참조하는 것은 허용하지 않습니다.

그 이유는 실제 인스턴스인 Tv의 멤버 개수보다 참조변수 c가 사용할 수 있는 멤버 개수가 더 많기 때문입니다.

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

CaptionTv c = new Tv(); // 에러 : 자손타입의 참조변수로 조상타입의 인스턴스 참조 X

위 코드를 보면 참조변수 c와 t가 생성된 CationTv 인스턴스를 참조하도록 하였습니다.

 

여기서 Tv 타입의 참조변수인 t는 CaptionTv 인스턴스의 모든 멤버를 사용할 수 없습니다.

t는 CaptionTv 인스턴스 중에서 Tv클래스의 멤버들만 사용할 수 있습니다.

CaptionTv 인스턴스만 가지고 있는 멤버를 t.displayCaption(), t.caption와 같이 사용할 수 없는 겁니다.

 

즉, 둘 다 같은 타입의 인스턴스지만, 참조변수의 타입에 따라 사용할 수 있는 멤버의 개수가 달라지게 됩니다.

 


업 캐스팅(up-casting), 다운 캐스팅 (down-casting)

 

기본형 변수와 같이 참조변수도 형변환이 가능합니다.

단, 서로 상속관계에 있는 클래스사이에서만 가능하며 자손타입의 참조변수를 조상타입으로 형변환하는 것은 생략이 가능합니다.

자손타입 -> 조상타입 : 형변환 생략 O - ( 업캐스팅 )
조상타입 -> 자손타입 : 형변환 생략 X - ( 다운캐스팅 )

 

형변환은 참조변수의 타입을 변경하는 것이지 인스턴스를 변환하는 것이 아니기 때문에 인스턴스에 아무런 영향을 미치지 않습니다.

단지 참조변수의 형변환을 통해, 참조하고 있는 인스턴스에서 사용할 수 있는 멤버의 개수를 조절하는 것 뿐입니다.

 

형변환 예시 코드 1
class CastingTest1 {
	public static void main(String args[]) {
		Car car = null;
		FireEngine fe = new FireEngine();
		FireEngine fe2 = null;

		fe.water();
		car = fe;    // car =(Car)fe;에서 형변환이 생략된 형태다.( 업 캐스팅 )
//		car.water();	car타입의 참조변수로는 water()메서드를 호출할 수 없다.
		fe2 = (FireEngine)car; // 자손타입 ← 조상타입 ( 다운 캐스팅 )
		fe2.water();
	}
}

class Car {
	String color;
	int door;

	void drive() { 		// 운전하는 기능
		System.out.println("drive, Brrrr~");
	}

	void stop() {		// 멈추는 기능	
		System.out.println("stop!!!");	
	}
}

class FireEngine extends Car {	// 소방차
	void water() {		// 물을 뿌리는 기능
		System.out.println("water!!!");
	}
}

출력 결과

  • car = fe;

 Car 타입의 참조변수 car가 참조변수 fe가 참조하고 있는 인스턴스를 참조합니다.

이 때 두 참조변수의 타입이 달라 fe가 형변환되어야 하지만, 자손타입 -> 조상타입으로 형변환이기 때문에 생략되었습니다.

참조변수 car는 FireEngine 인스턴스를 사용할 수 있게 되었지만, fe와 달리 car는 Car타입이기 때문에 Car 클래스의 멤버가 아닌 water()는 사용할 수 없습니다.

 

  • fe2 = (FireEngine)car;

조상타입 -> 자손타입으로 형변환을 생략하지 못해 (FireEngine)으로 형변환하였습니다.

fe는 FireEngine 타입으로 만들어진 참조변수FireEngine의 인스턴스의 모든 멤버를 사용할 수 있어 water()또한 호출 가능합니다.

 

 

형변환 예시 코드 2
class CastingTest2 {
	public static void main(String args[]) {
		Car car = new Car();
		Car car2 = null;
		FireEngine fe = null;
  
		car.drive();
		fe = (FireEngine)car; // 조상 타입의 인스턴스를 자손 타입의 참조 변수로 참조하는 것은 허용 x
		fe.drive();
		car2 = fe;
		car2.drive();
	}
}

이 예제는 실행 시 에러가 발생합니다.

그 이유가 무엇일까요 ?

바로 조상타입의 인스턴스를 자손타입의 참조변수로 참조하는 것이 허용되지 않기 때문입니다.

Car 타입의 car는 Car인스턴스를 참조한 참조변수이므로 자손 타입인 fe가 car참조변수를 참조하는 것은 허용되지 않습니다.

 

이 에러를 해결하기 위해서는 Car 타입의 참조변수 car가 참조하는 인스턴스를 다음과 같이 바꿀수 있습니다.

class CastingTest2 {
	public static void main(String args[]) {
		//Car car = new Car(); 컴파일 에러 해결을 위해 아래로 인스턴스 변경.
		Car car = new FireEngine();
		Car car2 = null;
		FireEngine fe = null;
  
		car.drive();
		fe = (FireEngine)car; // 조상 타입의 인스턴스를 자손 타입의 참조 변수로 참조하는 것은 허용 x
		fe.drive();
		car2 = fe;
		car2.drive();
	}
}

class Car {
	String color;
	int door;

	void drive() { 		// 운전하는 기능
		System.out.println("drive, Brrrr~");
	}

	void stop() {		// 멈추는 기능	
		System.out.println("stop!!!");	
	}
}

class FireEngine extends Car {	// 소방차
	void water() {		// 물을 뿌리는 기능
		System.out.println("water!!!");
	}
}

출력 결과

 

형변환을 하는 이유

굳이 헷갈리게 형변환을 왜 하는지 의문이 들 수도 있습니다.

그 이유로 객체지향의 특징 중에 하나인 다형성과 관련 있으며,

반복된 메서드를 형변환을 부모 클래스에서 가져와 자식 클래스를 호출할 때 사용할 수 있습니다. 

 

우리나라에서 아들과 딸의 성은 아버지의 성에서 따죠 ? 

 

이 그림을 다형성을 가진 코드로 표현 하면 아래와 같습니다.

class Father {
	public void familyName() { // 성을 호출하는 메서드
		System.out.print("신"); 
	}
	
	public void name() { // 이름을 호출하는 메서드
		familyName(); // 성을 호출
		System.out.println("형만"); 
	}
}

class Son extends Father {
	public void name() { // 이름을 호출하는 메서드
		familyName(); // 성을 호출
		System.out.println("짱구");
	}
}

class Daughter extends Father {
	public void name() { // 이름을 호출하는 메서드
		familyName(); // 성을 호출
		System.out.println("짱아");
	}
}

public class CastingEx {
	public static void main(String[] args) {
		Son s = new Son();
		Daughter d = new Daughter();
		
		Father p = (Father)s; // 업 캐스팅 (Father)생략가능
		Father p2 = (Father)d; // 업 캐스팅 (Father)생략가능
		
		p.name();  
		p2.name(); 
	}
}

출력 결과

 

Father 타입을 가진 참조변수 pSon 인스턴스를 참조하는 s를 참조하고 p.name()은 Son이 가진 메서드를 호출합니다.

Father 타입을 가진 참조변수 p2Dauhgter 인스턴스를 참조하는 d를 참조하고 p2.name()은 Dauhgter이 가진 메서드를 호출합니다.

 

 


instanceof

참조변수가 참조하고 있는 인스턴스의 실제 타입을 알아보기 위해 instanceof 연산자를 사용할 수 있습니다.

instanceof 연산자의 왼쪽에는 참조변수를, 오른족에는 타입(클래스명)이 피연산자로 위치하고 연산의 결과로 true나 false를 반환합니다.

 

class InstanceofTest {
	public static void main(String args[]) {
		FireEngine fe = new FireEngine();

		if(fe instanceof FireEngine) {
			System.out.println("This is a FireEngine instance.");
		} 

		if(fe instanceof Car) {
			System.out.println("This is a Car instance.");
		} 

		if(fe instanceof Object) {
			System.out.println("This is an Object instance.");
		} 

		System.out.println(fe.getClass().getName()); // 클래스 이름 출력
	}
} 

class Car {}
class FireEngine extends Car {}

출력 결과

 

위 코드의 결과를 그림으로 표현하면 아래와 같습니다.

 


참고자료 : 자바의 정석3